From 9ca87299d506a312cc4b2ceaf065c621ef7604f0 Mon Sep 17 00:00:00 2001 From: Denny Date: Mon, 6 Feb 2017 04:19:09 -0500 Subject: [PATCH 01/87] [WIP] Polyphase channelizer experiments --- .../channelizer/PolyphaseChannelizer.java | 223 ++++++++++++++++++ src/dsp/filter/design/FilterViewer.java | 127 +++++----- 2 files changed, 286 insertions(+), 64 deletions(-) create mode 100644 src/dsp/filter/channelizer/PolyphaseChannelizer.java diff --git a/src/dsp/filter/channelizer/PolyphaseChannelizer.java b/src/dsp/filter/channelizer/PolyphaseChannelizer.java new file mode 100644 index 00000000..daf257a6 --- /dev/null +++ b/src/dsp/filter/channelizer/PolyphaseChannelizer.java @@ -0,0 +1,223 @@ +/******************************************************************************* + * 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 dsp.filter.design.FilterDesignException; +import dsp.filter.fir.FIRFilterSpecification; +import dsp.filter.fir.real.RealFIRFilter; +import dsp.filter.fir.remez.RemezFIRFilterDesigner; +import dsp.mixer.Oscillator; +import org.jtransforms.fft.FloatFFT_1D; +import org.slf4j.Logger; +import org.slf4j.LoggerFactory; +import sample.complex.ComplexBuffer; + +import java.text.DecimalFormat; + +public class PolyphaseChannelizer +{ + private final static Logger mLog = LoggerFactory.getLogger(PolyphaseChannelizer.class); + + private RealFIRFilter[] mFilters; + private int mFilterPointer = 0; + private int mChannelCount; + private int mBlockSize; + private float[] mFilteredSamples; + private boolean mToggle = false; + private FloatFFT_1D mFFT; + + private DecimalFormat DECIMAL_FORMAT = new DecimalFormat("0.0000"); + + /** + * Creates a polyphase filter bank that divides the input frequency band into evenly spaced channels that + * are oversampled by 2x for output. + * + * @param taps of a low-pass filter designed for the inbound sample rate with a cutoff frequency + * equal to the channel bandwidth (sample rate / filters). If you need to synthesize (combine two or more + * filte outputs) a new bandwidth signal from the outputs of this filter, then the filter should be designed + * as a nyquist filter with -6 dB attenuation at the channel bandwidth cutoff frequency + * + * @param channels - number of filters/channels to output. Since this filter bank oversamples each filter + * output, this number must be even (divisible by the oversample rate). + */ + public PolyphaseChannelizer(float[] taps, int channels) + { + assert(channels % 2 == 0); + + mChannelCount = channels; + + initFilters(taps); + } + + public void filter(float inphase, float quadrature) + { + int iIndex = 2 * mFilterPointer; + int qIndex = iIndex + 1; + + mFilteredSamples[iIndex] = mFilters[iIndex].filter(inphase); + mFilteredSamples[qIndex] = mFilters[qIndex].filter(quadrature); + + if(mFilterPointer == 0) + { + calculate(); + mFilterPointer = mChannelCount - 1; + mToggle = !mToggle; + } + else if(mFilterPointer == mBlockSize) + { + calculate(); + mFilterPointer--; + } + else + { + mFilterPointer--; + } + } + + public void filter(ComplexBuffer buffer) + { + float[] samples = buffer.getSamples(); + + for(int x = 0; x < samples.length; x += 2) + { + filter(samples[x], samples[x + 1]); + } + } + + private void calculate() + { + float[] samples = new float[mFilters.length]; + + if(mToggle) + { + System.arraycopy(mFilteredSamples, 0, samples, mChannelCount, mChannelCount); + System.arraycopy(mFilteredSamples, mChannelCount, samples, 0, mChannelCount); + } + else + { + System.arraycopy(mFilteredSamples, 0, samples, 0, mFilteredSamples.length); + } + + //FFT is executed in-place where the output overwrites the input + mFFT.complexInverse(samples, true); + + dispatch(samples); + } + + 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()); + } + + private double magnitude(float real, float imaginary, int fftSize) + { + return Math.sqrt(Math.pow(real, 2.0) + Math.pow(imaginary, 2.0)); + } + + /** + * Distributes the filter taps to each of the polyphase filters + * + * @param taps + */ + private void initFilters(float[] taps) + { + int tapCount = (int)Math.ceil((double)taps.length / (double)mChannelCount); + + float[][] coefficients = new float[mChannelCount][tapCount]; + + for(int filter = 0; filter < mChannelCount; filter++) + { + for(int tap = 0; tap < tapCount; tap++) + { + int index = filter * mChannelCount + tap; + + if(index < taps.length) + { + coefficients[filter][tap] = taps[index]; + } + } + } + + mFilters = new RealFIRFilter[mChannelCount * 2]; + + for(int filter = 0; filter < mChannelCount; filter++) + { + //Inphase filter + mFilters[2 * filter] = new RealFIRFilter(coefficients[filter], 1.0f); + + //Quadrature filter + mFilters[2 * filter + 1] = new RealFIRFilter(coefficients[filter], 1.0f); + } + + mFilteredSamples = new float[mFilters.length]; + mFFT = new FloatFFT_1D(mChannelCount); + mBlockSize = mChannelCount / 2; + mFilterPointer = mChannelCount -1; + } + + public static void main(String[] args) + { + mLog.debug("Starting ..."); + + FIRFilterSpecification specification = FIRFilterSpecification.lowPassBuilder() + .sampleRate(4000) + .gridDensity(16) + .passBandCutoff(950) + .passBandAmplitude(1.0) + .passBandRipple(0.01) + .stopBandStart(1050) + .stopBandAmplitude(0.0) + .stopBandRipple(0.005) + .build(); + + RemezFIRFilterDesigner filterDesigner = new RemezFIRFilterDesigner(specification); + + if(filterDesigner.isValid()) + { + try + { + float[] taps = filterDesigner.getImpulseResponse(); + + PolyphaseChannelizer channelizer = new PolyphaseChannelizer(taps, 8); + + Oscillator oscillator = new Oscillator(-1000, 8000); + + for(int x = 0; x < 2000; x++) + { + channelizer.filter(oscillator.getComplex().inphase(), oscillator.getComplex().quadrature()); + oscillator.rotate(); + } + } + catch(FilterDesignException fde) + { + mLog.error("Couldn't design filter to specification"); + } + } + + mLog.debug("Finished!"); + } +} diff --git a/src/dsp/filter/design/FilterViewer.java b/src/dsp/filter/design/FilterViewer.java index 3044ae3c..5526d824 100644 --- a/src/dsp/filter/design/FilterViewer.java +++ b/src/dsp/filter/design/FilterViewer.java @@ -1,65 +1,64 @@ package dsp.filter.design; -import org.slf4j.Logger; -import org.slf4j.LoggerFactory; - import dsp.filter.fir.FIRFilterSpecification; import dsp.filter.fir.remez.RemezFIRFilterDesigner; import javafx.application.Application; import javafx.scene.Scene; import javafx.stage.Stage; +import org.slf4j.Logger; +import org.slf4j.LoggerFactory; public class FilterViewer extends Application { - private final static Logger mLog = LoggerFactory.getLogger( FilterViewer.class ); + private final static Logger mLog = LoggerFactory.getLogger(FilterViewer.class); - /** - * Developer tool to visualize filter designs - */ - public FilterViewer() - { - } - - @Override - public void start( Stage primaryStage ) throws Exception - { - Scene scene = new Scene( new FilterView( getFilter() ) ); - - primaryStage.setTitle( "Filter Viewer" ); - primaryStage.setScene( scene ); - primaryStage.show(); - } - - public static void main( String[] args ) - { - launch( args ); - } + /** + * Developer tool to visualize filter designs + */ + public FilterViewer() + { + } - /** - * Provides the filter to visualize. Modify this method to visualize your filter design. - */ - private float[] getFilter() - { -// FIRFilterSpecification specification = FIRFilterSpecification.lowPassBuilder() -// .sampleRate( 48000 ) -// .gridDensity( 16 ) -// .passBandCutoff( 3200 ) -// .passBandAmplitude( 1.0 ) -// .passBandRipple( 0.02 ) -// .stopBandStart( 4000 ) + @Override + public void start(Stage primaryStage) throws Exception + { + Scene scene = new Scene(new FilterView(getFilter())); + + primaryStage.setTitle("Filter Viewer"); + primaryStage.setScene(scene); + primaryStage.show(); + } + + public static void main(String[] args) + { + launch(args); + } + + /** + * Provides the filter to visualize. Modify this method to visualize your filter design. + */ + private float[] getFilter() + { + FIRFilterSpecification specification = FIRFilterSpecification.lowPassBuilder() + .sampleRate(4000) + .gridDensity(16) + .passBandCutoff(950) + .passBandAmplitude(1.0) + .passBandRipple(0.01) + .stopBandStart(1050) + .stopBandAmplitude(0.0) + .stopBandRipple(0.005) + .build(); + +// FIRFilterSpecification specification = FIRFilterSpecification.highPassBuilder() +// .sampleRate( 24000 ) +// .stopBandCutoff( 800 ) // .stopBandAmplitude( 0.0 ) // .stopBandRipple( 0.03 ) +// .passBandStart( 1000 ) +// .passBandAmplitude( 1.0 ) +// .passBandRipple( 0.08 ) // .build(); - - FIRFilterSpecification specification = FIRFilterSpecification.highPassBuilder() - .sampleRate( 24000 ) - .stopBandCutoff( 800 ) - .stopBandAmplitude( 0.0 ) - .stopBandRipple( 0.03 ) - .passBandStart( 1000 ) - .passBandAmplitude( 1.0 ) - .passBandRipple( 0.08 ) - .build(); // FIRFilterSpecification specification = FIRFilterSpecification.bandPassBuilder() // .sampleRate( 48000 ) @@ -71,24 +70,24 @@ public class FilterViewer extends Application // .passRipple( 0.008) // .build(); - float[] taps = null; + float[] taps = null; // taps = FilterFactory.getLowPass( 16000, 2400, 73, WindowType.HANNING ); - try - { - RemezFIRFilterDesigner designer = new RemezFIRFilterDesigner( specification ); - - if( designer.isValid() ) - { - taps = designer.getImpulseResponse(); - } - } - catch( FilterDesignException fde ) - { - mLog.error( "Filter design error", fde ); - } - - return taps; - } + try + { + RemezFIRFilterDesigner designer = new RemezFIRFilterDesigner(specification); + + if(designer.isValid()) + { + taps = designer.getImpulseResponse(); + } + } + catch(FilterDesignException fde) + { + mLog.error("Filter design error", fde); + } + + return taps; + } } From e868428c883713d230003899c229a3cf2424de57 Mon Sep 17 00:00:00 2001 From: Denny Date: Sat, 18 Feb 2017 11:05:21 -0500 Subject: [PATCH 02/87] [WIP] working on polyphase channelizer and testing viewer --- src/dsp/filter/FilterFactory.java | 18 +- .../filter/channelizer/ChannelizerViewer.java | 234 ++++++++++++++++++ .../channelizer/PolyphaseChannelizer.java | 86 +++---- src/dsp/filter/design/FilterViewer.java | 47 ++-- src/spectrum/SpectrumPanel.java | 16 +- 5 files changed, 319 insertions(+), 82 deletions(-) create mode 100644 src/dsp/filter/channelizer/ChannelizerViewer.java diff --git a/src/dsp/filter/FilterFactory.java b/src/dsp/filter/FilterFactory.java index 5ca5a078..15b673f2 100644 --- a/src/dsp/filter/FilterFactory.java +++ b/src/dsp/filter/FilterFactory.java @@ -662,16 +662,22 @@ public class FilterFactory * * Symbols = -44 * alpha + 33 * + * Polyphase Channelizer notes: + * -Set samples Per Symbol at 2 (or more) * number of channels + * -Set symbolCount to sufficient size to produce requireded attenuation + * -Set symbol rate in hertz + * -Set alpha = (desired channel bandwidth / (symbol rate * samples per symbol) - 1.0 + * -Channel bandwidth must be larger than symbol rate * samples per symbol, in order for there to be a positive alpha + * * @param samplesPerSymbol - number of samples per symbol - * @param symbols - number of symbols - must be even - * @param alpha - roll-off factor + * @param symbolCount - number of symbol intervals for the filter. This directly impacts the filter size + * @param alpha - roll-off factor. + * * @return - filter coefficients */ - public static float[] getRootRaisedCosine(int samplesPerSymbol, - int symbols, - float alpha) + public static float[] getRootRaisedCosine(int samplesPerSymbol, int symbolCount, float alpha) { - int taps = samplesPerSymbol * symbols + 1; + int taps = samplesPerSymbol * symbolCount; float scale = 0; diff --git a/src/dsp/filter/channelizer/ChannelizerViewer.java b/src/dsp/filter/channelizer/ChannelizerViewer.java new file mode 100644 index 00000000..94b8fb8c --- /dev/null +++ b/src/dsp/filter/channelizer/ChannelizerViewer.java @@ -0,0 +1,234 @@ +/******************************************************************************* + * 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 dsp.filter.FilterFactory; +import dsp.mixer.Oscillator; +import net.miginfocom.swing.MigLayout; +import org.slf4j.Logger; +import org.slf4j.LoggerFactory; +import sample.Listener; +import sample.SampleType; +import sample.complex.ComplexBuffer; +import settings.SettingsManager; +import source.tuner.configuration.TunerConfigurationModel; +import source.tuner.frequency.FrequencyChangeEvent; +import spectrum.DFTProcessor; +import spectrum.DFTSize; +import spectrum.SpectrumPanel; +import spectrum.converter.ComplexDecibelConverter; + +import javax.swing.*; +import java.awt.*; +import java.util.concurrent.Executors; +import java.util.concurrent.ScheduledExecutorService; +import java.util.concurrent.TimeUnit; + +public class ChannelizerViewer extends JFrame +{ + private final static Logger mLog = LoggerFactory.getLogger(ChannelizerViewer.class); + + private static final int CHANNEL_BANDWIDTH = 12500; + + 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 int mChannelCount; + private int mSampleRate; + private int mToneFrequency; + private DFTSize mDFTSize = DFTSize.FFT04096; + + public ChannelizerViewer(int channelCount) + { + mChannelCount = channelCount; + mSampleRate = mChannelCount * CHANNEL_BANDWIDTH; + mToneFrequency = 37500; //Set to channel 1 as default + + init(); + } + + private void init() + { + setTitle("Channelizer Viewer"); + setSize(1000, 800); + setDefaultCloseOperation(JFrame.EXIT_ON_CLOSE); + setLayout(new MigLayout("insets 0 0 0 0", "[grow,fill]", "[grow,fill]")); + setLocationRelativeTo(null); + add(getPrimaryPanel()); + + mOscillator = new Oscillator(mToneFrequency, mSampleRate); + } + + public void start() + { + ScheduledExecutorService scheduledExecutorService = Executors.newScheduledThreadPool(1); + scheduledExecutorService.scheduleAtFixedRate(new SampleGenerationTask(), 0, 100, TimeUnit.MILLISECONDS); + } + + public void setToneFrequency(int frequency) + { + mOscillator.setFrequency(frequency); + mToneFrequency = frequency; + } + + private void generateSamples() + { + int size = mDFTSize.getSize(); + + 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(); + } + + ComplexBuffer buffer = new ComplexBuffer(samples); + + getChannelPanel().receive(buffer); + mDFTProcessor.receive(buffer); + } + + private JPanel getPrimaryPanel() + { + if(mPrimaryPanel == null) + { + mPrimaryPanel = new JPanel(); + mPrimaryPanel.setLayout(new MigLayout("insets 0 0 0 0", "[grow,fill]", "[][grow,fill]")); + mPrimaryPanel.add(getSpectrumPanel(), "wrap"); + mPrimaryPanel.add(getChannelPanel()); + } + + return mPrimaryPanel; + } + + private SpectrumPanel getSpectrumPanel() + { + if(mSpectrumPanel == 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)); + } + + return mSpectrumPanel; + } + + private ChannelPanel getChannelPanel() + { + if(mChannelPanel == null) + { + mChannelPanel = new ChannelPanel(getFilter()); + } + + return mChannelPanel; + } + + private float[] getFilter() + { + int symbolRate = 4800; + int samplesPerSymbol = 2; + int symbolCount = 8; + + //Alpha is the residual channel bandwidth left over from the symbol rate and samples per symbol + float alpha = ((float)CHANNEL_BANDWIDTH / (float)(symbolRate * samplesPerSymbol)) - 1.0f; + + float[] taps = FilterFactory.getRootRaisedCosine(samplesPerSymbol * mChannelCount, symbolCount, alpha); + + StringBuilder sb = new StringBuilder(); + sb.append("\nPolyphase Channelizer\n"); + sb.append("Sample Rate:" + mSampleRate + " Channels:" + mChannelCount + " Channel Rate:" + CHANNEL_BANDWIDTH + "\n"); + sb.append("Alpha: " + alpha + " Tap Count:" + taps.length + "\n"); + sb.append("Channel:" + mToneFrequency); + mLog.debug(sb.toString()); + + return taps; + } + + public class ChannelPanel extends JPanel implements Listener + { + private PolyphaseChannelizer mPolyphaseChannelizer; + + public ChannelPanel(float[] taps) + { + mPolyphaseChannelizer = new PolyphaseChannelizer(taps, mChannelCount); + } + + @Override + public void receive(ComplexBuffer complexBuffer) + { + mPolyphaseChannelizer.receive(complexBuffer); + } + } + + public class SampleGenerator + { + public SampleGenerator() + { + mLog.debug("Starting ..."); + + + PolyphaseChannelizer channelizer = new PolyphaseChannelizer(getFilter(), mChannelCount); + + Oscillator oscillator = new Oscillator(mToneFrequency, mSampleRate); + + for(int x = 0; x < 2000; x++) + { + channelizer.filter(oscillator.getComplex().inphase(), oscillator.getComplex().quadrature()); + oscillator.rotate(); + } + + mLog.debug("Finished!"); + } + } + + public class SampleGenerationTask implements Runnable + { + @Override + public void run() + { + generateSamples(); + } + } + + public static void main(String[] args) + { + final ChannelizerViewer frame = new ChannelizerViewer(16); + + EventQueue.invokeLater(new Runnable() + { + @Override + public void run() + { + frame.setVisible(true); + frame.start(); + } + }); + } + +} diff --git a/src/dsp/filter/channelizer/PolyphaseChannelizer.java b/src/dsp/filter/channelizer/PolyphaseChannelizer.java index daf257a6..a41e07fb 100644 --- a/src/dsp/filter/channelizer/PolyphaseChannelizer.java +++ b/src/dsp/filter/channelizer/PolyphaseChannelizer.java @@ -18,6 +18,7 @@ ******************************************************************************/ package dsp.filter.channelizer; +import dsp.filter.FilterFactory; import dsp.filter.design.FilterDesignException; import dsp.filter.fir.FIRFilterSpecification; import dsp.filter.fir.real.RealFIRFilter; @@ -26,11 +27,12 @@ import dsp.mixer.Oscillator; import org.jtransforms.fft.FloatFFT_1D; import org.slf4j.Logger; import org.slf4j.LoggerFactory; +import sample.Listener; import sample.complex.ComplexBuffer; import java.text.DecimalFormat; -public class PolyphaseChannelizer +public class PolyphaseChannelizer implements Listener { private final static Logger mLog = LoggerFactory.getLogger(PolyphaseChannelizer.class); @@ -90,9 +92,10 @@ public class PolyphaseChannelizer } } - public void filter(ComplexBuffer buffer) + @Override + public void receive(ComplexBuffer complexBuffer) { - float[] samples = buffer.getSamples(); + float[] samples = complexBuffer.getSamples(); for(int x = 0; x < samples.length; x += 2) { @@ -115,22 +118,22 @@ public class PolyphaseChannelizer } //FFT is executed in-place where the output overwrites the input - mFFT.complexInverse(samples, true); + mFFT.complexForward(samples); dispatch(samples); } 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()); +// 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()); } private double magnitude(float real, float imaginary, int fftSize) @@ -183,39 +186,36 @@ public class PolyphaseChannelizer { mLog.debug("Starting ..."); - FIRFilterSpecification specification = FIRFilterSpecification.lowPassBuilder() - .sampleRate(4000) - .gridDensity(16) - .passBandCutoff(950) - .passBandAmplitude(1.0) - .passBandRipple(0.01) - .stopBandStart(1050) - .stopBandAmplitude(0.0) - .stopBandRipple(0.005) - .build(); + int symbolRate = 4800; + int samplesPerSymbol = 2; + int symbolCount = 4; + int channels = 10; + int channelBandwidth = 12500; + int sampleRate = channels * channelBandwidth; - RemezFIRFilterDesigner filterDesigner = new RemezFIRFilterDesigner(specification); + //Alpha is the residual channel bandwidth left over from the symbol rate and samples per symbol + float alpha = ((float)channelBandwidth / (float)(symbolRate * samplesPerSymbol)) - 1.0f; - if(filterDesigner.isValid()) + float[] taps = FilterFactory.getRootRaisedCosine(samplesPerSymbol * channels, symbolCount, alpha); + + int channelCenter = (int)(12500 * 1.5); + + StringBuilder sb = new StringBuilder(); + sb.append("\nPolyphase Channelizer\n"); + sb.append("Sample Rate:" + sampleRate + " Channels:" + channels + " Channel Rate:" + channelBandwidth + "\n"); + sb.append("Alpha: " + alpha + " Tap Count:" + taps.length + "\n"); + sb.append("Channel:" + channelCenter); + + mLog.debug(sb.toString()); + + PolyphaseChannelizer channelizer = new PolyphaseChannelizer(taps, 8); + + Oscillator oscillator = new Oscillator(channelCenter, 2 * sampleRate); + + for(int x = 0; x < 2000; x++) { - try - { - float[] taps = filterDesigner.getImpulseResponse(); - - PolyphaseChannelizer channelizer = new PolyphaseChannelizer(taps, 8); - - Oscillator oscillator = new Oscillator(-1000, 8000); - - for(int x = 0; x < 2000; x++) - { - channelizer.filter(oscillator.getComplex().inphase(), oscillator.getComplex().quadrature()); - oscillator.rotate(); - } - } - catch(FilterDesignException fde) - { - mLog.error("Couldn't design filter to specification"); - } + channelizer.filter(oscillator.getComplex().inphase(), oscillator.getComplex().quadrature()); + oscillator.rotate(); } mLog.debug("Finished!"); diff --git a/src/dsp/filter/design/FilterViewer.java b/src/dsp/filter/design/FilterViewer.java index 5526d824..bedc583f 100644 --- a/src/dsp/filter/design/FilterViewer.java +++ b/src/dsp/filter/design/FilterViewer.java @@ -1,5 +1,6 @@ package dsp.filter.design; +import dsp.filter.FilterFactory; import dsp.filter.fir.FIRFilterSpecification; import dsp.filter.fir.remez.RemezFIRFilterDesigner; import javafx.application.Application; @@ -39,16 +40,16 @@ public class FilterViewer extends Application */ private float[] getFilter() { - FIRFilterSpecification specification = FIRFilterSpecification.lowPassBuilder() - .sampleRate(4000) - .gridDensity(16) - .passBandCutoff(950) - .passBandAmplitude(1.0) - .passBandRipple(0.01) - .stopBandStart(1050) - .stopBandAmplitude(0.0) - .stopBandRipple(0.005) - .build(); +// FIRFilterSpecification specification = FIRFilterSpecification.lowPassBuilder() +// .sampleRate(4000) +// .gridDensity(16) +// .passBandCutoff(950) +// .passBandAmplitude(1.0) +// .passBandRipple(0.01) +// .stopBandStart(1050) +// .stopBandAmplitude(0.0) +// .stopBandRipple(0.005) +// .build(); // FIRFilterSpecification specification = FIRFilterSpecification.highPassBuilder() // .sampleRate( 24000 ) @@ -74,19 +75,21 @@ public class FilterViewer extends Application // taps = FilterFactory.getLowPass( 16000, 2400, 73, WindowType.HANNING ); - try - { - RemezFIRFilterDesigner designer = new RemezFIRFilterDesigner(specification); + taps = FilterFactory.getRootRaisedCosine(2 * 8, 16, (12500.0f / 9600.0f - 1.0f)); - if(designer.isValid()) - { - taps = designer.getImpulseResponse(); - } - } - catch(FilterDesignException fde) - { - mLog.error("Filter design error", fde); - } +// try +// { +// RemezFIRFilterDesigner designer = new RemezFIRFilterDesigner(specification); +// +// if(designer.isValid()) +// { +// taps = designer.getImpulseResponse(); +// } +// } +// catch(FilterDesignException fde) +// { +// mLog.error("Filter design error", fde); +// } return taps; } diff --git a/src/spectrum/SpectrumPanel.java b/src/spectrum/SpectrumPanel.java index d2295cbe..9e484cbc 100644 --- a/src/spectrum/SpectrumPanel.java +++ b/src/spectrum/SpectrumPanel.java @@ -45,24 +45,18 @@ import dsp.filter.smoothing.SmoothingFilter; import dsp.filter.smoothing.SmoothingFilter.SmoothingType; import dsp.filter.smoothing.TriangularSmoothingFilter; -public class SpectrumPanel extends JPanel - implements DFTResultsListener, - SettingChangeListener, - SpectralDisplayAdjuster +public class SpectrumPanel extends JPanel implements DFTResultsListener, SettingChangeListener, SpectralDisplayAdjuster { private static final long serialVersionUID = 1L; - private final static Logger mLog = - LoggerFactory.getLogger( SpectrumPanel.class ); + private final static Logger mLog = LoggerFactory.getLogger( SpectrumPanel.class ); private static final RenderingHints RENDERING_HINTS = - new RenderingHints( RenderingHints.KEY_ANTIALIASING, - RenderingHints.VALUE_ANTIALIAS_ON ); + new RenderingHints( RenderingHints.KEY_ANTIALIASING, RenderingHints.VALUE_ANTIALIAS_ON ); static { - RENDERING_HINTS.put( RenderingHints.KEY_RENDERING, - RenderingHints.VALUE_RENDER_QUALITY ); + RENDERING_HINTS.put( RenderingHints.KEY_RENDERING, RenderingHints.VALUE_RENDER_QUALITY ); } private Color mColorSpectrumBackground; @@ -77,7 +71,7 @@ public class SpectrumPanel extends JPanel private float[] mDisplayFFTBins = new float[ 1 ]; //Averaging across multiple DFT result sets - private int mAveraging = 4; + private int mAveraging = 1; //Smoothing across bins in the same DFT result set private SmoothingFilter mSmoothingFilter = new GaussianSmoothingFilter(); From 99ef002d073a0a4661daa8b2fc2cabdd7d3b1360 Mon Sep 17 00:00:00 2001 From: Denny Date: Sun, 19 Feb 2017 08:22:37 -0500 Subject: [PATCH 03/87] [WIP] working on polyphase channelizer and testing viewer --- .../channelizer/ChannelDistributor.java | 106 ++++++++++++ .../filter/channelizer/ChannelizerViewer.java | 159 ++++++++++++++---- .../channelizer/PolyphaseChannelizer.java | 19 ++- src/dsp/mixer/LowPhaseNoiseOscillator.java | 126 ++++++++++++++ src/dsp/mixer/Oscillator.java | 22 +++ 5 files changed, 387 insertions(+), 45 deletions(-) create mode 100644 src/dsp/filter/channelizer/ChannelDistributor.java create mode 100644 src/dsp/mixer/LowPhaseNoiseOscillator.java 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); + } } From 14b844f0bd698c19ca2ba5465a4bcd4002cd9902 Mon Sep 17 00:00:00 2001 From: Denny Date: Sun, 19 Feb 2017 10:37:44 -0500 Subject: [PATCH 04/87] [WIP] Channelizing 6.4 MHz sample rate into 512 channels with with 512-point FFT on each channel at frame rate of 20 fps. --- .../channelizer/ChannelDistributor.java | 3 +- .../filter/channelizer/ChannelizerViewer.java | 33 ++- .../channelizer/PolyphaseChannelizer.java | 6 +- .../channelizer/PolyphaseChannelizer2.java | 202 ++++++++++++++++++ src/dsp/mixer/LowPhaseNoiseOscillator.java | 54 ++++- 5 files changed, 277 insertions(+), 21 deletions(-) create mode 100644 src/dsp/filter/channelizer/PolyphaseChannelizer2.java diff --git a/src/dsp/filter/channelizer/ChannelDistributor.java b/src/dsp/filter/channelizer/ChannelDistributor.java index 6bc5a84c..a3cca8ac 100644 --- a/src/dsp/filter/channelizer/ChannelDistributor.java +++ b/src/dsp/filter/channelizer/ChannelDistributor.java @@ -26,6 +26,7 @@ import sample.complex.ComplexBuffer; import java.util.HashMap; import java.util.Map; +import java.util.Queue; public class ChannelDistributor { @@ -85,7 +86,7 @@ public class ChannelDistributor public void receive(float inphase, float quadrature) { mBuffer[mBufferPointer++] = inphase; - mBuffer[mBufferPointer++] = inphase; + mBuffer[mBufferPointer++] = quadrature; if(mBufferPointer >= mBufferSize) { diff --git a/src/dsp/filter/channelizer/ChannelizerViewer.java b/src/dsp/filter/channelizer/ChannelizerViewer.java index 17395e3e..45bba720 100644 --- a/src/dsp/filter/channelizer/ChannelizerViewer.java +++ b/src/dsp/filter/channelizer/ChannelizerViewer.java @@ -96,7 +96,7 @@ public class ChannelizerViewer extends JFrame private void generateSamples() { - int size = mChannelCount * CHANNEL_BANDWIDTH / CHANNEL_FFT_FRAME_RATE * 2; + int size = mSampleRate / CHANNEL_FFT_FRAME_RATE; float[] samples = mOscillator.generate(size); @@ -137,10 +137,13 @@ public class ChannelizerViewer extends JFrame if(mControlPanel == null) { mControlPanel = new JPanel(); + mControlPanel.setLayout(new MigLayout("insets 0 0 0 0", "", "")); + mControlPanel.add(new JLabel("Tone:"), "align left"); int maximumFrequency = mSampleRate / 2; - SpinnerNumberModel model = new SpinnerNumberModel(mToneFrequency, -maximumFrequency, maximumFrequency, CHANNEL_BANDWIDTH ); + SpinnerNumberModel model = new SpinnerNumberModel(mToneFrequency, -maximumFrequency, maximumFrequency, + 500 ); model.addChangeListener(new ChangeListener() { @@ -155,6 +158,12 @@ public class ChannelizerViewer extends JFrame JSpinner spinner = new JSpinner(model); mControlPanel.add(spinner); + mControlPanel.add(new JLabel("Hz")); + + mControlPanel.add(new JLabel("Sample Rate: " + mSampleRate + " Hz"), "push,align right"); + + mControlPanel.add(new JLabel("Max: " + (mSampleRate / 2) + " Hz"), "push,align right"); + mControlPanel.add(new JLabel("Channels: " + mChannelCount), "push,align right"); } return mControlPanel; @@ -174,7 +183,7 @@ public class ChannelizerViewer extends JFrame { int symbolRate = 4800; int samplesPerSymbol = 2; - int symbolCount = 8; + int symbolCount = 14; //Alpha is the residual channel bandwidth left over from the symbol rate and samples per symbol float alpha = ((float)CHANNEL_BANDWIDTH / (float)(symbolRate * samplesPerSymbol)) - 1.0f; @@ -193,7 +202,7 @@ public class ChannelizerViewer extends JFrame public class ChannelArrayPanel extends JPanel implements Listener { - private PolyphaseChannelizer mPolyphaseChannelizer; + private PolyphaseChannelizer2 mPolyphaseChannelizer; private ChannelDistributor mChannelDistributor; public ChannelArrayPanel(float[] taps) @@ -204,7 +213,7 @@ public class ChannelizerViewer extends JFrame bufferSize++; } - mPolyphaseChannelizer = new PolyphaseChannelizer(taps, mChannelCount); + mPolyphaseChannelizer = new PolyphaseChannelizer2(taps, mChannelCount); mChannelDistributor = new ChannelDistributor(bufferSize, mChannelCount); mPolyphaseChannelizer.setChannelDistributor(mChannelDistributor); @@ -213,21 +222,23 @@ public class ChannelizerViewer extends JFrame private void init() { - setLayout(new MigLayout("insets 0 0 0 0", "[grow,fill][grow,fill][grow,fill][grow,fill]", "")); + setLayout(new MigLayout("insets 0 0 0 0", "fill", "fill")); + + int channelsPerRow = 32; for(int x = 0; x < mChannelCount; x++) { ChannelPanel channelPanel = new ChannelPanel(mSettingsManager, CHANNEL_BANDWIDTH * 2); - channelPanel.setPreferredSize(new Dimension(250,100)); +// channelPanel.setPreferredSize(new Dimension(250,100)); channelPanel.setDFTSize(DFTSize.FFT00512); - if(x % 4 == 3) + if(x % channelsPerRow == channelsPerRow - 1) { - add(channelPanel, "wrap"); + add(channelPanel, "grow,push,wrap 2px"); } else { - add(channelPanel); + add(channelPanel, "grow,push"); } mChannelDistributor.addListener(x, channelPanel); @@ -303,7 +314,7 @@ public class ChannelizerViewer extends JFrame public static void main(String[] args) { - int channelCount = 24; + int channelCount = 512; final ChannelizerViewer frame = new ChannelizerViewer(channelCount); diff --git a/src/dsp/filter/channelizer/PolyphaseChannelizer.java b/src/dsp/filter/channelizer/PolyphaseChannelizer.java index cd6eeda8..2390cb00 100644 --- a/src/dsp/filter/channelizer/PolyphaseChannelizer.java +++ b/src/dsp/filter/channelizer/PolyphaseChannelizer.java @@ -153,11 +153,11 @@ public class PolyphaseChannelizer implements Listener float[][] coefficients = new float[mChannelCount][tapCount]; - for(int filter = 0; filter < mChannelCount; filter++) + for(int tap = 0; tap < tapCount; tap++) { - for(int tap = 0; tap < tapCount; tap++) + for(int filter = 0; filter < mChannelCount; filter++) { - int index = filter * mChannelCount + tap; + int index = tap * mChannelCount + filter; if(index < taps.length) { diff --git a/src/dsp/filter/channelizer/PolyphaseChannelizer2.java b/src/dsp/filter/channelizer/PolyphaseChannelizer2.java new file mode 100644 index 00000000..1168b89f --- /dev/null +++ b/src/dsp/filter/channelizer/PolyphaseChannelizer2.java @@ -0,0 +1,202 @@ +/******************************************************************************* + * 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 dsp.filter.FilterFactory; +import dsp.filter.fir.real.RealFIRFilter; +import dsp.mixer.Oscillator; +import org.jtransforms.fft.FloatFFT_1D; +import org.slf4j.Logger; +import org.slf4j.LoggerFactory; +import sample.Listener; +import sample.complex.ComplexBuffer; + +import java.text.DecimalFormat; + +public class PolyphaseChannelizer2 implements Listener +{ + private final static Logger mLog = LoggerFactory.getLogger(PolyphaseChannelizer2.class); + + private RealFIRFilter[] mInphaseFilters; + private RealFIRFilter[] mQuadratureFilters; + private int mFilterPointer = 0; + private int mChannelCount; + private int mBlockSize; + private float[] mFilteredSamples; + private FloatFFT_1D mFFT; + private ChannelDistributor mChannelDistributor; + + private DecimalFormat DECIMAL_FORMAT = new DecimalFormat("0.0000"); + + /** + * Creates a polyphase filter bank that divides the input frequency band into evenly spaced channels that + * are oversampled by 2x for output. + * + * @param taps of a low-pass filter designed for the inbound sample rate with a cutoff frequency + * equal to the channel bandwidth (sample rate / filters). If you need to synthesize (combine two or more + * filte outputs) a new bandwidth signal from the outputs of this filter, then the filter should be designed + * as a nyquist filter with -6 dB attenuation at the channel bandwidth cutoff frequency + * + * @param channels - number of filters/channels to output. Since this filter bank oversamples each filter + * output, this number must be even (divisible by the oversample rate). + */ + public PolyphaseChannelizer2(float[] taps, int channels) + { + assert(channels % 2 == 0); + + mChannelCount = channels; + + initFilters(taps); + } + + public void filter(float inphase, float quadrature) + { + int index = mFilterPointer * 2; + + mFilteredSamples[index] = mInphaseFilters[mFilterPointer].filter(inphase); + mFilteredSamples[index + 1] = mQuadratureFilters[mFilterPointer].filter(quadrature); + + if(mFilterPointer == 0) + { + calculate(); + mFilterPointer = mChannelCount - 1; + } + else + { + mFilterPointer--; + } + } + + @Override + public void receive(ComplexBuffer complexBuffer) + { + float[] samples = complexBuffer.getSamples(); + + for(int x = 0; x < samples.length; x += 2) + { + filter(samples[x], samples[x + 1]); + } + } + + private void calculate() + { + float[] samples = new float[mChannelCount * 2]; + System.arraycopy(mFilteredSamples, 0, samples, 0, mFilteredSamples.length); + + //FFT is executed in-place where the output overwrites the input + mFFT.complexForward(samples); + + dispatch(samples); + } + + private void dispatch(float[] channels) + { + if(mChannelDistributor != null) + { + mChannelDistributor.receive(channels); + } + } + + public void setChannelDistributor(ChannelDistributor channelDistributor) + { + mChannelDistributor = channelDistributor; + } + + private double magnitude(float real, float imaginary, int fftSize) + { + return Math.sqrt(Math.pow(real, 2.0) + Math.pow(imaginary, 2.0)); + } + + /** + * Distributes the filter taps to each of the polyphase filters + * + * @param taps + */ + private void initFilters(float[] taps) + { + int tapCount = (int)Math.ceil((double)taps.length / (double)mChannelCount); + + float[][] coefficients = new float[mChannelCount][tapCount]; + + for(int tap = 0; tap < tapCount; tap++) + { + for(int filter = 0; filter < mChannelCount; filter++) + { + int index = tap * mChannelCount + filter; + + if(index < taps.length) + { + coefficients[filter][tap] = taps[index]; + } + } + } + + mInphaseFilters = new RealFIRFilter[mChannelCount]; + mQuadratureFilters = new RealFIRFilter[mChannelCount]; + + for(int filter = 0; filter < mChannelCount; filter++) + { + mInphaseFilters[filter] = new RealFIRFilter(coefficients[filter], 1.0f); + mQuadratureFilters[filter] = new RealFIRFilter(coefficients[filter], 1.0f); + } + + mFilteredSamples = new float[mChannelCount * 2]; + mFFT = new FloatFFT_1D(mChannelCount); + mFilterPointer = mChannelCount -1; + } + + public static void main(String[] args) + { + mLog.debug("Starting ..."); + + int symbolRate = 4800; + int samplesPerSymbol = 2; + int symbolCount = 4; + int channels = 10; + int channelBandwidth = 12500; + int sampleRate = channels * channelBandwidth; + + //Alpha is the residual channel bandwidth left over from the symbol rate and samples per symbol + float alpha = ((float)channelBandwidth / (float)(symbolRate * samplesPerSymbol)) - 1.0f; + + float[] taps = FilterFactory.getRootRaisedCosine(samplesPerSymbol * channels, symbolCount, alpha); + + int channelCenter = (int)(12500 * 1.5); + + StringBuilder sb = new StringBuilder(); + sb.append("\nPolyphase Channelizer\n"); + sb.append("Sample Rate:" + sampleRate + " Channels:" + channels + " Channel Rate:" + channelBandwidth + "\n"); + sb.append("Alpha: " + alpha + " Tap Count:" + taps.length + "\n"); + sb.append("Channel:" + channelCenter); + + mLog.debug(sb.toString()); + + PolyphaseChannelizer2 channelizer = new PolyphaseChannelizer2(taps, 8); + + Oscillator oscillator = new Oscillator(channelCenter, 2 * sampleRate); + + for(int x = 0; x < 2000; x++) + { + channelizer.filter(oscillator.getComplex().inphase(), oscillator.getComplex().quadrature()); + oscillator.rotate(); + } + + mLog.debug("Finished!"); + } +} diff --git a/src/dsp/mixer/LowPhaseNoiseOscillator.java b/src/dsp/mixer/LowPhaseNoiseOscillator.java index fcbbf0c5..ad190e2d 100644 --- a/src/dsp/mixer/LowPhaseNoiseOscillator.java +++ b/src/dsp/mixer/LowPhaseNoiseOscillator.java @@ -20,6 +20,7 @@ package dsp.mixer; import org.slf4j.Logger; import org.slf4j.LoggerFactory; +import sample.complex.ComplexBuffer; import java.util.Arrays; @@ -111,16 +112,57 @@ public class LowPhaseNoiseOscillator public static void main(String[] args) { - LowPhaseNoiseOscillator o = new LowPhaseNoiseOscillator(10, 1); + int sampleRate = 10000000; + int frequency = 24000; + int sampleSize = 65536; + int warmUpIterations = 100000; + int testIterations = 500000; - float[] samples = o.generate(20); + LowPhaseNoiseOscillator o = new LowPhaseNoiseOscillator(sampleRate, frequency); - StringBuilder sb = new StringBuilder(); - for(int x = 0; x < samples.length; x += 2) + mLog.debug("Warming up ..."); + + for(int x = 0; x < warmUpIterations; x++) { - sb.append(samples[x]).append(" ").append(samples[x + 1]).append("\n"); + float[] samples = o.generate(sampleSize); } - mLog.debug("Samples:\n" + sb.toString()); + 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"); + } } From fd748fec793199c0a8826a76cdc0a0da7e5624e5 Mon Sep 17 00:00:00 2001 From: Denny Date: Mon, 20 Feb 2017 20:16:38 -0500 Subject: [PATCH 05/87] [WIP] Using different filters --- .../filter/channelizer/ChannelizerViewer.java | 44 ++++++++++++++--- .../channelizer/PolyphaseChannelizer2.java | 2 +- src/dsp/filter/design/FilterViewer.java | 47 +++++++++---------- 3 files changed, 62 insertions(+), 31 deletions(-) diff --git a/src/dsp/filter/channelizer/ChannelizerViewer.java b/src/dsp/filter/channelizer/ChannelizerViewer.java index 45bba720..9ad53ca1 100644 --- a/src/dsp/filter/channelizer/ChannelizerViewer.java +++ b/src/dsp/filter/channelizer/ChannelizerViewer.java @@ -19,6 +19,9 @@ package dsp.filter.channelizer; import dsp.filter.FilterFactory; +import dsp.filter.design.FilterDesignException; +import dsp.filter.fir.FIRFilterSpecification; +import dsp.filter.fir.remez.RemezFIRFilterDesigner; import dsp.mixer.LowPhaseNoiseOscillator; import dsp.mixer.Oscillator; import net.miginfocom.swing.MigLayout; @@ -65,7 +68,7 @@ public class ChannelizerViewer extends JFrame { mChannelCount = channelCount; mSampleRate = mChannelCount * CHANNEL_BANDWIDTH; - mToneFrequency = (int)(CHANNEL_BANDWIDTH * 3); //Set to channel 1 as default + mToneFrequency = (int)(CHANNEL_BANDWIDTH * 1); //Set to channel 1 as default init(); } @@ -143,7 +146,7 @@ public class ChannelizerViewer extends JFrame int maximumFrequency = mSampleRate / 2; SpinnerNumberModel model = new SpinnerNumberModel(mToneFrequency, -maximumFrequency, maximumFrequency, - 500 ); + 100 ); model.addChangeListener(new ChangeListener() { @@ -186,9 +189,38 @@ public class ChannelizerViewer extends JFrame int symbolCount = 14; //Alpha is the residual channel bandwidth left over from the symbol rate and samples per symbol - float alpha = ((float)CHANNEL_BANDWIDTH / (float)(symbolRate * samplesPerSymbol)) - 1.0f; +// float alpha = ((float)CHANNEL_BANDWIDTH / (float)(symbolRate * samplesPerSymbol)) - 1.0f; + + float alpha = 0.8f; + +// float[] taps = FilterFactory.getRootRaisedCosine(samplesPerSymbol * mChannelCount, symbolCount, alpha); + float[] taps = null; + + FIRFilterSpecification specification = FIRFilterSpecification.lowPassBuilder() + .sampleRate(CHANNEL_BANDWIDTH * 2 * mChannelCount) + .gridDensity(16) + .passBandAmplitude(1.0) + .passBandCutoff(11900) + .passBandRipple(0.01) + .stopBandAmplitude(0.0) + .stopBandRipple(0.001) + .stopBandStart(13100) + .build(); + + try + { + RemezFIRFilterDesigner designer = new RemezFIRFilterDesigner(specification); + + if(designer.isValid()) + { + taps = designer.getImpulseResponse(); + } + } + catch(FilterDesignException fde) + { + mLog.error("Filter design error", fde); + } - float[] taps = FilterFactory.getRootRaisedCosine(samplesPerSymbol * mChannelCount, symbolCount, alpha); StringBuilder sb = new StringBuilder(); sb.append("\nPolyphase Channelizer\n"); @@ -224,7 +256,7 @@ public class ChannelizerViewer extends JFrame { setLayout(new MigLayout("insets 0 0 0 0", "fill", "fill")); - int channelsPerRow = 32; + int channelsPerRow = 16; for(int x = 0; x < mChannelCount; x++) { @@ -314,7 +346,7 @@ public class ChannelizerViewer extends JFrame public static void main(String[] args) { - int channelCount = 512; + int channelCount = 4; final ChannelizerViewer frame = new ChannelizerViewer(channelCount); diff --git a/src/dsp/filter/channelizer/PolyphaseChannelizer2.java b/src/dsp/filter/channelizer/PolyphaseChannelizer2.java index 1168b89f..a95a8e7e 100644 --- a/src/dsp/filter/channelizer/PolyphaseChannelizer2.java +++ b/src/dsp/filter/channelizer/PolyphaseChannelizer2.java @@ -67,7 +67,7 @@ public class PolyphaseChannelizer2 implements Listener public void filter(float inphase, float quadrature) { - int index = mFilterPointer * 2; + int index = (mChannelCount - mFilterPointer - 1) * 2; mFilteredSamples[index] = mInphaseFilters[mFilterPointer].filter(inphase); mFilteredSamples[index + 1] = mQuadratureFilters[mFilterPointer].filter(quadrature); diff --git a/src/dsp/filter/design/FilterViewer.java b/src/dsp/filter/design/FilterViewer.java index bedc583f..f5d70995 100644 --- a/src/dsp/filter/design/FilterViewer.java +++ b/src/dsp/filter/design/FilterViewer.java @@ -40,16 +40,16 @@ public class FilterViewer extends Application */ private float[] getFilter() { -// FIRFilterSpecification specification = FIRFilterSpecification.lowPassBuilder() -// .sampleRate(4000) -// .gridDensity(16) -// .passBandCutoff(950) -// .passBandAmplitude(1.0) -// .passBandRipple(0.01) -// .stopBandStart(1050) -// .stopBandAmplitude(0.0) -// .stopBandRipple(0.005) -// .build(); + FIRFilterSpecification specification = FIRFilterSpecification.lowPassBuilder() + .sampleRate(100000) + .gridDensity(16) + .passBandAmplitude(1.0) + .passBandCutoff(12000) + .passBandRipple(0.01) + .stopBandAmplitude(0.0) + .stopBandRipple(0.001) + .stopBandStart(13000) + .build(); // FIRFilterSpecification specification = FIRFilterSpecification.highPassBuilder() // .sampleRate( 24000 ) @@ -75,21 +75,20 @@ public class FilterViewer extends Application // taps = FilterFactory.getLowPass( 16000, 2400, 73, WindowType.HANNING ); - taps = FilterFactory.getRootRaisedCosine(2 * 8, 16, (12500.0f / 9600.0f - 1.0f)); -// try -// { -// RemezFIRFilterDesigner designer = new RemezFIRFilterDesigner(specification); -// -// if(designer.isValid()) -// { -// taps = designer.getImpulseResponse(); -// } -// } -// catch(FilterDesignException fde) -// { -// mLog.error("Filter design error", fde); -// } + try + { + RemezFIRFilterDesigner designer = new RemezFIRFilterDesigner(specification); + + if(designer.isValid()) + { + taps = designer.getImpulseResponse(); + } + } + catch(FilterDesignException fde) + { + mLog.error("Filter design error", fde); + } return taps; } From 17c16e16ee829c511b6c51f5c5d61bb9f48815e9 Mon Sep 17 00:00:00 2001 From: Denny Date: Thu, 2 Mar 2017 19:17:22 -0500 Subject: [PATCH 06/87] Adds a new remez designer that uses BigDecimal to handle the larger values inherent in longer length filters. However, this designer is hugely inefficient and takes forever to calculate the filter. More experimentation needed. --- src/dsp/filter/design/FilterViewer.java | 38 +- .../filter/fir/FIRFilterSpecification.java | 172 ++++- .../remez/PolyphaseChannelizerDesigner.java | 37 + .../PolyphaseChannelizerFilterFactory.java | 132 ++++ .../fir/remez/RemezFIRFilterDesigner2.java | 707 ++++++++++++++++++ 5 files changed, 1059 insertions(+), 27 deletions(-) create mode 100644 src/dsp/filter/fir/remez/PolyphaseChannelizerDesigner.java create mode 100644 src/dsp/filter/fir/remez/PolyphaseChannelizerFilterFactory.java create mode 100644 src/dsp/filter/fir/remez/RemezFIRFilterDesigner2.java diff --git a/src/dsp/filter/design/FilterViewer.java b/src/dsp/filter/design/FilterViewer.java index f5d70995..348645b3 100644 --- a/src/dsp/filter/design/FilterViewer.java +++ b/src/dsp/filter/design/FilterViewer.java @@ -40,26 +40,26 @@ public class FilterViewer extends Application */ private float[] getFilter() { - FIRFilterSpecification specification = FIRFilterSpecification.lowPassBuilder() - .sampleRate(100000) - .gridDensity(16) - .passBandAmplitude(1.0) - .passBandCutoff(12000) - .passBandRipple(0.01) - .stopBandAmplitude(0.0) - .stopBandRipple(0.001) - .stopBandStart(13000) - .build(); +// FIRFilterSpecification specification = FIRFilterSpecification.lowPassBuilder() +// .sampleRate(10000) +// .gridDensity(16) +// .passBandAmplitude(1.0) +// .passBandCutoff(4000) +// .passBandRipple(0.01) +// .stopBandAmplitude(0.0) +// .stopBandRipple(0.001) +// .stopBandStart(5000) +// .build(); -// FIRFilterSpecification specification = FIRFilterSpecification.highPassBuilder() -// .sampleRate( 24000 ) -// .stopBandCutoff( 800 ) -// .stopBandAmplitude( 0.0 ) -// .stopBandRipple( 0.03 ) -// .passBandStart( 1000 ) -// .passBandAmplitude( 1.0 ) -// .passBandRipple( 0.08 ) -// .build(); + FIRFilterSpecification specification = FIRFilterSpecification.highPassBuilder() + .sampleRate( 24000 ) + .stopBandCutoff( 800 ) + .stopBandAmplitude( 0.0 ) + .stopBandRipple( 0.03 ) + .passBandStart( 1000 ) + .passBandAmplitude( 1.0 ) + .passBandRipple( 0.08 ) + .build(); // FIRFilterSpecification specification = FIRFilterSpecification.bandPassBuilder() // .sampleRate( 48000 ) diff --git a/src/dsp/filter/fir/FIRFilterSpecification.java b/src/dsp/filter/fir/FIRFilterSpecification.java index b03e82ec..ab599f89 100644 --- a/src/dsp/filter/fir/FIRFilterSpecification.java +++ b/src/dsp/filter/fir/FIRFilterSpecification.java @@ -1,3 +1,21 @@ +/******************************************************************************* + * 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.fir; import java.text.DecimalFormat; @@ -19,14 +37,12 @@ public class FIRFilterSpecification /** * Constructs a Remez filter specification. Use one of the filter builders to construct a Remez * filter specification. - * + * + * @param type of filter * @param order of the filter - * @param frequencyBands containing the start and stop values for each frequency band normalized - * to the sample rate where each frequency value is in the range (0.0 <> 0.5) - * @param amplitudes of each frequency band - * @param weights of the band ripples normalized to the largest ripple value + * @param gridDensity to use when calculating the coefficients */ - private FIRFilterSpecification( FIRLinearPhaseFilterType type, int order, int gridDensity ) + protected FIRFilterSpecification( FIRLinearPhaseFilterType type, int order, int gridDensity ) { mRemezFilterType = type; mOrder = order; @@ -70,6 +86,14 @@ public class FIRFilterSpecification updateGridSize(); } + /** + * Removes all frequency bands from this filter specification + */ + public void clearFrequencyBands() + { + mFrequencyBands.clear(); + } + public List getFrequencyBands() { return mFrequencyBands; @@ -868,7 +892,139 @@ public class FIRFilterSpecification } } - // /** + /** + * Builder for creating polyphase channelizer low-pass prototype filter. + */ + public static class PolyphaseChannelizerBuilder + { + private int mOrder; + private int mGridDensity = 16; + private int mSampleRate; + private int mChannelBandwidth; + private int mPassFrequencyBegin = 0; + private int mPassFrequencyEnd; + private int mStopFrequencyBegin; + private double mStopRipple = 0.001; + private double mPassRipple = 0.01; + private double mStopAmplitude = 0.0; + private double mPassAmplitude = 1.0; + + public PolyphaseChannelizerBuilder() + { + } + + /** + * Sets the filter sample rate in hertz + */ + public PolyphaseChannelizerBuilder sampleRate(int sampleRate ) + { + mSampleRate = sampleRate; + return this; + } + + /** + * Sets the channel bandwidth in hertz + */ + public PolyphaseChannelizerBuilder channelBandwidth(int channelBandwidth) + { + mChannelBandwidth = channelBandwidth; + return this; + } + + /** + * Sets the filter order. If the order is not specified, it will be calculated from the + * other parameters. If you specify and odd-length order, it will be corrected (+1) to make + * it an even order number. + */ + public PolyphaseChannelizerBuilder order(int order ) + { + mOrder = order; + return this; + } + + /** + * Sets the frequency grid density. Default is 16. + */ + public PolyphaseChannelizerBuilder gridDensity(int density ) + { + mGridDensity = density; + return this; + } + + /** + * Specifies the ripple in dB for both stop bands + */ + public PolyphaseChannelizerBuilder stopRipple(double rippleDb ) + { + mStopRipple = rippleDb; + return this; + } + + /** + * Specifies the ripple in dB for the pass band + */ + public PolyphaseChannelizerBuilder passRipple(double rippleDb ) + { + mPassRipple = rippleDb; + return this; + } + + /** + * Specifies the amplitude of the stop bands. Default is 0.0 + */ + public PolyphaseChannelizerBuilder stopAmplitude(double amplitude ) + { + mStopAmplitude = amplitude; + return this; + } + + /** + * Specifies the amplitude of the pass band. Default is 1.0 + */ + public PolyphaseChannelizerBuilder passAmplitude(double amplitude ) + { + mPassAmplitude = amplitude; + return this; + } + + public FIRFilterSpecification build() + { + FIRLinearPhaseFilterType type = FIRLinearPhaseFilterType.TYPE_1_ODD_LENGTH_EVEN_ORDER_SYMMETRICAL; + + if ( mOrder < 10 ) + { + mOrder = estimateBandPassOrder( mSampleRate, mPassFrequencyBegin, mPassFrequencyEnd, + mPassRipple, mStopRipple ); + } + + // Ensure even order since we're designing a Type 1 filter + if ( mOrder % 2 == 1 ) + { + mOrder++; + } + + FIRFilterSpecification spec = new FIRFilterSpecification( type, mOrder, mGridDensity ); + + //Set pass band to stop at about 80% of channel bandwidth as starting point + int passBandStop = (int)(mChannelBandwidth * .80); + + //Set the stop band to start at 40% into the adjacent channel, since we're oversampling by 2. + int stopBandStart = (int)(mChannelBandwidth * 1.40); + + FrequencyBand passBand = new FrequencyBand(mSampleRate, 0, passBandStop, 1.0, mPassRipple); + FrequencyBand stopBand = new FrequencyBand(mSampleRate, stopBandStart, (int)(mSampleRate / 2), + 0.0, mStopRipple); + + spec.addFrequencyBand(passBand); + spec.addFrequencyBand(stopBand); + + return spec; + } + } + + + + // /** // * Converts the set of band amplitude values into a set of weights // * normalized to the largest band amplitude value // */ @@ -918,7 +1074,7 @@ public class FIRFilterSpecification * * @return estimated filter length */ - private static int estimateFilterOrder( int sampleRate, int frequency1, int frequency2, + public static int estimateFilterOrder( int sampleRate, int frequency1, int frequency2, double passBandRipple, double stopBandRipple ) { double df = Math.abs( frequency2 - frequency1 ) / (double) sampleRate; diff --git a/src/dsp/filter/fir/remez/PolyphaseChannelizerDesigner.java b/src/dsp/filter/fir/remez/PolyphaseChannelizerDesigner.java new file mode 100644 index 00000000..f7436798 --- /dev/null +++ b/src/dsp/filter/fir/remez/PolyphaseChannelizerDesigner.java @@ -0,0 +1,37 @@ +/******************************************************************************* + * 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.fir.remez; + +import dsp.filter.fir.FIRFilterSpecification; + +public class PolyphaseChannelizerDesigner extends FIRFilterSpecification +{ + /** + * Constructs a Polyphase Channelizer Remez filter specification. + * + * Constructor is package private -- use the PolyphaseChannelizerFilterFactory to create a filter + * + * @param order of the filter + * @param gridDensity to use when calculating the coefficients + */ + PolyphaseChannelizerDesigner(int order, int gridDensity) + { + super(FIRLinearPhaseFilterType.TYPE_1_ODD_LENGTH_EVEN_ORDER_SYMMETRICAL, order, gridDensity); + } +} diff --git a/src/dsp/filter/fir/remez/PolyphaseChannelizerFilterFactory.java b/src/dsp/filter/fir/remez/PolyphaseChannelizerFilterFactory.java new file mode 100644 index 00000000..04417fa3 --- /dev/null +++ b/src/dsp/filter/fir/remez/PolyphaseChannelizerFilterFactory.java @@ -0,0 +1,132 @@ +/******************************************************************************* + * 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.fir.remez; + +import dsp.filter.fir.FIRFilterSpecification; +import org.slf4j.Logger; +import org.slf4j.LoggerFactory; + +public class PolyphaseChannelizerFilterFactory +{ + private final static Logger mLog = LoggerFactory.getLogger(PolyphaseChannelizerFilterFactory.class); + + private static final double OBJECTIVE_BAND_EDGE_COEFFICIENT_AMPLITUDE = Math.sqrt(2.0) / 2.0; //.707xxx + + public static float[] getFilter(int sampleRate, int channelBandwidth) + { + FIRLinearPhaseFilterType type = FIRLinearPhaseFilterType.TYPE_1_ODD_LENGTH_EVEN_ORDER_SYMMETRICAL; + + int passBandStart = 0; + int gridDensity = 32; + + //Set pass band to stop at about 80% of channel bandwidth as starting point +// int passBandStop = (int)(channelBandwidth * 0.95); + int passBandStop = channelBandwidth; + + //Set the stop band to start at 40% into the adjacent channel, since we're oversampling by 2. + int stopBandStart = (int)(channelBandwidth * 1.40); + + double passRipple = 0.1d; + double stopRipple = 0.01d; + +// int order = FIRFilterSpecification.estimateBandPassOrder(sampleRate, passBandStart, passBandStop, +// passRipple, stopRipple); + int order = FIRFilterSpecification.estimateFilterOrder( sampleRate, passBandStop, + stopBandStart, passRipple, stopRipple ); + + // Ensure even order since we're designing a Type 1 filter + if(order % 2 == 1) + { + order++; + } + + FIRFilterSpecification specification = new PolyphaseChannelizerDesigner(order, gridDensity); + + FIRFilterSpecification.FrequencyBand passBand = new FIRFilterSpecification.FrequencyBand(sampleRate, 0, + passBandStop, 1.0, passRipple); + FIRFilterSpecification.FrequencyBand stopBand = new FIRFilterSpecification.FrequencyBand(sampleRate, + stopBandStart, (int)(sampleRate / 2), 0.0, stopRipple); + + specification.addFrequencyBand(passBand); + specification.addFrequencyBand(stopBand); + + boolean complete = false; + int stepSize = 100; + double bandEdgeFrequency = Math.cos(Math.PI * (double)channelBandwidth / (double)(sampleRate / 2)); + float[] filter = null; + + while(!complete) + { + mLog.debug("Step Size: " + stepSize); + + RemezFIRFilterDesigner2 designer = new RemezFIRFilterDesigner2(specification); + + complete = true; //temporary + + double bandEdgeAmplitude = designer.getFrequencyResponse(bandEdgeFrequency); + mLog.debug("Coefficient Amplitude at Band Edge is: " + bandEdgeAmplitude + " for frequency: " + passBandStop); + + if(bandEdgeAmplitude < OBJECTIVE_BAND_EDGE_COEFFICIENT_AMPLITUDE) + { + if(stepSize <= 1) + { + complete = true; + try + { + filter = designer.getImpulseResponse(); + } + catch(Exception e) + { + mLog.debug("Error getting filter response", e); + } + } + else + { + passBandStop += stepSize; + stepSize /= 10; + } + } + else + { + passBandStop -= stepSize; + } + + if(!complete) + { + specification.clearFrequencyBands(); + + passBand = new FIRFilterSpecification.FrequencyBand(sampleRate, 0, + passBandStop, 1.0, passRipple); + specification.addFrequencyBand(passBand); + specification.addFrequencyBand(stopBand); + } + } + + return filter; + } + + public static void main(String[] args) + { + mLog.debug("Starting ..."); + + float[] filter = getFilter(2500000, 12500); + + mLog.debug("Finished ..."); + } +} diff --git a/src/dsp/filter/fir/remez/RemezFIRFilterDesigner2.java b/src/dsp/filter/fir/remez/RemezFIRFilterDesigner2.java new file mode 100644 index 00000000..2890d939 --- /dev/null +++ b/src/dsp/filter/fir/remez/RemezFIRFilterDesigner2.java @@ -0,0 +1,707 @@ +/******************************************************************************* + * 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.fir.remez; + +import dsp.filter.design.FilterDesignException; +import dsp.filter.fir.FIRFilterSpecification; +import org.slf4j.Logger; +import org.slf4j.LoggerFactory; + +import java.math.BigDecimal; +import java.math.RoundingMode; +import java.util.ArrayList; +import java.util.Iterator; +import java.util.List; + +public class RemezFIRFilterDesigner2 +{ + private final static Logger mLog = LoggerFactory.getLogger(RemezFIRFilterDesigner2.class); + + private static final double CONVERGENCE_THRESHOLD = 0.0001; + public static final int MAXIMUM_ITERATION_COUNT = 40; + public static final double TWO_PI = 2.0 * Math.PI; + + private FIRFilterSpecification mSpecification; + private Grid mGrid; + private List mExtremalIndices; + + private BigDecimal[] mD; + private double[] mGridErrors; + private double[] mGridFrequencyResponse; + private double[] mIdealFrequencyResponse; + + private double mDelta; + + private boolean mConverged; + + /** + * Linear FIR filter designer using the Parks-McClelland Remez exchange algorithm. + * + * Construct a filter specification using the builder methods in the FIRFilterSpecification + * class and construct an instance of this class with the specification. Check the isValid() + * method after construction to see if the filter design was successful. If successful, use the + * getFrequencyResponse() and getImpulseResponse() methods to access the filter taps as the + * specified length. + * + * If the filter cannot be designed from the specification the isValid() method will return + * false and any attempts to access the getFrequencyResponse() or getImpulseResponse() methods + * will throw a FilterDesignException. + * + * @param specification that defines a Type 1-4 linear phase FIR Filter + */ + public RemezFIRFilterDesigner2(FIRFilterSpecification specification) + { + mSpecification = specification; + + design(); + } + + /** + * Designs the filter according to the specification + */ + private void design() + { + mLog.debug("Designing filter ..."); + mGrid = new Grid(mSpecification); + mExtremalIndices = getInitialExtremalIndices(); + + int iterationCount = 0; + + try + { + do + { + mLog.debug("Calculating grid frequency response"); + calculateGridFrequencyResponse(); + mLog.debug("Calculating grid error"); + calculateGridError(); + mLog.debug("Finding extremal indices"); + findExtremalIndices(); + mLog.debug("Checking Convergence"); + checkConvergence(); + + iterationCount++; + + mLog.debug("Iteration: " + iterationCount); + } + while(!mConverged && iterationCount < MAXIMUM_ITERATION_COUNT); + } + catch(FilterDesignException fde) + { + mLog.error("Filter design error - couldn't find extremal indices at count [" + + iterationCount + "] try changing filter order up/down from [" + + mSpecification.getOrder() + "]"); + + mConverged = false; + } + + if(mConverged) + { + //Update frequency response using the final set of extremal indices + calculateGridFrequencyResponse(); + } + } + + /** + * Indicates if the filter was successfully created from the filter specification. Check this + * method before accessing frequency response or filter response, otherwise a FilterDesignException + * is thrown. + * + * @return true if the filter was successfully designed. + */ + public boolean isValid() + { + return mConverged; + } + + /** + * Frequency response of the designed filter with double precision + * + * @return half ( length / 2 ) of filter frequency response sampled at 0.0 - PI radians + * @throws FilterDesignException if the specified filter cannot be designed + */ + public double[] getFrequencyResponse() throws FilterDesignException + { + if(!mConverged) + { + throw new FilterDesignException("Can't create filter from specification - failed " + + "to converge"); + } + + //Resample the final polynomial at the desired filter length + double[] resampled = resample(); + + //Apply Parks/McClelland frequency response corrections according to the filter type + return correctFrequencyResponse(resampled); + } + + /** + * Impulse response of the designed filter with floating point precision. + * + * @return filter impulse response + * @throws FilterDesignException if the specified filter cannot be designed + */ + public float[] getImpulseResponse() throws FilterDesignException + { + return convertToFloatArray(getImpulseResponseDoubles()); + } + + /** + * Impulse response of the designed filter with double precision + */ + public double[] getImpulseResponseDoubles() throws FilterDesignException + { + double[] frequencyResponse = getFrequencyResponse(); + + int length = mSpecification.getFilterLength(); + + double[] impulseResponse = new double[length]; + + double M; + + switch(mSpecification.getFilterType()) + { + case TYPE_1_ODD_LENGTH_EVEN_ORDER_SYMMETRICAL: + M = ((double)length - 1.0) / 2.0; + + for(int n = 0; n < length; n++) + { + double accumulator = frequencyResponse[0]; + + double frequency = TWO_PI * (n - M) / length; + + for(int k = 1; k <= M; k++) + { + accumulator += 2.0 * frequencyResponse[k] * Math.cos(frequency * (double)k); + } + + impulseResponse[n] = accumulator / (double)length; + } + break; + case TYPE_2_EVEN_LENGTH_ODD_ORDER_SYMMETRICAL: + double offset = (double)(length - 1) / 2.0; + + for(int n = 0; n < length; n++) + { + double accumulator = frequencyResponse[0]; + + double frequency = TWO_PI * ((double)n - offset) / (double)length; + + for(int k = 1; k < frequencyResponse.length; k++) + { + accumulator += 2.0 * frequencyResponse[k] * Math.cos(frequency * (double)k); + } + + impulseResponse[n] = accumulator / (double)length; + } + break; + } + + return impulseResponse; + } + + /** + * Calculates the frequency response of this filter at the specified cosine of the frequency + * + * Implements Oppenheim/Schafer Discrete Time Signal Processing, 3e, 2016, equation 116a to + * determine the actual response of the specified frequency cosine using Lagrange + * interpolation of the polynomial formed at the extremal index set. + * + * @param cosineOfFrequency to calculate the frequency response + * @return actual frequency response for the specified frequency with the current set of + * extreaml indices + */ + public double getFrequencyResponse(double cosineOfFrequency) + { + BigDecimal numerator = BigDecimal.ZERO; + BigDecimal denominator = BigDecimal.ZERO; + + for(int k = 0; k < mExtremalIndices.size() - 1; k++) + { + BigDecimal cosineDelta = BigDecimal.valueOf(cosineOfFrequency - + mGrid.getCosineFrequencyGrid()[mExtremalIndices.get(k)]); + + //If this frequency is close to one of the polynomial points, use c[k] for that point + if(cosineDelta.abs().compareTo(BigDecimal.valueOf(1.0e-7)) < 0) + { + numerator = BigDecimal.valueOf(mIdealFrequencyResponse[k]); + denominator = BigDecimal.ONE; + break; + } + else + { + BigDecimal dkOverCosineDelta = mD[k].divide(cosineDelta, 25, RoundingMode.HALF_UP); + + numerator = numerator.add(dkOverCosineDelta.multiply(BigDecimal.valueOf(mIdealFrequencyResponse[k]))); + denominator = denominator.add(dkOverCosineDelta); + } + } + + return numerator.divide(denominator, 25, RoundingMode.HALF_UP).doubleValue(); + } + + /** + * Calculated Delta value + * + * @return delta value + */ + public double getDelta() + { + return mDelta; + } + + /** + * Creates initial array of extremal frequency indices spaced at intervals of the grid frequency + * set for all frequencies where a frequency band weighting is specified. + */ + private List getInitialExtremalIndices() + { + int count = mSpecification.getExtremaCount(); + + int density = mSpecification.getGridDensity(); + + List extremalIndices = new ArrayList<>(); + + for(int i = 0; i < count; i++) + { + extremalIndices.add(i * density); + } + + return extremalIndices; + } + + /** + * Calculates the actual frequency response of a dense frequency grid using the polynomial + * represented by the extremal indices. + */ + private void calculateGridFrequencyResponse() + { + mLog.debug("\tCalculating B"); + BigDecimal[] b = calculateB(); + + mLog.debug("\tCalculating Delta"); + calculateDelta(b); + + mLog.debug("\tCalculating C"); + calculateC(); + + mLog.debug("\tCalculating D"); + calculateD(b); + + mLog.debug("\tUpdating Grid Frequency Response"); + updateGridFrequencyResponse(); + } + + /** + * Updates the frequency response of frequencies specified in the dense frequency grid + */ + private void updateGridFrequencyResponse() + { + mGridFrequencyResponse = new double[mGrid.getSize()]; + + double[] gridFrequencyCosines = mGrid.getCosineFrequencyGrid(); + + for(int i = 0; i < mGridFrequencyResponse.length; i++) + { + //TODO: parallelize this part!!!! + mGridFrequencyResponse[i] = getFrequencyResponse(gridFrequencyCosines[i]); + + if(i % 1000 == 0) + { + mLog.debug("\t\tUpdating Frequency Response - index:" + i + " of " + + mGridFrequencyResponse.length + " Value:" + mGridFrequencyResponse[i]); + } + } + } + + /** + * Calculates the value of b across the set of extremal indices. + * + * Implements Oppenheim/Schafer Discrete Time Signal Processing, 3e, 2016, equation 115 + * + * @return the array of b values of length L+2 + */ + private BigDecimal[] calculateB() + { + int length = mExtremalIndices.size(); + + BigDecimal[] b = new BigDecimal[length]; + + for(int k = 0; k < length; k++) + { + b[k] = BigDecimal.ONE; + + double xk = mGrid.getCosineFrequencyGrid()[mExtremalIndices.get(k)]; + + for(int i = 0; i < length; i++) + { + if(i != k) + { + double xi = mGrid.getCosineFrequencyGrid()[mExtremalIndices.get(i)]; + + double denominator = xk - xi; + + if(Math.abs(denominator) < 0.00001) + { + denominator = 0.00001; + } + + b[k] = b[k].multiply(BigDecimal.valueOf(1.0 / denominator)); + } + } + } + + return b; + } + + /** + * Calculates the value of delta that represents the maximum ripple for the current set of + * extremal indices (L+2) + * + * Implements Oppenheim/Schafer Discrete Time Signal Processing, 3e, 2016, equation 114 + * + * @param b calculated for the grid from equation 115 + */ + private void calculateDelta(BigDecimal[] b) + { + BigDecimal numerator = BigDecimal.ZERO; + BigDecimal denominator = BigDecimal.ZERO; + + double sign = 1.0; + + for(int k = 0; k < b.length; k++) + { + if(k < mExtremalIndices.size()) + { + int extremalIndex = mExtremalIndices.get(k); + + numerator = numerator.add(b[k].multiply(BigDecimal.valueOf(mGrid.getDesiredResponse()[extremalIndex]))); + denominator = denominator.add(b[k].multiply(BigDecimal.valueOf(sign / mGrid.getWeight()[extremalIndex]))); + sign = -sign; + } + else + { + mLog.error("Something went wrong -- the length of b exceeds the set of extremal indices"); + } + } + + mDelta = numerator.divide(denominator, 1023, BigDecimal.ROUND_HALF_DOWN).doubleValue(); + } + + /** + * Calculates the ideal frequency response (C) of the current extremal index set. This reference + * is used to determine the level of error in the set of actual frequency responses calculated + * from this set of extremal indices to determine convergence. + * + * Implements Oppenheim/Schafer Discrete Time Signal Processing, 3e, 2016, equation 116b + */ + private void calculateC() + { + int length = mSpecification.getExtremaCount() - 1; + + mIdealFrequencyResponse = new double[length]; + + double sign = 1.0; + + for(int k = 0; k < length; k++) + { + if(k < mExtremalIndices.size()) + { + int index = mExtremalIndices.get(k); + + mIdealFrequencyResponse[k] = mGrid.getDesiredResponse()[index] - + (sign * mDelta / mGrid.getWeight()[index]); + + sign = -sign; + } + } + } + + /** + * Calculates the set of D values for the current extremal index set. + * + * Implements Oppenheim/Schafer Discrete Time Signal Processing, 3e, 2016, equation 116c + */ + private void calculateD(BigDecimal[] b) + { + int length = mExtremalIndices.size() - 1; + + mD = new BigDecimal[length]; + + for(int k = 0; k < length; k++) + { + //Note: extremalCosines array is one index longer than d array, so we use length as + //a pointer to the final (L+2) extremalCosine index + mD[k] = b[k].multiply(BigDecimal.valueOf((mGrid.getCosineFrequencyGrid()[mExtremalIndices.get(k)] - + mGrid.getCosineFrequencyGrid()[mExtremalIndices.get(length)]))); + } + } + + /** + * Calculates the weighted error between the specification desired frequency response and the + * actual frequency response of the current extremal index set across the frequencies in the + * dense frequency grid. + * + * Implements Oppenheim/Schafer Discrete Time Signal Processing, 3e, 2016, equation 112 + */ + public void calculateGridError() + { + int length = mGridFrequencyResponse.length; + + mGridErrors = new double[length]; + + for(int i = 0; i < length; i++) + { + mGridErrors[i] = mGrid.getWeight()[i] * + (mGrid.getDesiredResponse()[i] - mGridFrequencyResponse[i]); + } + } + + /** + * Identifies the indexes of the extremal error values using the Alternation theorem + * + * @throws FilterDesignException if the full set of extremal indices can't be identified + */ + private void findExtremalIndices() throws FilterDesignException + { + mExtremalIndices.clear(); + + //Check for extremum at error index 0 + if(((mGridErrors[0] > 0.0 && mGridErrors[0] > mGridErrors[1]) || + (mGridErrors[0] < 0.0 && mGridErrors[0] < mGridErrors[1])) && + isGTEDelta(mGridErrors[0])) + { + mExtremalIndices.add(0); + } + + //Check for extrema in middle error indices ) + for(int x = 1; x < mGridErrors.length - 1; x++) + { + if(((mGridErrors[x] > 0.0 && (mGridErrors[x - 1] <= mGridErrors[x] && mGridErrors[x] > mGridErrors[x + 1])) || + (mGridErrors[x] < 0.0 && (mGridErrors[x - 1] >= mGridErrors[x] && mGridErrors[x] < mGridErrors[x + 1]))) && + isGTEDelta(mGridErrors[x])) + { + mExtremalIndices.add(x); + } + } + + //Check for extremum at final error index + int last = mGridErrors.length - 1; + + if(((mGridErrors[last] > 0.0 && (mGridErrors[last] > mGridErrors[last - 1])) || + (mGridErrors[last] < 0.0 && (mGridErrors[last] < mGridErrors[last - 1]))) && + isGTEDelta(mGridErrors[last])) + { + mExtremalIndices.add(last); + } + + //Ensure we have the minimum of extremals before we continue + if(mExtremalIndices.size() < mSpecification.getExtremaCount()) + { + throw new FilterDesignException("Couldn't find the minimum extremal frequencies in " + + "error set"); + } + + //Enforce alternation theory -- only one extremal for each transition about the zero axis + List indicesToRemove = new ArrayList<>(); + + Iterator it = mExtremalIndices.iterator(); + Integer current = it.next(); + Integer next; + + boolean positiveAxis = mGridErrors[current] > 0.0; + + while(it.hasNext()) + { + next = it.next(); + + //Check for consecutive (redundant) indices on same side of zero axis - retain largest + if(!(positiveAxis ^ (mGridErrors[next] > 0.0))) + { + if(Math.abs(mGridErrors[next]) <= Math.abs(mGridErrors[current])) + { + //Remove next error index that is less than current index and on + it.remove(); + next = current; + } + else + { + //Since we can't remove the current index with the iterator, queue for later removal + indicesToRemove.add(current); + } + } + else + { + positiveAxis = !positiveAxis; + } + + current = next; + } + + //Remove redundant extremal indices that couldn't be removed via the iterator + mExtremalIndices.removeAll(indicesToRemove); + + //Truncate the list to one larger than needed by removing excess tailing indices + while(mExtremalIndices.size() > mSpecification.getExtremaCount()) + { + mExtremalIndices.remove(mExtremalIndices.size() - 1); + } + + //If we have one too many indices, delete the smaller of the first or last + if(mExtremalIndices.size() > mSpecification.getExtremaCount()) + { + int lastIndex = mExtremalIndices.size() - 1; + + if(Math.abs(mGridErrors[mExtremalIndices.get(0)]) > + Math.abs(mGridErrors[mExtremalIndices.get(lastIndex)])) + { + mExtremalIndices.remove(lastIndex); + } + else + { + mExtremalIndices.remove(0); + } + } + + //Detect if we have too few indices + if(mExtremalIndices.size() < mSpecification.getExtremaCount()) + { + throw new FilterDesignException("Couldn't find the minimum extremal frequencies in " + + "error set"); + } + } + + /** + * Indicates if the absolute value of the argument is greater than or equal to the delta + * value with accuracy to 14 digits of precision. This avoids rounding errors at 15 digits or + * precision or greater. + * + * @param value to evaluate + * @return true if the absolute value is greater than or equal to the absolute delta value + */ + private boolean isGTEDelta(double value) + { + return Math.abs(value) - Math.abs(mDelta) > -1.0e-5; + } + + /** + * Checks for convergence of the frequency response of the current set of extremal indices to + * the filter specification by comparing the maximum absolute error value against the delta + * value to determine if these two values are within the convergence threshold. + */ + public void checkConvergence() + { + double maximum = Math.abs(mGridErrors[mExtremalIndices.get(0)]); + + for(int i = 1; i < mExtremalIndices.size(); i++) + { + double current = Math.abs(mGridErrors[mExtremalIndices.get(i)]); + + if(current > maximum) + { + maximum = current; + } + } + + double convergence = maximum - Math.abs(mDelta); + + mConverged = convergence < CONVERGENCE_THRESHOLD; + } + + /** + * Coverts/casts the double array to a float array + */ + private static float[] convertToFloatArray(double[] samples) + { + float[] converted = new float[samples.length]; + + for(int x = 0; x < samples.length; x++) + { + converted[x] = (float)samples[x]; + } + + return converted; + } + + /** + * Resamples the designed filter at half of the filter length at evenly spaced intervals of the + * frequency spectrum from 0 to PI radians. + * + * @return frequency response of the filter sampled the desired filter length and order + */ + private double[] resample() + { + int length = mSpecification.getFilterLength(); + + if(length % 2 == 0) + { + length--; + } + + double half = (double)length / 2.0; + + double[] resampled = new double[(int)Math.ceil(half)]; + + for(int x = 0; x < resampled.length; x++) + { + resampled[x] = getFrequencyResponse(Math.cos(Math.PI * (double)x / half)); + } + + return resampled; + } + + /** + * Applies Parks/McClelland filter type correction to the final frequency response array + * + * @param frequencyResponse of the resampled filter design from 0 <> ~ PI radians + * @return corrected frequency response + */ + private double[] correctFrequencyResponse(double[] frequencyResponse) + { + double filterLength = mSpecification.getFilterLength(); + + switch(mSpecification.getFilterType()) + { + case TYPE_1_ODD_LENGTH_EVEN_ORDER_SYMMETRICAL: + //No correction needed + break; + case TYPE_2_EVEN_LENGTH_ODD_ORDER_SYMMETRICAL: + //No correction needed + break; + case TYPE_3_ODD_LENGTH_EVEN_ORDER_ANTI_SYMMETRICAL: + for(int x = 0; x < frequencyResponse.length; x++) + { + double frequencyRadians = Math.PI * (((double)x) / filterLength); + frequencyResponse[x] *= Math.sin(TWO_PI * frequencyRadians); + } + break; + case TYPE_4_EVEN_LENGTH_ODD_ORDER_ANTI_SYMMETRICAL: + for(int x = 0; x < frequencyResponse.length; x++) + { + double frequencyRadians = Math.PI * (((double)x) / filterLength); + frequencyResponse[x] *= Math.sin(Math.PI * frequencyRadians); + } + break; + default: + break; + } + + return frequencyResponse; + } +} From e2fe06638ba3237903ec11ed2c03a455f0cde960 Mon Sep 17 00:00:00 2001 From: Denny Date: Sun, 5 Mar 2017 05:55:04 -0500 Subject: [PATCH 07/87] Incrementing build to version 0.3.0 beta8 --- .idea/libraries/commons_math3_3_6_1.xml | 9 + sdrtrunk.iml | 1 + src/dsp/filter/design/FilterViewer.java | 48 +- .../filter/fir/FIRFilterSpecification.java | 1990 ++++++++--------- .../PolyphaseChannelizerFilterFactory.java | 89 +- .../fir/remez/RemezFIRFilterDesigner.java | 9 +- .../fir/remez/RemezFIRFilterDesigner2.java | 19 +- .../RemezFIRFilterDesignerWithLagrange.java | 686 ++++++ 8 files changed, 1664 insertions(+), 1187 deletions(-) create mode 100644 .idea/libraries/commons_math3_3_6_1.xml create mode 100644 src/dsp/filter/fir/remez/RemezFIRFilterDesignerWithLagrange.java diff --git a/.idea/libraries/commons_math3_3_6_1.xml b/.idea/libraries/commons_math3_3_6_1.xml new file mode 100644 index 00000000..b692344c --- /dev/null +++ b/.idea/libraries/commons_math3_3_6_1.xml @@ -0,0 +1,9 @@ + + + + + + + + + \ No newline at end of file diff --git a/sdrtrunk.iml b/sdrtrunk.iml index 04cc0636..0621246d 100644 --- a/sdrtrunk.iml +++ b/sdrtrunk.iml @@ -21,5 +21,6 @@ + \ No newline at end of file diff --git a/src/dsp/filter/design/FilterViewer.java b/src/dsp/filter/design/FilterViewer.java index 348645b3..9bdf1ac0 100644 --- a/src/dsp/filter/design/FilterViewer.java +++ b/src/dsp/filter/design/FilterViewer.java @@ -2,7 +2,9 @@ package dsp.filter.design; import dsp.filter.FilterFactory; import dsp.filter.fir.FIRFilterSpecification; +import dsp.filter.fir.remez.PolyphaseChannelizerFilterFactory; import dsp.filter.fir.remez.RemezFIRFilterDesigner; +import dsp.filter.fir.remez.RemezFIRFilterDesignerWithLagrange; import javafx.application.Application; import javafx.scene.Scene; import javafx.stage.Stage; @@ -51,15 +53,15 @@ public class FilterViewer extends Application // .stopBandStart(5000) // .build(); - FIRFilterSpecification specification = FIRFilterSpecification.highPassBuilder() - .sampleRate( 24000 ) - .stopBandCutoff( 800 ) - .stopBandAmplitude( 0.0 ) - .stopBandRipple( 0.03 ) - .passBandStart( 1000 ) - .passBandAmplitude( 1.0 ) - .passBandRipple( 0.08 ) - .build(); +// FIRFilterSpecification specification = FIRFilterSpecification.highPassBuilder() +// .sampleRate( 24000 ) +// .stopBandCutoff( 800 ) +// .stopBandAmplitude( 0.0 ) +// .stopBandRipple( 0.03 ) +// .passBandStart( 1000 ) +// .passBandAmplitude( 1.0 ) +// .passBandRipple( 0.08 ) +// .build(); // FIRFilterSpecification specification = FIRFilterSpecification.bandPassBuilder() // .sampleRate( 48000 ) @@ -76,20 +78,22 @@ public class FilterViewer extends Application // taps = FilterFactory.getLowPass( 16000, 2400, 73, WindowType.HANNING ); - try - { - RemezFIRFilterDesigner designer = new RemezFIRFilterDesigner(specification); - - if(designer.isValid()) - { - taps = designer.getImpulseResponse(); - } - } - catch(FilterDesignException fde) - { - mLog.error("Filter design error", fde); - } +// try +// { +// RemezFIRFilterDesignerWithLagrange designer = new RemezFIRFilterDesignerWithLagrange(specification); +//// RemezFIRFilterDesigner designer = new RemezFIRFilterDesigner(specification); +// +// if(designer.isValid()) +// { +// taps = designer.getImpulseResponse(); +// } +// } +// catch(FilterDesignException fde) +// { +// mLog.error("Filter design error", fde); +// } + taps = PolyphaseChannelizerFilterFactory.getFilter(10000000, 312500, 0.21); return taps; } } diff --git a/src/dsp/filter/fir/FIRFilterSpecification.java b/src/dsp/filter/fir/FIRFilterSpecification.java index ab599f89..da4aa40a 100644 --- a/src/dsp/filter/fir/FIRFilterSpecification.java +++ b/src/dsp/filter/fir/FIRFilterSpecification.java @@ -18,879 +18,677 @@ ******************************************************************************/ package dsp.filter.fir; +import dsp.filter.fir.remez.FIRLinearPhaseFilterType; + import java.text.DecimalFormat; import java.util.ArrayList; import java.util.Collection; import java.util.List; -import dsp.filter.fir.remez.FIRLinearPhaseFilterType; - public class FIRFilterSpecification { - private static final DecimalFormat DECIMAL_FORMATTER = new DecimalFormat( "0.00000" ); + private static final DecimalFormat DECIMAL_FORMATTER = new DecimalFormat("0.00000"); - private FIRLinearPhaseFilterType mRemezFilterType; - private int mOrder; - private int mGridDensity = 16; - private List mFrequencyBands = new ArrayList<>(); + private FIRLinearPhaseFilterType mRemezFilterType; + private int mOrder; + private int mGridDensity = 16; + private List mFrequencyBands = new ArrayList<>(); - /** - * Constructs a Remez filter specification. Use one of the filter builders to construct a Remez - * filter specification. - * - * @param type of filter - * @param order of the filter - * @param gridDensity to use when calculating the coefficients - */ - protected FIRFilterSpecification( FIRLinearPhaseFilterType type, int order, int gridDensity ) - { - mRemezFilterType = type; - mOrder = order; - mGridDensity = gridDensity; - } + /** + * Constructs a Remez filter specification. Use one of the filter builders to construct a Remez + * filter specification. + * + * @param type of filter + * @param order of the filter + * @param gridDensity to use when calculating the coefficients + */ + protected FIRFilterSpecification(FIRLinearPhaseFilterType type, int order, int gridDensity) + { + mRemezFilterType = type; + mOrder = order; + mGridDensity = gridDensity; + } - public String toString() - { - StringBuilder sb = new StringBuilder(); - sb.append( "Type: " ); - sb.append( mRemezFilterType.name() ); - sb.append( " Order: " ); - sb.append( mOrder ); - sb.append( " Extrema: " ); - sb.append( getExtremaCount() ); - sb.append( " Grid Density: " ); - sb.append( mGridDensity ); - sb.append( " Grid Size: " ); - sb.append( getGridSize() ); + public String toString() + { + StringBuilder sb = new StringBuilder(); + sb.append("Type: "); + sb.append(mRemezFilterType.name()); + sb.append(" Order: "); + sb.append(mOrder); + sb.append(" Extrema: "); + sb.append(getExtremaCount()); + sb.append(" Grid Density: "); + sb.append(mGridDensity); + sb.append(" Grid Size: "); + sb.append(getGridSize()); - for ( FrequencyBand band : mFrequencyBands ) - { - sb.append( "\n" ); - sb.append( band.toString() ); - } + for(FrequencyBand band : mFrequencyBands) + { + sb.append("\n"); + sb.append(band.toString()); + } - return sb.toString(); - } + return sb.toString(); + } - public void addFrequencyBand( FrequencyBand band ) - { - mFrequencyBands.add( band ); + public void addFrequencyBand(FrequencyBand band) + { + mFrequencyBands.add(band); - updateGridSize(); - } + updateGridSize(); + } - public void addFrequencyBands( Collection bands ) - { - mFrequencyBands.addAll( bands ); + public void addFrequencyBands(Collection bands) + { + mFrequencyBands.addAll(bands); - updateGridSize(); - } + updateGridSize(); + } /** * Removes all frequency bands from this filter specification */ - public void clearFrequencyBands() + public void clearFrequencyBands() { mFrequencyBands.clear(); } - public List getFrequencyBands() - { - return mFrequencyBands; - } - - /** - * Returns the total bandwidth of the defined frequency bands in the specification. - * - * @return total bandwidth -- should be less than or equal to 0.5 - */ - public double getTotalBandwidth() - { - double bandwidth = 0.0; - - for ( FrequencyBand band : mFrequencyBands ) - { - bandwidth += band.getBandWidth(); - } - - return bandwidth; - } - - /** - * Creates a Low-Pass FIR filter specification builder that allows you to define the filter - * parameters and create a filter specification. - */ - public static LowPassBuilder lowPassBuilder() - { - return new LowPassBuilder(); - } - - /** - * Creates a High-Pass FIR filter specification builder that allows you to define the filter - * parameters and create a filter specification. - */ - public static HighPassBuilder highPassBuilder() - { - return new HighPassBuilder(); - } - - /** - * Creates a Band-Pass FIR filter specification builder that allows you to define the filter - * parameters and create a filter specification. - */ - public static BandPassBuilder bandPassBuilder() - { - return new BandPassBuilder(); - } - - public FIRLinearPhaseFilterType getFilterType() - { - return mRemezFilterType; - } - - public boolean isSymmetrical() - { - return mRemezFilterType == FIRLinearPhaseFilterType.TYPE_1_ODD_LENGTH_EVEN_ORDER_SYMMETRICAL || - mRemezFilterType == FIRLinearPhaseFilterType.TYPE_2_EVEN_LENGTH_ODD_ORDER_SYMMETRICAL; - } - - public boolean isAntiSymmetrical() - { - return mRemezFilterType == FIRLinearPhaseFilterType.TYPE_3_ODD_LENGTH_EVEN_ORDER_ANTI_SYMMETRICAL || - mRemezFilterType == FIRLinearPhaseFilterType.TYPE_4_EVEN_LENGTH_ODD_ORDER_ANTI_SYMMETRICAL; - } - - public int getOrder() - { - return mOrder; - } - - public boolean isEvenOrder() - { - return mOrder % 2 == 0; - } - - public boolean isOddOrder() - { - return mOrder % 2 == 1; - } - - public int getFilterLength() - { - return mOrder + 1; - } - - public int getBandCount() - { - return mFrequencyBands.size(); - } - - /** - * Grid density setting. Default value is 16. - */ - public int getGridDensity() - { - return mGridDensity; - } - - /** - * Sets the grid density setting. Default value is 16. - */ - public void setGridDensity( int density ) - { - mGridDensity = density; - - updateGridSize(); - } - - /** - * Maximum number of alternations/extrema (L + 2 or M + 2) - */ - public int getExtremaCount() - { - return getHalfFilterOrder() + 2; - } - - /** - * Half of the filter order. - * - * This value is referred to as L or M, depending on the source. - */ - public int getHalfFilterOrder() - { - switch ( mRemezFilterType ) - { - case TYPE_1_ODD_LENGTH_EVEN_ORDER_SYMMETRICAL: - return ( mOrder ) / 2; - case TYPE_2_EVEN_LENGTH_ODD_ORDER_SYMMETRICAL: - return ( mOrder - 1 ) / 2; - case TYPE_3_ODD_LENGTH_EVEN_ORDER_ANTI_SYMMETRICAL: - return ( mOrder - 2 ) / 2; - case TYPE_4_EVEN_LENGTH_ODD_ORDER_ANTI_SYMMETRICAL: - return ( mOrder - 1 ) / 2; - default: - return 0; - } - } - - /** - * Determines the max band ripple from across the frequency bands in this specification. - */ - public double getMaxBandAmplitude() - { - double maxRippleAmplitude = 0.0; - - for ( FrequencyBand band : mFrequencyBands ) - { - double bandRippleAmplitude = band.getRippleAmplitude(); - - if ( bandRippleAmplitude > maxRippleAmplitude ) - { - maxRippleAmplitude = bandRippleAmplitude; - } - } - - return maxRippleAmplitude; - } - - public int getNominalGridSize() - { - return ( getExtremaCount() - 1 ) * getGridDensity() + 1; - } - - public int getGridSize() - { - int gridSize = 0; - - for ( FrequencyBand band : mFrequencyBands ) - { - gridSize += band.getGridSize(); - } - - return gridSize; - } - - private void updateGridSize() - { - if ( !mFrequencyBands.isEmpty() ) - { - int gridSize = getNominalGridSize(); - - double totalBandwidth = getTotalBandwidth(); - - for ( FrequencyBand band : mFrequencyBands ) - { - band.setGridSize( gridSize, totalBandwidth ); - } - } - } - - // /** - // * Determines the grid size needed to represent the percentage of weighted - // frequency bands - // * over the entire frequency band. - // */ - // public int getGridSize() - // { - // int gridSize = 0; - // - // int maxGridSize = getMaxGridSize(); - // - // //Add the proportional index counts from each weighted band - // for( FrequencyBand band: mFrequencyBands ) - // { - // gridSize += band.getBandGridSize( maxGridSize ); - // } - // - // if( getSymmetry() == Symmetry.NEGATIVE ) - // { - // gridSize--; - // } - // - // return gridSize; - // } - - /** - * Dense frequency spacing for extrema count * grid density over a frequency range of 0.0 - 1.0 - */ - public double getGridFrequencyInterval() - { - return getTotalBandwidth() / (double) ( ( getGridSize() - mFrequencyBands.size() ) ); - } - - // /** - // * ''' FIR order estimator (lowpass, highpass, bandpass, mulitiband). - // * - // * (n, fo, ao, w) = remezord (f, a, dev) (n, fo, ao, w) = remezord (f, a, - // * dev, fs) - // * - // * (n, fo, ao, w) = remezord (f, a, dev) finds the approximate order, - // * normalized frequency band edges, frequency band amplitudes, and weights - // * that meet input specifications f, a, and dev, to use with the remez - // * command. - // * - // * f is a sequence of frequency band edges (between 0 and Fs/2, where Fs - // is - // * the sampling frequency), and a is a sequence specifying the desired - // * amplitude on the bands defined by f. The length of f is twice the - // length - // * of a, minus 2. The desired function is piecewise constant. - // * - // * dev is a sequence the same size as a that specifies the maximum - // allowable - // * deviation or ripples between the frequency response and the desired - // * amplitude of the output filter, for each band. - // * - // * Use remez with the resulting order n, frequency sequence fo, amplitude - // * response sequence ao, and weights w to design the filter b which - // * approximately meets the specifications given by remezord input - // parameters - // * f, a, and dev: - // * - // * b = remez (n, fo, ao, w) - // * - // * (n, fo, ao, w) = remezord (f, a, dev, Fs) specifies a sampling - // frequency - // * Fs. - // * - // * Fs defaults to 2 Hz, implying a Nyquist frequency of 1 Hz. You can - // * therefore specify band edges scaled to a particular applications - // sampling - // * frequency. - // * - // * In some cases remezord underestimates the order n. If the filter does - // not - // * meet the specifications, try a higher order such as n+1 or n+2. ''' - // * - // * @param fcuts - // * @param mags - // * @param devs - // * @param fsamp - // */ - // // TODO: rename to calculateFilterOrder() - // public static FIRFilterSpecification remezOrd( int[] fcutoffs, - // double[] mags, double[] devs, int fsamp ) - // { - // // def remezord (fcuts, mags, devs, fsamp = 2): - // - // double[] fcuts = new double[ fcutoffs.length ]; - // - // for ( int x = 0; x < fcuts.length; x++ ) - // { - // fcuts[ x ] = (double) fcutoffs[ x ] / (double) fsamp; - // } - // - // int nf = fcuts.length; - // int nm = mags.length; - // int nd = devs.length; - // int nbands = nm; - // - // if ( mags.length != devs.length ) - // { - // throw new IllegalArgumentException( - // "Length of magnitudes and deviations must be equal" ); - // } - // - // if ( fcuts.length != ( 2 * ( mags.length - 1 ) ) ) - // { - // throw new IllegalArgumentException( - // "Length of frequency band edges must be 2 * mags length - 2" ); - // } - // - // for ( int x = 0; x < mags.length; x++ ) - // { - // if ( mags[ x ] != 0 ) - // { - // devs[ x ] = devs[ x ] / mags[ x ]; - // } - // } - // - // // Separate the passband and stopband edges - // double[] f1 = new double[ fcuts.length / 2 ]; - // double[] f2 = new double[ fcuts.length / 2 ]; - // - // for ( int x = 0; x < fcuts.length / 2; x++ ) - // { - // f1[ x ] = fcuts[ 2 * x ]; - // f2[ x ] = fcuts[ 2 * x + 1 ]; - // } - // - // int n = 0; - // - // double min_delta = 2.0; - // - // for ( int x = 0; x < f1.length; x++ ) - // { - // if ( f2[ x ] - f1[ x ] < min_delta ) - // { - // n = x; - // min_delta = f2[ x ] - f1[ x ]; - // } - // } - // - // double l; - // - // if ( nbands == 2 ) - // { - // // lowpass or highpass case (use formula) - // l = lporder( f1[ n ], f2[ n ], devs[ 0 ], devs[ 1 ] ); - // } else - // { - // // bandpass or multipass case - // // try different lowpasses and take the worst one that - // // goes through the BP specs - // l = 0; - // - // for ( int x = 1; x < nbands - 1; x++ ) - // { - // double l1 = lporder( f1[ x - 1 ], f2[ x - 1 ], devs[ x ], - // devs[ x - 1 ] ); - // double l2 = lporder( f1[ x ], f2[ x ], devs[ x ], - // devs[ x + 1 ] ); - // l = Math.max( l1, l2 ); - // } - // } - // - // n = (int) ( Math.ceil( l ) - 1 ); // need order, not length for remez - // - // // // cook up remez compatible result - // double[] ff = new double[ fcuts.length + 2 ]; - // ff[ 0 ] = 0.0; - // ff[ ff.length - 1 ] = 1.0; - // - // System.arraycopy( fcuts, 0, ff, 1, fcuts.length ); - // - // for ( int x = 1; x < ff.length - 1; x++ ) - // { - // ff[ x ] *= 2; - // } - // - // double[] aa = new double[ mags.length * 2 ]; - // - // for ( int x = 0; x < mags.length; x++ ) - // { - // aa[ 2 * x ] = mags[ x ]; - // aa[ 2 * x + 1 ] = mags[ x ]; - // } - // - // double max_dev = 0; - // - // for ( double dev : devs ) - // { - // if ( dev > max_dev ) - // { - // max_dev = dev; - // } - // } - // - // double[] wts = new double[ devs.length ]; - // - // for ( int x = 0; x < devs.length; x++ ) - // { - // wts[ x ] = max_dev / devs[ x ]; - // } - // - // return null; - // // return new RemezFilterSpecification( RemezFilterType.BANDPASS, n, ff, - // // aa, wts, 16); - // } - - /** - * Low-pass FIR filter specification builder - */ - public static class LowPassBuilder - { - private int mSampleRate; - private int mOrder; - private int mGridDensity = 16; - private int mPassBandEndFrequency; - private int mStopBandStartFrequency; - private double mPassBandRipple; - private double mStopBandRipple; - private double mPassBandAmplitude = 1.0; - private double mStopBandAmplitude = 0.0; - - public LowPassBuilder() - { - } - - public LowPassBuilder sampleRate( int sampleRateHz ) - { - mSampleRate = sampleRateHz; - return this; - } - - /** - * Sets the filter order. If the order is not specified, it will be calculated from the - * other parameters. - */ - public LowPassBuilder order( int order ) - { - mOrder = order; - return this; - } - - public LowPassBuilder gridDensity( int density ) - { - mGridDensity = density; - return this; - } - - public LowPassBuilder passBandCutoff( int frequencyHz ) - { - mPassBandEndFrequency = frequencyHz; - return this; - } - - public LowPassBuilder stopBandStart( int frequencyHz ) - { - mStopBandStartFrequency = frequencyHz; - return this; - } - - public LowPassBuilder passBandRipple( double rippleDb ) - { - mPassBandRipple = rippleDb; - return this; - } - - public LowPassBuilder stopBandRipple( double rippleDb ) - { - mStopBandRipple = rippleDb; - return this; - } - - public LowPassBuilder passBandAmplitude( double amplitude ) - { - mPassBandAmplitude = amplitude; - return this; - } - - public LowPassBuilder stopBandAmplitude( double amplitude ) - { - mStopBandAmplitude = amplitude; - return this; - } - - public FIRFilterSpecification build() - { - if ( mOrder < 6 ) - { - mOrder = estimateFilterOrder( mSampleRate, mPassBandEndFrequency, - mStopBandStartFrequency, mPassBandRipple, mStopBandRipple ); - } - - FIRLinearPhaseFilterType type = ( mOrder % 2 == 0 - ? FIRLinearPhaseFilterType.TYPE_1_ODD_LENGTH_EVEN_ORDER_SYMMETRICAL - : FIRLinearPhaseFilterType.TYPE_2_EVEN_LENGTH_ODD_ORDER_SYMMETRICAL ); - - FIRFilterSpecification spec = new FIRFilterSpecification( type, mOrder, mGridDensity ); - - FrequencyBand passBand = new FrequencyBand( mSampleRate, 0, mPassBandEndFrequency, - mPassBandAmplitude, mPassBandRipple ); - spec.addFrequencyBand( passBand ); - - FrequencyBand stopBand = new FrequencyBand( mSampleRate, mStopBandStartFrequency, - mSampleRate / 2, mStopBandAmplitude, mStopBandRipple ); - spec.addFrequencyBand( stopBand ); - - return spec; - } - } - - /** - * Remez high-pass FIR filter specification builder - */ - public static class HighPassBuilder - { - private int mSampleRate; - private int mOrder = -1; - private int mGridDensity = 16; - private int mPassBandStartFrequency; - private int mStopBandEndFrequency; - private double mPassBandRipple; - private double mStopBandRipple; - private double mPassBandAmplitude = 1.0; - private double mStopBandAmplitude = 0.0; - - public HighPassBuilder() - { - } - - public HighPassBuilder sampleRate( int sampleRateHz ) - { - mSampleRate = sampleRateHz; - return this; - } - - /** - * Sets the filter order. If the order is not specified, it will be calculated from the - * other parameters. - */ - public HighPassBuilder order( int order ) - { - mOrder = order; - return this; - } - - public HighPassBuilder gridDensity( int density ) - { - mGridDensity = density; - return this; - } - - public HighPassBuilder passBandStart( int frequencyHz ) - { - mPassBandStartFrequency = frequencyHz; - return this; - } - - public HighPassBuilder stopBandCutoff( int frequencyHz ) - { - mStopBandEndFrequency = frequencyHz; - return this; - } - - public HighPassBuilder passBandRipple( double rippleDb ) - { - mPassBandRipple = rippleDb; - return this; - } - - public HighPassBuilder stopBandRipple( double rippleDb ) - { - mStopBandRipple = rippleDb; - return this; - } - - public HighPassBuilder passBandAmplitude( double amplitude ) - { - mPassBandAmplitude = amplitude; - return this; - } - - public HighPassBuilder stopBandAmplitude( double amplitude ) - { - mStopBandAmplitude = amplitude; - return this; - } - - public FIRFilterSpecification build() - { - if ( mOrder < 6 ) - { - mOrder = estimateFilterOrder( mSampleRate, mStopBandEndFrequency, - mPassBandStartFrequency, mPassBandRipple, mStopBandRipple ); - } - - // High pass is a Type 1 filter -- ensure order is even - if ( mOrder % 2 == 1 ) - { - mOrder++; - } - - FIRFilterSpecification spec = new FIRFilterSpecification( - FIRLinearPhaseFilterType.TYPE_1_ODD_LENGTH_EVEN_ORDER_SYMMETRICAL, mOrder, - mGridDensity ); - - FrequencyBand stopBand = new FrequencyBand( mSampleRate, 0, mStopBandEndFrequency, - mStopBandAmplitude, mStopBandRipple ); - spec.addFrequencyBand( stopBand ); - - FrequencyBand passBand = new FrequencyBand( mSampleRate, mPassBandStartFrequency, - mSampleRate / 2, mPassBandAmplitude, mPassBandRipple ); - spec.addFrequencyBand( passBand ); - - return spec; - } - } - - /** - * Band-pass or band-stop FIR filter specification builder - */ - public static class BandPassBuilder - { - private int mOrder; - private int mGridDensity = 16; - private int mSampleRate; - private int mStopFrequency1; - private int mPassFrequencyBegin; - private int mPassFrequencyEnd; - private int mStopFrequency2; - private double mStopRipple = 0.01; - private double mPassRipple = 0.01; - private double mStopAmplitude = 0.0; - private double mPassAmplitude = 1.0; - - public BandPassBuilder() - { - } - - /** - * Sets the filter order. If the order is not specified, it will be calculated from the - * other parameters. If you specify and odd-length order, it will be corrected (+1) to make - * it an even order number. - */ - public BandPassBuilder order( int order ) - { - mOrder = order; - return this; - } - - /** - * Sets the frequency grid density. Default is 16. - */ - public BandPassBuilder gridDensity( int density ) - { - mGridDensity = density; - return this; - } - - /** - * Sets the filter sample rate in hertz - */ - public BandPassBuilder sampleRate( int sampleRate ) - { - mSampleRate = sampleRate; - return this; - } - - /** - * Specifies the ending frequency of the first stop band - */ - public BandPassBuilder stopFrequency1( int stopFrequency ) - { - mStopFrequency1 = stopFrequency; - return this; - } - - /** - * Specifies the beginning frequency of the second stop band - */ - public BandPassBuilder stopFrequency2( int stopFrequency ) - { - mStopFrequency2 = stopFrequency; - return this; - } - - /** - * Specifies the pass band beginning frequency - */ - public BandPassBuilder passFrequencyBegin( int passFrequency ) - { - mPassFrequencyBegin = passFrequency; - return this; - } - - /** - * Specifies the pass band ending frequency - */ - public BandPassBuilder passFrequencyEnd( int passFrequency ) - { - mPassFrequencyEnd = passFrequency; - return this; - } - - /** - * Specifies the ripple in dB for both stop bands - */ - public BandPassBuilder stopRipple( double rippleDb ) - { - mStopRipple = rippleDb; - return this; - } - - /** - * Specifies the ripple in dB for the pass band - */ - public BandPassBuilder passRipple( double rippleDb ) - { - mPassRipple = rippleDb; - return this; - } - - /** - * Specifies the amplitude of the stop bands. Default is 0.0 - */ - public BandPassBuilder stopAmplitude( double amplitude ) - { - mStopAmplitude = amplitude; - return this; - } - - /** - * Specifies the amplitude of the pass band. Default is 1.0 - */ - public BandPassBuilder passAmplitude( double amplitude ) - { - mPassAmplitude = amplitude; - return this; - } - - /** - * Validates the frequency bands to test for overlap and completeness. - */ - private void validateFrequencyBands() - { - if ( mStopFrequency1 >= mPassFrequencyBegin ) - { - throw new IllegalArgumentException( "Stop band 1 ending frequency [" - + mStopFrequency1 + "] must be less than the pass band start frequency [" - + mPassFrequencyBegin + "]" ); - } - - if ( mPassFrequencyBegin >= mPassFrequencyEnd ) - { - throw new IllegalArgumentException( "Pass band begin frequency [" - + mPassFrequencyBegin + "] must be less than the pass band end frequency [" - + mPassFrequencyEnd + "]" ); - } - - if ( mPassFrequencyEnd >= mStopFrequency2 ) - { - throw new IllegalArgumentException( "Pass band end frequency [" + mPassFrequencyEnd - + "] must be less than stop band 2 beginning frequency [" + mStopFrequency2 - + "]" ); - } - - if ( mStopFrequency2 >= ( mSampleRate / 2 ) ) - { - throw new IllegalArgumentException( "Stop band 2 beginning frequency [" - + mStopFrequency2 + "] must be less than half of the sample rate [" - + ( mSampleRate / 2 ) + "]" ); - } - } - - public FIRFilterSpecification build() - { - validateFrequencyBands(); - - FIRLinearPhaseFilterType type = FIRLinearPhaseFilterType.TYPE_1_ODD_LENGTH_EVEN_ORDER_SYMMETRICAL; - - if ( mOrder < 10 ) - { - mOrder = estimateBandPassOrder( mSampleRate, mPassFrequencyBegin, mPassFrequencyEnd, - mPassRipple, mStopRipple ); - } - - // Ensure even order since we're designing a Type 1 filter - if ( mOrder % 2 == 1 ) - { - mOrder++; - } - - FIRFilterSpecification spec = new FIRFilterSpecification( type, mOrder, mGridDensity ); - - spec.addFrequencyBand( new FrequencyBand( mSampleRate, 0, mStopFrequency1, - mStopAmplitude, mStopRipple ) ); - spec.addFrequencyBand( new FrequencyBand( mSampleRate, mPassFrequencyBegin, - mPassFrequencyEnd, mPassAmplitude, mPassRipple ) ); - spec.addFrequencyBand( new FrequencyBand( mSampleRate, mStopFrequency2, - (int) ( mSampleRate / 2 ), mStopAmplitude, mStopRipple ) ); - - return spec; - } - } + public List getFrequencyBands() + { + return mFrequencyBands; + } + + /** + * Returns the total bandwidth of the defined frequency bands in the specification. + * + * @return total bandwidth -- should be less than or equal to 0.5 + */ + public double getTotalBandwidth() + { + double bandwidth = 0.0; + + for(FrequencyBand band : mFrequencyBands) + { + bandwidth += band.getBandWidth(); + } + + return bandwidth; + } + + /** + * Creates a Low-Pass FIR filter specification builder that allows you to define the filter + * parameters and create a filter specification. + */ + public static LowPassBuilder lowPassBuilder() + { + return new LowPassBuilder(); + } + + /** + * Creates a High-Pass FIR filter specification builder that allows you to define the filter + * parameters and create a filter specification. + */ + public static HighPassBuilder highPassBuilder() + { + return new HighPassBuilder(); + } + + /** + * Creates a Band-Pass FIR filter specification builder that allows you to define the filter + * parameters and create a filter specification. + */ + public static BandPassBuilder bandPassBuilder() + { + return new BandPassBuilder(); + } + + public FIRLinearPhaseFilterType getFilterType() + { + return mRemezFilterType; + } + + public boolean isSymmetrical() + { + return mRemezFilterType == FIRLinearPhaseFilterType.TYPE_1_ODD_LENGTH_EVEN_ORDER_SYMMETRICAL || + mRemezFilterType == FIRLinearPhaseFilterType.TYPE_2_EVEN_LENGTH_ODD_ORDER_SYMMETRICAL; + } + + public boolean isAntiSymmetrical() + { + return mRemezFilterType == FIRLinearPhaseFilterType.TYPE_3_ODD_LENGTH_EVEN_ORDER_ANTI_SYMMETRICAL || + mRemezFilterType == FIRLinearPhaseFilterType.TYPE_4_EVEN_LENGTH_ODD_ORDER_ANTI_SYMMETRICAL; + } + + public int getOrder() + { + return mOrder; + } + + public boolean isEvenOrder() + { + return mOrder % 2 == 0; + } + + public boolean isOddOrder() + { + return mOrder % 2 == 1; + } + + public int getFilterLength() + { + return mOrder + 1; + } + + public int getBandCount() + { + return mFrequencyBands.size(); + } + + /** + * Grid density setting. Default value is 16. + */ + public int getGridDensity() + { + return mGridDensity; + } + + /** + * Sets the grid density setting. Default value is 16. + */ + public void setGridDensity(int density) + { + mGridDensity = density; + + updateGridSize(); + } + + /** + * Maximum number of alternations/extrema (L + 2 or M + 2) + */ + public int getExtremaCount() + { + return getHalfFilterOrder() + 2; + } + + /** + * Half of the filter order. + * + * This value is referred to as L or M, depending on the source. + */ + public int getHalfFilterOrder() + { + switch(mRemezFilterType) + { + case TYPE_1_ODD_LENGTH_EVEN_ORDER_SYMMETRICAL: + return (mOrder) / 2; + case TYPE_2_EVEN_LENGTH_ODD_ORDER_SYMMETRICAL: + return (mOrder - 1) / 2; + case TYPE_3_ODD_LENGTH_EVEN_ORDER_ANTI_SYMMETRICAL: + return (mOrder - 2) / 2; + case TYPE_4_EVEN_LENGTH_ODD_ORDER_ANTI_SYMMETRICAL: + return (mOrder - 1) / 2; + default: + return 0; + } + } + + /** + * Determines the max band ripple from across the frequency bands in this specification. + */ + public double getMaxBandAmplitude() + { + double maxRippleAmplitude = 0.0; + + for(FrequencyBand band : mFrequencyBands) + { + double bandRippleAmplitude = band.getRippleAmplitude(); + + if(bandRippleAmplitude > maxRippleAmplitude) + { + maxRippleAmplitude = bandRippleAmplitude; + } + } + + return maxRippleAmplitude; + } + + public int getNominalGridSize() + { + return (getExtremaCount() - 1) * getGridDensity() + 1; + } + + public int getGridSize() + { + int gridSize = 0; + + for(FrequencyBand band : mFrequencyBands) + { + gridSize += band.getGridSize(); + } + + return gridSize; + } + + private void updateGridSize() + { + if(!mFrequencyBands.isEmpty()) + { + int gridSize = getNominalGridSize(); + + double totalBandwidth = getTotalBandwidth(); + + for(FrequencyBand band : mFrequencyBands) + { + band.setGridSize(gridSize, totalBandwidth); + } + } + } + + /** + * Dense frequency spacing for extrema count * grid density over a frequency range of 0.0 - 1.0 + */ + public double getGridFrequencyInterval() + { + return getTotalBandwidth() / (double)((getGridSize() - mFrequencyBands.size())); + } + + /** + * Low-pass FIR filter specification builder + */ + public static class LowPassBuilder + { + private int mSampleRate; + private int mOrder; + private int mGridDensity = 16; + private int mPassBandEndFrequency; + private int mStopBandStartFrequency; + private double mPassBandRipple; + private double mStopBandRipple; + private double mPassBandAmplitude = 1.0; + private double mStopBandAmplitude = 0.0; + + public LowPassBuilder() + { + } + + public LowPassBuilder sampleRate(int sampleRateHz) + { + mSampleRate = sampleRateHz; + return this; + } + + /** + * Sets the filter order. If the order is not specified, it will be calculated from the + * other parameters. + */ + public LowPassBuilder order(int order) + { + mOrder = order; + return this; + } + + public LowPassBuilder gridDensity(int density) + { + mGridDensity = density; + return this; + } + + public LowPassBuilder passBandCutoff(int frequencyHz) + { + mPassBandEndFrequency = frequencyHz; + return this; + } + + public LowPassBuilder stopBandStart(int frequencyHz) + { + mStopBandStartFrequency = frequencyHz; + return this; + } + + public LowPassBuilder passBandRipple(double rippleDb) + { + mPassBandRipple = rippleDb; + return this; + } + + public LowPassBuilder stopBandRipple(double rippleDb) + { + mStopBandRipple = rippleDb; + return this; + } + + public LowPassBuilder passBandAmplitude(double amplitude) + { + mPassBandAmplitude = amplitude; + return this; + } + + public LowPassBuilder stopBandAmplitude(double amplitude) + { + mStopBandAmplitude = amplitude; + return this; + } + + public FIRFilterSpecification build() + { + if(mOrder < 6) + { + mOrder = estimateFilterOrder(mSampleRate, mPassBandEndFrequency, + mStopBandStartFrequency, mPassBandRipple, mStopBandRipple); + } + + FIRLinearPhaseFilterType type = (mOrder % 2 == 0 + ? FIRLinearPhaseFilterType.TYPE_1_ODD_LENGTH_EVEN_ORDER_SYMMETRICAL + : FIRLinearPhaseFilterType.TYPE_2_EVEN_LENGTH_ODD_ORDER_SYMMETRICAL); + + FIRFilterSpecification spec = new FIRFilterSpecification(type, mOrder, mGridDensity); + + FrequencyBand passBand = new FrequencyBand(mSampleRate, 0, mPassBandEndFrequency, + mPassBandAmplitude, mPassBandRipple); + spec.addFrequencyBand(passBand); + + FrequencyBand stopBand = new FrequencyBand(mSampleRate, mStopBandStartFrequency, + mSampleRate / 2, mStopBandAmplitude, mStopBandRipple); + spec.addFrequencyBand(stopBand); + + return spec; + } + } + + /** + * Remez high-pass FIR filter specification builder + */ + public static class HighPassBuilder + { + private int mSampleRate; + private int mOrder = -1; + private int mGridDensity = 16; + private int mPassBandStartFrequency; + private int mStopBandEndFrequency; + private double mPassBandRipple; + private double mStopBandRipple; + private double mPassBandAmplitude = 1.0; + private double mStopBandAmplitude = 0.0; + + public HighPassBuilder() + { + } + + public HighPassBuilder sampleRate(int sampleRateHz) + { + mSampleRate = sampleRateHz; + return this; + } + + /** + * Sets the filter order. If the order is not specified, it will be calculated from the + * other parameters. + */ + public HighPassBuilder order(int order) + { + mOrder = order; + return this; + } + + public HighPassBuilder gridDensity(int density) + { + mGridDensity = density; + return this; + } + + public HighPassBuilder passBandStart(int frequencyHz) + { + mPassBandStartFrequency = frequencyHz; + return this; + } + + public HighPassBuilder stopBandCutoff(int frequencyHz) + { + mStopBandEndFrequency = frequencyHz; + return this; + } + + public HighPassBuilder passBandRipple(double rippleDb) + { + mPassBandRipple = rippleDb; + return this; + } + + public HighPassBuilder stopBandRipple(double rippleDb) + { + mStopBandRipple = rippleDb; + return this; + } + + public HighPassBuilder passBandAmplitude(double amplitude) + { + mPassBandAmplitude = amplitude; + return this; + } + + public HighPassBuilder stopBandAmplitude(double amplitude) + { + mStopBandAmplitude = amplitude; + return this; + } + + public FIRFilterSpecification build() + { + if(mOrder < 6) + { + mOrder = estimateFilterOrder(mSampleRate, mStopBandEndFrequency, + mPassBandStartFrequency, mPassBandRipple, mStopBandRipple); + } + + // High pass is a Type 1 filter -- ensure order is even + if(mOrder % 2 == 1) + { + mOrder++; + } + + FIRFilterSpecification spec = new FIRFilterSpecification( + FIRLinearPhaseFilterType.TYPE_1_ODD_LENGTH_EVEN_ORDER_SYMMETRICAL, mOrder, + mGridDensity); + + FrequencyBand stopBand = new FrequencyBand(mSampleRate, 0, mStopBandEndFrequency, + mStopBandAmplitude, mStopBandRipple); + spec.addFrequencyBand(stopBand); + + FrequencyBand passBand = new FrequencyBand(mSampleRate, mPassBandStartFrequency, + mSampleRate / 2, mPassBandAmplitude, mPassBandRipple); + spec.addFrequencyBand(passBand); + + return spec; + } + } + + /** + * Band-pass or band-stop FIR filter specification builder + */ + public static class BandPassBuilder + { + private int mOrder; + private int mGridDensity = 16; + private int mSampleRate; + private int mStopFrequency1; + private int mPassFrequencyBegin; + private int mPassFrequencyEnd; + private int mStopFrequency2; + private double mStopRipple = 0.01; + private double mPassRipple = 0.01; + private double mStopAmplitude = 0.0; + private double mPassAmplitude = 1.0; + + public BandPassBuilder() + { + } + + /** + * Sets the filter order. If the order is not specified, it will be calculated from the + * other parameters. If you specify and odd-length order, it will be corrected (+1) to make + * it an even order number. + */ + public BandPassBuilder order(int order) + { + mOrder = order; + return this; + } + + /** + * Sets the frequency grid density. Default is 16. + */ + public BandPassBuilder gridDensity(int density) + { + mGridDensity = density; + return this; + } + + /** + * Sets the filter sample rate in hertz + */ + public BandPassBuilder sampleRate(int sampleRate) + { + mSampleRate = sampleRate; + return this; + } + + /** + * Specifies the ending frequency of the first stop band + */ + public BandPassBuilder stopFrequency1(int stopFrequency) + { + mStopFrequency1 = stopFrequency; + return this; + } + + /** + * Specifies the beginning frequency of the second stop band + */ + public BandPassBuilder stopFrequency2(int stopFrequency) + { + mStopFrequency2 = stopFrequency; + return this; + } + + /** + * Specifies the pass band beginning frequency + */ + public BandPassBuilder passFrequencyBegin(int passFrequency) + { + mPassFrequencyBegin = passFrequency; + return this; + } + + /** + * Specifies the pass band ending frequency + */ + public BandPassBuilder passFrequencyEnd(int passFrequency) + { + mPassFrequencyEnd = passFrequency; + return this; + } + + /** + * Specifies the ripple in dB for both stop bands + */ + public BandPassBuilder stopRipple(double rippleDb) + { + mStopRipple = rippleDb; + return this; + } + + /** + * Specifies the ripple in dB for the pass band + */ + public BandPassBuilder passRipple(double rippleDb) + { + mPassRipple = rippleDb; + return this; + } + + /** + * Specifies the amplitude of the stop bands. Default is 0.0 + */ + public BandPassBuilder stopAmplitude(double amplitude) + { + mStopAmplitude = amplitude; + return this; + } + + /** + * Specifies the amplitude of the pass band. Default is 1.0 + */ + public BandPassBuilder passAmplitude(double amplitude) + { + mPassAmplitude = amplitude; + return this; + } + + /** + * Validates the frequency bands to test for overlap and completeness. + */ + private void validateFrequencyBands() + { + if(mStopFrequency1 >= mPassFrequencyBegin) + { + throw new IllegalArgumentException("Stop band 1 ending frequency [" + + mStopFrequency1 + "] must be less than the pass band start frequency [" + + mPassFrequencyBegin + "]"); + } + + if(mPassFrequencyBegin >= mPassFrequencyEnd) + { + throw new IllegalArgumentException("Pass band begin frequency [" + + mPassFrequencyBegin + "] must be less than the pass band end frequency [" + + mPassFrequencyEnd + "]"); + } + + if(mPassFrequencyEnd >= mStopFrequency2) + { + throw new IllegalArgumentException("Pass band end frequency [" + mPassFrequencyEnd + + "] must be less than stop band 2 beginning frequency [" + mStopFrequency2 + + "]"); + } + + if(mStopFrequency2 >= (mSampleRate / 2)) + { + throw new IllegalArgumentException("Stop band 2 beginning frequency [" + + mStopFrequency2 + "] must be less than half of the sample rate [" + + (mSampleRate / 2) + "]"); + } + } + + public FIRFilterSpecification build() + { + validateFrequencyBands(); + + FIRLinearPhaseFilterType type = FIRLinearPhaseFilterType.TYPE_1_ODD_LENGTH_EVEN_ORDER_SYMMETRICAL; + + if(mOrder < 10) + { + mOrder = estimateBandPassOrder(mSampleRate, mPassFrequencyBegin, mPassFrequencyEnd, + mPassRipple, mStopRipple); + } + + // Ensure even order since we're designing a Type 1 filter + if(mOrder % 2 == 1) + { + mOrder++; + } + + FIRFilterSpecification spec = new FIRFilterSpecification(type, mOrder, mGridDensity); + + spec.addFrequencyBand(new FrequencyBand(mSampleRate, 0, mStopFrequency1, + mStopAmplitude, mStopRipple)); + spec.addFrequencyBand(new FrequencyBand(mSampleRate, mPassFrequencyBegin, + mPassFrequencyEnd, mPassAmplitude, mPassRipple)); + spec.addFrequencyBand(new FrequencyBand(mSampleRate, mStopFrequency2, + (int)(mSampleRate / 2), mStopAmplitude, mStopRipple)); + + return spec; + } + } /** * Builder for creating polyphase channelizer low-pass prototype filter. @@ -916,7 +714,7 @@ public class FIRFilterSpecification /** * Sets the filter sample rate in hertz */ - public PolyphaseChannelizerBuilder sampleRate(int sampleRate ) + public PolyphaseChannelizerBuilder sampleRate(int sampleRate) { mSampleRate = sampleRate; return this; @@ -936,7 +734,7 @@ public class FIRFilterSpecification * other parameters. If you specify and odd-length order, it will be corrected (+1) to make * it an even order number. */ - public PolyphaseChannelizerBuilder order(int order ) + public PolyphaseChannelizerBuilder order(int order) { mOrder = order; return this; @@ -945,7 +743,7 @@ public class FIRFilterSpecification /** * Sets the frequency grid density. Default is 16. */ - public PolyphaseChannelizerBuilder gridDensity(int density ) + public PolyphaseChannelizerBuilder gridDensity(int density) { mGridDensity = density; return this; @@ -954,7 +752,7 @@ public class FIRFilterSpecification /** * Specifies the ripple in dB for both stop bands */ - public PolyphaseChannelizerBuilder stopRipple(double rippleDb ) + public PolyphaseChannelizerBuilder stopRipple(double rippleDb) { mStopRipple = rippleDb; return this; @@ -963,7 +761,7 @@ public class FIRFilterSpecification /** * Specifies the ripple in dB for the pass band */ - public PolyphaseChannelizerBuilder passRipple(double rippleDb ) + public PolyphaseChannelizerBuilder passRipple(double rippleDb) { mPassRipple = rippleDb; return this; @@ -972,7 +770,7 @@ public class FIRFilterSpecification /** * Specifies the amplitude of the stop bands. Default is 0.0 */ - public PolyphaseChannelizerBuilder stopAmplitude(double amplitude ) + public PolyphaseChannelizerBuilder stopAmplitude(double amplitude) { mStopAmplitude = amplitude; return this; @@ -981,7 +779,7 @@ public class FIRFilterSpecification /** * Specifies the amplitude of the pass band. Default is 1.0 */ - public PolyphaseChannelizerBuilder passAmplitude(double amplitude ) + public PolyphaseChannelizerBuilder passAmplitude(double amplitude) { mPassAmplitude = amplitude; return this; @@ -991,19 +789,19 @@ public class FIRFilterSpecification { FIRLinearPhaseFilterType type = FIRLinearPhaseFilterType.TYPE_1_ODD_LENGTH_EVEN_ORDER_SYMMETRICAL; - if ( mOrder < 10 ) + if(mOrder < 10) { - mOrder = estimateBandPassOrder( mSampleRate, mPassFrequencyBegin, mPassFrequencyEnd, - mPassRipple, mStopRipple ); + mOrder = estimateBandPassOrder(mSampleRate, mPassFrequencyBegin, mPassFrequencyEnd, + mPassRipple, mStopRipple); } // Ensure even order since we're designing a Type 1 filter - if ( mOrder % 2 == 1 ) + if(mOrder % 2 == 1) { mOrder++; } - FIRFilterSpecification spec = new FIRFilterSpecification( type, mOrder, mGridDensity ); + FIRFilterSpecification spec = new FIRFilterSpecification(type, mOrder, mGridDensity); //Set pass band to stop at about 80% of channel bandwidth as starting point int passBandStop = (int)(mChannelBandwidth * .80); @@ -1023,265 +821,273 @@ public class FIRFilterSpecification } - // /** - // * Converts the set of band amplitude values into a set of weights - // * normalized to the largest band amplitude value - // */ - // private static double[] getBandWeights( double... amplitudes ) - // { - // double maximum = 0.0; - // - // for ( double amplitude : amplitudes ) - // { - // if ( amplitude > maximum ) - // { - // maximum = amplitude; - // } - // } - // - // double[] weights = new double[ amplitudes.length ]; - // - // for ( int x = 0; x < amplitudes.length; x++ ) - // { - // weights[ x ] = maximum / amplitudes[ x ]; - // } - // - // return weights; - // } + // * Converts the set of band amplitude values into a set of weights + // * normalized to the largest band amplitude value + // */ + // private static double[] getBandWeights( double... amplitudes ) + // { + // double maximum = 0.0; + // + // for ( double amplitude : amplitudes ) + // { + // if ( amplitude > maximum ) + // { + // maximum = amplitude; + // } + // } + // + // double[] weights = new double[ amplitudes.length ]; + // + // for ( int x = 0; x < amplitudes.length; x++ ) + // { + // weights[ x ] = maximum / amplitudes[ x ]; + // } + // + // return weights; + // } - /** - * Calculates filter order from filter length - */ - private static int getFilterOrder( double length ) - { - return (int) ( Math.ceil( length ) - 1 ); - } + /** + * Calculates filter order from filter length + */ + private static int getFilterOrder(double length) + { + return (int)(Math.ceil(length) - 1); + } - /** - * Estimates filter length for a two-band (high or low pass) FIR filter. - * - * Note: this estimator doesn't work well if the transition frequency is near 0 or sampleRate/2 - * - * From Herrmann et al (1973), Practical design rules for optimum finite impulse response - * filters. Bell System Technical J., 52, 769-99 - * - * @param sampleRate in hertz - * @param frequency1 in hertz - * @param frequency2 in hertz - * @param passBandRipple pass band ripple in dB - * @param stopBandRipple stop band ripple in dB - * - * @return estimated filter length - */ - public static int estimateFilterOrder( int sampleRate, int frequency1, int frequency2, - double passBandRipple, double stopBandRipple ) - { - double df = Math.abs( frequency2 - frequency1 ) / (double) sampleRate; + /** + * Estimates filter length for a two-band (high or low pass) FIR filter. + * + * Note: this estimator doesn't work well if the transition frequency is near 0 or sampleRate/2 + * + * From Herrmann et al (1973), Practical design rules for optimum finite impulse response + * filters. Bell System Technical J., 52, 769-99 + * + * @param sampleRate in hertz + * @param frequency1 in hertz + * @param frequency2 in hertz + * @param passBandRipple pass band ripple in dB + * @param stopBandRipple stop band ripple in dB + * @return estimated filter length + */ + public static int estimateFilterOrder(int sampleRate, int frequency1, int frequency2, + double passBandRipple, double stopBandRipple) + { + double df = Math.abs(frequency2 - frequency1) / (double)sampleRate; - double ddp = Math - .log10( stopBandRipple <= passBandRipple ? passBandRipple : stopBandRipple ); - double dds = Math - .log10( stopBandRipple <= passBandRipple ? stopBandRipple : passBandRipple ); + double ddp = Math + .log10(stopBandRipple <= passBandRipple ? passBandRipple : stopBandRipple); + double dds = Math + .log10(stopBandRipple <= passBandRipple ? stopBandRipple : passBandRipple); - double a1 = 5.309e-3; - double a2 = 7.114e-2; - double a3 = -4.761e-1; - double a4 = -2.66e-3; - double a5 = -5.941e-1; - double a6 = -4.278e-1; + double a1 = 5.309e-3; + double a2 = 7.114e-2; + double a3 = -4.761e-1; + double a4 = -2.66e-3; + double a5 = -5.941e-1; + double a6 = -4.278e-1; - double b1 = 11.01217; - double b2 = 0.5124401; + double b1 = 11.01217; + double b2 = 0.5124401; - double t1 = a1 * ddp * ddp; - double t2 = a2 * ddp; - double t3 = a4 * ddp * ddp; - double t4 = a5 * ddp; + double t1 = a1 * ddp * ddp; + double t2 = a2 * ddp; + double t3 = a4 * ddp * ddp; + double t4 = a5 * ddp; - double dinf = ( ( t1 + t2 + a3 ) * dds ) + ( t3 + t4 + a6 ); - double ff = b1 + b2 * ( ddp - dds ); - double n = dinf / df - ff * df + 1.0; + double dinf = ((t1 + t2 + a3) * dds) + (t3 + t4 + a6); + double ff = b1 + b2 * (ddp - dds); + double n = dinf / df - ff * df + 1.0; - return (int) Math.ceil( n ); - } + return (int)Math.ceil(n); + } - /** - * Estimates band pass filter length; - * - * @param passBandStart frequency normalized to sample rate - * @param passBandEnd frequency normalized to sample rate - * @param passBandRippleDb - * @param stopBandRippleDb - */ - public static int estimateBandPassOrder( int sampleRate, int passBandStart, int passBandEnd, - double passBandRippleDb, double stopBandRippleDb ) - { - double df = (double) Math.abs( passBandEnd - passBandStart ) / (double) sampleRate; - double ddp = (double) Math.log10( passBandRippleDb ); - double dds = (double) Math.log10( stopBandRippleDb ); + /** + * Estimates band pass filter length; + * + * @param passBandStart frequency normalized to sample rate + * @param passBandEnd frequency normalized to sample rate + * @param passBandRippleDb + * @param stopBandRippleDb + */ + public static int estimateBandPassOrder(int sampleRate, int passBandStart, int passBandEnd, + double passBandRippleDb, double stopBandRippleDb) + { + double df = (double)Math.abs(passBandEnd - passBandStart) / (double)sampleRate; + double ddp = (double)Math.log10(passBandRippleDb); + double dds = (double)Math.log10(stopBandRippleDb); - double a1 = 0.01201; - double a2 = 0.09664; - double a3 = -0.51325; - double a4 = 0.00203; - double a5 = -0.57054; - double a6 = -0.44314; + double a1 = 0.01201; + double a2 = 0.09664; + double a3 = -0.51325; + double a4 = 0.00203; + double a5 = -0.57054; + double a6 = -0.44314; - double t1 = a1 * ddp * ddp; - double t2 = a2 * ddp; - double t3 = a4 * ddp * ddp; - double t4 = a5 * ddp; + double t1 = a1 * ddp * ddp; + double t2 = a2 * ddp; + double t3 = a4 * ddp * ddp; + double t4 = a5 * ddp; - double cinf = dds * ( t1 + t2 + a3 ) + t3 + t4 + a6; - double ginf = -14.6f * (double) Math.log10( passBandRippleDb / stopBandRippleDb ) - 16.9; - double n = cinf / df + ginf * df + 1.0; + double cinf = dds * (t1 + t2 + a3) + t3 + t4 + a6; + double ginf = -14.6f * (double)Math.log10(passBandRippleDb / stopBandRippleDb) - 16.9; + double n = cinf / df + ginf * df + 1.0; - return (int) Math.ceil( n ); - } + return (int)Math.ceil(n); + } - /** - * Frequency band - */ - public static class FrequencyBand - { - private int mGridSize; - private double mStart; - private double mEnd; - private double mAmplitude; - private double mRippleDB; + /** + * Frequency band + */ + public static class FrequencyBand + { + private int mGridSize; + private double mStart; + private double mEnd; + private double mAmplitude; + private double mRippleDB; + private Double mWeight; - /** - * Constructs a frequency band where the start and end frequencies are normalized to a - * sample rate of 1. - * - * @param start edge of the frequency band (0.0 - 1.0) - * @param end edge of the frequency band (0.0 - 1.0) - * @param amplitude of the frequency band on the unity scale (0.0 - 1.0) - * @param ripple of the frequency band specified as db ripple - */ - public FrequencyBand( double start, double end, double amplitude, double ripple ) - { - assert ( 0.0 <= mStart ); - assert ( mStart < mEnd ); - assert ( mEnd <= 1.0 ); - assert ( 0.0 <= amplitude && amplitude <= 1.0 ); - assert ( 0.0 <= ripple ); + /** + * Constructs a frequency band where the start and end frequencies are normalized to a + * sample rate of 1. + * + * @param start edge of the frequency band (0.0 - 1.0) + * @param end edge of the frequency band (0.0 - 1.0) + * @param amplitude of the frequency band on the unity scale (0.0 - 1.0) + * @param ripple of the frequency band specified as db ripple + */ + public FrequencyBand(double start, double end, double amplitude, double ripple) + { + assert (0.0 <= mStart); + assert (mStart <= mEnd); + assert (mEnd <= 1.0); + assert (0.0 <= amplitude && amplitude <= 1.0); + assert (0.0 <= ripple); - mStart = start; - mEnd = end; - mAmplitude = amplitude; - mRippleDB = ripple; - } + mStart = start; + mEnd = end; + mAmplitude = amplitude; + mRippleDB = ripple; + } - /** - * Constructs a frequency band for the sample rate and start and end frequencies. - * - * @param sampleRate in hertz - * @param start edge of the frequency band in hertz - * @param end edge of the frequency band in hertz - * @param amplitude of the frequency band relative to unity (0.0 - 1.0) - * @param ripple of the frequency band specified as db ripple - */ - public FrequencyBand( int sampleRate, int start, int end, double amplitude, double ripple ) - { - this( (double) start / (double) sampleRate, (double) end / (double) sampleRate, - amplitude, ripple ); - } + /** + * Constructs a frequency band for the sample rate and start and end frequencies. + * + * @param sampleRate in hertz + * @param start edge of the frequency band in hertz + * @param end edge of the frequency band in hertz + * @param amplitude of the frequency band relative to unity (0.0 - 1.0) + * @param ripple of the frequency band specified as db ripple + */ + public FrequencyBand(int sampleRate, int start, int end, double amplitude, double ripple) + { + this((double)start / (double)sampleRate, (double)end / (double)sampleRate, + amplitude, ripple); + } - public int getGridSize() - { - return mGridSize; - } + public FrequencyBand(int sampleRate, int start, int end, double amplitude, double ripple, double weight) + { + this(sampleRate, start, end, amplitude, ripple); + mWeight = weight; + } - public void setGridSize( int totalGridSize, double totalBandwidth ) - { - mGridSize = (int) Math - .ceil( (double) totalGridSize * ( getBandWidth() / totalBandwidth ) ); - } + public int getGridSize() + { + return mGridSize; + } - public String toString() - { - StringBuilder sb = new StringBuilder(); + public void setGridSize(int totalGridSize, double totalBandwidth) + { + mGridSize = Math.max(1, (int)Math.ceil((double)totalGridSize * (getBandWidth() / totalBandwidth))); + } - sb.append( "Band Start: " ); - sb.append( mStart ); - sb.append( " End:" ); - sb.append( mEnd ); - sb.append( " Amplitude: " ); - sb.append( mAmplitude ); - sb.append( " Ripple dB: " ); - sb.append( mRippleDB ); - sb.append( " Ripple Amplitude: " ); - sb.append( DECIMAL_FORMATTER.format( getRippleAmplitude() ) ); - sb.append( " Grid Size: " ); - sb.append( mGridSize ); + public String toString() + { + StringBuilder sb = new StringBuilder(); - return sb.toString(); - } + sb.append("Band Start: "); + sb.append(mStart); + sb.append(" End:"); + sb.append(mEnd); + sb.append(" Amplitude: "); + sb.append(mAmplitude); + sb.append(" Ripple dB: "); + sb.append(mRippleDB); + sb.append(" Ripple Amplitude: "); + sb.append(DECIMAL_FORMATTER.format(getRippleAmplitude())); + sb.append(" Grid Size: "); + sb.append(mGridSize); - /** - * Start frequency edge for this band normalized to 1 Hz. - */ - public double getStart() - { - return mStart; - } + return sb.toString(); + } - /** - * End frequency edge for this band normalized to 1 Hz. - */ - public double getEnd() - { - return mEnd; - } + /** + * Start frequency edge for this band normalized to 1 Hz. + */ + public double getStart() + { + return mStart; + } - /** - * Desired amplitude response for this frequency band - * - * @return amplitude (0.0 - 1.0) - */ - public double getAmplitude() - { - return mAmplitude; - } + /** + * End frequency edge for this band normalized to 1 Hz. + */ + public double getEnd() + { + return mEnd; + } - /** - * Frequency band ripple in decibels - */ - public double getRippleDB() - { - return mRippleDB; - } + /** + * Desired amplitude response for this frequency band + * + * @return amplitude (0.0 - 1.0) + */ + public double getAmplitude() + { + return mAmplitude; + } - /** - * Frequency band ripple as an amplitude value - */ - public double getRippleAmplitude() - { - return ( Math.pow( 10.0, ( mRippleDB / 20 ) ) - 1 ) - / ( Math.pow( 10.0, ( mRippleDB / 20 ) ) + 1 ); - } + /** + * Frequency band ripple in decibels + */ + public double getRippleDB() + { + return mRippleDB; + } - /** - * Calculates the weighting of this frequency band's ripple relative to the max ripple for a - * filter specification. - * - * @param maxRippleAmplitude for the filter specification - * - * @return weight of this frequency band - */ - public double getWeight( double maxRippleAmplitude ) - { - return 1.0 / ( getRippleAmplitude() / maxRippleAmplitude ); - } + /** + * Frequency band ripple as an amplitude value + */ + public double getRippleAmplitude() + { + return (Math.pow(10.0, (mRippleDB / 20)) - 1) + / (Math.pow(10.0, (mRippleDB / 20)) + 1); + } - /** - * Double-sided bandwidth of this frequency band - */ - public double getBandWidth() - { - return mEnd - mStart; - } - } + /** + * Calculates the weighting of this frequency band's ripple relative to the max ripple for a + * filter specification. + * + * @param maxRippleAmplitude for the filter specification + * @return weight of this frequency band + */ + public double getWeight(double maxRippleAmplitude) + { + if(mWeight == null) + { + return 1.0 / (getRippleAmplitude() / maxRippleAmplitude); + } + + return mWeight; + } + + /** + * Double-sided bandwidth of this frequency band + */ + public double getBandWidth() + { + return mEnd - mStart; + } + } } diff --git a/src/dsp/filter/fir/remez/PolyphaseChannelizerFilterFactory.java b/src/dsp/filter/fir/remez/PolyphaseChannelizerFilterFactory.java index 04417fa3..7e21f6fa 100644 --- a/src/dsp/filter/fir/remez/PolyphaseChannelizerFilterFactory.java +++ b/src/dsp/filter/fir/remez/PolyphaseChannelizerFilterFactory.java @@ -28,27 +28,25 @@ public class PolyphaseChannelizerFilterFactory private static final double OBJECTIVE_BAND_EDGE_COEFFICIENT_AMPLITUDE = Math.sqrt(2.0) / 2.0; //.707xxx - public static float[] getFilter(int sampleRate, int channelBandwidth) + public static float[] getFilter(int sampleRate, int channelBandwidth, double alpha) { FIRLinearPhaseFilterType type = FIRLinearPhaseFilterType.TYPE_1_ODD_LENGTH_EVEN_ORDER_SYMMETRICAL; int passBandStart = 0; - int gridDensity = 32; + int gridDensity = 50; - //Set pass band to stop at about 80% of channel bandwidth as starting point -// int passBandStop = (int)(channelBandwidth * 0.95); - int passBandStop = channelBandwidth; + int rolloffFrequency = (int)(alpha * (double)channelBandwidth); + + int passBandStop = channelBandwidth - rolloffFrequency; //Set the stop band to start at 40% into the adjacent channel, since we're oversampling by 2. - int stopBandStart = (int)(channelBandwidth * 1.40); + int stopBandStart = channelBandwidth + rolloffFrequency; - double passRipple = 0.1d; - double stopRipple = 0.01d; + double passRipple = 0.01d; + double stopRipple = 0.001d; //Approximately 90 dB of attenuation -// int order = FIRFilterSpecification.estimateBandPassOrder(sampleRate, passBandStart, passBandStop, -// passRipple, stopRipple); - int order = FIRFilterSpecification.estimateFilterOrder( sampleRate, passBandStop, - stopBandStart, passRipple, stopRipple ); + int order = FIRFilterSpecification.estimateFilterOrder( sampleRate, passBandStop, stopBandStart, + passRipple, stopRipple ); // Ensure even order since we're designing a Type 1 filter if(order % 2 == 1) @@ -56,66 +54,39 @@ public class PolyphaseChannelizerFilterFactory order++; } + mLog.info("Filter Order: " + order); + FIRFilterSpecification specification = new PolyphaseChannelizerDesigner(order, gridDensity); - FIRFilterSpecification.FrequencyBand passBand = new FIRFilterSpecification.FrequencyBand(sampleRate, 0, - passBandStop, 1.0, passRipple); + FIRFilterSpecification.FrequencyBand passBand = new FIRFilterSpecification.FrequencyBand(sampleRate, passBandStart, + passBandStop, 1.0, passRipple, 1.0); + + //Use the filter order as the weighting for the pass band edge frequency + double weight = order; + + FIRFilterSpecification.FrequencyBand transitionBand = new FIRFilterSpecification.FrequencyBand(sampleRate, + channelBandwidth, channelBandwidth, OBJECTIVE_BAND_EDGE_COEFFICIENT_AMPLITUDE, passRipple, weight); + FIRFilterSpecification.FrequencyBand stopBand = new FIRFilterSpecification.FrequencyBand(sampleRate, stopBandStart, (int)(sampleRate / 2), 0.0, stopRipple); specification.addFrequencyBand(passBand); + specification.addFrequencyBand(transitionBand); specification.addFrequencyBand(stopBand); - boolean complete = false; - int stepSize = 100; double bandEdgeFrequency = Math.cos(Math.PI * (double)channelBandwidth / (double)(sampleRate / 2)); float[] filter = null; - while(!complete) + try { - mLog.debug("Step Size: " + stepSize); - - RemezFIRFilterDesigner2 designer = new RemezFIRFilterDesigner2(specification); - - complete = true; //temporary - + RemezFIRFilterDesignerWithLagrange designer = new RemezFIRFilterDesignerWithLagrange(specification); + filter = designer.getImpulseResponse(); double bandEdgeAmplitude = designer.getFrequencyResponse(bandEdgeFrequency); mLog.debug("Coefficient Amplitude at Band Edge is: " + bandEdgeAmplitude + " for frequency: " + passBandStop); - - if(bandEdgeAmplitude < OBJECTIVE_BAND_EDGE_COEFFICIENT_AMPLITUDE) - { - if(stepSize <= 1) - { - complete = true; - try - { - filter = designer.getImpulseResponse(); - } - catch(Exception e) - { - mLog.debug("Error getting filter response", e); - } - } - else - { - passBandStop += stepSize; - stepSize /= 10; - } - } - else - { - passBandStop -= stepSize; - } - - if(!complete) - { - specification.clearFrequencyBands(); - - passBand = new FIRFilterSpecification.FrequencyBand(sampleRate, 0, - passBandStop, 1.0, passRipple); - specification.addFrequencyBand(passBand); - specification.addFrequencyBand(stopBand); - } + } + catch(Exception e) + { + mLog.error("Error designing filter", e); } return filter; @@ -125,7 +96,7 @@ public class PolyphaseChannelizerFilterFactory { mLog.debug("Starting ..."); - float[] filter = getFilter(2500000, 12500); + float[] filter = getFilter(200000, 12500, 0.21); mLog.debug("Finished ..."); } diff --git a/src/dsp/filter/fir/remez/RemezFIRFilterDesigner.java b/src/dsp/filter/fir/remez/RemezFIRFilterDesigner.java index e5ca196b..dba1fd76 100644 --- a/src/dsp/filter/fir/remez/RemezFIRFilterDesigner.java +++ b/src/dsp/filter/fir/remez/RemezFIRFilterDesigner.java @@ -212,15 +212,12 @@ public class RemezFIRFilterDesigner for( int k = 0; k < mExtremalIndices.size() - 1; k++ ) { - double cosineDelta = cosineOfFrequency - - mGrid.getCosineFrequencyGrid()[ mExtremalIndices.get( k ) ]; + double cosineDelta = cosineOfFrequency - mGrid.getCosineFrequencyGrid()[ mExtremalIndices.get( k ) ]; - //If this frequency is or is close to one of the polynomial points, use c[k] for that point + //If this frequency is close to one of the polynomial points, use the polynomial point for the response if( Math.abs( cosineDelta ) < 1.0e-7 ) { - numerator = mIdealFrequencyResponse[ k ]; - denominator = 1.0; - break; + return mIdealFrequencyResponse[ k ]; } else { diff --git a/src/dsp/filter/fir/remez/RemezFIRFilterDesigner2.java b/src/dsp/filter/fir/remez/RemezFIRFilterDesigner2.java index 2890d939..521b4f65 100644 --- a/src/dsp/filter/fir/remez/RemezFIRFilterDesigner2.java +++ b/src/dsp/filter/fir/remez/RemezFIRFilterDesigner2.java @@ -36,6 +36,7 @@ public class RemezFIRFilterDesigner2 private static final double CONVERGENCE_THRESHOLD = 0.0001; public static final int MAXIMUM_ITERATION_COUNT = 40; public static final double TWO_PI = 2.0 * Math.PI; + public static final double FREQUENCY_PROXIMITY_THRESHOLD = 1.0e-7; private FIRFilterSpecification mSpecification; private Grid mGrid; @@ -234,25 +235,27 @@ public class RemezFIRFilterDesigner2 for(int k = 0; k < mExtremalIndices.size() - 1; k++) { - BigDecimal cosineDelta = BigDecimal.valueOf(cosineOfFrequency - - mGrid.getCosineFrequencyGrid()[mExtremalIndices.get(k)]); + double cosineDelta = cosineOfFrequency - mGrid.getCosineFrequencyGrid()[mExtremalIndices.get(k)]; - //If this frequency is close to one of the polynomial points, use c[k] for that point - if(cosineDelta.abs().compareTo(BigDecimal.valueOf(1.0e-7)) < 0) + //If this frequency is close to one of the polynomial points, use the point as the response value + if(Math.abs(cosineDelta) < FREQUENCY_PROXIMITY_THRESHOLD) { - numerator = BigDecimal.valueOf(mIdealFrequencyResponse[k]); - denominator = BigDecimal.ONE; - break; + return mIdealFrequencyResponse[k]; } else { - BigDecimal dkOverCosineDelta = mD[k].divide(cosineDelta, 25, RoundingMode.HALF_UP); + BigDecimal dkOverCosineDelta = mD[k].divide(BigDecimal.valueOf(cosineDelta), 25, RoundingMode.HALF_UP); numerator = numerator.add(dkOverCosineDelta.multiply(BigDecimal.valueOf(mIdealFrequencyResponse[k]))); denominator = denominator.add(dkOverCosineDelta); } } + if(denominator.doubleValue() == 0.0) + { + int a = 0; + } + return numerator.divide(denominator, 25, RoundingMode.HALF_UP).doubleValue(); } diff --git a/src/dsp/filter/fir/remez/RemezFIRFilterDesignerWithLagrange.java b/src/dsp/filter/fir/remez/RemezFIRFilterDesignerWithLagrange.java new file mode 100644 index 00000000..d3fee4cf --- /dev/null +++ b/src/dsp/filter/fir/remez/RemezFIRFilterDesignerWithLagrange.java @@ -0,0 +1,686 @@ +/******************************************************************************* + * 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.fir.remez; + +import dsp.filter.design.FilterDesignException; +import dsp.filter.fir.FIRFilterSpecification; +import org.apache.commons.math3.analysis.polynomials.PolynomialFunctionLagrangeForm; +import org.slf4j.Logger; +import org.slf4j.LoggerFactory; + +import java.math.BigDecimal; +import java.text.DecimalFormat; +import java.util.ArrayList; +import java.util.Iterator; +import java.util.List; + +public class RemezFIRFilterDesignerWithLagrange +{ + private final static Logger mLog = LoggerFactory.getLogger(RemezFIRFilterDesignerWithLagrange.class); + + private static final double CONVERGENCE_THRESHOLD = 0.0001; + public static final int MAXIMUM_ITERATION_COUNT = 40; + public static final double TWO_PI = 2.0 * Math.PI; + + private static final DecimalFormat DECIMAL_FORMATTER = new DecimalFormat("0.00000000"); + + private FIRFilterSpecification mSpecification; + private Grid mGrid; + private List mExtremalIndices; + + private double[] mD; + private double[] mGridErrors; + private double[] mGridFrequencyResponse; + private double[] mIdealFrequencyResponse; + private PolynomialFunctionLagrangeForm mLagrangeInterpolator; + + private double mDelta; + + private boolean mConverged; + + /** + * Linear FIR filter designer using the Parks-McClelland Remez exchange algorithm. + * + * Construct a filter specification using the builder methods in the FIRFilterSpecification + * class and construct an instance of this class with the specification. Check the isValid() + * method after construction to see if the filter design was successful. If successful, use the + * getFrequencyResponse() and getImpulseResponse() methods to access the filter taps as the + * specified length. + * + * If the filter cannot be designed from the specification the isValid() method will return + * false and any attempts to access the getFrequencyResponse() or getImpulseResponse() methods + * will throw a FilterDesignException. + * + * @param specification that defines a Type 1-4 linear phase FIR Filter + */ + public RemezFIRFilterDesignerWithLagrange(FIRFilterSpecification specification) throws FilterDesignException + { + mSpecification = specification; + + design(); + } + + /** + * Designs the filter according to the specification + */ + private void design() throws FilterDesignException + { + mGrid = new Grid(mSpecification); + mExtremalIndices = getInitialExtremalIndices(); + + int iterationCount = 0; + + do + { + calculateGridFrequencyResponse(); + calculateGridError(); + findExtremalIndices(); + checkConvergence(); + + iterationCount++; + } + while(!mConverged && iterationCount < MAXIMUM_ITERATION_COUNT); + + if(mConverged) + { + //Update frequency response using the final set of extremal indices + calculateGridFrequencyResponse(); + } + else + { + throw new FilterDesignException("Unable to design filter after [" + iterationCount + "] iterations"); + } + } + + /** + * Indicates if the filter was successfully created from the filter specification. Check this + * method before accessing frequency response or filter response, otherwise a FilterDesignException + * is thrown. + * + * @return true if the filter was successfully designed. + */ + public boolean isValid() + { + return mConverged; + } + + /** + * Frequency response of the designed filter with double precision + * + * @return half ( length / 2 ) of filter frequency response sampled at 0.0 - PI radians + * @throws FilterDesignException if the specified filter cannot be designed + */ + public double[] getFrequencyResponse() throws FilterDesignException + { + if(!mConverged) + { + throw new FilterDesignException("Can't create filter from specification - failed " + + "to converge "); + } + + //Resample the final polynomial at the desired filter length + double[] resampled = resample(); + + //Apply Parks/McClelland frequency response corrections according to the filter type + return correctFrequencyResponse(resampled); + } + + /** + * Impulse response of the designed filter with floating point precision. + * + * @return filter impulse response + * @throws FilterDesignException if the specified filter cannot be designed + */ + public float[] getImpulseResponse() throws FilterDesignException + { + return convertToFloatArray(getImpulseResponseDoubles()); + } + + /** + * Impulse response of the designed filter with double precision + */ + public double[] getImpulseResponseDoubles() throws FilterDesignException + { + double[] frequencyResponse = getFrequencyResponse(); + + int length = mSpecification.getFilterLength(); + + double[] impulseResponse = new double[length]; + + double M; + + switch(mSpecification.getFilterType()) + { + case TYPE_1_ODD_LENGTH_EVEN_ORDER_SYMMETRICAL: + M = ((double)length - 1.0) / 2.0; + + for(int n = 0; n < length; n++) + { + double accumulator = frequencyResponse[0]; + + double frequency = TWO_PI * (n - M) / length; + + for(int k = 1; k <= M; k++) + { + accumulator += 2.0 * frequencyResponse[k] * Math.cos(frequency * (double)k); + } + + impulseResponse[n] = accumulator / (double)length; + } + break; + case TYPE_2_EVEN_LENGTH_ODD_ORDER_SYMMETRICAL: + double offset = (double)(length - 1) / 2.0; + + for(int n = 0; n < length; n++) + { + double accumulator = frequencyResponse[0]; + + double frequency = TWO_PI * ((double)n - offset) / (double)length; + + for(int k = 1; k < frequencyResponse.length; k++) + { + accumulator += 2.0 * frequencyResponse[k] * Math.cos(frequency * (double)k); + } + + impulseResponse[n] = accumulator / (double)length; + } + break; + } + + return impulseResponse; + } + + /** + * Calculates the frequency response of this filter at the specified cosine of the frequency + * + * Implements Oppenheim/Schafer Discrete Time Signal Processing, 3e, 2016, equation 116a to + * determine the actual response of the specified frequency cosine using Lagrange + * interpolation of the polynomial formed at the extremal index set. + * + * @param cosineOfFrequency to calculate the frequency response + * @return actual frequency response for the specified frequency with the current set of + * extreaml indices + */ + public double getFrequencyResponse(double cosineOfFrequency) + { + if(mLagrangeInterpolator == null) + { + throw new IllegalStateException("Lagrange interpolator is not setup - can't calculate frequency response"); + } + + return mLagrangeInterpolator.value(cosineOfFrequency); + +// double numerator = 0.0; +// double denominator = 0.0; +// +// for( int k = 0; k < mExtremalIndices.size() - 1; k++ ) +// { +// double cosineDelta = cosineOfFrequency - mGrid.getCosineFrequencyGrid()[ mExtremalIndices.get( k ) ]; +// +// //If this frequency is close to one of the polynomial points, use the polynomial point for the response +// if( Math.abs( cosineDelta ) < 1.0e-7 ) +// { +// return mIdealFrequencyResponse[ k ]; +// } +// else +// { +// double dkOverCosineDelta = mD[ k ] / cosineDelta; +// +// numerator += dkOverCosineDelta * mIdealFrequencyResponse[ k ]; +// denominator += dkOverCosineDelta; +// } +// } +// +// return numerator / denominator; + } + + /** + * Calculated Delta value + * + * @return delta value + */ + public double getDelta() + { + return mDelta; + } + + /** + * Creates initial array of extremal frequency indices spaced at intervals of the grid frequency + * set for all frequencies where a frequency band weighting is specified. + */ + private List getInitialExtremalIndices() + { + int count = mSpecification.getExtremaCount(); + + int density = mSpecification.getGridDensity(); + + List extremalIndices = new ArrayList<>(); + + for(int i = 0; i < count; i++) + { + extremalIndices.add(i * density); + } + + return extremalIndices; + } + + /** + * Calculates the actual frequency response of a dense frequency grid using the polynomial + * represented by the extremal indices. + */ + private void calculateGridFrequencyResponse() + { + BigDecimal[] b = calculateB(); + + calculateDelta(b); + + calculateC(); + + //Setup the lagrange polynomial so that we can determine the frequency response at the grid frequencies + //where mExtremalIndices are the polynomial points, the subset of cosine frequency grid identified by the + //extremal indices is the x value set and mIdealFrequencyResponse is the y axis value set + double[] xAxis = new double[mIdealFrequencyResponse.length]; + + for(int x = 0; x < mExtremalIndices.size() - 1; x++) + { + int index = mExtremalIndices.get(x); + + xAxis[x] = mGrid.getCosineFrequencyGrid()[index]; + } + + mLagrangeInterpolator = new PolynomialFunctionLagrangeForm(xAxis, mIdealFrequencyResponse); + + updateGridFrequencyResponse(); + } + + /** + * Updates the frequency response of frequencies specified in the dense frequency grid + */ + private void updateGridFrequencyResponse() + { + mGridFrequencyResponse = new double[mGrid.getSize()]; + + double[] gridFrequencyCosines = mGrid.getCosineFrequencyGrid(); + + for(int i = 0; i < mGridFrequencyResponse.length; i++) + { + mGridFrequencyResponse[i] = getFrequencyResponse(gridFrequencyCosines[i]); + } + } + + /** + * Calculates the value of b across the set of extremal indices. + * + * Implements Oppenheim/Schafer Discrete Time Signal Processing, 3e, 2016, equation 115 + * + * @return the array of b values of length L+2 + */ + private BigDecimal[] calculateB() + { + int length = mExtremalIndices.size(); + + BigDecimal[] b = new BigDecimal[length]; + + for(int k = 0; k < length; k++) + { + b[k] = BigDecimal.ONE; + + double xk = mGrid.getCosineFrequencyGrid()[mExtremalIndices.get(k)]; + + for(int i = 0; i < length; i++) + { + if(i != k) + { + double xi = mGrid.getCosineFrequencyGrid()[mExtremalIndices.get(i)]; + + double denominator = xk - xi; + + if(Math.abs(denominator) < 0.00001) + { + denominator = 0.00001; + } + + b[k] = b[k].multiply(BigDecimal.valueOf(1.0 / denominator)); +// b[ k ] *= 1.0 / denominator; + } + } + } + + return b; + } + + /** + * Calculates the value of delta that represents the maximum ripple for the current set of + * extremal indices (L+2) + * + * Implements Oppenheim/Schafer Discrete Time Signal Processing, 3e, 2016, equation 114 + * + * @param b calculated for the grid from equation 115 + */ + private void calculateDelta(BigDecimal[] b) + { + BigDecimal numerator = BigDecimal.ZERO; + BigDecimal denominator = BigDecimal.ZERO; + + double sign = 1.0; + + for(int k = 0; k < b.length; k++) + { + if(k < mExtremalIndices.size()) + { + int extremalIndex = mExtremalIndices.get(k); + + numerator = numerator.add(b[k].multiply(BigDecimal.valueOf(mGrid.getDesiredResponse()[extremalIndex]))); + denominator = denominator.add(b[k].multiply(BigDecimal.valueOf(sign / mGrid.getWeight()[extremalIndex]))); + + sign = -sign; + } + else + { + mLog.error("Something went wrong -- the length of b exceeds the set of extremal indices"); + } + } + + //Set the scale to the maximum scale we could see for a primitive decimal + mDelta = numerator.divide(denominator, 1023, BigDecimal.ROUND_HALF_DOWN).doubleValue(); + } + + /** + * Calculates the ideal frequency response (C) of the current extremal index set. This reference + * is used to determine the level of error in the set of actual frequency responses calculated + * from this set of extremal indices to determine convergence. + * + * Implements Oppenheim/Schafer Discrete Time Signal Processing, 3e, 2016, equation 116b + */ + private void calculateC() + { + int length = mSpecification.getExtremaCount() - 1; + + mIdealFrequencyResponse = new double[length]; + + double sign = 1.0; + + for(int k = 0; k < length; k++) + { + if(k < mExtremalIndices.size()) + { + int index = mExtremalIndices.get(k); + + mIdealFrequencyResponse[k] = mGrid.getDesiredResponse()[index] - + (sign * mDelta / mGrid.getWeight()[index]); + + sign = -sign; + } + } + } + + /** + * Calculates the weighted error between the specification desired frequency response and the + * actual frequency response of the current extremal index set across the frequencies in the + * dense frequency grid. + * + * Implements Oppenheim/Schafer Discrete Time Signal Processing, 3e, 2016, equation 112 + */ + public void calculateGridError() + { + int length = mGridFrequencyResponse.length; + + mGridErrors = new double[length]; + + for(int i = 0; i < length; i++) + { + mGridErrors[i] = mGrid.getWeight()[i] * (mGrid.getDesiredResponse()[i] - mGridFrequencyResponse[i]); + } + } + + /** + * Identifies the indexes of the extremal error values using the Alternation theorem + * + * @throws FilterDesignException if the full set of extremal indices can't be identified + */ + private void findExtremalIndices() throws FilterDesignException + { + mExtremalIndices.clear(); + + //Check for extremum at error index 0 + if(((mGridErrors[0] > 0.0 && mGridErrors[0] > mGridErrors[1]) || + (mGridErrors[0] < 0.0 && mGridErrors[0] < mGridErrors[1])) && + isGTEDelta(mGridErrors[0])) + { + mExtremalIndices.add(0); + } + + //Check for extrema in middle error indices ) + for(int x = 1; x < mGridErrors.length - 1; x++) + { + if(((mGridErrors[x] > 0.0 && (mGridErrors[x - 1] <= mGridErrors[x] && mGridErrors[x] > mGridErrors[x + 1])) || + (mGridErrors[x] < 0.0 && (mGridErrors[x - 1] >= mGridErrors[x] && mGridErrors[x] < mGridErrors[x + 1]))) && + isGTEDelta(mGridErrors[x])) + { + mExtremalIndices.add(x); + } + } + + //Check for extremum at final error index + int last = mGridErrors.length - 1; + + if(((mGridErrors[last] > 0.0 && (mGridErrors[last] > mGridErrors[last - 1])) || + (mGridErrors[last] < 0.0 && (mGridErrors[last] < mGridErrors[last - 1]))) && + isGTEDelta(mGridErrors[last])) + { + mExtremalIndices.add(last); + } + + //Ensure we have the minimum of extremals before we continue + if(mExtremalIndices.size() < mSpecification.getExtremaCount()) + { + throw new FilterDesignException("Couldn't find the minimum extremal frequencies in " + + "error set before checking for alternation theory - found:" + mExtremalIndices.size() + + " required:" + mSpecification.getExtremaCount()); + } + + //Enforce alternation theory -- only one extremal for each transition about the zero axis + List indicesToRemove = new ArrayList<>(); + + Iterator it = mExtremalIndices.iterator(); + Integer current = it.next(); + Integer next; + + boolean positiveAxis = mGridErrors[current] > 0.0; + + while(it.hasNext()) + { + next = it.next(); + + //Check for consecutive (redundant) indices on same side of zero axis - retain largest + if(!(positiveAxis ^ (mGridErrors[next] > 0.0))) + { + if(Math.abs(mGridErrors[next]) <= Math.abs(mGridErrors[current])) + { + //Remove next error index that is less than current index and on + it.remove(); + next = current; + } + else + { + //Since we can't remove the current index with the iterator, queue for later removal + indicesToRemove.add(current); + } + } + else + { + positiveAxis = !positiveAxis; + } + + current = next; + } + + //Remove redundant extremal indices that couldn't be removed via the iterator + mExtremalIndices.removeAll(indicesToRemove); + + //Truncate the list to one larger than needed by removing excess tailing indices + while(mExtremalIndices.size() > mSpecification.getExtremaCount()) + { + mExtremalIndices.remove(mExtremalIndices.size() - 1); + } + + //If we have one too many indices, delete the smaller of the first or last + if(mExtremalIndices.size() > mSpecification.getExtremaCount()) + { + int lastIndex = mExtremalIndices.size() - 1; + + if(Math.abs(mGridErrors[mExtremalIndices.get(0)]) > + Math.abs(mGridErrors[mExtremalIndices.get(lastIndex)])) + { + mExtremalIndices.remove(lastIndex); + } + else + { + mExtremalIndices.remove(0); + } + } + + //Detect if we have too few indices + if(mExtremalIndices.size() < mSpecification.getExtremaCount()) + { + throw new FilterDesignException("Couldn't find the minimum extremal frequencies in " + + "error set - found:" + mExtremalIndices.size() + " required:" + mSpecification.getExtremaCount()); + } + } + + /** + * Indicates if the absolute value of the argument is greater than or equal to the delta + * value with accuracy to 14 digits of precision. This avoids rounding errors at 15 digits or + * precision or greater. + * + * @param value to evaluate + * @return true if the absolute value is greater than or equal to the absolute delta value + */ + private boolean isGTEDelta(double value) + { + return Math.abs(value) - Math.abs(mDelta) > -1.0e-5; + } + + /** + * Checks for convergence of the frequency response of the current set of extremal indices to + * the filter specification by comparing the maximum absolute error value against the delta + * value to determine if these two values are within the convergence threshold. + */ + public void checkConvergence() + { + double maximum = Math.abs(mGridErrors[mExtremalIndices.get(0)]); + + for(int i = 1; i < mExtremalIndices.size(); i++) + { + double current = Math.abs(mGridErrors[mExtremalIndices.get(i)]); + + if(current > maximum) + { + maximum = current; + } + } + + double convergence = maximum - Math.abs(mDelta); + + mLog.debug("Convergence: " + convergence); + + mConverged = convergence < CONVERGENCE_THRESHOLD; + } + + /** + * Coverts/casts the double array to a float array + */ + private static float[] convertToFloatArray(double[] samples) + { + float[] converted = new float[samples.length]; + + for(int x = 0; x < samples.length; x++) + { + converted[x] = (float)samples[x]; + } + + return converted; + } + + /** + * Resamples the designed filter at half of the filter length at evenly spaced intervals of the + * frequency spectrum from 0 to PI radians. + * + * @return frequency response of the filter sampled the desired filter length and order + */ + private double[] resample() + { + int length = mSpecification.getFilterLength(); + + if(length % 2 == 0) + { + length--; + } + + double half = (double)length / 2.0; + + double[] resampled = new double[(int)Math.ceil(half)]; + + for(int x = 0; x < resampled.length; x++) + { + resampled[x] = getFrequencyResponse(Math.cos(Math.PI * (double)x / half)); + } + + return resampled; + } + + /** + * Applies Parks/McClelland filter type correction to the final frequency response array + * + * @param frequencyResponse of the resampled filter design from 0 <> ~ PI radians + * @return corrected frequency response + */ + private double[] correctFrequencyResponse(double[] frequencyResponse) + { + double filterLength = mSpecification.getFilterLength(); + + switch(mSpecification.getFilterType()) + { + case TYPE_1_ODD_LENGTH_EVEN_ORDER_SYMMETRICAL: + //No correction needed + break; + case TYPE_2_EVEN_LENGTH_ODD_ORDER_SYMMETRICAL: + //No correction needed + break; + case TYPE_3_ODD_LENGTH_EVEN_ORDER_ANTI_SYMMETRICAL: + for(int x = 0; x < frequencyResponse.length; x++) + { + double frequencyRadians = Math.PI * (((double)x) / filterLength); + frequencyResponse[x] *= Math.sin(TWO_PI * frequencyRadians); + } + break; + case TYPE_4_EVEN_LENGTH_ODD_ORDER_ANTI_SYMMETRICAL: + for(int x = 0; x < frequencyResponse.length; x++) + { + double frequencyRadians = Math.PI * (((double)x) / filterLength); + frequencyResponse[x] *= Math.sin(Math.PI * frequencyRadians); + } + break; + default: + break; + } + + return frequencyResponse; + } +} From 131290fcd28dbcb10de90a7a3ca46b97dc12aa3a Mon Sep 17 00:00:00 2001 From: Denny Date: Sat, 11 Mar 2017 06:08:23 -0500 Subject: [PATCH 08/87] WIP - experiments --- src/dsp/filter/design/FilterViewer.java | 23 ++++++++++++++---- .../PolyphaseChannelizerFilterFactory.java | 24 +++++++------------ 2 files changed, 27 insertions(+), 20 deletions(-) diff --git a/src/dsp/filter/design/FilterViewer.java b/src/dsp/filter/design/FilterViewer.java index 9bdf1ac0..2ea1f536 100644 --- a/src/dsp/filter/design/FilterViewer.java +++ b/src/dsp/filter/design/FilterViewer.java @@ -1,6 +1,7 @@ package dsp.filter.design; import dsp.filter.FilterFactory; +import dsp.filter.Window; import dsp.filter.fir.FIRFilterSpecification; import dsp.filter.fir.remez.PolyphaseChannelizerFilterFactory; import dsp.filter.fir.remez.RemezFIRFilterDesigner; @@ -25,11 +26,20 @@ public class FilterViewer extends Application @Override public void start(Stage primaryStage) throws Exception { - Scene scene = new Scene(new FilterView(getFilter())); + float[] taps = getFilter(); - primaryStage.setTitle("Filter Viewer"); - primaryStage.setScene(scene); - primaryStage.show(); + if(taps != null) + { + Scene scene = new Scene(new FilterView(taps)); + + primaryStage.setTitle("Filter Viewer"); + primaryStage.setScene(scene); + primaryStage.show(); + } + else + { + throw new Exception("Couldn't start application - filter error"); + } } public static void main(String[] args) @@ -93,7 +103,10 @@ public class FilterViewer extends Application // mLog.error("Filter design error", fde); // } - taps = PolyphaseChannelizerFilterFactory.getFilter(10000000, 312500, 0.21); +// taps = PolyphaseChannelizerFilterFactory.getFilter(2000000, 12500, 0.3); + + taps = FilterFactory.getSinc(10000000, 12500, 450, Window.WindowType.HANNING); + return taps; } } diff --git a/src/dsp/filter/fir/remez/PolyphaseChannelizerFilterFactory.java b/src/dsp/filter/fir/remez/PolyphaseChannelizerFilterFactory.java index 7e21f6fa..6b0ea0ff 100644 --- a/src/dsp/filter/fir/remez/PolyphaseChannelizerFilterFactory.java +++ b/src/dsp/filter/fir/remez/PolyphaseChannelizerFilterFactory.java @@ -30,10 +30,10 @@ public class PolyphaseChannelizerFilterFactory public static float[] getFilter(int sampleRate, int channelBandwidth, double alpha) { - FIRLinearPhaseFilterType type = FIRLinearPhaseFilterType.TYPE_1_ODD_LENGTH_EVEN_ORDER_SYMMETRICAL; + FIRLinearPhaseFilterType type = FIRLinearPhaseFilterType.TYPE_2_EVEN_LENGTH_ODD_ORDER_SYMMETRICAL; int passBandStart = 0; - int gridDensity = 50; + int gridDensity = 60; int rolloffFrequency = (int)(alpha * (double)channelBandwidth); @@ -43,13 +43,16 @@ public class PolyphaseChannelizerFilterFactory int stopBandStart = channelBandwidth + rolloffFrequency; double passRipple = 0.01d; + double transitionRipple = 0.02d; double stopRipple = 0.001d; //Approximately 90 dB of attenuation int order = FIRFilterSpecification.estimateFilterOrder( sampleRate, passBandStop, stopBandStart, passRipple, stopRipple ); - // Ensure even order since we're designing a Type 1 filter - if(order % 2 == 1) +// order = 1001; + + // Ensure odd order since we're designing a Type 2 filter + if(order % 2 == 0) { order++; } @@ -62,10 +65,10 @@ public class PolyphaseChannelizerFilterFactory passBandStop, 1.0, passRipple, 1.0); //Use the filter order as the weighting for the pass band edge frequency - double weight = order; +// double weight = order; FIRFilterSpecification.FrequencyBand transitionBand = new FIRFilterSpecification.FrequencyBand(sampleRate, - channelBandwidth, channelBandwidth, OBJECTIVE_BAND_EDGE_COEFFICIENT_AMPLITUDE, passRipple, weight); + channelBandwidth, channelBandwidth, OBJECTIVE_BAND_EDGE_COEFFICIENT_AMPLITUDE, transitionRipple, 5.0); FIRFilterSpecification.FrequencyBand stopBand = new FIRFilterSpecification.FrequencyBand(sampleRate, stopBandStart, (int)(sampleRate / 2), 0.0, stopRipple); @@ -91,13 +94,4 @@ public class PolyphaseChannelizerFilterFactory return filter; } - - public static void main(String[] args) - { - mLog.debug("Starting ..."); - - float[] filter = getFilter(200000, 12500, 0.21); - - mLog.debug("Finished ..."); - } } From 7d5371db4c3aeef86edb0dbade690c01c0b600d3 Mon Sep 17 00:00:00 2001 From: Denny Date: Mon, 20 Mar 2017 06:12:15 -0400 Subject: [PATCH 09/87] Updates build to 0.3.0 Beta 9 --- .idea/dictionaries/denny.xml | 1 + src/dsp/filter/FilterFactory.java | 344 +++++----- src/dsp/filter/Window.java | 602 ++++++++++++------ .../filter/channelizer/ChannelizerViewer.java | 47 +- .../channelizer/PolyphaseChannelizer2.java | 11 +- src/dsp/filter/design/FilterView.java | 273 ++++---- src/dsp/filter/design/FilterViewer.java | 44 +- src/module/decode/p25/P25_LSMDecoder.java | 2 +- .../audio/DemodulatedAudioFilterModule.java | 2 +- src/spectrum/DFTProcessor.java | 2 +- 10 files changed, 771 insertions(+), 557 deletions(-) diff --git a/.idea/dictionaries/denny.xml b/.idea/dictionaries/denny.xml index 223a1c26..015bac87 100644 --- a/.idea/dictionaries/denny.xml +++ b/.idea/dictionaries/denny.xml @@ -7,6 +7,7 @@ fleetsync icecast ident + nutall overflowable polyphase remez diff --git a/src/dsp/filter/FilterFactory.java b/src/dsp/filter/FilterFactory.java index 15b673f2..6486a758 100644 --- a/src/dsp/filter/FilterFactory.java +++ b/src/dsp/filter/FilterFactory.java @@ -1,24 +1,20 @@ /******************************************************************************* - * SDR Trunk - * Copyright (C) 2014,2015 Dennis Sheirer + * sdrtrunk + * Copyright (C) 2014-2017 Dennis Sheirer * - * Root Raised Cosine filter designer: - * Copyright 2002,2007,2008,2012,2013 Free Software Foundation, Inc. - * http://gnuradio.org/redmine/projects/gnuradio/repository/changes/gr-filter - * /lib/firdes.cc?rev=435b1d166f0c7092bbd5e1f788e75dbb6ade3a4b + * 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 * - * You should have received a copy of the GNU General Public License - * along with this program. If not, see ******************************************************************************/ package dsp.filter; @@ -31,14 +27,12 @@ import org.jtransforms.fft.FloatFFT_1D; import org.slf4j.Logger; import org.slf4j.LoggerFactory; -import java.util.Arrays; import java.util.Set; import java.util.TreeSet; public class FilterFactory { - private final static Logger mLog = - LoggerFactory.getLogger(FilterFactory.class); + private final static Logger mLog = LoggerFactory.getLogger(FilterFactory.class); /** * Generates coefficients for a unity-gain, windowed low-pass filter @@ -49,43 +43,49 @@ public class FilterFactory * @param window - to apply against the coefficients * @return */ - public static float[] getSinc(int sampleRate, - long frequency, - int length, - WindowType window) + public static float[] getSinc(int sampleRate, long frequency, int length, WindowType window) { - //Ensure we have an odd length - assert (length % 2 == 0); + int evenLength = length % 2 == 0 ? length : length + 1; //Get unity response array (one element longer to align with IDFT size) - float[] frequencyResponse = getUnityResponseArray(sampleRate, frequency, length + 1); + float[] frequencyResponse = getUnityResponseArray(sampleRate, frequency, evenLength); //Apply Inverse DFT against frequency response unity values, leaving the //IDFT bin results in the frequency response array - FloatFFT_1D idft = new FloatFFT_1D(length + 1); - idft.realInverseFull(frequencyResponse, false); + FloatFFT_1D idft = new FloatFFT_1D(evenLength); + idft.realInverseFull(frequencyResponse, true); //Transfer the IDFT results to the return array float[] coefficients = new float[length]; - int middleCoefficient = (int) (length / 2); + int middleCoefficient = (int)(length / 2); - //Bin 0 of the idft is our center coefficient - coefficients[middleCoefficient] = frequencyResponse[0]; - - //The remaining idft bins from 1 to (middle - 1) are the mirror image - //coefficients - for(int x = 1; x < middleCoefficient; x++) + if(length % 2 == 0) //Even length { - coefficients[middleCoefficient + x] = frequencyResponse[2 * x]; - coefficients[middleCoefficient - x] = frequencyResponse[2 * x]; + //The remaining idft bins from 1 to (middle - 1) are the mirror image + //coefficients + for(int x = 0; x < middleCoefficient; x++) + { + coefficients[middleCoefficient + x] = frequencyResponse[2 * x]; + coefficients[middleCoefficient - x] = frequencyResponse[2 * x]; + } + } + else + { + //Bin 0 of the idft is our center coefficient + coefficients[middleCoefficient] = frequencyResponse[0]; + + //The remaining idft bins from 1 to (middle - 1) are the mirror image + //coefficients + for(int x = 1; x < middleCoefficient; x++) + { + coefficients[middleCoefficient + x] = frequencyResponse[2 * x]; + coefficients[middleCoefficient - x] = frequencyResponse[2 * x]; + } } //Apply the window against the coefficients coefficients = Window.apply(window, coefficients); - //Normalize to unity (1) gain - coefficients = normalize(coefficients); - return coefficients; } @@ -96,7 +96,7 @@ public class FilterFactory * @param coefficients * @return */ - private static float[] normalize(float[] coefficients) + public static float[] normalize(float[] coefficients) { float accumulator = 0; @@ -129,17 +129,15 @@ public class FilterFactory * @param length * @return */ - public static float[] getUnityResponseArray(int sampleRate, - long frequency, - int length) + public static float[] getUnityResponseArray(int sampleRate, long frequency, int length) { float[] unityArray = new float[length * 2]; - int binCount = (int) ((Math.round( - (float) frequency / (float) sampleRate * (float) length))); if(length % 2 == 0) //even length { + int binCount = (int)((Math.round((float)frequency / (float)sampleRate * (float)length))); + for(int x = 0; x < binCount; x++) { unityArray[x] = 1.0f; @@ -148,9 +146,11 @@ public class FilterFactory } else //odd length { + int binCount = (int)((Math.round((float)frequency / (float)sampleRate * (float)length + 0.5))); + unityArray[0] = 1.0f; - for(int x = 1; x <= binCount; x++) + for(int x = 1; x < binCount; x++) { unityArray[x] = 1.0f; unityArray[length - x] = 1.0f; @@ -160,25 +160,6 @@ public class FilterFactory return unityArray; } - public static float[] getSine(float sampleRate, float frequency, int length) - { - float[] retVal = new float[length]; - - float radianFrequency = (float) (2.0d * Math.PI * (frequency / sampleRate)); - - int middle = (int) (length / 2); - - for(int x = 0; x < middle; x++) - { - float val = (float) Math.sin(radianFrequency * x); - - retVal[middle + x] = val; - retVal[middle - x] = -val; - } - - return retVal; - } - /** * Applies a repeating sequence of 1, -1 to the coefficients to invert * the frequency response of the filter. Used in converting a low-pass @@ -212,25 +193,9 @@ public class FilterFactory * @param windowType - window to apply against the generated coefficients * @return */ - public static float[] getLowPass(int sampleRate, - long cutoff, - int filterLength, - WindowType windowType) + public static float[] getLowPass(int sampleRate, long cutoff, int filterLength, WindowType windowType) { - if(filterLength % 2 == 0) //even length - { - float[] values = getSinc(sampleRate, cutoff, filterLength + 2, windowType); - - //throw away the 0 index and the last index - return Arrays.copyOfRange(values, 1, values.length - 2); - } - else - { - float[] values = getSinc(sampleRate, cutoff, filterLength + 1, windowType); - - //throw away the 0 index - return Arrays.copyOfRange(values, 1, values.length); - } + return getSinc(sampleRate, cutoff, filterLength, windowType); } /** @@ -247,26 +212,17 @@ public class FilterFactory * - passFrequency < stopFrequency * - stopFrequency <= sampleRate/2 */ - public static float[] getLowPass(int sampleRate, - int passFrequency, - int stopFrequency, - int attenuation, - WindowType windowType, - boolean forceOddLength) + public static float[] getLowPass(int sampleRate, int passFrequency, int stopFrequency, int attenuation, + WindowType windowType, boolean forceOddLength) { if(stopFrequency < passFrequency || stopFrequency > (sampleRate / 2)) { - throw new IllegalArgumentException("FilterFactory - low pass " - + "filter pass frequency [" + passFrequency + "] must be " - + "less than the stop frequency [" + stopFrequency + "] " - + "and must be less than half [" + (int) (sampleRate / 2) + - "] of the sample rate [" + sampleRate + "]"); + throw new IllegalArgumentException("FilterFactory - low pass filter pass frequency [" + passFrequency + + "] must be less than the stop frequency [" + stopFrequency + "] and must be less than half [" + + (int)(sampleRate / 2) + "] of the sample rate [" + sampleRate + "]"); } - int tapCount = getTapCount(sampleRate, - passFrequency, - stopFrequency, - attenuation); + int tapCount = getTapCount(sampleRate, passFrequency, stopFrequency, attenuation); if(forceOddLength) { @@ -300,10 +256,7 @@ public class FilterFactory //inversion, its at the correct frequency long convertedCutoff = sampleRate / 2 - cutoff; - return invert(getSinc(sampleRate, - convertedCutoff, - filterLength, - windowType)); + return invert(getSinc(sampleRate, convertedCutoff, filterLength, windowType)); } public static float[] getHighPass(int sampleRate, @@ -378,16 +331,16 @@ public class FilterFactory long stop, int attenuation) { - double frequency = ((double) stop - (double) pass) / (double) sampleRate; + double frequency = ((double)stop - (double)pass) / (double)sampleRate; - return (int) (Math.round((double) attenuation / (22.0d * frequency))); + return (int)(Math.round((double)attenuation / (22.0d * frequency))); } public static ComplexPrimeCICDecimate getDecimationFilter(int sampleRate, int decimatedRate, int order, int passFrequency, int attenuation, WindowType windowType) { - int decimationRate = (int) (sampleRate / decimatedRate); + int decimationRate = (int)(sampleRate / decimatedRate); return new ComplexPrimeCICDecimate(decimationRate, order, passFrequency, attenuation, windowType); @@ -415,7 +368,7 @@ public class FilterFactory + "integer multiple of sample rate"); } - int decimation = (int) (sampleRate / decimatedRate); + int decimation = (int)(sampleRate / decimatedRate); //Decimation rates below 20 will use single stage polyphase filter if(decimation < 20) @@ -448,7 +401,7 @@ public class FilterFactory { rates = new int[2]; - int stage2 = (int) (decimation / stage1); + int stage2 = (int)(decimation / stage1); if(stage1 > stage2) { @@ -499,11 +452,11 @@ public class FilterFactory private static Set getFactors(int value) { Set factors = new TreeSet(); - + /* Brute force */ for(int x = 1; x <= value; x++) { - int remainder = (int) (value / x); + int remainder = (int)(value / x); if(remainder * x == value) { @@ -530,25 +483,15 @@ public class FilterFactory * @return optimum integer decimation rate for the first stage decimation * filter */ - public static int getOptimalStageOneRate(int sampleRate, - int decimation, - long passFrequency, - long stopFrequency) + public static int getOptimalStageOneRate(int sampleRate, int decimation, long passFrequency, long stopFrequency) { double ratio = getBandwidthRatio(passFrequency, stopFrequency); - double numerator = 1.0d - ( - Math.sqrt((double) decimation * ratio / (2.0d - ratio))); + double numerator = 1.0d - (Math.sqrt((double)decimation * ratio / (2.0d - ratio))); double denominator = 2.0d - (ratio * (decimation + 1.0d)); - int retVal = (int) (2.0d * decimation * (numerator / denominator)); - -// mLog.info( "Optimal Stage 1 Decimation - rate [" + sampleRate + -// "] pass [" + passFrequency + -// "] bw ratio [" + ratio + -// "] optimal [" + retVal + -// "]" ); + int retVal = (int)(2.0d * decimation * (numerator / denominator)); return retVal; } @@ -564,8 +507,8 @@ public class FilterFactory { assert (passFrequency < stopFrequency); - return ((double) (stopFrequency - passFrequency) / - (double) stopFrequency); + return ((double)(stopFrequency - passFrequency) / + (double)stopFrequency); } /** @@ -576,14 +519,11 @@ public class FilterFactory * @param outputSampleRate * @return */ - public static float[] getCICCleanupFilter(int outputSampleRate, - int passFrequency, - int attenuation, - WindowType window) + public static float[] getCICCleanupFilter(int outputSampleRate, int passFrequency, int attenuation, WindowType window) { int taps = getTapCount(outputSampleRate, passFrequency, passFrequency + 1500, attenuation); - + /* Make tap count odd */ if(taps % 2 == 0) { @@ -600,7 +540,7 @@ public class FilterFactory //Transfer the IDFT results to the odd length return array float[] coefficients = new float[taps]; - int middleCoefficient = (int) (taps / 2); + int middleCoefficient = (int)(taps / 2); //Bin 0 of the idft is our center coefficient coefficients[middleCoefficient] = frequencyResponse[0]; @@ -621,26 +561,22 @@ public class FilterFactory return coefficients; } - public static float[] getCICResponseArray(int sampleRate, - int frequency, - int length) + public static float[] getCICResponseArray(int sampleRate, int frequency, int length) { float[] cicArray = new float[length * 2]; - int binCount = (int) ((Math.round( - (double) frequency / (double) sampleRate * 2.0d * (double) length))); + int binCount = (int)((Math.round( + (double)frequency / (double)sampleRate * 2.0d * (double)length))); cicArray[0] = 1.0f; - float unityResponse = (float) (Math.sin(1.0d / (double) length) / - (1.0d / (double) length)); + float unityResponse = (float)(Math.sin(1.0d / (double)length) / + (1.0d / (double)length)); for(int x = 1; x <= binCount; x++) { - /* Calculate unity response plus amplification for droop */ - float compensated = 1.0f + (unityResponse - - (float) (Math.sin((double) x / (double) length) / - ((double) x / (double) length))); + /* Calculate unity response plus amplification for droop */ + float compensated = 1.0f + (unityResponse - (float)(Math.sin((double)x / (double)length) / ((double)x / (double)length))); cicArray[x] = compensated; cicArray[length - x] = compensated; @@ -672,7 +608,6 @@ public class FilterFactory * @param samplesPerSymbol - number of samples per symbol * @param symbolCount - number of symbol intervals for the filter. This directly impacts the filter size * @param alpha - roll-off factor. - * * @return - filter coefficients */ public static float[] getRootRaisedCosine(int samplesPerSymbol, int symbolCount, float alpha) @@ -685,10 +620,10 @@ public class FilterFactory for(int x = 0; x < taps; x++) { - float index = (float) x - ((float) taps / 2.0f); + float index = (float)x - ((float)taps / 2.0f); - float x1 = (float) Math.PI * index / (float) samplesPerSymbol; - float x2 = 4.0f * alpha * index / (float) samplesPerSymbol; + float x1 = (float)Math.PI * index / (float)samplesPerSymbol; + float x2 = 4.0f * alpha * index / (float)samplesPerSymbol; float x3 = x2 * x2 - 1.0f; float numerator, denominator; @@ -697,17 +632,15 @@ public class FilterFactory { if(x != taps / 2) { - numerator = (float) Math.cos((1.0 + alpha) * x1) + - (float) Math.sin((1.0f - alpha) * x1) / - (4.0f * alpha * index / (float) samplesPerSymbol); + numerator = (float)Math.cos((1.0 + alpha) * x1) + + (float)Math.sin((1.0f - alpha) * x1) / (4.0f * alpha * index / (float)samplesPerSymbol); } else { - numerator = (float) Math.cos((1.0f + alpha) * x1) + - (1.0f - alpha) * (float) Math.PI / (4.0f * alpha); + numerator = (float)Math.cos((1.0f + alpha) * x1) + (1.0f - alpha) * (float)Math.PI / (4.0f * alpha); } - denominator = x3 * (float) Math.PI; + denominator = x3 * (float)Math.PI; } else { @@ -720,14 +653,11 @@ public class FilterFactory x3 = (1.0f - alpha) * x1; x2 = (1.0f + alpha) * x1; - numerator = (float) (Math.sin(x2) * (1.0f + alpha) * Math.PI - - Math.cos(x3) * ((1.0 - alpha) * Math.PI * - (double) samplesPerSymbol) / (4 * alpha * index) + - Math.sin(x3) * (double) samplesPerSymbol * - (double) samplesPerSymbol / (4.0 * alpha * index * index)); + numerator = (float)(Math.sin(x2) * (1.0f + alpha) * Math.PI - Math.cos(x3) * ((1.0 - alpha) * Math.PI * + (double)samplesPerSymbol) / (4.0 * alpha * index) + Math.sin(x3) * (double)samplesPerSymbol * + (double)samplesPerSymbol / (4.0 * alpha * index * index)); - denominator = (float) (-32.0 * Math.PI * alpha * alpha * index / - (double) samplesPerSymbol); + denominator = (float)(-32.0 * Math.PI * alpha * alpha * index / (double)samplesPerSymbol); } coefficients[x] = 4.0f * alpha * numerator / denominator; @@ -764,4 +694,100 @@ public class FilterFactory return null; } + + /** + * Evaluates the filter to determine the magnitude response (dB) at the specified frequency. + * + * @param filter to evaluate + * @param frequency to calculate (0 <> 0.5) + * @return magnitude frequency response in decibels + */ + public static double evaluate(float[] filter, double frequency) + { + double real = 0.0; + double imag = 0.0; + + for(int x = 0; x < filter.length; x++) + { + real += filter[x] * Math.cos(Math.PI * frequency * (double)x); + imag += filter[x] * Math.sin(Math.PI * frequency * (double)x); + } + + return decibel(real, imag); + } + + public static double decibel(double real, double imag) + { + return (float)(10.0 * Math.log10(Math.pow(real, 2.0) + Math.pow(imag, 2.0))); + } + + public static float[] getChannelizer(int channelBandwidth, int channels, int tapsPerChannel, WindowType windowType) + { + int sampleRate = channelBandwidth * channels * 2; + int filterLength = channels * tapsPerChannel; + + double objective = -6.0; + int cutoff = channelBandwidth; + int increment = (int)((double)channelBandwidth * .1); + + float[] taps = null; + double response = 0.0; + + while(increment > 1) + { + taps = FilterFactory.getLowPass(sampleRate, cutoff, filterLength, windowType); + + double responseToCompare = FilterFactory.evaluate(taps, (double)channelBandwidth / (double)sampleRate ); + + mLog.debug("Comparing: " + responseToCompare + " Previous:" + response + " Cutoff:" + cutoff + " Increment:" + increment); + + double a = Math.abs(objective - responseToCompare); + double b = Math.abs(objective - response); + + if(a == b) + { + cutoff -= increment; + } + else if(a < b) + { + response = responseToCompare; + cutoff -= increment; + } + else + { + //reset the cutoff + if(cutoff < channelBandwidth) + { + cutoff += increment; + } + + increment /= 2; + + if(increment == 1) + { + taps = FilterFactory.getLowPass(sampleRate, cutoff, filterLength, windowType); + } + } + } + + if(response < objective) + { + mLog.debug("Checking ..."); + float[] tapsToCompare = FilterFactory.getLowPass(sampleRate, cutoff + 1, filterLength, windowType); + double responseToCompare = FilterFactory.evaluate(tapsToCompare, (double)channelBandwidth / (double)sampleRate ); + + if(Math.abs(objective - responseToCompare) < Math.abs(objective - response)) + { + mLog.debug("Using Cutoff:" + cutoff + 1); + taps = tapsToCompare; + } + else + { + mLog.debug("Staying at cutoff: " + cutoff + " And Response at 12500 of:" + response); + } + } + + + return taps; + } } diff --git a/src/dsp/filter/Window.java b/src/dsp/filter/Window.java index 72e846b4..b3d00845 100644 --- a/src/dsp/filter/Window.java +++ b/src/dsp/filter/Window.java @@ -1,234 +1,456 @@ /******************************************************************************* - * SDR Trunk - * Copyright (C) 2014 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 + * 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; +import org.slf4j.Logger; +import org.slf4j.LoggerFactory; + import java.util.Arrays; /** - * Window creator - * - * Blackman, Hamming and Hanning windows - * based on windowing methods described at: - * http://www.mstarlabs.com/dsp/goertzel/goertzel.html - * and - * Understanding Digital Signal Processing 3e, Lyons, p.91 - * and - * Digital Filters for Everyone, Allred, p. 132 - * + * Window factory and corresponding utility methods. */ public class Window { - public static double[] getWindow( WindowType type, int length ) + private final static Logger mLog = LoggerFactory.getLogger(Window.class); + + private static final double TWO_PI = Math.PI * 2.0d; + private static final double FOUR_PI = Math.PI * 4.0d; + private static final double SIX_PI = Math.PI * 6.0d; + private static final double EIGHT_PI = Math.PI * 8.0d; + + /** + * Creates a window of the specified type and length. + * + * @param type of window + * @param length of window + * @return window of the specified type and length + */ + public static double[] getWindow(WindowType type, int length) { - switch( type ) - { - case BLACKMAN: - return getBlackmanWindow( length ); - case COSINE: - return getCosineWindow( length ); - case HAMMING: - return getHammingWindow( length ); - case HANNING: - return getHanningWindow( length ); - case NONE: - default: - return getRectangularWindow( length ); - } + switch(type) + { + case BLACKMAN: + return getBlackman(length); + case BLACKMAN_HARRIS_4: + return getBlackmanHarris4(length); + case BLACKMAN_HARRIS_7: + return getBlackmanHarris7(length); + case BLACKMAN_NUTALL: + return getBlackmanNutall(length); + case COSINE: + return getCosine(length); + case FLAT_TOP: + return getFlatTop(length); + case HAMMING: + return getHamming(length); + case HANN: + return getHann(length); + case NUTALL: + return getNutall(length); + case NONE: + default: + return getRectangular(length); + } } - public static double[] getRectangularWindow( int length ) + /** + * Creates an all-pass or rectangular window. + * + * @param length of the window + * @return rectangular window + */ + public static double[] getRectangular(int length) { - double[] coefficients = new double[ length ]; + double[] coefficients = new double[length]; - Arrays.fill( coefficients, 1.0d ); - - return coefficients; - } - - public static double[] getCosineWindow( int length ) - { - double[] coefficients = new double[ length ]; + Arrays.fill(coefficients, 1.0d); - - if( length % 2 == 0 ) //Even length - { - int half = (int)( ( length - 1 ) / 2 ); - - for( int x = -half; x < length / 2 + 1; x++ ) - { - coefficients[ x + half ] = Math.cos( - ( (double)x * Math.PI ) / ( (double)length + 1.0d ) ); - } - } - else //Odd length - { - int half = (int) length / 2; - - for( int x = -half; x < half + 1; x++ ) - { - coefficients[ x + half ] = Math.cos( - ( (double)x * Math.PI ) / ( (double)length + 1.0d ) ); - } - } - - return coefficients; + return coefficients; } - public static double[] getBlackmanWindow( int length ) + /** + * Creates a cosine window of the specified length. + * + * @param length of the window + * @return cosine window + */ + public static double[] getCosine(int length) { - double[] coefficients = new double[ length ]; - - for( int x = 0; x < length; x++ ) - { - coefficients[ x ] = .426591D - - ( .496561D * Math.cos( ( Math.PI * 2.0D * (double)x ) / (double)( length - 1 ) ) + - ( .076848D * Math.cos( ( Math.PI * 4.0D * (double)x ) / (double)( length - 1 ) ) ) ); - } + double[] coefficients = new double[length]; - return coefficients; - } - - public static double[] getHammingWindow( int length ) - { - double[] coefficients = new double[ length ]; - - if( length % 2 == 0 ) //Even length - { - for( int x = 0; x < length; x++ ) - { - coefficients[ x ] = .54D - ( .46D * - Math.cos( ( Math.PI * ( 2.0D * (double)x + 1.0d ) ) / (double)( length - 1 ) ) ); - } - } - else //Odd length - { - for( int x = 0; x < length; x++ ) - { - coefficients[ x ] = .54D - ( .46D * - Math.cos( ( 2.0D * Math.PI * (double)x ) / (double)( length - 1 ) ) ); - } - } + if(length % 2 == 0) //Even length + { + int half = (int)((length - 1) / 2); - return coefficients; + for(int x = -half; x < length / 2 + 1; x++) + { + coefficients[x + half] = Math.cos( + ((double)x * Math.PI) / ((double)length + 1.0d)); + } + } + else //Odd length + { + int half = (int)length / 2; + + for(int x = -half; x < half + 1; x++) + { + coefficients[x + half] = Math.cos( + ((double)x * Math.PI) / ((double)length + 1.0d)); + } + } + + return coefficients; } - public static double[] getHanningWindow( int length ) + /** + * Creates a 3-term Blackman window. + * + * @param length of the window + * @return Blackman window + */ + public static double[] getBlackman(int length) { - double[] coefficients = new double[ length ]; + double[] coefficients = new double[length]; - if( length % 2 == 0 ) //Even length - { - for( int x = 0; x < length; x++ ) - { - coefficients[ x ] = .5D - ( .5D * - Math.cos( ( Math.PI * ( 2.0D * (double)x + 1 ) ) / (double)( length - 1 ) ) ); - } - } - else //Odd length - { - for( int x = 0; x < length; x++ ) - { - coefficients[ x ] = .5D - ( .5D * - Math.cos( ( 2.0D * Math.PI * (double)x ) / (double)( length - 1 ) ) ); - } - } + double denominator = length - 1; + + double a0 = 0.426590713672; + double a1 = 0.496560619089; + double a2 = 0.0768486672399; + + for(int x = 0; x < length; x++) + { + coefficients[x] = a0 - + (a1 * Math.cos((TWO_PI * (double)x) / denominator) + + (a2 * Math.cos((FOUR_PI * (double)x) / denominator))); + } + + return coefficients; + } + + /** + * Creates a 4-term Nutall window + * + * @param length of the window + * @return Nutall window + */ + public static double[] getNutall(int length) + { + double[] coefficients = new double[length]; + + double denominator = length - 1; + double a0 = 0.355768; + double a1 = 0.487396; + double a2 = 0.144232; + double a3 = 0.012604; + + for(int x = 0; x < length; x++) + { + coefficients[x] = a0 - + (a1 * Math.cos(TWO_PI * (double)x / denominator)) + + (a2 * Math.cos(FOUR_PI * (double)x / denominator)) - + (a3 * Math.cos(SIX_PI * (double)x / denominator)); + } + + return coefficients; + } + + /** + * Creates a 4-term Blackman-Nutall window + * + * @param length of the window + * @return Blackman-Nutall window + */ + public static double[] getBlackmanNutall(int length) + { + double[] coefficients = new double[length]; + + double denominator = length - 1; + double a0 = 0.3635819; + double a1 = 0.4891775; + double a2 = 0.1365995; + double a3 = 0.0106411; + + for(int x = 0; x < length; x++) + { + coefficients[x] = a0 - + (a1 * Math.cos(TWO_PI * (double)x / denominator)) + + (a2 * Math.cos(FOUR_PI * (double)x / denominator)) - + (a3 * Math.cos(SIX_PI * (double)x / denominator)); + } + + return coefficients; + } + + /** + * Creates a 4-term Blackman-Harris window + * + * @param length of the window + * @return Blackman-Harris window + */ + public static double[] getBlackmanHarris4(int length) + { + double[] coefficients = new double[length]; + + double denominator = length - 1; + double a0 = 0.35875; + double a1 = 0.48829; + double a2 = 0.14128; + double a3 = 0.01168; + + for(int x = 0; x < length; x++) + { + coefficients[x] = a0 - + (a1 * Math.cos(TWO_PI * (double)x / denominator)) + + (a2 * Math.cos(FOUR_PI * (double)x / denominator)) - + (a3 * Math.cos(SIX_PI * (double)x / denominator)); + } + + return coefficients; + } + + /** + * Creates a 7-term Blackman-Harris window + * + * @param length of the window + * @return Blackman-Harris window + */ + public static double[] getBlackmanHarris7(int length) + { + double[] coefficients = new double[length]; + + double denominator = length - 1; + + double a0 = 0.27105140069342; + double a1 = 0.43329793923448; + double a2 = 0.21812299954311; + double a3 = 0.06592544638803; + double a4 = 0.01081174209837; + double a5 = 0.00077658482522; + double a6 = 0.00001388721735; - return coefficients; + for(int x = 0; x < length; x++) + { + double w = TWO_PI * (double)x / denominator; + + coefficients[x] = a0 - + (a1 * Math.cos(w)) + + (a2 * Math.cos(2.0 * w)) - + (a3 * Math.cos(3.0 * w)) + + (a4 * Math.cos(4.0 * w)) - + (a5 * Math.cos(5.0 * w)) + + (a6 * Math.cos(6.0 * w)); + } + + return coefficients; + } + + /** + * Creates a Hamming window + * + * @param length of the window + * @return Hamming window + */ + public static double[] getHamming(int length) + { + double[] coefficients = new double[length]; + + if(length % 2 == 0) //Even length + { + for(int x = 0; x < length; x++) + { + coefficients[x] = .54D - (.46D * + Math.cos((Math.PI * (2.0D * (double)x + 1.0d)) / (double)(length - 1))); + } + } + else //Odd length + { + for(int x = 0; x < length; x++) + { + coefficients[x] = .54D - (.46D * + Math.cos((2.0D * Math.PI * (double)x) / (double)(length - 1))); + } + } + + return coefficients; + } + + /** + * Creates a Hann window + * + * @param length of the window + * @return Hann window + */ + public static double[] getHann(int length) + { + double[] coefficients = new double[length]; + + if(length % 2 == 0) //Even length + { + for(int x = 0; x < length; x++) + { + coefficients[x] = .5D - (.5D * + Math.cos((Math.PI * (2.0D * (double)x + 1)) / (double)(length - 1))); + } + } + else //Odd length + { + for(int x = 0; x < length; x++) + { + coefficients[x] = .5D - (.5D * + Math.cos((2.0D * Math.PI * (double)x) / (double)(length - 1))); + } + } + + + return coefficients; + } + + /** + * Creates a 4-term Blackman-Harris window + * + * @param length of the window + * @return Blackman-Harris window + */ + public static double[] getFlatTop(int length) + { + double[] coefficients = new double[length]; + + double denominator = length - 1; + + double a0 = 0.215578948; + double a1 = 0.41663158; + double a2 = 0.277263158; + double a3 = 0.083578947; + double a4 = 0.006947368; + + for(int x = 0; x < length; x++) + { + coefficients[x] = a0 - + (a1 * Math.cos(TWO_PI * (double)x / denominator)) + + (a2 * Math.cos(FOUR_PI * (double)x / denominator)) - + (a3 * Math.cos(SIX_PI * (double)x / denominator)) + + (a4 * Math.cos(EIGHT_PI * (double)x / denominator)); + } + + return coefficients; + } + + + private static double getKaiserBeta(double attenuation) + { + if(attenuation > 50.0) + { + return 0.1102 * (attenuation - 8.7); + } + else if(attenuation >= 21.0) + { + return (0.5842 * Math.pow(attenuation - 21.0, 0.4)) + (0.07886 * (attenuation - 21.0)); + } + else + { + return 0.0; + } } /** * Apply the window against an array of float-type samples */ - public static float[] apply( double[] coefficients, float[] samples ) + public static float[] apply(double[] coefficients, float[] samples) { - for( int x = 0; x < coefficients.length; x++ ) - { - samples[ x ] = (float)( samples[ x ] * coefficients[ x ] ); - } - - return samples; - } - - public static float[] apply( WindowType type, float[] samples ) - { - double[] coefficients = getWindow( type, samples.length ); - - return apply( coefficients, samples ); - } - - /** - * Apply the window against an array of float-type samples - */ - public static double[] apply( double[] coefficients, double[] samples ) - { - for( int x = 0; x < coefficients.length; x++ ) - { - samples[ x ] = samples[ x ] * coefficients[ x ]; - } - - return samples; + for(int x = 0; x < coefficients.length; x++) + { + samples[x] = (float)(samples[x] * coefficients[x]); + } + + return samples; } - public static double[] apply( WindowType type, double[] samples ) + /** + * Apply the window type against the float array of samples + * + * @param type of window to use + * @param samples to be windowed + * @return windowed samples + */ + public static float[] apply(WindowType type, float[] samples) { - double[] coefficients = getWindow( type, samples.length ); - - return apply( coefficients, samples ); + double[] coefficients = getWindow(type, samples.length); + + return apply(coefficients, samples); } - - - + + /** + * Apply the window against an array of double-type samples + */ + public static double[] apply(double[] coefficients, double[] samples) + { + for(int x = 0; x < coefficients.length; x++) + { + samples[x] = samples[x] * coefficients[x]; + } + + return samples; + } + + /** + * Apply the window type against the double array of samples + * + * @param type of window to use + * @param samples to be windowed + * @return windowed samples + */ + public static double[] apply(WindowType type, double[] samples) + { + double[] coefficients = getWindow(type, samples.length); + + return apply(coefficients, samples); + } + /** * Window types */ public enum WindowType { - BLACKMAN, COSINE, HAMMING, HANNING, NONE; - } - - /** - * Utility to log the double arrays - */ - public static String arrayToString( double[] array, boolean breaks ) - { - StringBuilder sb = new StringBuilder(); - for( int x = 0; x < array.length; x++ ) - { - sb.append( x + ":" + array[ x ] ); + BLACKMAN("Blackman"), + BLACKMAN_HARRIS_4("Blackman-Harris 4"), + BLACKMAN_HARRIS_7("Blackman-Harris 7"), + BLACKMAN_NUTALL("Blackman-Nutall"), + COSINE("Cosine"), + FLAT_TOP("Flat Top"), + HAMMING("Hamming"), + HANN("Hann"), + NUTALL("Nutall"), + NONE("None"); - if( breaks ) - { - sb.append( "\n" ); - } - else - { - if( x % 8 == 7 ) - { - sb.append( "\n" ); - } - else - { - sb.append( "\t" ); - } - } - } - - return sb.toString(); - } + private String mLabel; + + WindowType(String label) + { + mLabel = label; + } + + public String toString() + { + return mLabel; + } + } } diff --git a/src/dsp/filter/channelizer/ChannelizerViewer.java b/src/dsp/filter/channelizer/ChannelizerViewer.java index 9ad53ca1..65392538 100644 --- a/src/dsp/filter/channelizer/ChannelizerViewer.java +++ b/src/dsp/filter/channelizer/ChannelizerViewer.java @@ -18,7 +18,8 @@ ******************************************************************************/ package dsp.filter.channelizer; -import dsp.filter.FilterFactory; +import dsp.filter.*; +import dsp.filter.Window; import dsp.filter.design.FilterDesignException; import dsp.filter.fir.FIRFilterSpecification; import dsp.filter.fir.remez.RemezFIRFilterDesigner; @@ -184,48 +185,12 @@ public class ChannelizerViewer extends JFrame private float[] getFilter() { - int symbolRate = 4800; - int samplesPerSymbol = 2; - int symbolCount = 14; - - //Alpha is the residual channel bandwidth left over from the symbol rate and samples per symbol -// float alpha = ((float)CHANNEL_BANDWIDTH / (float)(symbolRate * samplesPerSymbol)) - 1.0f; - - float alpha = 0.8f; - -// float[] taps = FilterFactory.getRootRaisedCosine(samplesPerSymbol * mChannelCount, symbolCount, alpha); - float[] taps = null; - - FIRFilterSpecification specification = FIRFilterSpecification.lowPassBuilder() - .sampleRate(CHANNEL_BANDWIDTH * 2 * mChannelCount) - .gridDensity(16) - .passBandAmplitude(1.0) - .passBandCutoff(11900) - .passBandRipple(0.01) - .stopBandAmplitude(0.0) - .stopBandRipple(0.001) - .stopBandStart(13100) - .build(); - - try - { - RemezFIRFilterDesigner designer = new RemezFIRFilterDesigner(specification); - - if(designer.isValid()) - { - taps = designer.getImpulseResponse(); - } - } - catch(FilterDesignException fde) - { - mLog.error("Filter design error", fde); - } - + float[] taps = FilterFactory.getChannelizer(CHANNEL_BANDWIDTH, mChannelCount, 16, Window.WindowType.BLACKMAN_HARRIS_7); StringBuilder sb = new StringBuilder(); sb.append("\nPolyphase Channelizer\n"); sb.append("Sample Rate:" + mSampleRate + " Channels:" + mChannelCount + " Channel Rate:" + CHANNEL_BANDWIDTH + "\n"); - sb.append("Alpha: " + alpha + " Tap Count:" + taps.length + "\n"); + sb.append("Tap Count:" + taps.length + "\n"); sb.append("Channel:" + mToneFrequency); mLog.debug(sb.toString()); @@ -256,7 +221,7 @@ public class ChannelizerViewer extends JFrame { setLayout(new MigLayout("insets 0 0 0 0", "fill", "fill")); - int channelsPerRow = 16; + int channelsPerRow = 20; for(int x = 0; x < mChannelCount; x++) { @@ -346,7 +311,7 @@ public class ChannelizerViewer extends JFrame public static void main(String[] args) { - int channelCount = 4; + int channelCount = 10; final ChannelizerViewer frame = new ChannelizerViewer(channelCount); diff --git a/src/dsp/filter/channelizer/PolyphaseChannelizer2.java b/src/dsp/filter/channelizer/PolyphaseChannelizer2.java index a95a8e7e..5fd700c2 100644 --- a/src/dsp/filter/channelizer/PolyphaseChannelizer2.java +++ b/src/dsp/filter/channelizer/PolyphaseChannelizer2.java @@ -19,6 +19,7 @@ package dsp.filter.channelizer; import dsp.filter.FilterFactory; +import dsp.filter.Window; import dsp.filter.fir.real.RealFIRFilter; import dsp.mixer.Oscillator; import org.jtransforms.fft.FloatFFT_1D; @@ -167,22 +168,18 @@ public class PolyphaseChannelizer2 implements Listener int symbolRate = 4800; int samplesPerSymbol = 2; - int symbolCount = 4; - int channels = 10; + int channels = 800; int channelBandwidth = 12500; int sampleRate = channels * channelBandwidth; - //Alpha is the residual channel bandwidth left over from the symbol rate and samples per symbol - float alpha = ((float)channelBandwidth / (float)(symbolRate * samplesPerSymbol)) - 1.0f; - - float[] taps = FilterFactory.getRootRaisedCosine(samplesPerSymbol * channels, symbolCount, alpha); + float[] taps = FilterFactory.getLowPass( 10000000, 12500, 5600, Window.WindowType.BLACKMAN_HARRIS_7 ); int channelCenter = (int)(12500 * 1.5); StringBuilder sb = new StringBuilder(); sb.append("\nPolyphase Channelizer\n"); sb.append("Sample Rate:" + sampleRate + " Channels:" + channels + " Channel Rate:" + channelBandwidth + "\n"); - sb.append("Alpha: " + alpha + " Tap Count:" + taps.length + "\n"); + sb.append("Tap Count:" + taps.length + "\n"); sb.append("Channel:" + channelCenter); mLog.debug(sb.toString()); diff --git a/src/dsp/filter/design/FilterView.java b/src/dsp/filter/design/FilterView.java index 12191462..bfec7971 100644 --- a/src/dsp/filter/design/FilterView.java +++ b/src/dsp/filter/design/FilterView.java @@ -1,13 +1,5 @@ package dsp.filter.design; -import org.jtransforms.fft.FloatFFT_1D; -import org.slf4j.Logger; -import org.slf4j.LoggerFactory; - -import dsp.filter.FilterFactory; -import dsp.filter.Window.WindowType; -import dsp.filter.fir.FIRFilterSpecification; -import dsp.filter.fir.remez.RemezFIRFilterDesigner; import gui.control.CurveFittedAreaChart; import javafx.collections.ObservableList; import javafx.scene.chart.NumberAxis; @@ -16,156 +8,157 @@ import javafx.scene.chart.XYChart.Series; import javafx.scene.layout.BorderPane; import javafx.scene.layout.HBox; import javafx.scene.layout.Priority; +import org.jtransforms.fft.FloatFFT_1D; +import org.slf4j.Logger; +import org.slf4j.LoggerFactory; 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); + 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, 50.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 ); + mDFTChart.getStylesheets().add(css); - 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 series = new XYChart.Series<>(); - - for( int x = 0; x < taps.length; x++ ) + 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++) { - if( taps[ x ] < smallest ) - { - smallest = taps[ x ]; - } - - if( taps[ x ] > largest ) - { - largest = taps[ x ]; - } - - series.getData().add( new XYChart.Data( x, taps[ x ] ) ); + convertedTaps[x] = (float)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 ); + + setImpulseTaps(convertedTaps); + } + + public void setImpulseTaps(float[] taps) + { + double smallest = 0.0; + double largest = 0.0; + + final XYChart.Series 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(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> list = mImpulseChart.getData(); - list.add( series ); - } - - public void setDFTTaps( float[] taps ) - { - double smallest = 0.0; - - final XYChart.Series 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( x, taps[ x ] ) ); - } - - ObservableList> 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 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(x, taps[x])); + } + + ObservableList> 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] = 10.0f * (float)Math.log10((dft[index] * dft[index]) + (dft[index + 1] * dft[index + 1])); + } + + return decibels; } } diff --git a/src/dsp/filter/design/FilterViewer.java b/src/dsp/filter/design/FilterViewer.java index 2ea1f536..8a6a8f91 100644 --- a/src/dsp/filter/design/FilterViewer.java +++ b/src/dsp/filter/design/FilterViewer.java @@ -6,12 +6,15 @@ import dsp.filter.fir.FIRFilterSpecification; import dsp.filter.fir.remez.PolyphaseChannelizerFilterFactory; import dsp.filter.fir.remez.RemezFIRFilterDesigner; import dsp.filter.fir.remez.RemezFIRFilterDesignerWithLagrange; +import filter.Filter; import javafx.application.Application; import javafx.scene.Scene; import javafx.stage.Stage; import org.slf4j.Logger; import org.slf4j.LoggerFactory; +import java.util.Arrays; + public class FilterViewer extends Application { private final static Logger mLog = LoggerFactory.getLogger(FilterViewer.class); @@ -85,27 +88,34 @@ public class FilterViewer extends Application float[] taps = null; -// taps = FilterFactory.getLowPass( 16000, 2400, 73, WindowType.HANNING ); +// int tapCount = FilterFactory.getTapCount(10000000, 12500, 18750, 80); +// taps = FilterFactory.getLowPass( 10000000, 12500, 5600, Window.WindowType.BLACKMAN_HARRIS_7 ); - -// try -// { -// RemezFIRFilterDesignerWithLagrange designer = new RemezFIRFilterDesignerWithLagrange(specification); -//// RemezFIRFilterDesigner designer = new RemezFIRFilterDesigner(specification); +// int channels = 10; +// int channelRate = 12500; +// int cutoff = 8000; +// int sampleRate = channelRate * channels; +// int filterLength = channels * 7; // -// if(designer.isValid()) -// { -// taps = designer.getImpulseResponse(); -// } -// } -// catch(FilterDesignException fde) -// { -// mLog.error("Filter design error", fde); -// } +// taps = FilterFactory.getLowPass(sampleRate, cutoff, filterLength, Window.WindowType.BLACKMAN_HARRIS_7); +// +// double response = FilterFactory.evaluate(taps, (double)channelRate / (double)sampleRate ); +// +// mLog.debug("Taps:" + taps.length + " With response at [" + channelRate + "]: " + response); -// taps = PolyphaseChannelizerFilterFactory.getFilter(2000000, 12500, 0.3); + int channelBandwidth = 12500; + int channels = 4; + int tapsPerChannel = 20; - taps = FilterFactory.getSinc(10000000, 12500, 450, Window.WindowType.HANNING); + taps = FilterFactory.getChannelizer(channelBandwidth, channels, tapsPerChannel, Window.WindowType.BLACKMAN_HARRIS_4); + + double sampleRate = channelBandwidth * channels * 2; + + for(int x = 0; x < 25000; x += 100) + { + double toEval = (double)x / sampleRate; + mLog.debug("Response at [" + x + "] is:" + FilterFactory.evaluate(taps, toEval)); + } return taps; } diff --git a/src/module/decode/p25/P25_LSMDecoder.java b/src/module/decode/p25/P25_LSMDecoder.java index 011ff83d..4f86d2c8 100644 --- a/src/module/decode/p25/P25_LSMDecoder.java +++ b/src/module/decode/p25/P25_LSMDecoder.java @@ -69,7 +69,7 @@ public class P25_LSMDecoder extends P25Decoder implements IComplexBufferListener super( aliasList ); mBasebandFilter = new ComplexFIRFilter_CB_CB( FilterFactory.getLowPass( - 48000, 7250, 8000, 60, WindowType.HANNING, true ), 1.0f ); + 48000, 7250, 8000, 60, WindowType.HANN, true ), 1.0f ); mBasebandFilter.setListener( mStreamConverter ); diff --git a/src/module/demodulate/audio/DemodulatedAudioFilterModule.java b/src/module/demodulate/audio/DemodulatedAudioFilterModule.java index 4efb3d8b..a28ff3fd 100644 --- a/src/module/demodulate/audio/DemodulatedAudioFilterModule.java +++ b/src/module/demodulate/audio/DemodulatedAudioFilterModule.java @@ -53,7 +53,7 @@ public class DemodulatedAudioFilterModule extends Module implements IUnFilteredR //TODO: change this to a band pass filter mBandPassFilter = new RealFIRFilter_RB_RB(FilterFactory.getLowPass(48000, pass, stop, 60, - WindowType.HANNING, true), 1.0f); + WindowType.HANN, true), 1.0f); mDCFilter.setListener(mBandPassFilter); } diff --git a/src/spectrum/DFTProcessor.java b/src/spectrum/DFTProcessor.java index 795c3f2b..0f9e066c 100644 --- a/src/spectrum/DFTProcessor.java +++ b/src/spectrum/DFTProcessor.java @@ -70,7 +70,7 @@ public class DFTProcessor implements Listener, IFrequencyChangePr /* The Cosine and Hanning windows seem to offer the best spectral display * with minimal bin leakage/smearing */ - private WindowType mWindowType = Window.WindowType.HANNING; + private WindowType mWindowType = WindowType.BLACKMAN_HARRIS_7; private FloatFFT_1D mFFT = new FloatFFT_1D(mDFTSize.getSize()); From 0e1da8fb3b72a13e31603af09d2174e40aad0d96 Mon Sep 17 00:00:00 2001 From: Denny Date: Thu, 23 Mar 2017 11:31:15 +0000 Subject: [PATCH 10/87] Updates intellij files and includes missing library --- .idea/libraries/commons_math3_3_6_1.xml | 9 -------- .idea/libraries/imports.xml | 11 ++++++++++ ...ynchttpclient_async_http_client_2_0_18.xml | 20 ------------------ imports/commons-math3-3.6.1.jar | Bin 0 -> 2213560 bytes sdrtrunk.iml | 12 ----------- 5 files changed, 11 insertions(+), 41 deletions(-) delete mode 100644 .idea/libraries/commons_math3_3_6_1.xml delete mode 100644 .idea/libraries/org_asynchttpclient_async_http_client_2_0_18.xml create mode 100644 imports/commons-math3-3.6.1.jar diff --git a/.idea/libraries/commons_math3_3_6_1.xml b/.idea/libraries/commons_math3_3_6_1.xml deleted file mode 100644 index b692344c..00000000 --- a/.idea/libraries/commons_math3_3_6_1.xml +++ /dev/null @@ -1,9 +0,0 @@ - - - - - - - - - \ No newline at end of file diff --git a/.idea/libraries/imports.xml b/.idea/libraries/imports.xml index 91ffaa83..3be82b35 100644 --- a/.idea/libraries/imports.xml +++ b/.idea/libraries/imports.xml @@ -28,6 +28,17 @@ + + + + + + + + + + + diff --git a/.idea/libraries/org_asynchttpclient_async_http_client_2_0_18.xml b/.idea/libraries/org_asynchttpclient_async_http_client_2_0_18.xml deleted file mode 100644 index 884ea3f9..00000000 --- a/.idea/libraries/org_asynchttpclient_async_http_client_2_0_18.xml +++ /dev/null @@ -1,20 +0,0 @@ - - - - - - - - - - - - - - - - - - - - \ No newline at end of file diff --git a/imports/commons-math3-3.6.1.jar b/imports/commons-math3-3.6.1.jar new file mode 100644 index 0000000000000000000000000000000000000000..0ff582cfcb687e5c9608457628f78e9a782be932 GIT binary patch literal 2213560 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

@` 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 xwX}1zf${}}E^kZR8=4F=SPA8Q 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
dl!~PI5`>*YCZ>1f}>^tB{ZM`Rx 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 z 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
9(D3_o9ZxzWG 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

H~ z(`dDKYT&X+EZ%N{gTpAHC5T1i+BDoYrf=2^1LNzmV2|tEx5?#UVw3?^8Sh(bI^F;a z_->}QouhtN|G?+GtDwZrgv??NVxB%gyS!VI+e2cvWxxAXRB|$v+osAFx{ui^+&5OX49G`to=SJNwh!u3^nO zMMgJ)r0B&z15R?2L`NFF9NroY$h<(Ex4iSm_aepmzJCph^kH6CS0Hj>CQ$jibjWoP zq2jl`k<|#{TWHmS>-u^+Y?V#+T+c@#&YzgSZade2Cg<=UMmIb+IZnPOn_j%vTWowh z+#vW7WS{1PqI{+Xc>#-gnA9xLuMAO9ctoS;wV$^J%Hcm0+Y>PoP-Ls-*eOoairCE0R*;tSRv8-KiC8S#3)($JBhxUAnT4i~^jkTN zVxpJU-9CfV(9?D^@H>-DdRrUWq(+O=S|AhC(-eR-$jTb8$*#>a?gKca8hrgsJ#-J1KU4=e91t}BV9 zfd)Pp*czit7;Bm{)TaxxcV8`qq!?Wt)e(F^lt|L70=R8D8!wyc%b2qt?HjaH3GPFr z5_0xTG(fn#vy&;IDGQ32y9U_za&sD)jEFRjRYZCKPa9bOER_citN3(EdrVE9=8erl z7eR$nBT>rrc}$tP;BsCg^V?gLu}^a*7ecAD_wCiLv>$8R!KP$-5Hx=+4Mf zj=#}S74OU{XBnJKM#s@wIjP9njn|vXAXmF9?M1O-f2S?i_d_}by9K~SZp?)^xYwS} zV%NYJ{}STFh1~^fW&3?)x#}+lxEe0z@_rsNMwdnC(C#2vg6i?Wdu^S)|Cm9r%@~W zh1a709uI0XUon-uTCDX|&yIpc2HjQ?*S}f}_H?fFt@xbH=&5qt)yF=?fGbP%t zJ+FD?n8`YoqBYHjIvrtV$+e4AJ7J&W*QDH3x@J7a^r{TOfGmIsVRmr5A!SO!=C5j z7z}h^xaygh^t?Jg)(=vPI@RD&0<^*e7>hVkVLH&2#f;v}+(qUA#KRtu0lmOu;{}bzNXid3Rz>+7bbkiH5nXUoHC(A|f@7 z(gSsOrZuY_cl-laO57uxSyNM*XlA34SE_iy!s3z9v{u-S=#p6R?&7-$k;7nHAxB

(7ge%xRH?;Jf!4KrC;8M4p47aTpg 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ⅈ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⁡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%0Ao+;qZwLc6aUrE&+4YjZ!cnvc_Z4^5?OoRWZuM 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�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℞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

-$k&(>zOF)8B9`jI!WblQ+|0p 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 Z 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%0DItC%;(bC@ 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

ynrU&8RI-4oC9OLA73Y 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 *yOfXc#rLyB)`4I*Z1|Ja~>7Wp&Rm7 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

-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 literal 0 HcmV?d00001 diff --git a/sdrtrunk.iml b/sdrtrunk.iml index 0621246d..390626c0 100644 --- a/sdrtrunk.iml +++ b/sdrtrunk.iml @@ -6,21 +6,9 @@ - - - - - - - - - - - - \ No newline at end of file From 6d9e12908713babf72e8ebd70910ad7dcf0e8d28 Mon Sep 17 00:00:00 2001 From: Denny Date: Sun, 26 Mar 2017 10:23:38 -0400 Subject: [PATCH 11/87] WIP - adds windowed-sinc (nyquist) filter designer and polyphase channelizer filter designer to FilterFactory. Resolves a few issues with Window designers. ChannelizerViewer is now able to show 800 channels with 12.5 kHz channelization of a 10 MHz bandwidth input signal. --- src/dsp/filter/FilterFactory.java | 299 +++++++++++------- src/dsp/filter/Window.java | 47 +-- .../filter/channelizer/ChannelizerViewer.java | 80 ++--- src/dsp/filter/design/FilterView.java | 5 +- src/dsp/filter/design/FilterViewer.java | 55 ++-- 5 files changed, 267 insertions(+), 219 deletions(-) diff --git a/src/dsp/filter/FilterFactory.java b/src/dsp/filter/FilterFactory.java index 6486a758..0889cd68 100644 --- a/src/dsp/filter/FilterFactory.java +++ b/src/dsp/filter/FilterFactory.java @@ -30,9 +30,13 @@ import org.slf4j.LoggerFactory; import java.util.Set; import java.util.TreeSet; +/** + * Utility methods for designing various types of filters. + */ public class FilterFactory { private final static Logger mLog = LoggerFactory.getLogger(FilterFactory.class); + private static final double PERFECT_RECONSTRUCTION_GAIN_AT_BAND_EDGE = -6.020599842071533; //decibel(0.5) /** * Generates coefficients for a unity-gain, windowed low-pass filter @@ -246,10 +250,7 @@ public class FilterFactory * @param windowType - window to apply against the generated coefficients * @return */ - public static float[] getHighPass(int sampleRate, - long cutoff, - int filterLength, - WindowType windowType) + public static float[] getHighPass(int sampleRate, long cutoff, int filterLength, WindowType windowType) { //Convert the high frequency cutoff to its low frequency cutoff //equivalent, so that when we generate the low pass filter, prior to @@ -259,18 +260,11 @@ public class FilterFactory return invert(getSinc(sampleRate, convertedCutoff, filterLength, windowType)); } - public static float[] getHighPass(int sampleRate, - long stopFrequency, - long passFrequency, - int attenuation, - WindowType windowType, - boolean forceOddLength) + public static float[] getHighPass(int sampleRate, long stopFrequency, long passFrequency, int attenuation, + WindowType windowType, boolean forceOddLength) { /* reverse the stop and pass frequency to get the low pass variant */ - int tapCount = getTapCount(sampleRate, - stopFrequency, - passFrequency, - attenuation); + int tapCount = getTapCount(sampleRate, stopFrequency, passFrequency, attenuation); if(forceOddLength) { @@ -314,11 +308,10 @@ public class FilterFactory } /** - * Determines the number of fir filter taps required to produce the - * specified frequency response with passband ripple near .1dB. + * Determines the number of fir filter taps required to produce the specified frequency response with passband + * ripple near 0.1dB. * - * Implements the algorithm from Understanding Digital Signal Processing, 3e - * , Lyons, section 5.10.5. + * Implements the algorithm from Understanding Digital Signal Processing, 3e, Lyons, section 5.10.5. * * @param sampleRate in hertz * @param pass pass frequency in hertz @@ -326,19 +319,15 @@ public class FilterFactory * @param attenuation in dB * @return */ - public static int getTapCount(int sampleRate, - long pass, - long stop, - int attenuation) + public static int getTapCount(int sampleRate, long pass, long stop, int attenuation) { double frequency = ((double)stop - (double)pass) / (double)sampleRate; return (int)(Math.round((double)attenuation / (22.0d * frequency))); } - public static ComplexPrimeCICDecimate getDecimationFilter(int sampleRate, - int decimatedRate, int order, int passFrequency, int attenuation, - WindowType windowType) + public static ComplexPrimeCICDecimate getDecimationFilter(int sampleRate, int decimatedRate, int order, + int passFrequency, int attenuation, WindowType windowType) { int decimationRate = (int)(sampleRate / decimatedRate); @@ -355,10 +344,7 @@ public class FilterFactory * polyphase filter decimation chain * @throws - AssertionException if sample rate is not a multiple of 48 kHz */ - public static int[] getPolyphaseDecimationRates(int sampleRate, - int decimatedRate, - long passFrequency, - long stopFrequency) + public static int[] getPolyphaseDecimationRates(int sampleRate, int decimatedRate, long passFrequency, long stopFrequency) { int[] rates; @@ -386,11 +372,6 @@ public class FilterFactory int stage1 = findClosest(optimalStage1, factors); -// mLog.info( "Decimation rate [" + decimation + -// "] stage1 optimal [" + optimalStage1 + -// "] stage1 actual [" + stage1 + -// "]"); - if(stage1 == decimation || stage1 == 1) { rates = new int[1]; @@ -419,8 +400,7 @@ public class FilterFactory } /** - * Finds the factor that is closest to the desired factor, from an ordered - * list of factors. + * Finds the factor that is closest to the desired factor, from an ordered list of factors. */ private static int findClosest(int desiredFactor, Set factors) { @@ -442,9 +422,8 @@ public class FilterFactory } /** - * Determines the factors that make up an integer. Uses a brute force - * method to iterate all integers from 1 to value, determining which factors - * are evenly divisible into the value. + * Determines the factors that make up an integer. Uses a brute force method to iterate all integers from 1 to + * value, determining which factors are evenly divisible into the value. * * @param value - integer decimation value * @return - ordered set of factors for value @@ -468,14 +447,10 @@ public class FilterFactory } /** - * Determines the optimal decimation rate for stage 1 of a two-stage - * poly-phase decimation filter chain, to produce a final sample rate of - * 48 kHz using a pass bandwidth of 25 kHz. + * Determines the optimal decimation rate for stage 1 of a two-stage poly-phase decimation filter chain, to produce + * a final sample rate of 48 kHz using a pass bandwidth of 25 kHz. Use for total decimation rates of 20 or higher. * - * Use for total decimation rates of 20 or higher. - * - * Implements the algorithm described in Lyons, Understanding Digital Signal - * Processing, 3e, section 10.2.1, page 511. + * Implements the algorithm described in Lyons, Understanding Digital Signal Processing, 3e, section 10.2.1, page 511. * * @param sampleRate * @param decimation @@ -497,11 +472,8 @@ public class FilterFactory } /** - * Determines the F ratio as described in Lyons, Understanding Digital - * Signal Processing, 3e, section 10.2.1, page 511 - * - * Used in conjunction with the optimal stage one decimation rate method - * above. + * Determines the F ratio as described in Lyons, Understanding Digital Signal Processing, 3e, section 10.2.1, page 511 + * Used in conjunction with the optimal stage one decimation rate method above. */ private static double getBandwidthRatio(long passFrequency, long stopFrequency) { @@ -586,21 +558,17 @@ public class FilterFactory } /** - * Creates root raised cosine filter coefficients with a tap count equal - * to the symbols x samplesPerSymbol + 1. + * Creates root raised cosine filter coefficients with a tap count equal to the symbols x samplesPerSymbol + 1. * * Symbol count should be an even integer. - * * Ported to java from gnuradio/filter/firdes.cc * - * For 40db attenuation, calculate the number of symbols based on the - * following formula: - * - * Symbols = -44 * alpha + 33 + * For 40db attenuation, calculate the number of symbols based on the following formula: + * Symbols = -44 * alpha + 33 * * Polyphase Channelizer notes: * -Set samples Per Symbol at 2 (or more) * number of channels - * -Set symbolCount to sufficient size to produce requireded attenuation + * -Set symbolCount to sufficient size to produce required attenuation * -Set symbol rate in hertz * -Set alpha = (desired channel bandwidth / (symbol rate * samples per symbol) - 1.0 * -Channel bandwidth must be larger than symbol rate * samples per symbol, in order for there to be a positive alpha @@ -677,10 +645,10 @@ public class FilterFactory } /** - * Creates a filter from the filter specification + * Creates a filter from the filter specification using the remez exchange design algorithm * * @param specification - * @return + * @return filter coefficients * @throws FilterDesignException if the filter cannot be designed */ public static float[] getTaps(FIRFilterSpecification specification) throws FilterDesignException @@ -716,78 +684,195 @@ public class FilterFactory return decibel(real, imag); } + /** + * Calculates the decibel magnitude of the real and imaginary complex sample + * @param real (x-axis) value + * @param imag (y-axis) value + * @return magnitude in decibels + */ public static double decibel(double real, double imag) { return (float)(10.0 * Math.log10(Math.pow(real, 2.0) + Math.pow(imag, 2.0))); } - public static float[] getChannelizer(int channelBandwidth, int channels, int tapsPerChannel, WindowType windowType) + /** + * 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 + * adjusted to achieve a -6.02 dB attenuation at the channel band edge to enable perfect reconstruction of adjacent + * channels. + * + * The odd length sync filter that meets the design objectives will be increased in length by pre-padding a zero + * coefficient to produce a filter with a length that is an integral of the channel count. + * + * Note: the tapsPerChannel value will be iteratively increased (up to 10x) if a filter cannot be designed to meet + * the channel band edge attenuation objective. + * + * Note: a channelizer filter designed using a Blackman-Harris-7 window and 18-21 taps per channel creates a filter + * with attenuation in range of -60 to -80 dB at the edge of the 2x sampled channel. + * + * @param channelBandwidth - full bandwidth of the channel in hertz + * @param channels - number of channels + * @param tapsPerChannel - desired filter tap count per channel (may be increased to meet objective) + * @param windowType - to use when designing the windowed-sync filter + * @param logResults - to log a debug summary of the filter characteristics + * + * @return filter coefficients + * + * @throws FilterDesignException if a filter cannot be designed with a band edge attenuation of -6.02 dB + */ + public static float[] getChannelizer(int channelBandwidth, int channels, int tapsPerChannel, WindowType windowType, + boolean logResults) throws FilterDesignException { + int currentTapsPerChannel = tapsPerChannel; int sampleRate = channelBandwidth * channels * 2; - int filterLength = channels * tapsPerChannel; + int filterLength = (channels * currentTapsPerChannel) - 1; - double objective = -6.0; - int cutoff = channelBandwidth; - int increment = (int)((double)channelBandwidth * .1); + double cutoffFrequency = ((double)channelBandwidth / (double)sampleRate); + double bandEdge = cutoffFrequency; + double increment = cutoffFrequency * 0.1; - float[] taps = null; - double response = 0.0; + //Get an initial filter and band edge frequency response using the channel bandwidth as a cutoff + float[] taps = FilterFactory.getSinc(cutoffFrequency, filterLength, windowType); + double response = FilterFactory.evaluate(taps, bandEdge); - while(increment > 1) + //Set cutoff adjustment threshold - we'll test cutoff frequencies to around 1 hertz resolution + double incrementThreshold = 1.0 / (double)sampleRate; + + //Iteratively evaluate filters to find the filter with the highest cutoff frequency that meets the objective + while(increment > incrementThreshold) { - taps = FilterFactory.getLowPass(sampleRate, cutoff, filterLength, windowType); - - double responseToCompare = FilterFactory.evaluate(taps, (double)channelBandwidth / (double)sampleRate ); - - mLog.debug("Comparing: " + responseToCompare + " Previous:" + response + " Cutoff:" + cutoff + " Increment:" + increment); - - double a = Math.abs(objective - responseToCompare); - double b = Math.abs(objective - response); - - if(a == b) + //If the current cutoff meets the objective, test if a higher cutoff also meets the objective + if(matchesObjective(response, PERFECT_RECONSTRUCTION_GAIN_AT_BAND_EDGE) && + (cutoffFrequency + increment <= bandEdge)) { - cutoff -= increment; + float[] higherCutoffTaps = FilterFactory.getSinc(cutoffFrequency + increment, filterLength, windowType); + double higherCutoffResponse = FilterFactory.evaluate(higherCutoffTaps, bandEdge); + + if(matchesObjective(higherCutoffResponse, PERFECT_RECONSTRUCTION_GAIN_AT_BAND_EDGE)) + { + cutoffFrequency += increment; + taps = higherCutoffTaps; + response = higherCutoffResponse; + } + else + { + increment /= 2.0; + } } - else if(a < b) + //If the current cutoff meets the objective, decrease the increment to test for smaller resolution cutoffs + else if(matchesObjective(response, PERFECT_RECONSTRUCTION_GAIN_AT_BAND_EDGE)) { - response = responseToCompare; - cutoff -= increment; + increment /= 2.0; } + //Decrease the cutoff frequency - we can't meet the objective else { - //reset the cutoff - if(cutoff < channelBandwidth) + cutoffFrequency -= increment; + + if(cutoffFrequency <= 0) { - cutoff += increment; + if(logResults) + { + mLog.debug("Warning: cannot design channelizer filter with tap count [" + currentTapsPerChannel + + "] increasing tap count and starting over"); + } + + currentTapsPerChannel++; + + //After we've attempted increasing the taps per channel 10x, give up + if(currentTapsPerChannel > (tapsPerChannel + 10)) + { + throw new FilterDesignException("Couldn't design filter with taps per channel count in the " + + "range of " + tapsPerChannel + " - " + (tapsPerChannel + 10)); + } + + filterLength = channels * currentTapsPerChannel - 1; + cutoffFrequency = ((double)channelBandwidth / (double)sampleRate); + increment = cutoffFrequency * 0.1; } - increment /= 2; - - if(increment == 1) - { - taps = FilterFactory.getLowPass(sampleRate, cutoff, filterLength, windowType); - } + taps = FilterFactory.getSinc(cutoffFrequency, filterLength, windowType); + response = FilterFactory.evaluate(taps, bandEdge); } } - if(response < objective) + //This will probably never happen, but ... + if(!matchesObjective(response, PERFECT_RECONSTRUCTION_GAIN_AT_BAND_EDGE)) { - mLog.debug("Checking ..."); - float[] tapsToCompare = FilterFactory.getLowPass(sampleRate, cutoff + 1, filterLength, windowType); - double responseToCompare = FilterFactory.evaluate(tapsToCompare, (double)channelBandwidth / (double)sampleRate ); - - if(Math.abs(objective - responseToCompare) < Math.abs(objective - response)) - { - mLog.debug("Using Cutoff:" + cutoff + 1); - taps = tapsToCompare; - } - else - { - mLog.debug("Staying at cutoff: " + cutoff + " And Response at 12500 of:" + response); - } + throw new FilterDesignException("Cannot design filter to specifications"); } + if(logResults) + { + mLog.debug("Polyphase Channelizer Filter Design Summary"); + mLog.debug("-------------------------------------------"); + mLog.debug("Channels: " + channels); + mLog.debug("Taps Per Channel: " + tapsPerChannel); + mLog.debug("Filter Length: " + (taps.length + 1)); + mLog.debug("Channel Bandwidth: " + channelBandwidth); + mLog.debug("Cutoff Frequency: " + (sampleRate * cutoffFrequency)); + mLog.debug("Attenuation at 1.00 OBJECTIVE: " + PERFECT_RECONSTRUCTION_GAIN_AT_BAND_EDGE); + mLog.debug("Attenuation at 1.00 Channels: " + evaluate(taps, bandEdge * 1.00)); + mLog.debug("Attenuation at 1.25 Channels: " + evaluate(taps, bandEdge * 1.25)); + mLog.debug("Attenuation at 1.50 Channels: " + evaluate(taps, bandEdge * 1.50)); + mLog.debug("Attenuation at 1.75 Channels: " + evaluate(taps, bandEdge * 1.75)); + mLog.debug("Attenuation at 2.00 Channels: " + evaluate(taps, bandEdge * 2.00)); + } - return taps; + //This is an odd length filter - increase the length by 1 by pre-padding a zero coefficient + float[] extendedTaps = new float[taps.length + 1]; + System.arraycopy(taps, 0, extendedTaps, 1, taps.length); + + return extendedTaps; + } + + /** + * Creates a windowed-sync (Nyquist) filter. + * + * @param cutoff frequency (0.0 <> 0.5) + * @param length of the filter - must be odd-length + * @param windowType to use in designing the filter + * @return filter coefficients. + * @throws FilterDesignException if the requested length is not odd + */ + public static float[] getSinc(double cutoff, int length, WindowType windowType) throws FilterDesignException + { + if(length % 2 == 0) + { + throw new FilterDesignException("Sinc filters must be odd-length"); + } + + float[] coefficients = new float[length]; + int half = length / 2; + + double[] window = Window.getWindow(windowType, length); + + double scalor = 2.0 * cutoff; + double piScalor = Math.PI * scalor; + + coefficients[half] = (float)(1.0 * scalor); + + for(int x = 1; x <= half; x++) + { + double a = piScalor * x; + double coefficient = scalor * Math.sin(a) / a; + + coefficient *= window[half + x]; + coefficients[half + x] = (float)coefficient; + coefficients[half - x] = (float)coefficient; + } + + return coefficients; + } + + /** + * Compares two doubles for equals and avoids any rounding error that are present. + * @param a value to compare + * @param objective value to compare + * @return true if they are equivalent within the margin of error + */ + private static boolean matchesObjective(double a, double objective) + { + return Math.abs(a - objective) < 0.0000000001; } } diff --git a/src/dsp/filter/Window.java b/src/dsp/filter/Window.java index b3d00845..6d1d5ac5 100644 --- a/src/dsp/filter/Window.java +++ b/src/dsp/filter/Window.java @@ -138,8 +138,8 @@ public class Window for(int x = 0; x < length; x++) { coefficients[x] = a0 - - (a1 * Math.cos((TWO_PI * (double)x) / denominator) + - (a2 * Math.cos((FOUR_PI * (double)x) / denominator))); + (a1 * Math.cos((TWO_PI * (double)x) / denominator)) + + (a2 * Math.cos((FOUR_PI * (double)x) / denominator)); } return coefficients; @@ -273,21 +273,13 @@ public class Window { double[] coefficients = new double[length]; - if(length % 2 == 0) //Even length + double a0 = 0.54; + double a1 = 0.46; + double denominator = length; + + for(int x = 0; x < length; x++) { - for(int x = 0; x < length; x++) - { - coefficients[x] = .54D - (.46D * - Math.cos((Math.PI * (2.0D * (double)x + 1.0d)) / (double)(length - 1))); - } - } - else //Odd length - { - for(int x = 0; x < length; x++) - { - coefficients[x] = .54D - (.46D * - Math.cos((2.0D * Math.PI * (double)x) / (double)(length - 1))); - } + coefficients[x] = a0 - (a1 * Math.cos(TWO_PI * (double)x / denominator)); } return coefficients; @@ -303,23 +295,14 @@ public class Window { double[] coefficients = new double[length]; - if(length % 2 == 0) //Even length - { - for(int x = 0; x < length; x++) - { - coefficients[x] = .5D - (.5D * - Math.cos((Math.PI * (2.0D * (double)x + 1)) / (double)(length - 1))); - } - } - else //Odd length - { - for(int x = 0; x < length; x++) - { - coefficients[x] = .5D - (.5D * - Math.cos((2.0D * Math.PI * (double)x) / (double)(length - 1))); - } - } + double a0 = 0.5; + double a1 = 0.5; + double denominator = length; + for(int x = 0; x < length; x++) + { + coefficients[x] = a0 - (a1 * Math.cos(TWO_PI * (double)x / denominator)); + } return coefficients; } diff --git a/src/dsp/filter/channelizer/ChannelizerViewer.java b/src/dsp/filter/channelizer/ChannelizerViewer.java index 65392538..0519486a 100644 --- a/src/dsp/filter/channelizer/ChannelizerViewer.java +++ b/src/dsp/filter/channelizer/ChannelizerViewer.java @@ -18,13 +18,10 @@ ******************************************************************************/ package dsp.filter.channelizer; -import dsp.filter.*; +import dsp.filter.FilterFactory; import dsp.filter.Window; import dsp.filter.design.FilterDesignException; -import dsp.filter.fir.FIRFilterSpecification; -import dsp.filter.fir.remez.RemezFIRFilterDesigner; import dsp.mixer.LowPhaseNoiseOscillator; -import dsp.mixer.Oscillator; import net.miginfocom.swing.MigLayout; import org.slf4j.Logger; import org.slf4j.LoggerFactory; @@ -61,13 +58,22 @@ public class ChannelizerViewer extends JFrame private ChannelArrayPanel mChannelPanel; private LowPhaseNoiseOscillator mOscillator; private int mChannelCount; + private int mChannelsPerRow; private int mSampleRate; private int mToneFrequency; - private DFTSize mDFTSize = DFTSize.FFT04096; + private DFTSize mMainPanelDFTSize = DFTSize.FFT32768; + private DFTSize mChannelPanelDFTSize = DFTSize.FFT00512; - public ChannelizerViewer(int channelCount) + /** + * GUI Test utility for researching polyphase channelizers. + * + * @param channelCount + * @param channelsPerRow + */ + public ChannelizerViewer(int channelCount, int channelsPerRow) { mChannelCount = channelCount; + mChannelsPerRow = channelsPerRow; mSampleRate = mChannelCount * CHANNEL_BANDWIDTH; mToneFrequency = (int)(CHANNEL_BANDWIDTH * 1); //Set to channel 1 as default @@ -130,7 +136,7 @@ public class ChannelizerViewer extends JFrame { mPrimarySpectrumPanel = new ChannelPanel(mSettingsManager, mSampleRate); mPrimarySpectrumPanel.setPreferredSize(new Dimension(1000, 200)); - mPrimarySpectrumPanel.setDFTSize(DFTSize.FFT32768); + mPrimarySpectrumPanel.setDFTSize(mMainPanelDFTSize); } return mPrimarySpectrumPanel; @@ -177,24 +183,27 @@ public class ChannelizerViewer extends JFrame { if(mChannelPanel == null) { - mChannelPanel = new ChannelArrayPanel(getFilter()); + float[] taps = null; + + try + { + taps = getFilter(); + } + catch(FilterDesignException fde) + { + mLog.debug("Couldn't design filter", fde); + } + + mChannelPanel = new ChannelArrayPanel(taps); } return mChannelPanel; } - private float[] getFilter() + private float[] getFilter() throws FilterDesignException { - float[] taps = FilterFactory.getChannelizer(CHANNEL_BANDWIDTH, mChannelCount, 16, Window.WindowType.BLACKMAN_HARRIS_7); - - StringBuilder sb = new StringBuilder(); - sb.append("\nPolyphase Channelizer\n"); - sb.append("Sample Rate:" + mSampleRate + " Channels:" + mChannelCount + " Channel Rate:" + CHANNEL_BANDWIDTH + "\n"); - sb.append("Tap Count:" + taps.length + "\n"); - sb.append("Channel:" + mToneFrequency); - mLog.debug(sb.toString()); - - return taps; + return FilterFactory.getChannelizer(CHANNEL_BANDWIDTH, mChannelCount, 19, + Window.WindowType.BLACKMAN_HARRIS_7, true); } public class ChannelArrayPanel extends JPanel implements Listener @@ -221,15 +230,12 @@ public class ChannelizerViewer extends JFrame { setLayout(new MigLayout("insets 0 0 0 0", "fill", "fill")); - int channelsPerRow = 20; - 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); + channelPanel.setDFTSize(mChannelPanelDFTSize); - if(x % channelsPerRow == channelsPerRow - 1) + if(x % mChannelsPerRow == mChannelsPerRow - 1) { add(channelPanel, "grow,push,wrap 2px"); } @@ -279,27 +285,6 @@ public class ChannelizerViewer extends JFrame } } - public class SampleGenerator - { - public SampleGenerator() - { - mLog.debug("Starting ..."); - - - PolyphaseChannelizer channelizer = new PolyphaseChannelizer(getFilter(), mChannelCount); - - Oscillator oscillator = new Oscillator(mToneFrequency, mSampleRate); - - for(int x = 0; x < 2000; x++) - { - channelizer.filter(oscillator.getComplex().inphase(), oscillator.getComplex().quadrature()); - oscillator.rotate(); - } - - mLog.debug("Finished!"); - } - } - public class SampleGenerationTask implements Runnable { @Override @@ -311,9 +296,10 @@ public class ChannelizerViewer extends JFrame public static void main(String[] args) { - int channelCount = 10; + int channelCount = 100; + int channelsPerRow = 25; - final ChannelizerViewer frame = new ChannelizerViewer(channelCount); + final ChannelizerViewer frame = new ChannelizerViewer(channelCount, channelsPerRow); EventQueue.invokeLater(new Runnable() { diff --git a/src/dsp/filter/design/FilterView.java b/src/dsp/filter/design/FilterView.java index bfec7971..de30c8e3 100644 --- a/src/dsp/filter/design/FilterView.java +++ b/src/dsp/filter/design/FilterView.java @@ -22,7 +22,7 @@ public class FilterView extends BorderPane private CurveFittedAreaChart mDFTChart; private NumberAxis mDFTXAxis; private NumberAxis mDFTYAxis; - private static final int FFT_SIZE = 4096; + private static final int FFT_SIZE = 2048; private FloatFFT_1D mFFT = new FloatFFT_1D(FFT_SIZE); public FilterView(float[] taps) @@ -156,7 +156,8 @@ public class FilterView extends BorderPane for(int x = 0; x < decibels.length; x++) { index = x * 2; - decibels[x] = 10.0f * (float)Math.log10((dft[index] * dft[index]) + (dft[index + 1] * dft[index + 1])); + double magnitude = Math.sqrt(Math.pow(dft[index], 2.0) + Math.pow(dft[index + 1], 2.0)); + decibels[x] = (float)(20.0 * Math.log10(magnitude)); } return decibels; diff --git a/src/dsp/filter/design/FilterViewer.java b/src/dsp/filter/design/FilterViewer.java index 8a6a8f91..a7b973ee 100644 --- a/src/dsp/filter/design/FilterViewer.java +++ b/src/dsp/filter/design/FilterViewer.java @@ -5,6 +5,7 @@ import dsp.filter.Window; import dsp.filter.fir.FIRFilterSpecification; import dsp.filter.fir.remez.PolyphaseChannelizerFilterFactory; import dsp.filter.fir.remez.RemezFIRFilterDesigner; +import dsp.filter.fir.remez.RemezFIRFilterDesigner2; import dsp.filter.fir.remez.RemezFIRFilterDesignerWithLagrange; import filter.Filter; import javafx.application.Application; @@ -55,15 +56,17 @@ public class FilterViewer extends Application */ private float[] getFilter() { + float[] taps = null; + // FIRFilterSpecification specification = FIRFilterSpecification.lowPassBuilder() -// .sampleRate(10000) +// .sampleRate(1000) // .gridDensity(16) // .passBandAmplitude(1.0) -// .passBandCutoff(4000) +// .passBandCutoff(100) // .passBandRipple(0.01) // .stopBandAmplitude(0.0) -// .stopBandRipple(0.001) -// .stopBandStart(5000) +// .stopBandRipple(0.0001) +// .stopBandStart(150) // .build(); // FIRFilterSpecification specification = FIRFilterSpecification.highPassBuilder() @@ -86,35 +89,25 @@ public class FilterViewer extends Application // .passRipple( 0.008) // .build(); - float[] taps = null; +// try +// { +// RemezFIRFilterDesignerWithLagrange remez = new RemezFIRFilterDesignerWithLagrange(specification); +// taps = remez.getImpulseResponse(); +// } +// catch(Exception e) +// { +// mLog.error("Error designing filter", e); +// } -// int tapCount = FilterFactory.getTapCount(10000000, 12500, 18750, 80); -// taps = FilterFactory.getLowPass( 10000000, 12500, 5600, Window.WindowType.BLACKMAN_HARRIS_7 ); - -// int channels = 10; -// int channelRate = 12500; -// int cutoff = 8000; -// int sampleRate = channelRate * channels; -// int filterLength = channels * 7; -// -// taps = FilterFactory.getLowPass(sampleRate, cutoff, filterLength, Window.WindowType.BLACKMAN_HARRIS_7); -// -// double response = FilterFactory.evaluate(taps, (double)channelRate / (double)sampleRate ); -// -// mLog.debug("Taps:" + taps.length + " With response at [" + channelRate + "]: " + response); - - int channelBandwidth = 12500; - int channels = 4; - int tapsPerChannel = 20; - - taps = FilterFactory.getChannelizer(channelBandwidth, channels, tapsPerChannel, Window.WindowType.BLACKMAN_HARRIS_4); - - double sampleRate = channelBandwidth * channels * 2; - - for(int x = 0; x < 25000; x += 100) +// taps = FilterFactory.getSinc(0.1, 51, Window.WindowType.BLACKMAN_HARRIS_7); + try { - double toEval = (double)x / sampleRate; - mLog.debug("Response at [" + x + "] is:" + FilterFactory.evaluate(taps, toEval)); + taps = FilterFactory.getChannelizer(12500, 50, 14, + Window.WindowType.BLACKMAN_HARRIS_7, true); + } + catch(Exception e) + { + mLog.error("Couldn't design filter", e); } return taps; From ffc92a8f0e533f3bb40d080152079f48226781dd Mon Sep 17 00:00:00 2001 From: Denny Date: Thu, 30 Mar 2017 19:16:37 -0400 Subject: [PATCH 12/87] WIP - adds remez channelizer builder, working toward combined support (remez & sync) via the filter factory. --- src/dsp/filter/FilterFactory.java | 35 ++++- .../filter/channelizer/ChannelizerViewer.java | 2 +- src/dsp/filter/design/FilterViewer.java | 14 +- .../filter/fir/FIRFilterSpecification.java | 130 +++++++++++------- 4 files changed, 120 insertions(+), 61 deletions(-) diff --git a/src/dsp/filter/FilterFactory.java b/src/dsp/filter/FilterFactory.java index 0889cd68..f45299d5 100644 --- a/src/dsp/filter/FilterFactory.java +++ b/src/dsp/filter/FilterFactory.java @@ -23,6 +23,7 @@ import dsp.filter.cic.ComplexPrimeCICDecimate; import dsp.filter.design.FilterDesignException; import dsp.filter.fir.FIRFilterSpecification; import dsp.filter.fir.remez.RemezFIRFilterDesigner; +import dsp.filter.fir.remez.RemezFIRFilterDesignerWithLagrange; import org.jtransforms.fft.FloatFFT_1D; import org.slf4j.Logger; import org.slf4j.LoggerFactory; @@ -36,7 +37,7 @@ import java.util.TreeSet; public class FilterFactory { private final static Logger mLog = LoggerFactory.getLogger(FilterFactory.class); - private static final double PERFECT_RECONSTRUCTION_GAIN_AT_BAND_EDGE = -6.020599842071533; //decibel(0.5) + private static final double PERFECT_RECONSTRUCTION_GAIN_AT_BAND_EDGE = -6.020599842071533; //decibel(0.5, 0.0) /** * Generates coefficients for a unity-gain, windowed low-pass filter @@ -695,6 +696,34 @@ public class FilterFactory return (float)(10.0 * Math.log10(Math.pow(real, 2.0) + Math.pow(imag, 2.0))); } + + public static float[] getRemezChannelizer(int channelBandwidth, int channels, int tapsPerChannel, double alpha, + double passRipple, double stopRipple) throws FilterDesignException + { + FIRFilterSpecification specification = FIRFilterSpecification.channelizerBuilder() + .sampleRate(channels * channelBandwidth * 4) + .channels(channels) + .channelBandwidth(channelBandwidth) + .tapsPerChannel(tapsPerChannel) + .alpha(alpha) + .passRipple(passRipple) + .stopRipple(stopRipple) + .build(); + + RemezFIRFilterDesignerWithLagrange designer = new RemezFIRFilterDesignerWithLagrange(specification); + + float[] taps = designer.getImpulseResponse(); + + double bandEdgeFrequency = (double)channelBandwidth / (double)(channels * channelBandwidth * 2); + + double response = decibel(designer.getFrequencyResponse(Math.cos(bandEdgeFrequency * Math.PI)), 0.0); + mLog.debug("Frequency Response at 1.0: " + response); + response = decibel(designer.getFrequencyResponse(Math.cos(bandEdgeFrequency * Math.PI * 2.0)), 0.0); + mLog.debug("Frequency Response at 2.0: " + response); + + return taps; + } + /** * 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 @@ -720,8 +749,8 @@ public class FilterFactory * * @throws FilterDesignException if a filter cannot be designed with a band edge attenuation of -6.02 dB */ - public static float[] getChannelizer(int channelBandwidth, int channels, int tapsPerChannel, WindowType windowType, - boolean logResults) throws FilterDesignException + public static float[] getSincChannelizer(int channelBandwidth, int channels, int tapsPerChannel, WindowType windowType, + boolean logResults) throws FilterDesignException { int currentTapsPerChannel = tapsPerChannel; int sampleRate = channelBandwidth * channels * 2; diff --git a/src/dsp/filter/channelizer/ChannelizerViewer.java b/src/dsp/filter/channelizer/ChannelizerViewer.java index 0519486a..6d726a54 100644 --- a/src/dsp/filter/channelizer/ChannelizerViewer.java +++ b/src/dsp/filter/channelizer/ChannelizerViewer.java @@ -202,7 +202,7 @@ public class ChannelizerViewer extends JFrame private float[] getFilter() throws FilterDesignException { - return FilterFactory.getChannelizer(CHANNEL_BANDWIDTH, mChannelCount, 19, + return FilterFactory.getSincChannelizer(CHANNEL_BANDWIDTH, mChannelCount, 19, Window.WindowType.BLACKMAN_HARRIS_7, true); } diff --git a/src/dsp/filter/design/FilterViewer.java b/src/dsp/filter/design/FilterViewer.java index a7b973ee..14db2b9c 100644 --- a/src/dsp/filter/design/FilterViewer.java +++ b/src/dsp/filter/design/FilterViewer.java @@ -2,20 +2,12 @@ package dsp.filter.design; import dsp.filter.FilterFactory; import dsp.filter.Window; -import dsp.filter.fir.FIRFilterSpecification; -import dsp.filter.fir.remez.PolyphaseChannelizerFilterFactory; -import dsp.filter.fir.remez.RemezFIRFilterDesigner; -import dsp.filter.fir.remez.RemezFIRFilterDesigner2; -import dsp.filter.fir.remez.RemezFIRFilterDesignerWithLagrange; -import filter.Filter; import javafx.application.Application; import javafx.scene.Scene; import javafx.stage.Stage; import org.slf4j.Logger; import org.slf4j.LoggerFactory; -import java.util.Arrays; - public class FilterViewer extends Application { private final static Logger mLog = LoggerFactory.getLogger(FilterViewer.class); @@ -102,8 +94,10 @@ public class FilterViewer extends Application // taps = FilterFactory.getSinc(0.1, 51, Window.WindowType.BLACKMAN_HARRIS_7); try { - taps = FilterFactory.getChannelizer(12500, 50, 14, - Window.WindowType.BLACKMAN_HARRIS_7, true); +// taps = FilterFactory.getSincChannelizer(12500, 50, 14, +// Window.WindowType.BLACKMAN_HARRIS_7, true); + taps = FilterFactory.getRemezChannelizer(12500, 4, 22, 0.4, + 0.01, 0.0001); } catch(Exception e) { diff --git a/src/dsp/filter/fir/FIRFilterSpecification.java b/src/dsp/filter/fir/FIRFilterSpecification.java index da4aa40a..2ef0d820 100644 --- a/src/dsp/filter/fir/FIRFilterSpecification.java +++ b/src/dsp/filter/fir/FIRFilterSpecification.java @@ -18,7 +18,11 @@ ******************************************************************************/ package dsp.filter.fir; +import dsp.filter.FilterFactory; import dsp.filter.fir.remez.FIRLinearPhaseFilterType; +import org.apache.commons.lang3.Validate; +import org.slf4j.Logger; +import org.slf4j.LoggerFactory; import java.text.DecimalFormat; import java.util.ArrayList; @@ -27,6 +31,9 @@ import java.util.List; public class FIRFilterSpecification { + private final static Logger mLog = LoggerFactory.getLogger(FIRFilterSpecification.class); + + private static final double PERFECT_RECONSTRUCTION_GAIN_AT_BAND_EDGE = -6.020599842071533; //0.5 magnitude private static final DecimalFormat DECIMAL_FORMATTER = new DecimalFormat("0.00000"); private FIRLinearPhaseFilterType mRemezFilterType; @@ -143,6 +150,15 @@ public class FIRFilterSpecification return new BandPassBuilder(); } + /** + * Creates a Polyphase Channelizer filter specification builder that allows you to define the filter parameters and + * create a filter specification + */ + public static ChannelizerBuilder channelizerBuilder() + { + return new ChannelizerBuilder(); + } + public FIRLinearPhaseFilterType getFilterType() { return mRemezFilterType; @@ -693,57 +709,64 @@ public class FIRFilterSpecification /** * Builder for creating polyphase channelizer low-pass prototype filter. */ - public static class PolyphaseChannelizerBuilder + public static class ChannelizerBuilder { - private int mOrder; private int mGridDensity = 16; private int mSampleRate; + private int mChannelCount; private int mChannelBandwidth; - private int mPassFrequencyBegin = 0; - private int mPassFrequencyEnd; - private int mStopFrequencyBegin; + private int mTapsPerChannel; private double mStopRipple = 0.001; private double mPassRipple = 0.01; - private double mStopAmplitude = 0.0; - private double mPassAmplitude = 1.0; + private double mAlpha = 0.2; - public PolyphaseChannelizerBuilder() + public ChannelizerBuilder() { } /** * Sets the filter sample rate in hertz */ - public PolyphaseChannelizerBuilder sampleRate(int sampleRate) + public ChannelizerBuilder sampleRate(int sampleRate) { mSampleRate = sampleRate; return this; } + /** + * Number of Channels + * @param channels - must be a multiple of 2 + * @return + */ + public ChannelizerBuilder channels(int channels) + { + Validate.isTrue(channels % 2 == 0, "Channel count must be a multiple of 2"); + mChannelCount = channels; + return this; + } + /** * Sets the channel bandwidth in hertz */ - public PolyphaseChannelizerBuilder channelBandwidth(int channelBandwidth) + public ChannelizerBuilder channelBandwidth(int channelBandwidth) { mChannelBandwidth = channelBandwidth; return this; } /** - * Sets the filter order. If the order is not specified, it will be calculated from the - * other parameters. If you specify and odd-length order, it will be corrected (+1) to make - * it an even order number. + * Number of filter taps per channel. Note: total filter length will be channels * tapsPerChannel */ - public PolyphaseChannelizerBuilder order(int order) + public ChannelizerBuilder tapsPerChannel(int tapsPerChannel) { - mOrder = order; + mTapsPerChannel = tapsPerChannel; return this; } /** * Sets the frequency grid density. Default is 16. */ - public PolyphaseChannelizerBuilder gridDensity(int density) + public ChannelizerBuilder gridDensity(int density) { mGridDensity = density; return this; @@ -752,7 +775,7 @@ public class FIRFilterSpecification /** * Specifies the ripple in dB for both stop bands */ - public PolyphaseChannelizerBuilder stopRipple(double rippleDb) + public ChannelizerBuilder stopRipple(double rippleDb) { mStopRipple = rippleDb; return this; @@ -761,66 +784,69 @@ public class FIRFilterSpecification /** * Specifies the ripple in dB for the pass band */ - public PolyphaseChannelizerBuilder passRipple(double rippleDb) + public ChannelizerBuilder passRipple(double rippleDb) { mPassRipple = rippleDb; return this; } /** - * Specifies the amplitude of the stop bands. Default is 0.0 + * Rolloff factor for the start band and stop band. + * @param alpha - value in range 0.0 - 1.0 */ - public PolyphaseChannelizerBuilder stopAmplitude(double amplitude) + public ChannelizerBuilder alpha(double alpha) { - mStopAmplitude = amplitude; - return this; - } + Validate.isTrue(0.0 <= alpha && alpha <= 1.0); + mAlpha = alpha; - /** - * Specifies the amplitude of the pass band. Default is 1.0 - */ - public PolyphaseChannelizerBuilder passAmplitude(double amplitude) - { - mPassAmplitude = amplitude; return this; } public FIRFilterSpecification build() { - FIRLinearPhaseFilterType type = FIRLinearPhaseFilterType.TYPE_1_ODD_LENGTH_EVEN_ORDER_SYMMETRICAL; + FIRLinearPhaseFilterType type = FIRLinearPhaseFilterType.TYPE_2_EVEN_LENGTH_ODD_ORDER_SYMMETRICAL; - if(mOrder < 10) + int passFrequencyEnd = (int)(mChannelBandwidth * (1.0 - mAlpha)); + int stopFrequencyBegin = (int)(mChannelBandwidth * (1.0 + mAlpha)); + + int estimatedOrder = estimateFilterOrder(mSampleRate, passFrequencyEnd, stopFrequencyBegin, mPassRipple, + mStopRipple); + + // Ensure odd order since we're designing a Type 2 filter + if(estimatedOrder % 2 == 0) { - mOrder = estimateBandPassOrder(mSampleRate, mPassFrequencyBegin, mPassFrequencyEnd, - mPassRipple, mStopRipple); + estimatedOrder++; } - // Ensure even order since we're designing a Type 1 filter - if(mOrder % 2 == 1) - { - mOrder++; - } + int order = mChannelCount * mTapsPerChannel - 1; - FIRFilterSpecification spec = new FIRFilterSpecification(type, mOrder, mGridDensity); + mLog.debug("Order:" + order + " Sample Rate:" + mSampleRate + " Estimated Order:" + estimatedOrder + " Requested Order:" + order); - //Set pass band to stop at about 80% of channel bandwidth as starting point - int passBandStop = (int)(mChannelBandwidth * .80); + FIRFilterSpecification spec = new FIRFilterSpecification(type, order, mGridDensity); - //Set the stop band to start at 40% into the adjacent channel, since we're oversampling by 2. - int stopBandStart = (int)(mChannelBandwidth * 1.40); + double bandEdge = (double)mChannelBandwidth / (double)mSampleRate; + double passEnd = bandEdge * (1.0 - mAlpha); + double stopStart = bandEdge * (1.0 + mAlpha); - FrequencyBand passBand = new FrequencyBand(mSampleRate, 0, passBandStop, 1.0, mPassRipple); - FrequencyBand stopBand = new FrequencyBand(mSampleRate, stopBandStart, (int)(mSampleRate / 2), - 0.0, mStopRipple); + mLog.debug("Pass End:" + passEnd + " Band Edge:" + bandEdge + " Stop Begin:" + stopStart); + mLog.debug("Pass End:" + (int)(mSampleRate * passEnd) + + " Band Edge:" + (int)(mSampleRate * bandEdge) + + " Stop Begin:" + (int)(mSampleRate * stopStart)); + + FrequencyBand passBand = new FrequencyBand(0.0, passEnd, 1.0, mPassRipple); + FrequencyBand edgeBand = new FrequencyBand(bandEdge, bandEdge, 0.5, mPassRipple); + edgeBand.setWeight(8.0 * edgeBand.getWeight(mPassRipple)); + FrequencyBand stopBand = new FrequencyBand(stopStart, 0.5, 0.0, mStopRipple); spec.addFrequencyBand(passBand); + spec.addFrequencyBand(edgeBand); spec.addFrequencyBand(stopBand); + mLog.debug(spec.toString()); return spec; } } - // /** // * Converts the set of band amplitude values into a set of weights // * normalized to the largest band amplitude value @@ -992,6 +1018,14 @@ public class FIRFilterSpecification mWeight = weight; } + /** + * Explicitly sets the weight of this frequency band. + */ + public void setWeight(double weight) + { + mWeight = weight; + } + public int getGridSize() { return mGridSize; @@ -1018,6 +1052,8 @@ public class FIRFilterSpecification sb.append(DECIMAL_FORMATTER.format(getRippleAmplitude())); sb.append(" Grid Size: "); sb.append(mGridSize); + sb.append(" Weight: "); + sb.append(getWeight(mRippleDB)); return sb.toString(); } From afae1654d500ded6933d467dd1cdcb14776a441b Mon Sep 17 00:00:00 2001 From: Denny Date: Sat, 1 Apr 2017 15:10:02 -0400 Subject: [PATCH 13/87] WIP --- .../filter/channelizer/ChannelizerViewer.java | 8 +- ....java => ComplexPolyphaseChannelizer.java} | 13 +- .../channelizer/PolyphaseChannelizer.java | 224 ------------------ 3 files changed, 12 insertions(+), 233 deletions(-) rename src/dsp/filter/channelizer/{PolyphaseChannelizer2.java => ComplexPolyphaseChannelizer.java} (92%) delete mode 100644 src/dsp/filter/channelizer/PolyphaseChannelizer.java diff --git a/src/dsp/filter/channelizer/ChannelizerViewer.java b/src/dsp/filter/channelizer/ChannelizerViewer.java index 6d726a54..7164e57b 100644 --- a/src/dsp/filter/channelizer/ChannelizerViewer.java +++ b/src/dsp/filter/channelizer/ChannelizerViewer.java @@ -208,7 +208,7 @@ public class ChannelizerViewer extends JFrame public class ChannelArrayPanel extends JPanel implements Listener { - private PolyphaseChannelizer2 mPolyphaseChannelizer; + private ComplexPolyphaseChannelizer mPolyphaseChannelizer; private ChannelDistributor mChannelDistributor; public ChannelArrayPanel(float[] taps) @@ -219,7 +219,7 @@ public class ChannelizerViewer extends JFrame bufferSize++; } - mPolyphaseChannelizer = new PolyphaseChannelizer2(taps, mChannelCount); + mPolyphaseChannelizer = new ComplexPolyphaseChannelizer(taps, mChannelCount, 1); mChannelDistributor = new ChannelDistributor(bufferSize, mChannelCount); mPolyphaseChannelizer.setChannelDistributor(mChannelDistributor); @@ -296,8 +296,8 @@ public class ChannelizerViewer extends JFrame public static void main(String[] args) { - int channelCount = 100; - int channelsPerRow = 25; + int channelCount = 4; + int channelsPerRow = 4; final ChannelizerViewer frame = new ChannelizerViewer(channelCount, channelsPerRow); diff --git a/src/dsp/filter/channelizer/PolyphaseChannelizer2.java b/src/dsp/filter/channelizer/ComplexPolyphaseChannelizer.java similarity index 92% rename from src/dsp/filter/channelizer/PolyphaseChannelizer2.java rename to src/dsp/filter/channelizer/ComplexPolyphaseChannelizer.java index 5fd700c2..117b0382 100644 --- a/src/dsp/filter/channelizer/PolyphaseChannelizer2.java +++ b/src/dsp/filter/channelizer/ComplexPolyphaseChannelizer.java @@ -30,9 +30,9 @@ import sample.complex.ComplexBuffer; import java.text.DecimalFormat; -public class PolyphaseChannelizer2 implements Listener +public class ComplexPolyphaseChannelizer implements Listener { - private final static Logger mLog = LoggerFactory.getLogger(PolyphaseChannelizer2.class); + private final static Logger mLog = LoggerFactory.getLogger(ComplexPolyphaseChannelizer.class); private RealFIRFilter[] mInphaseFilters; private RealFIRFilter[] mQuadratureFilters; @@ -57,9 +57,12 @@ public class PolyphaseChannelizer2 implements Listener * @param channels - number of filters/channels to output. Since this filter bank oversamples each filter * output, this number must be even (divisible by the oversample rate). */ - public PolyphaseChannelizer2(float[] taps, int channels) + public ComplexPolyphaseChannelizer(float[] taps, int channels, int oversample) { - assert(channels % 2 == 0); + if(oversample > 1 && channels % 2 != 0) + { + throw new IllegalArgumentException("Channel count must be a multiple of 2 when oversampling"); + } mChannelCount = channels; @@ -184,7 +187,7 @@ public class PolyphaseChannelizer2 implements Listener mLog.debug(sb.toString()); - PolyphaseChannelizer2 channelizer = new PolyphaseChannelizer2(taps, 8); + ComplexPolyphaseChannelizer channelizer = new ComplexPolyphaseChannelizer(taps, 8, 1); Oscillator oscillator = new Oscillator(channelCenter, 2 * sampleRate); diff --git a/src/dsp/filter/channelizer/PolyphaseChannelizer.java b/src/dsp/filter/channelizer/PolyphaseChannelizer.java deleted file mode 100644 index 2390cb00..00000000 --- a/src/dsp/filter/channelizer/PolyphaseChannelizer.java +++ /dev/null @@ -1,224 +0,0 @@ -/******************************************************************************* - * 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 dsp.filter.FilterFactory; -import dsp.filter.design.FilterDesignException; -import dsp.filter.fir.FIRFilterSpecification; -import dsp.filter.fir.real.RealFIRFilter; -import dsp.filter.fir.remez.RemezFIRFilterDesigner; -import dsp.mixer.Oscillator; -import org.jtransforms.fft.FloatFFT_1D; -import org.slf4j.Logger; -import org.slf4j.LoggerFactory; -import sample.Listener; -import sample.complex.ComplexBuffer; - -import java.text.DecimalFormat; - -public class PolyphaseChannelizer implements Listener -{ - private final static Logger mLog = LoggerFactory.getLogger(PolyphaseChannelizer.class); - - private RealFIRFilter[] mFilters; - private int mFilterPointer = 0; - private int mChannelCount; - private int mBlockSize; - private float[] mFilteredSamples; - private boolean mToggle = false; - private FloatFFT_1D mFFT; - private ChannelDistributor mChannelDistributor; - - private DecimalFormat DECIMAL_FORMAT = new DecimalFormat("0.0000"); - - /** - * Creates a polyphase filter bank that divides the input frequency band into evenly spaced channels that - * are oversampled by 2x for output. - * - * @param taps of a low-pass filter designed for the inbound sample rate with a cutoff frequency - * equal to the channel bandwidth (sample rate / filters). If you need to synthesize (combine two or more - * filte outputs) a new bandwidth signal from the outputs of this filter, then the filter should be designed - * as a nyquist filter with -6 dB attenuation at the channel bandwidth cutoff frequency - * - * @param channels - number of filters/channels to output. Since this filter bank oversamples each filter - * output, this number must be even (divisible by the oversample rate). - */ - public PolyphaseChannelizer(float[] taps, int channels) - { - assert(channels % 2 == 0); - - mChannelCount = channels; - - initFilters(taps); - } - - public void filter(float inphase, float quadrature) - { - int iIndex = 2 * mFilterPointer; - int qIndex = iIndex + 1; - - mFilteredSamples[iIndex] = mFilters[iIndex].filter(inphase); - mFilteredSamples[qIndex] = mFilters[qIndex].filter(quadrature); - - if(mFilterPointer == 0) - { - calculate(); - mFilterPointer = mChannelCount - 1; - mToggle = !mToggle; - } - else if(mFilterPointer == mBlockSize) - { - calculate(); - mFilterPointer--; - } - else - { - mFilterPointer--; - } - } - - @Override - public void receive(ComplexBuffer complexBuffer) - { - float[] samples = complexBuffer.getSamples(); - - for(int x = 0; x < samples.length; x += 2) - { - filter(samples[x], samples[x + 1]); - } - } - - private void calculate() - { - float[] samples = new float[mFilters.length]; - - if(mToggle) - { - System.arraycopy(mFilteredSamples, 0, samples, mChannelCount, mChannelCount); - System.arraycopy(mFilteredSamples, mChannelCount, samples, 0, mChannelCount); - } - else - { - System.arraycopy(mFilteredSamples, 0, samples, 0, mFilteredSamples.length); - } - - //FFT is executed in-place where the output overwrites the input - mFFT.complexForward(samples); - - dispatch(samples); - } - - private void dispatch(float[] channels) - { - if(mChannelDistributor != null) - { - mChannelDistributor.receive(channels); - } - } - - public void setChannelDistributor(ChannelDistributor channelDistributor) - { - mChannelDistributor = channelDistributor; - } - - private double magnitude(float real, float imaginary, int fftSize) - { - return Math.sqrt(Math.pow(real, 2.0) + Math.pow(imaginary, 2.0)); - } - - /** - * Distributes the filter taps to each of the polyphase filters - * - * @param taps - */ - private void initFilters(float[] taps) - { - int tapCount = (int)Math.ceil((double)taps.length / (double)mChannelCount); - - float[][] coefficients = new float[mChannelCount][tapCount]; - - for(int tap = 0; tap < tapCount; tap++) - { - for(int filter = 0; filter < mChannelCount; filter++) - { - int index = tap * mChannelCount + filter; - - if(index < taps.length) - { - coefficients[filter][tap] = taps[index]; - } - } - } - - mFilters = new RealFIRFilter[mChannelCount * 2]; - - for(int filter = 0; filter < mChannelCount; filter++) - { - //Inphase filter - mFilters[2 * filter] = new RealFIRFilter(coefficients[filter], 1.0f); - - //Quadrature filter - mFilters[2 * filter + 1] = new RealFIRFilter(coefficients[filter], 1.0f); - } - - mFilteredSamples = new float[mFilters.length]; - mFFT = new FloatFFT_1D(mChannelCount); - mBlockSize = mChannelCount / 2; - mFilterPointer = mChannelCount -1; - } - - public static void main(String[] args) - { - mLog.debug("Starting ..."); - - int symbolRate = 4800; - int samplesPerSymbol = 2; - int symbolCount = 4; - int channels = 10; - int channelBandwidth = 12500; - int sampleRate = channels * channelBandwidth; - - //Alpha is the residual channel bandwidth left over from the symbol rate and samples per symbol - float alpha = ((float)channelBandwidth / (float)(symbolRate * samplesPerSymbol)) - 1.0f; - - float[] taps = FilterFactory.getRootRaisedCosine(samplesPerSymbol * channels, symbolCount, alpha); - - int channelCenter = (int)(12500 * 1.5); - - StringBuilder sb = new StringBuilder(); - sb.append("\nPolyphase Channelizer\n"); - sb.append("Sample Rate:" + sampleRate + " Channels:" + channels + " Channel Rate:" + channelBandwidth + "\n"); - sb.append("Alpha: " + alpha + " Tap Count:" + taps.length + "\n"); - sb.append("Channel:" + channelCenter); - - mLog.debug(sb.toString()); - - PolyphaseChannelizer channelizer = new PolyphaseChannelizer(taps, 8); - - Oscillator oscillator = new Oscillator(channelCenter, 2 * sampleRate); - - for(int x = 0; x < 2000; x++) - { - channelizer.filter(oscillator.getComplex().inphase(), oscillator.getComplex().quadrature()); - oscillator.rotate(); - } - - mLog.debug("Finished!"); - } -} From 41d307cdadfc30aadb7063ab46959dd70f69ff4c Mon Sep 17 00:00:00 2001 From: Denny Date: Mon, 3 Apr 2017 19:04:00 -0400 Subject: [PATCH 14/87] WIP - checkpointing update for 2x oversampling where we're only computing half of the coefficients. --- .../filter/channelizer/ChannelizerViewer.java | 4 +- .../ComplexPolyphaseChannelizer2.java | 288 ++++++++++++++++++ 2 files changed, 290 insertions(+), 2 deletions(-) create mode 100644 src/dsp/filter/channelizer/ComplexPolyphaseChannelizer2.java diff --git a/src/dsp/filter/channelizer/ChannelizerViewer.java b/src/dsp/filter/channelizer/ChannelizerViewer.java index 7164e57b..343ce1d9 100644 --- a/src/dsp/filter/channelizer/ChannelizerViewer.java +++ b/src/dsp/filter/channelizer/ChannelizerViewer.java @@ -208,7 +208,7 @@ public class ChannelizerViewer extends JFrame public class ChannelArrayPanel extends JPanel implements Listener { - private ComplexPolyphaseChannelizer mPolyphaseChannelizer; + private ComplexPolyphaseChannelizer2 mPolyphaseChannelizer; private ChannelDistributor mChannelDistributor; public ChannelArrayPanel(float[] taps) @@ -219,7 +219,7 @@ public class ChannelizerViewer extends JFrame bufferSize++; } - mPolyphaseChannelizer = new ComplexPolyphaseChannelizer(taps, mChannelCount, 1); + mPolyphaseChannelizer = new ComplexPolyphaseChannelizer2(taps, mChannelCount); mChannelDistributor = new ChannelDistributor(bufferSize, mChannelCount); mPolyphaseChannelizer.setChannelDistributor(mChannelDistributor); diff --git a/src/dsp/filter/channelizer/ComplexPolyphaseChannelizer2.java b/src/dsp/filter/channelizer/ComplexPolyphaseChannelizer2.java new file mode 100644 index 00000000..6351483f --- /dev/null +++ b/src/dsp/filter/channelizer/ComplexPolyphaseChannelizer2.java @@ -0,0 +1,288 @@ +/******************************************************************************* + * 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 dsp.filter.FilterFactory; +import dsp.filter.Window; +import dsp.filter.design.FilterDesignException; +import dsp.mixer.Oscillator; +import org.jtransforms.fft.FloatFFT_1D; +import org.slf4j.Logger; +import org.slf4j.LoggerFactory; +import sample.Listener; +import sample.complex.ComplexBuffer; + +import java.text.DecimalFormat; +import java.util.Arrays; + +public class ComplexPolyphaseChannelizer2 implements Listener +{ + private final static Logger mLog = LoggerFactory.getLogger(ComplexPolyphaseChannelizer2.class); + + private float[][] mFilterCoefficients; + + + private float[][] mBuffer; + private int mBufferRowPointer = 0; + private int mBufferColumnPointer = 0; + private int[][] mIndexMap; + private int mStrideLength; + private int mStrideCount; + + private int mFilterPointer = 0; + private int mChannelCount; + private float[] mFilteredSamples; + private FloatFFT_1D mFFT; + private ChannelDistributor mChannelDistributor; + + private DecimalFormat DECIMAL_FORMAT = new DecimalFormat("0.0000"); + + /** + * Creates a polyphase filter bank that divides the input frequency band into evenly spaced channels that + * are oversampled by 2x for output. + * + * @param taps of a low-pass filter designed for the inbound sample rate with a cutoff frequency + * equal to the channel bandwidth (sample rate / filters). If you need to synthesize (combine two or more + * filte outputs) a new bandwidth signal from the outputs of this filter, then the filter should be designed + * as a nyquist filter with -6 dB attenuation at the channel bandwidth cutoff frequency + * + * @param channels - number of filters/channels to output. Since this filter bank oversamples each filter + * output, this number must be even (divisible by the oversample rate). + */ + public ComplexPolyphaseChannelizer2(float[] taps, int channels) + { + if(channels % 2 != 0) + { + throw new IllegalArgumentException("Channel count must be a multiple of 2"); + } + + mChannelCount = channels; + + initFilters(taps); + } + + public void filter(float inphase, float quadrature) + { + mBuffer[mBufferRowPointer][mBufferColumnPointer] = quadrature; + + calculate(); + mBufferRowPointer--; + + mBuffer[mBufferRowPointer][mBufferColumnPointer] = inphase; + calculate(); + mBufferRowPointer--; + + mStrideCount++; + + if(mStrideCount >= mStrideLength) + { + dispatch(); + mStrideCount = 0; + } + + //Adjust the circular buffer pointers, wrapping as necessary + if(mBufferRowPointer < 0) + { + mBufferRowPointer += mBuffer.length; + mBufferColumnPointer--; + + if(mBufferColumnPointer < 0) + { + mBufferColumnPointer += mBuffer[mBufferRowPointer].length; + } + } + } + + @Override + public void receive(ComplexBuffer complexBuffer) + { + float[] samples = complexBuffer.getSamples(); + + for(int x = 0; x < samples.length; x += 2) + { + filter(samples[x], samples[x + 1]); + } + } + + private void dispatch() + { + //Create a copy of the filtered samples + float[] samples = Arrays.copyOf(mFilteredSamples, mFilteredSamples.length); + + //Copy the first half of the samples copy back to the second half of the array to prepare for the next iteration + System.arraycopy(samples, 0, mFilteredSamples, mChannelCount, mChannelCount); + + //FFT is executed in-place where the output overwrites the input + mFFT.complexForward(samples); + + dispatch(samples); + } + + /** + * Calculates the value of the current data row and the current filter coefficient set and assigns the value to the + * current filtered samples array + */ + private void calculate() + { + float accumulator = 0.0f; + + float[] coefficients = mFilterCoefficients[mBufferRowPointer / 2]; + float[] samples = mBuffer[mBufferRowPointer]; + + for( int x = 0; x < coefficients.length; x++ ) + { + int column = mIndexMap[mBufferColumnPointer][x]; + + accumulator += coefficients[ x ] * samples[column]; + } + + if(mBufferRowPointer < mChannelCount) + { + mFilteredSamples[mBufferRowPointer] = accumulator; + } + else + { + mFilteredSamples[mBufferRowPointer - mChannelCount] = accumulator; + } + } + + /** + * Dispatches the filtered, de-spun samples as an array of channel I/Q sample pairs + * @param channels arranged as: channel0i, channel0q, channel1i, channel1q ... channelN-1i, channelN-1q + */ + private void dispatch(float[] channels) + { + if(mChannelDistributor != null) + { + mChannelDistributor.receive(channels); + } + } + + public void setChannelDistributor(ChannelDistributor channelDistributor) + { + mChannelDistributor = channelDistributor; + } + + private double magnitude(float real, float imaginary, int fftSize) + { + return Math.sqrt(Math.pow(real, 2.0) + Math.pow(imaginary, 2.0)); + } + + /** + * Distributes the filter taps to each of the polyphase filters + * + * @param taps + */ + private void initFilters(float[] taps) + { + int tapCount = (int)Math.ceil((double)taps.length / (double)mChannelCount); + + mBuffer = new float[mChannelCount * 2][tapCount]; + mFilterCoefficients = new float[mChannelCount][tapCount]; + + for(int tap = 0; tap < tapCount; tap++) + { + for(int filter = 0; filter < mChannelCount; filter++) + { + int index = tap * mChannelCount + filter; + + if(index < taps.length) + { + mFilterCoefficients[filter][tap] = taps[index]; + } + } + } + + generateIndexMap(tapCount); + + mFilteredSamples = new float[mChannelCount * 2]; + mFFT = new FloatFFT_1D(mChannelCount); + mFilterPointer = mChannelCount -1; + mStrideLength = mChannelCount / 2; //channel count * 2 / 2 + mStrideCount = 0; + mBufferRowPointer = mChannelCount - 1; //Start filling data half way into buffer + mBufferColumnPointer = mBuffer[mBufferRowPointer].length - 1; + } + + /** + * 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; + } + } + } + + + public static void main(String[] args) + { + mLog.debug("Starting ..."); + + int channelBandwidth = 12500; + int channels = 4; + int tapsPerChannel = 15; + + float[] taps = null; + + try + { + taps = FilterFactory.getSincChannelizer( channelBandwidth, channels, tapsPerChannel, + Window.WindowType.BLACKMAN_HARRIS_7, true); + + } + catch(FilterDesignException fde) + { + mLog.error("Error in filter design", fde); + } + + int channelCenter = (int)(channelBandwidth * 1.5); + + StringBuilder sb = new StringBuilder(); + sb.append("\nPolyphase Channelizer\n"); + sb.append("Sample Rate:" + (channelBandwidth * channels) + "\n"); + sb.append("Channels:" + channels + "\n"); + sb.append("Channel Rate:" + channelBandwidth + "\n"); + sb.append("Tap Count:" + taps.length + "\n"); + sb.append("Channel:" + channelCenter + "\n"); + + mLog.debug(sb.toString()); + + ComplexPolyphaseChannelizer2 channelizer = new ComplexPolyphaseChannelizer2(taps, channels); + + Oscillator oscillator = new Oscillator(channelCenter, 2 * (channelBandwidth * channels)); + + for(int x = 0; x < 2000; x++) + { + channelizer.filter(oscillator.getComplex().inphase(), oscillator.getComplex().quadrature()); + oscillator.rotate(); + } + + mLog.debug("Finished!"); + } +} From f7cd798f07375630b3e9a9adcb53a24f0b12bdae Mon Sep 17 00:00:00 2001 From: Denny Date: Sat, 8 Apr 2017 08:35:48 -0400 Subject: [PATCH 15/87] WIP --- .../filter/channelizer/ChannelizerViewer.java | 4 +- .../ComplexPolyphaseChannelizer2.java | 90 ++++--- .../ComplexPolyphaseChannelizer3.java | 231 ++++++++++++++++++ 3 files changed, 287 insertions(+), 38 deletions(-) create mode 100644 src/dsp/filter/channelizer/ComplexPolyphaseChannelizer3.java diff --git a/src/dsp/filter/channelizer/ChannelizerViewer.java b/src/dsp/filter/channelizer/ChannelizerViewer.java index 343ce1d9..00c1affd 100644 --- a/src/dsp/filter/channelizer/ChannelizerViewer.java +++ b/src/dsp/filter/channelizer/ChannelizerViewer.java @@ -208,7 +208,7 @@ public class ChannelizerViewer extends JFrame public class ChannelArrayPanel extends JPanel implements Listener { - private ComplexPolyphaseChannelizer2 mPolyphaseChannelizer; + private ComplexPolyphaseChannelizer3 mPolyphaseChannelizer; private ChannelDistributor mChannelDistributor; public ChannelArrayPanel(float[] taps) @@ -219,7 +219,7 @@ public class ChannelizerViewer extends JFrame bufferSize++; } - mPolyphaseChannelizer = new ComplexPolyphaseChannelizer2(taps, mChannelCount); + mPolyphaseChannelizer = new ComplexPolyphaseChannelizer3(taps, mChannelCount); mChannelDistributor = new ChannelDistributor(bufferSize, mChannelCount); mPolyphaseChannelizer.setChannelDistributor(mChannelDistributor); diff --git a/src/dsp/filter/channelizer/ComplexPolyphaseChannelizer2.java b/src/dsp/filter/channelizer/ComplexPolyphaseChannelizer2.java index 6351483f..2579f2b7 100644 --- a/src/dsp/filter/channelizer/ComplexPolyphaseChannelizer2.java +++ b/src/dsp/filter/channelizer/ComplexPolyphaseChannelizer2.java @@ -37,15 +37,14 @@ public class ComplexPolyphaseChannelizer2 implements Listener private float[][] mFilterCoefficients; - - private float[][] mBuffer; + private float[][] mIBuffer; + private float[][] mQBuffer; private int mBufferRowPointer = 0; private int mBufferColumnPointer = 0; private int[][] mIndexMap; private int mStrideLength; private int mStrideCount; - private int mFilterPointer = 0; private int mChannelCount; private float[] mFilteredSamples; private FloatFFT_1D mFFT; @@ -79,13 +78,9 @@ public class ComplexPolyphaseChannelizer2 implements Listener public void filter(float inphase, float quadrature) { - mBuffer[mBufferRowPointer][mBufferColumnPointer] = quadrature; + mIBuffer[mBufferRowPointer][mBufferColumnPointer] = inphase; + mQBuffer[mBufferRowPointer][mBufferColumnPointer] = quadrature; - calculate(); - mBufferRowPointer--; - - mBuffer[mBufferRowPointer][mBufferColumnPointer] = inphase; - calculate(); mBufferRowPointer--; mStrideCount++; @@ -99,12 +94,12 @@ public class ComplexPolyphaseChannelizer2 implements Listener //Adjust the circular buffer pointers, wrapping as necessary if(mBufferRowPointer < 0) { - mBufferRowPointer += mBuffer.length; + mBufferRowPointer += mChannelCount; mBufferColumnPointer--; if(mBufferColumnPointer < 0) { - mBufferColumnPointer += mBuffer[mBufferRowPointer].length; + mBufferColumnPointer += mIBuffer[mBufferRowPointer].length; } } } @@ -122,11 +117,7 @@ public class ComplexPolyphaseChannelizer2 implements Listener private void dispatch() { - //Create a copy of the filtered samples - float[] samples = Arrays.copyOf(mFilteredSamples, mFilteredSamples.length); - - //Copy the first half of the samples copy back to the second half of the array to prepare for the next iteration - System.arraycopy(samples, 0, mFilteredSamples, mChannelCount, mChannelCount); + float[] samples = calculate(); //FFT is executed in-place where the output overwrites the input mFFT.complexForward(samples); @@ -138,28 +129,54 @@ public class ComplexPolyphaseChannelizer2 implements Listener * Calculates the value of the current data row and the current filter coefficient set and assigns the value to the * current filtered samples array */ - private void calculate() + private float[] calculate() { - float accumulator = 0.0f; + float[] samples = new float[mChannelCount * 2]; - float[] coefficients = mFilterCoefficients[mBufferRowPointer / 2]; - float[] samples = mBuffer[mBufferRowPointer]; - - for( int x = 0; x < coefficients.length; x++ ) + try { - int column = mIndexMap[mBufferColumnPointer][x]; + for(int x = 0; x < mChannelCount; x++) + { + float iAccumulator = 0.0f; + float qAccumulator = 0.0f; - accumulator += coefficients[ x ] * samples[column]; + int coefficientIndex; + + if(mBufferRowPointer <= 0) + { + coefficientIndex = x; + } + else if(x < mChannelCount / 2) + { + coefficientIndex = x + mChannelCount / 2; + } + else + { + coefficientIndex = x - mChannelCount / 2; + } + + float[] coefficients = mFilterCoefficients[coefficientIndex]; + float[] iSamples = mIBuffer[x]; + float[] qSamples = mQBuffer[x]; + + for( int y = 0; y < coefficients.length; y++ ) + { + int column = mIndexMap[mBufferColumnPointer][y]; + + iAccumulator += coefficients[y] * iSamples[column]; + qAccumulator += coefficients[y] * qSamples[column]; + } + + samples[coefficientIndex * 2] = iAccumulator; + samples[coefficientIndex * 2 + 1] = qAccumulator; + } + } + catch(Exception e) + { + mLog.error("Error", e); } - if(mBufferRowPointer < mChannelCount) - { - mFilteredSamples[mBufferRowPointer] = accumulator; - } - else - { - mFilteredSamples[mBufferRowPointer - mChannelCount] = accumulator; - } + return samples; } /** @@ -193,7 +210,9 @@ public class ComplexPolyphaseChannelizer2 implements Listener { int tapCount = (int)Math.ceil((double)taps.length / (double)mChannelCount); - mBuffer = new float[mChannelCount * 2][tapCount]; + mIBuffer = new float[mChannelCount][tapCount]; + mQBuffer = new float[mChannelCount][tapCount]; + mFilterCoefficients = new float[mChannelCount][tapCount]; for(int tap = 0; tap < tapCount; tap++) @@ -213,11 +232,10 @@ public class ComplexPolyphaseChannelizer2 implements Listener mFilteredSamples = new float[mChannelCount * 2]; mFFT = new FloatFFT_1D(mChannelCount); - mFilterPointer = mChannelCount -1; mStrideLength = mChannelCount / 2; //channel count * 2 / 2 mStrideCount = 0; - mBufferRowPointer = mChannelCount - 1; //Start filling data half way into buffer - mBufferColumnPointer = mBuffer[mBufferRowPointer].length - 1; + mBufferRowPointer = mChannelCount / 2 - 1; //Start filling data half way into buffer + mBufferColumnPointer = tapCount - 1; } /** diff --git a/src/dsp/filter/channelizer/ComplexPolyphaseChannelizer3.java b/src/dsp/filter/channelizer/ComplexPolyphaseChannelizer3.java new file mode 100644 index 00000000..e5ddfd30 --- /dev/null +++ b/src/dsp/filter/channelizer/ComplexPolyphaseChannelizer3.java @@ -0,0 +1,231 @@ +/******************************************************************************* + * 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 dsp.filter.FilterFactory; +import dsp.filter.Window; +import dsp.filter.fir.real.RealFIRFilter; +import dsp.mixer.Oscillator; +import org.jtransforms.fft.FloatFFT_1D; +import org.slf4j.Logger; +import org.slf4j.LoggerFactory; +import sample.Listener; +import sample.complex.ComplexBuffer; + +import java.text.DecimalFormat; + +public class ComplexPolyphaseChannelizer3 implements Listener +{ + private final static Logger mLog = LoggerFactory.getLogger(ComplexPolyphaseChannelizer3.class); + + private RealFIRFilter[] mInphaseFilters; + private RealFIRFilter[] mQuadratureFilters; + private int mInputPointer = 0; + private int mChannelCount; + private int mBlockSize; + private float[] mInputISamples; + private float[] mInputQSamples; + private float[] mOutputSamples; + private FloatFFT_1D mFFT; + private ChannelDistributor mChannelDistributor; + private boolean mFlipFlop = false; + + private DecimalFormat DECIMAL_FORMAT = new DecimalFormat("0.0000"); + + /** + * Creates a polyphase filter bank that divides the input frequency band into evenly spaced channels that + * are oversampled by 2x for output. + * + * @param taps of a low-pass filter designed for the inbound sample rate with a cutoff frequency + * equal to the channel bandwidth (sample rate / filters). If you need to synthesize (combine two or more + * filte outputs) a new bandwidth signal from the outputs of this filter, then the filter should be designed + * as a nyquist filter with -6 dB attenuation at the channel bandwidth cutoff frequency + * + * @param channels - number of filters/channels to output. Since this filter bank oversamples each filter + * output, this number must be even (divisible by the oversample rate). + */ + public ComplexPolyphaseChannelizer3(float[] taps, int channels) + { + if(channels % 2 != 0) + { + throw new IllegalArgumentException("Channel count must be a multiple of 2"); + } + + mChannelCount = channels; + + initFilters(taps); + } + + public void filter(float inphase, float quadrature) + { + mInputISamples[mInputPointer] = inphase; + mInputQSamples[mInputPointer] = quadrature; + + if(mInputPointer == 0) + { + calculate(); + mInputPointer = mChannelCount / 2 - 1; + } + else + { + mInputPointer--; + } + } + + @Override + public void receive(ComplexBuffer complexBuffer) + { + float[] samples = complexBuffer.getSamples(); + + for(int x = 0; x < samples.length; x += 2) + { + filter(samples[x], samples[x + 1]); + } + } + + private void calculate() + { + float[] filteredSamples = new float[mChannelCount * 2]; + + for(int x = 0; x < mChannelCount; x++) + { + int index = x * 2; + + filteredSamples[index] = mInphaseFilters[x].filter(mInputISamples[x]); + filteredSamples[index + 1] = mQuadratureFilters[x].filter(mInputQSamples[x]); + } + + //Toggled flip-flop here + if(mFlipFlop) + { + float[] flippedSamples = new float[mChannelCount * 2]; + + System.arraycopy(filteredSamples, 0, flippedSamples, mChannelCount, mChannelCount); + System.arraycopy(filteredSamples, mChannelCount, flippedSamples, 0, mChannelCount); + + filteredSamples = flippedSamples; + } + + mFlipFlop = !mFlipFlop; + + //FFT is executed in-place where the output overwrites the input + mFFT.complexForward(filteredSamples); + + dispatch(filteredSamples); + + int half = mChannelCount / 2; + + //Serpentine shift the 0 to m/2 samples to bottom of the input arrays + System.arraycopy(mInputISamples, half, mInputISamples, 0, half); + System.arraycopy(mInputQSamples, half, mInputQSamples, 0, half); + } + + private void dispatch(float[] channels) + { + if(mChannelDistributor != null) + { + mChannelDistributor.receive(channels); + } + } + + public void setChannelDistributor(ChannelDistributor channelDistributor) + { + mChannelDistributor = channelDistributor; + } + + private double magnitude(float real, float imaginary, int fftSize) + { + return Math.sqrt(Math.pow(real, 2.0) + Math.pow(imaginary, 2.0)); + } + + /** + * Distributes the filter taps to each of the polyphase filters + * + * @param taps + */ + private void initFilters(float[] taps) + { + int tapCount = (int)Math.ceil((double)taps.length / (double)mChannelCount); + + float[][] coefficients = new float[mChannelCount][tapCount]; + + for(int tap = 0; tap < tapCount; tap++) + { + for(int filter = 0; filter < mChannelCount; filter++) + { + int index = tap * mChannelCount + filter; + + if(index < taps.length) + { + coefficients[filter][tap] = taps[index]; + } + } + } + + mInphaseFilters = new RealFIRFilter[mChannelCount]; + mQuadratureFilters = new RealFIRFilter[mChannelCount]; + + for(int filter = 0; filter < mChannelCount; filter++) + { + mInphaseFilters[filter] = new RealFIRFilter(coefficients[filter], 1.0f); + mQuadratureFilters[filter] = new RealFIRFilter(coefficients[filter], 1.0f); + } + + mInputISamples = new float[mChannelCount]; + mInputQSamples = new float[mChannelCount]; + mOutputSamples = new float[mChannelCount * 2]; + mFFT = new FloatFFT_1D(mChannelCount); + mInputPointer = mChannelCount -1; + } + + public static void main(String[] args) + { + mLog.debug("Starting ..."); + + int symbolRate = 4800; + int samplesPerSymbol = 2; + int channels = 800; + int channelBandwidth = 12500; + int sampleRate = channels * channelBandwidth; + + float[] taps = FilterFactory.getLowPass( 10000000, 12500, 5600, Window.WindowType.BLACKMAN_HARRIS_7 ); + + int channelCenter = (int)(12500 * 1.5); + + StringBuilder sb = new StringBuilder(); + sb.append("\nPolyphase Channelizer\n"); + sb.append("Sample Rate:" + sampleRate + " Channels:" + channels + " Channel Rate:" + channelBandwidth + "\n"); + sb.append("Tap Count:" + taps.length + "\n"); + sb.append("Channel:" + channelCenter); + + mLog.debug(sb.toString()); + + ComplexPolyphaseChannelizer3 channelizer = new ComplexPolyphaseChannelizer3(taps, 8); + + Oscillator oscillator = new Oscillator(channelCenter, 2 * sampleRate); + + for(int x = 0; x < 2000; x++) + { + channelizer.filter(oscillator.getComplex().inphase(), oscillator.getComplex().quadrature()); + oscillator.rotate(); + } + + mLog.debug("Finished!"); + } +} From ac686f5f647e4052a7f45d10d7c7317984f045e9 Mon Sep 17 00:00:00 2001 From: Denny Date: Fri, 14 Apr 2017 06:41:43 -0400 Subject: [PATCH 16/87] WIP - correctly implements the 2x oversampled polyphase channelizer. --- src/buffer/FloatCircularBuffer.java | 206 ++++++------ src/buffer/ReverseFloatCircularBuffer.java | 89 +++++ .../filter/channelizer/ChannelizerViewer.java | 15 +- .../ComplexPolyphaseChannelizer.java | 19 +- .../ComplexPolyphaseChannelizer2.java | 306 ------------------ ...ava => ComplexPolyphaseChannelizerM2.java} | 159 +++++---- .../filter/correction/IQCorrectionFilter.java | 51 +++ src/dsp/gain/AutomaticGainControl.java | 2 +- .../gain/ComplexFeedForwardGainControl.java | 2 +- 9 files changed, 360 insertions(+), 489 deletions(-) create mode 100644 src/buffer/ReverseFloatCircularBuffer.java delete mode 100644 src/dsp/filter/channelizer/ComplexPolyphaseChannelizer2.java rename src/dsp/filter/channelizer/{ComplexPolyphaseChannelizer3.java => ComplexPolyphaseChannelizerM2.java} (58%) create mode 100644 src/dsp/filter/correction/IQCorrectionFilter.java diff --git a/src/buffer/FloatCircularBuffer.java b/src/buffer/FloatCircularBuffer.java index ccb749e1..4f6d600a 100644 --- a/src/buffer/FloatCircularBuffer.java +++ b/src/buffer/FloatCircularBuffer.java @@ -1,19 +1,20 @@ /******************************************************************************* - * SDR Trunk - * Copyright (C) 2014 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 + * 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 buffer; @@ -21,98 +22,105 @@ import java.util.Arrays; /** * Circular sample buffer - allocates a buffer and stores samples in a circular - * fashion, overwriting older samples with newly arrived samples. Initially - * fills buffer with 0-valued samples - * - * Can be used as a delay-type buffer, to delay samples by the 'size' amount - * @author denny + * fashion, overwriting older samples with newly arrived samples. * + * Can be used as a delay-type buffer, to delay samples by the 'size' amount */ public class FloatCircularBuffer { - float[] mBuffer; - int mBufferPointer = 0; - - public FloatCircularBuffer( int size ) - { - mBuffer = new float[ size ]; - - Arrays.fill( mBuffer, (float)0.0 ); - } - - public int getSize() - { - return mBuffer.length; - } - - public float[] getBuffer() - { - return mBuffer; - } + float[] mBuffer; + int mBufferPointer = 0; - /** - * Puts the new value into the buffer and returns the oldest buffer value - * that it replaced - * @param newValue - * @return - */ - public float get( float newValue ) - { - float oldestSample = mBuffer[ mBufferPointer ]; + public FloatCircularBuffer(int size) + { + mBuffer = new float[size]; + } - mBuffer[ mBufferPointer ] = newValue; - - mBufferPointer++; - - if( mBufferPointer >= mBuffer.length ) - { - mBufferPointer = 0; - } - - return oldestSample; - } + public int getSize() + { + return mBuffer.length; + } - /** - * Convenience method to put a new value in the buffer, while discarding - * the old value - */ - public void put( float newValue ) - { - get( newValue ); - } + public float[] getBuffer() + { + return mBuffer; + } - /** - * Returns an array of arraySize, loaded with sampleCount of the - * oldest samples, loaded into the array starting at index 0. - * - * This is useful for fetching an array filled half way with samples to - * use in FFT processing. - * @param arraySize - size of the array (must be larger than sampleCount) - * @param sampleCount - number of the oldest samples in the buffer to load - * @return - */ - public float[] get( int arraySize, int sampleCount ) - { - float[] samples = new float[ arraySize ]; + /** + * Puts the new value into the buffer and returns the oldest buffer value + * that it replaced + * + * @param newValue + * @return + */ + public float putAndGet(float newValue) + { + float oldestSample = mBuffer[mBufferPointer]; - if( arraySize > sampleCount ) - { - int tempCounter = mBufferPointer; + put(newValue); - for( int x = 0; x < sampleCount; x++ ) - { - samples[ x ] = mBuffer[ tempCounter ]; - - tempCounter++; - - if( tempCounter > mBuffer.length ) - { - tempCounter = 0; - } - } - } - - return samples; - } - + return oldestSample; + } + + /** + * Adds the new value to this buffer. + */ + public void put(float newValue) + { + mBuffer[mBufferPointer] = newValue; + + mBufferPointer++; + + if(mBufferPointer >= mBuffer.length) + { + mBufferPointer = 0; + } + } + + /** + * Returns the sample at the specified index, where index 0 is the oldest sample and index (length - 1) is the + * newest sample. + * @param index in range of 0 to length-1 + * @return value at indexed position + */ + + public float get(int index) + { + if(index < mBuffer.length) + { + int pointer = mBufferPointer + index; + + if(pointer >= mBuffer.length) + { + pointer -= mBuffer.length; + } + + return mBuffer[pointer]; + } + else + { + throw new ArrayIndexOutOfBoundsException("Index [" + index + "] is not valid for the buffer size of [" + + mBuffer.length + "]"); + } + } + + /** + * Returns the contents of the circular buffer unwrapped where index 0 is the oldest sample and index length-1 + * is the newest sample. + * + * @return array of samples + */ + public float[] get() + { + float[] samples = new float[mBuffer.length]; + + System.arraycopy(mBuffer, mBufferPointer, samples, 0, mBuffer.length - mBufferPointer); + + if(mBufferPointer != 0) + { + System.arraycopy(mBuffer, 0, samples, mBuffer.length - mBufferPointer, mBufferPointer); + } + + return samples; + } } diff --git a/src/buffer/ReverseFloatCircularBuffer.java b/src/buffer/ReverseFloatCircularBuffer.java new file mode 100644 index 00000000..3c3adb40 --- /dev/null +++ b/src/buffer/ReverseFloatCircularBuffer.java @@ -0,0 +1,89 @@ +/******************************************************************************* + * 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 buffer; + +/** + * Circular sample buffer - allocates a buffer and stores samples in a circular + * fashion, overwriting older samples with newly arrived samples. + */ +public class ReverseFloatCircularBuffer +{ + private float[] mBuffer; + private int mBufferPointer = 0; + + public ReverseFloatCircularBuffer(int size) + { + mBuffer = new float[size]; + mBufferPointer = mBuffer.length - 1; + } + + /** + * Size of the buffer + * @return + */ + public int getSize() + { + return mBuffer.length; + } + + public float[] getBuffer() + { + return mBuffer; + } + + /** + * Adds the new value to this buffer. + */ + public void put(float newValue) + { + mBuffer[mBufferPointer--] = newValue; + + if(mBufferPointer < 0) + { + mBufferPointer = mBuffer.length - 1; + } + } + + /** + * Returns the sample at the specified index, where index 0 is the newest sample and index (length - 1) is the + * oldest sample. + * @param index in range of 0 to length-1 + * @return value at indexed position + */ + + public float get(int index) + { + if(index < mBuffer.length) + { + int pointer = mBufferPointer + 1 + index; + + if(pointer >= mBuffer.length) + { + pointer -= mBuffer.length; + } + + return mBuffer[pointer]; + } + else + { + throw new ArrayIndexOutOfBoundsException("Index [" + index + "] is not valid for the buffer size of [" + + mBuffer.length + "]"); + } + } +} diff --git a/src/dsp/filter/channelizer/ChannelizerViewer.java b/src/dsp/filter/channelizer/ChannelizerViewer.java index 00c1affd..b6064c62 100644 --- a/src/dsp/filter/channelizer/ChannelizerViewer.java +++ b/src/dsp/filter/channelizer/ChannelizerViewer.java @@ -20,6 +20,7 @@ package dsp.filter.channelizer; import dsp.filter.FilterFactory; import dsp.filter.Window; +import dsp.filter.correction.IQCorrectionFilter; import dsp.filter.design.FilterDesignException; import dsp.mixer.LowPhaseNoiseOscillator; import net.miginfocom.swing.MigLayout; @@ -59,6 +60,7 @@ public class ChannelizerViewer extends JFrame private LowPhaseNoiseOscillator mOscillator; private int mChannelCount; private int mChannelsPerRow; + private int mFilterTapsPerChannel; private int mSampleRate; private int mToneFrequency; private DFTSize mMainPanelDFTSize = DFTSize.FFT32768; @@ -70,10 +72,12 @@ public class ChannelizerViewer extends JFrame * @param channelCount * @param channelsPerRow */ - public ChannelizerViewer(int channelCount, int channelsPerRow) + public ChannelizerViewer(int channelCount, int channelsPerRow, int tapsPerChannel) { mChannelCount = channelCount; mChannelsPerRow = channelsPerRow; + mFilterTapsPerChannel = tapsPerChannel; + mSampleRate = mChannelCount * CHANNEL_BANDWIDTH; mToneFrequency = (int)(CHANNEL_BANDWIDTH * 1); //Set to channel 1 as default @@ -202,13 +206,13 @@ public class ChannelizerViewer extends JFrame private float[] getFilter() throws FilterDesignException { - return FilterFactory.getSincChannelizer(CHANNEL_BANDWIDTH, mChannelCount, 19, + return FilterFactory.getSincChannelizer(CHANNEL_BANDWIDTH, mChannelCount, mFilterTapsPerChannel, Window.WindowType.BLACKMAN_HARRIS_7, true); } public class ChannelArrayPanel extends JPanel implements Listener { - private ComplexPolyphaseChannelizer3 mPolyphaseChannelizer; + private ComplexPolyphaseChannelizerM2 mPolyphaseChannelizer; private ChannelDistributor mChannelDistributor; public ChannelArrayPanel(float[] taps) @@ -219,7 +223,7 @@ public class ChannelizerViewer extends JFrame bufferSize++; } - mPolyphaseChannelizer = new ComplexPolyphaseChannelizer3(taps, mChannelCount); + mPolyphaseChannelizer = new ComplexPolyphaseChannelizerM2(taps, mChannelCount); mChannelDistributor = new ChannelDistributor(bufferSize, mChannelCount); mPolyphaseChannelizer.setChannelDistributor(mChannelDistributor); @@ -298,8 +302,9 @@ public class ChannelizerViewer extends JFrame { int channelCount = 4; int channelsPerRow = 4; + int tapsPerChannel = 17; - final ChannelizerViewer frame = new ChannelizerViewer(channelCount, channelsPerRow); + final ChannelizerViewer frame = new ChannelizerViewer(channelCount, channelsPerRow, tapsPerChannel); EventQueue.invokeLater(new Runnable() { diff --git a/src/dsp/filter/channelizer/ComplexPolyphaseChannelizer.java b/src/dsp/filter/channelizer/ComplexPolyphaseChannelizer.java index 117b0382..408f844d 100644 --- a/src/dsp/filter/channelizer/ComplexPolyphaseChannelizer.java +++ b/src/dsp/filter/channelizer/ComplexPolyphaseChannelizer.java @@ -38,32 +38,23 @@ public class ComplexPolyphaseChannelizer implements Listener private RealFIRFilter[] mQuadratureFilters; private int mFilterPointer = 0; private int mChannelCount; - private int mBlockSize; private float[] mFilteredSamples; private FloatFFT_1D mFFT; private ChannelDistributor mChannelDistributor; - private DecimalFormat DECIMAL_FORMAT = new DecimalFormat("0.0000"); - /** - * Creates a polyphase filter bank that divides the input frequency band into evenly spaced channels that - * are oversampled by 2x for output. + * Maximally Decimated Polyphase Filter Bank (MDPFB) channelizer that divides the input frequency band into + * equal bandwidth channels. * * @param taps of a low-pass filter designed for the inbound sample rate with a cutoff frequency * equal to the channel bandwidth (sample rate / filters). If you need to synthesize (combine two or more * filte outputs) a new bandwidth signal from the outputs of this filter, then the filter should be designed * as a nyquist filter with -6 dB attenuation at the channel bandwidth cutoff frequency * - * @param channels - number of filters/channels to output. Since this filter bank oversamples each filter - * output, this number must be even (divisible by the oversample rate). + * @param channels - number of channels to output. */ - public ComplexPolyphaseChannelizer(float[] taps, int channels, int oversample) + public ComplexPolyphaseChannelizer(float[] taps, int channels) { - if(oversample > 1 && channels % 2 != 0) - { - throw new IllegalArgumentException("Channel count must be a multiple of 2 when oversampling"); - } - mChannelCount = channels; initFilters(taps); @@ -187,7 +178,7 @@ public class ComplexPolyphaseChannelizer implements Listener mLog.debug(sb.toString()); - ComplexPolyphaseChannelizer channelizer = new ComplexPolyphaseChannelizer(taps, 8, 1); + ComplexPolyphaseChannelizer channelizer = new ComplexPolyphaseChannelizer(taps, 8); Oscillator oscillator = new Oscillator(channelCenter, 2 * sampleRate); diff --git a/src/dsp/filter/channelizer/ComplexPolyphaseChannelizer2.java b/src/dsp/filter/channelizer/ComplexPolyphaseChannelizer2.java deleted file mode 100644 index 2579f2b7..00000000 --- a/src/dsp/filter/channelizer/ComplexPolyphaseChannelizer2.java +++ /dev/null @@ -1,306 +0,0 @@ -/******************************************************************************* - * 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 dsp.filter.FilterFactory; -import dsp.filter.Window; -import dsp.filter.design.FilterDesignException; -import dsp.mixer.Oscillator; -import org.jtransforms.fft.FloatFFT_1D; -import org.slf4j.Logger; -import org.slf4j.LoggerFactory; -import sample.Listener; -import sample.complex.ComplexBuffer; - -import java.text.DecimalFormat; -import java.util.Arrays; - -public class ComplexPolyphaseChannelizer2 implements Listener -{ - private final static Logger mLog = LoggerFactory.getLogger(ComplexPolyphaseChannelizer2.class); - - private float[][] mFilterCoefficients; - - private float[][] mIBuffer; - private float[][] mQBuffer; - private int mBufferRowPointer = 0; - private int mBufferColumnPointer = 0; - private int[][] mIndexMap; - private int mStrideLength; - private int mStrideCount; - - private int mChannelCount; - private float[] mFilteredSamples; - private FloatFFT_1D mFFT; - private ChannelDistributor mChannelDistributor; - - private DecimalFormat DECIMAL_FORMAT = new DecimalFormat("0.0000"); - - /** - * Creates a polyphase filter bank that divides the input frequency band into evenly spaced channels that - * are oversampled by 2x for output. - * - * @param taps of a low-pass filter designed for the inbound sample rate with a cutoff frequency - * equal to the channel bandwidth (sample rate / filters). If you need to synthesize (combine two or more - * filte outputs) a new bandwidth signal from the outputs of this filter, then the filter should be designed - * as a nyquist filter with -6 dB attenuation at the channel bandwidth cutoff frequency - * - * @param channels - number of filters/channels to output. Since this filter bank oversamples each filter - * output, this number must be even (divisible by the oversample rate). - */ - public ComplexPolyphaseChannelizer2(float[] taps, int channels) - { - if(channels % 2 != 0) - { - throw new IllegalArgumentException("Channel count must be a multiple of 2"); - } - - mChannelCount = channels; - - initFilters(taps); - } - - public void filter(float inphase, float quadrature) - { - mIBuffer[mBufferRowPointer][mBufferColumnPointer] = inphase; - mQBuffer[mBufferRowPointer][mBufferColumnPointer] = quadrature; - - mBufferRowPointer--; - - mStrideCount++; - - if(mStrideCount >= mStrideLength) - { - dispatch(); - mStrideCount = 0; - } - - //Adjust the circular buffer pointers, wrapping as necessary - if(mBufferRowPointer < 0) - { - mBufferRowPointer += mChannelCount; - mBufferColumnPointer--; - - if(mBufferColumnPointer < 0) - { - mBufferColumnPointer += mIBuffer[mBufferRowPointer].length; - } - } - } - - @Override - public void receive(ComplexBuffer complexBuffer) - { - float[] samples = complexBuffer.getSamples(); - - for(int x = 0; x < samples.length; x += 2) - { - filter(samples[x], samples[x + 1]); - } - } - - private void dispatch() - { - float[] samples = calculate(); - - //FFT is executed in-place where the output overwrites the input - mFFT.complexForward(samples); - - dispatch(samples); - } - - /** - * Calculates the value of the current data row and the current filter coefficient set and assigns the value to the - * current filtered samples array - */ - private float[] calculate() - { - float[] samples = new float[mChannelCount * 2]; - - try - { - for(int x = 0; x < mChannelCount; x++) - { - float iAccumulator = 0.0f; - float qAccumulator = 0.0f; - - int coefficientIndex; - - if(mBufferRowPointer <= 0) - { - coefficientIndex = x; - } - else if(x < mChannelCount / 2) - { - coefficientIndex = x + mChannelCount / 2; - } - else - { - coefficientIndex = x - mChannelCount / 2; - } - - float[] coefficients = mFilterCoefficients[coefficientIndex]; - float[] iSamples = mIBuffer[x]; - float[] qSamples = mQBuffer[x]; - - for( int y = 0; y < coefficients.length; y++ ) - { - int column = mIndexMap[mBufferColumnPointer][y]; - - iAccumulator += coefficients[y] * iSamples[column]; - qAccumulator += coefficients[y] * qSamples[column]; - } - - samples[coefficientIndex * 2] = iAccumulator; - samples[coefficientIndex * 2 + 1] = qAccumulator; - } - } - catch(Exception e) - { - mLog.error("Error", e); - } - - return samples; - } - - /** - * Dispatches the filtered, de-spun samples as an array of channel I/Q sample pairs - * @param channels arranged as: channel0i, channel0q, channel1i, channel1q ... channelN-1i, channelN-1q - */ - private void dispatch(float[] channels) - { - if(mChannelDistributor != null) - { - mChannelDistributor.receive(channels); - } - } - - public void setChannelDistributor(ChannelDistributor channelDistributor) - { - mChannelDistributor = channelDistributor; - } - - private double magnitude(float real, float imaginary, int fftSize) - { - return Math.sqrt(Math.pow(real, 2.0) + Math.pow(imaginary, 2.0)); - } - - /** - * Distributes the filter taps to each of the polyphase filters - * - * @param taps - */ - private void initFilters(float[] taps) - { - int tapCount = (int)Math.ceil((double)taps.length / (double)mChannelCount); - - mIBuffer = new float[mChannelCount][tapCount]; - mQBuffer = new float[mChannelCount][tapCount]; - - mFilterCoefficients = new float[mChannelCount][tapCount]; - - for(int tap = 0; tap < tapCount; tap++) - { - for(int filter = 0; filter < mChannelCount; filter++) - { - int index = tap * mChannelCount + filter; - - if(index < taps.length) - { - mFilterCoefficients[filter][tap] = taps[index]; - } - } - } - - generateIndexMap(tapCount); - - mFilteredSamples = new float[mChannelCount * 2]; - mFFT = new FloatFFT_1D(mChannelCount); - mStrideLength = mChannelCount / 2; //channel count * 2 / 2 - mStrideCount = 0; - mBufferRowPointer = mChannelCount / 2 - 1; //Start filling data half way into buffer - mBufferColumnPointer = tapCount - 1; - } - - /** - * 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; - } - } - } - - - public static void main(String[] args) - { - mLog.debug("Starting ..."); - - int channelBandwidth = 12500; - int channels = 4; - int tapsPerChannel = 15; - - float[] taps = null; - - try - { - taps = FilterFactory.getSincChannelizer( channelBandwidth, channels, tapsPerChannel, - Window.WindowType.BLACKMAN_HARRIS_7, true); - - } - catch(FilterDesignException fde) - { - mLog.error("Error in filter design", fde); - } - - int channelCenter = (int)(channelBandwidth * 1.5); - - StringBuilder sb = new StringBuilder(); - sb.append("\nPolyphase Channelizer\n"); - sb.append("Sample Rate:" + (channelBandwidth * channels) + "\n"); - sb.append("Channels:" + channels + "\n"); - sb.append("Channel Rate:" + channelBandwidth + "\n"); - sb.append("Tap Count:" + taps.length + "\n"); - sb.append("Channel:" + channelCenter + "\n"); - - mLog.debug(sb.toString()); - - ComplexPolyphaseChannelizer2 channelizer = new ComplexPolyphaseChannelizer2(taps, channels); - - Oscillator oscillator = new Oscillator(channelCenter, 2 * (channelBandwidth * channels)); - - for(int x = 0; x < 2000; x++) - { - channelizer.filter(oscillator.getComplex().inphase(), oscillator.getComplex().quadrature()); - oscillator.rotate(); - } - - mLog.debug("Finished!"); - } -} diff --git a/src/dsp/filter/channelizer/ComplexPolyphaseChannelizer3.java b/src/dsp/filter/channelizer/ComplexPolyphaseChannelizerM2.java similarity index 58% rename from src/dsp/filter/channelizer/ComplexPolyphaseChannelizer3.java rename to src/dsp/filter/channelizer/ComplexPolyphaseChannelizerM2.java index e5ddfd30..4ee0f32d 100644 --- a/src/dsp/filter/channelizer/ComplexPolyphaseChannelizer3.java +++ b/src/dsp/filter/channelizer/ComplexPolyphaseChannelizerM2.java @@ -18,9 +18,9 @@ ******************************************************************************/ package dsp.filter.channelizer; +import buffer.ReverseFloatCircularBuffer; import dsp.filter.FilterFactory; import dsp.filter.Window; -import dsp.filter.fir.real.RealFIRFilter; import dsp.mixer.Oscillator; import org.jtransforms.fft.FloatFFT_1D; import org.slf4j.Logger; @@ -30,27 +30,22 @@ import sample.complex.ComplexBuffer; import java.text.DecimalFormat; -public class ComplexPolyphaseChannelizer3 implements Listener +public class ComplexPolyphaseChannelizerM2 implements Listener { - private final static Logger mLog = LoggerFactory.getLogger(ComplexPolyphaseChannelizer3.class); + private final static Logger mLog = LoggerFactory.getLogger(ComplexPolyphaseChannelizerM2.class); - private RealFIRFilter[] mInphaseFilters; - private RealFIRFilter[] mQuadratureFilters; - private int mInputPointer = 0; + private ReverseFloatCircularBuffer[] mDataBuffers; + private float[][] mFilterCoefficients; + private int mFilterPointer = 0; private int mChannelCount; - private int mBlockSize; - private float[] mInputISamples; - private float[] mInputQSamples; - private float[] mOutputSamples; private FloatFFT_1D mFFT; private ChannelDistributor mChannelDistributor; - private boolean mFlipFlop = false; private DecimalFormat DECIMAL_FORMAT = new DecimalFormat("0.0000"); /** - * Creates a polyphase filter bank that divides the input frequency band into evenly spaced channels that - * are oversampled by 2x for output. + * Non-Maximally Decimated Polyphase Filter Bank (NMDPFB) channelizer that divides the input frequency band into + * equal bandwidth channels that are each oversampled by 2x for output. * * @param taps of a low-pass filter designed for the inbound sample rate with a cutoff frequency * equal to the channel bandwidth (sample rate / filters). If you need to synthesize (combine two or more @@ -60,7 +55,7 @@ public class ComplexPolyphaseChannelizer3 implements Listener * @param channels - number of filters/channels to output. Since this filter bank oversamples each filter * output, this number must be even (divisible by the oversample rate). */ - public ComplexPolyphaseChannelizer3(float[] taps, int channels) + public ComplexPolyphaseChannelizerM2(float[] taps, int channels) { if(channels % 2 != 0) { @@ -74,17 +69,26 @@ public class ComplexPolyphaseChannelizer3 implements Listener public void filter(float inphase, float quadrature) { - mInputISamples[mInputPointer] = inphase; - mInputQSamples[mInputPointer] = quadrature; + try + { + mDataBuffers[mFilterPointer--].put(quadrature); + mDataBuffers[mFilterPointer].put(inphase); - if(mInputPointer == 0) - { - calculate(); - mInputPointer = mChannelCount / 2 - 1; + if(mFilterPointer == 0) + { + calculate(); + mFilterPointer = mChannelCount * 2; + } + else if(mFilterPointer == mChannelCount) + { + calculate(); + } + + mFilterPointer--; } - else + catch(Exception e) { - mInputPointer--; + mLog.error("Error!", e); } } @@ -101,39 +105,73 @@ public class ComplexPolyphaseChannelizer3 implements Listener private void calculate() { - float[] filteredSamples = new float[mChannelCount * 2]; + float[] samples = new float[mChannelCount * 2]; - for(int x = 0; x < mChannelCount; x++) + if(mFilterPointer == 0) { - int index = x * 2; + for(int x = 0; x < mFilterCoefficients.length; x++) + { + int index = x * 2; - filteredSamples[index] = mInphaseFilters[x].filter(mInputISamples[x]); - filteredSamples[index + 1] = mQuadratureFilters[x].filter(mInputQSamples[x]); + //Inphase sample + samples[index] = convolve(mFilterCoefficients[x], mDataBuffers[index]); + + //Quadrature sample + samples[index + 1] = convolve(mFilterCoefficients[x], mDataBuffers[index + 1]); + + } + } + else + { + int half = mFilterCoefficients.length / 2; + + for(int x = 0; x < half; x++) + { + int index = x * 2; + int filterIndex = x + half; + + //Inphase sample + samples[index] = convolve(mFilterCoefficients[filterIndex], mDataBuffers[index]); + + //Quadrature sample + samples[index + 1] = convolve(mFilterCoefficients[filterIndex], mDataBuffers[index + 1]); + } + + for(int x = half; x < mFilterCoefficients.length; x++) + { + int index = x * 2; + int filterIndex = x - half; + + //Inphase sample + samples[index] = convolve(mFilterCoefficients[filterIndex], mDataBuffers[index]); + + //Quadrature sample + samples[index + 1] = convolve(mFilterCoefficients[filterIndex], mDataBuffers[index + 1]); + } } - //Toggled flip-flop here - if(mFlipFlop) + //IFFT is executed in-place with the output overwriting the input + mFFT.complexInverse(samples, true); + + dispatch(samples); + } + + /** + * Filters the samples contained in the buffer against the filter coefficients + * @param coefficients of the sub filter + * @param buffer containing data samples + * @return + */ + private float convolve(float[] coefficients, ReverseFloatCircularBuffer buffer) + { + float accumulator = 0.0f; + + for( int x = 0; x < coefficients.length; x++ ) { - float[] flippedSamples = new float[mChannelCount * 2]; - - System.arraycopy(filteredSamples, 0, flippedSamples, mChannelCount, mChannelCount); - System.arraycopy(filteredSamples, mChannelCount, flippedSamples, 0, mChannelCount); - - filteredSamples = flippedSamples; + accumulator += coefficients[ x ] * buffer.get(x); } - mFlipFlop = !mFlipFlop; - - //FFT is executed in-place where the output overwrites the input - mFFT.complexForward(filteredSamples); - - dispatch(filteredSamples); - - int half = mChannelCount / 2; - - //Serpentine shift the 0 to m/2 samples to bottom of the input arrays - System.arraycopy(mInputISamples, half, mInputISamples, 0, half); - System.arraycopy(mInputQSamples, half, mInputQSamples, 0, half); + return accumulator; } private void dispatch(float[] channels) @@ -163,7 +201,14 @@ public class ComplexPolyphaseChannelizer3 implements Listener { int tapCount = (int)Math.ceil((double)taps.length / (double)mChannelCount); - float[][] coefficients = new float[mChannelCount][tapCount]; + mDataBuffers = new ReverseFloatCircularBuffer[mChannelCount * 2]; + + for(int x = 0; x < mChannelCount * 2; x++) + { + mDataBuffers[x] = new ReverseFloatCircularBuffer(tapCount); + } + + mFilterCoefficients = new float[mChannelCount][tapCount]; for(int tap = 0; tap < tapCount; tap++) { @@ -173,25 +218,13 @@ public class ComplexPolyphaseChannelizer3 implements Listener if(index < taps.length) { - coefficients[filter][tap] = taps[index]; + mFilterCoefficients[filter][tap] = taps[index]; } } } - mInphaseFilters = new RealFIRFilter[mChannelCount]; - mQuadratureFilters = new RealFIRFilter[mChannelCount]; - - for(int filter = 0; filter < mChannelCount; filter++) - { - mInphaseFilters[filter] = new RealFIRFilter(coefficients[filter], 1.0f); - mQuadratureFilters[filter] = new RealFIRFilter(coefficients[filter], 1.0f); - } - - mInputISamples = new float[mChannelCount]; - mInputQSamples = new float[mChannelCount]; - mOutputSamples = new float[mChannelCount * 2]; mFFT = new FloatFFT_1D(mChannelCount); - mInputPointer = mChannelCount -1; + mFilterPointer = mChannelCount - 1; //Start at M/2 } public static void main(String[] args) @@ -216,7 +249,7 @@ public class ComplexPolyphaseChannelizer3 implements Listener mLog.debug(sb.toString()); - ComplexPolyphaseChannelizer3 channelizer = new ComplexPolyphaseChannelizer3(taps, 8); + ComplexPolyphaseChannelizerM2 channelizer = new ComplexPolyphaseChannelizerM2(taps, 8); Oscillator oscillator = new Oscillator(channelCenter, 2 * sampleRate); diff --git a/src/dsp/filter/correction/IQCorrectionFilter.java b/src/dsp/filter/correction/IQCorrectionFilter.java new file mode 100644 index 00000000..14f84de8 --- /dev/null +++ b/src/dsp/filter/correction/IQCorrectionFilter.java @@ -0,0 +1,51 @@ +/******************************************************************************* + * 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.correction; + +import sample.complex.ComplexBuffer; + +public class IQCorrectionFilter +{ + private float mRatio = 0.00001f; + private float mAverageInphase = 0.0f; + private float mAverageQuadrature = 0.0f; + + public IQCorrectionFilter(float ratio) + { + mRatio = ratio; + } + + public IQCorrectionFilter() + { + } + + public void filter(ComplexBuffer buffer) + { + float[] samples = buffer.getSamples(); + + for(int x = 0; x < samples.length; x += 2) + { + mAverageInphase = mAverageInphase + (mRatio * (samples[x] - mAverageInphase)); + samples[x] -= mAverageInphase; + + mAverageQuadrature = mAverageQuadrature + (mRatio * (samples[x + 1] - mAverageQuadrature)); + samples[x + 1] -= mAverageQuadrature; + } + } +} diff --git a/src/dsp/gain/AutomaticGainControl.java b/src/dsp/gain/AutomaticGainControl.java index 8f397ebd..b46dd105 100644 --- a/src/dsp/gain/AutomaticGainControl.java +++ b/src/dsp/gain/AutomaticGainControl.java @@ -142,7 +142,7 @@ public class AutomaticGainControl public float process( float currentSample ) { - float delayedSample = mDelayBuffer.get( currentSample ); + float delayedSample = mDelayBuffer.putAndGet( currentSample ); double gain = MANUAL_AGC_GAIN; diff --git a/src/dsp/gain/ComplexFeedForwardGainControl.java b/src/dsp/gain/ComplexFeedForwardGainControl.java index 6abcd62c..20449301 100644 --- a/src/dsp/gain/ComplexFeedForwardGainControl.java +++ b/src/dsp/gain/ComplexFeedForwardGainControl.java @@ -74,7 +74,7 @@ public class ComplexFeedForwardGainControl implements ComplexSampleListener } /* Replace oldest envelope value with current envelope value */ - float oldestEnvelope = mEnvelopeHistory.get( envelope ); + float oldestEnvelope = mEnvelopeHistory.putAndGet( envelope ); /* If the oldest envelope value was the max envelope value, then we * have to rediscover the max value from the envelope history */ From 7c6b7494b8b7f2f79a880e88edf25036e7118db5 Mon Sep 17 00:00:00 2001 From: Denny Date: Fri, 21 Apr 2017 12:17:55 -0400 Subject: [PATCH 17/87] wip-channelizer --- src/channel/state/ChannelState.java | 26 +- .../traffic/TrafficChannelManager.java | 8 +- src/controller/channel/Channel.java | 16 +- src/dsp/filter/FilterFactory.java | 6 +- .../AbstractComplexPolyphaseChannelizer.java | 128 +++ src/dsp/filter/channelizer/Channelizer.java | 135 +++ .../filter/channelizer/ChannelizerViewer.java | 12 +- .../ComplexPolyphaseChannelizer.java | 83 +- .../ComplexPolyphaseChannelizerM2.java | 77 +- .../channelizer/PolyphaseChannelSource.java | 201 +++++ .../filter/fir/FIRFilterSpecification.java | 2 +- .../remez/PolyphaseChannelizerDesigner.java | 2 +- src/dsp/psk/LSMDemodulator.java | 12 +- src/gui/control/JFrequencyControl.java | 127 ++- src/module/ProcessingChain.java | 40 +- src/module/decode/p25/C4FMSymbolFilter.java | 846 +++++++++--------- src/module/decode/p25/P25Decoder.java | 6 +- src/module/decode/p25/P25_C4FMDecoder.java | 16 +- src/module/decode/p25/P25_LSMDecoder.java | 12 +- src/sample/SampleType.java | 5 +- src/source/ComplexSource.java | 27 +- src/source/ISourceEventListener.java | 35 + src/source/ISourceEventProcessor.java | 24 + ...rovider.java => ISourceEventProvider.java} | 10 +- src/source/RealSource.java | 28 +- src/source/Source.java | 44 +- src/source/SourceEvent.java | 192 ++++ src/source/config/SourceConfigRecording.java | 106 +-- src/source/config/SourceConfigTuner.java | 103 ++- src/source/mixer/RealMixerSource.java | 8 +- src/source/recording/Recording.java | 12 +- src/source/tuner/ComplexChannelSource.java | 169 ++++ src/source/tuner/Tuner.java | 372 ++++---- src/source/tuner/TunerChannel.java | 195 ++-- src/source/tuner/TunerChannelSource.java | 64 +- src/source/tuner/TunerController.java | 14 +- src/source/tuner/TunerSourceEditor.java | 8 +- .../frequency/DCTrackingFrequencyControl.java | 128 --- .../frequency/DCTrackingSourceControl.java | 130 +++ .../tuner/frequency/FrequencyChangeEvent.java | 34 - .../FrequencyChangeProcessorWrapper.java | 45 - .../tuner/frequency/FrequencyController.java | 374 ++++---- .../frequency/IFrequencyChangeListener.java | 34 - .../frequency/IFrequencyChangeProcessor.java | 8 - src/source/wave/ComplexWaveSource.java | 8 +- src/source/wave/RealWaveSource.java | 8 +- src/spectrum/ChannelSpectrumPanel.java | 7 +- src/spectrum/DFTProcessor.java | 8 +- src/spectrum/OverlayPanel.java | 8 +- src/spectrum/SpectralDisplayPanel.java | 67 +- 50 files changed, 2360 insertions(+), 1670 deletions(-) create mode 100644 src/dsp/filter/channelizer/AbstractComplexPolyphaseChannelizer.java create mode 100644 src/dsp/filter/channelizer/Channelizer.java create mode 100644 src/dsp/filter/channelizer/PolyphaseChannelSource.java create mode 100644 src/source/ISourceEventListener.java create mode 100644 src/source/ISourceEventProcessor.java rename src/source/{tuner/frequency/IFrequencyChangeProvider.java => ISourceEventProvider.java} (79%) create mode 100644 src/source/SourceEvent.java create mode 100644 src/source/tuner/ComplexChannelSource.java delete mode 100644 src/source/tuner/frequency/DCTrackingFrequencyControl.java create mode 100644 src/source/tuner/frequency/DCTrackingSourceControl.java delete mode 100644 src/source/tuner/frequency/FrequencyChangeEvent.java delete mode 100644 src/source/tuner/frequency/FrequencyChangeProcessorWrapper.java delete mode 100644 src/source/tuner/frequency/IFrequencyChangeListener.java delete mode 100644 src/source/tuner/frequency/IFrequencyChangeProcessor.java diff --git a/src/channel/state/ChannelState.java b/src/channel/state/ChannelState.java index d9407c0f..eb3600a4 100644 --- a/src/channel/state/ChannelState.java +++ b/src/channel/state/ChannelState.java @@ -36,18 +36,18 @@ import org.slf4j.Logger; import org.slf4j.LoggerFactory; import sample.Listener; import sample.real.IOverflowListener; -import source.tuner.frequency.FrequencyChangeEvent; -import source.tuner.frequency.IFrequencyChangeListener; +import source.SourceEvent; +import source.ISourceEventListener; import java.util.concurrent.RejectedExecutionException; import java.util.concurrent.ScheduledExecutorService; import java.util.concurrent.ScheduledFuture; import java.util.concurrent.TimeUnit; -import static source.tuner.frequency.FrequencyChangeEvent.Event.NOTIFICATION_FREQUENCY_CHANGE; +import static source.SourceEvent.Event.NOTIFICATION_FREQUENCY_CHANGE; public class ChannelState extends Module implements ICallEventProvider, IDecoderStateEventListener, - IDecoderStateEventProvider, IFrequencyChangeListener, ISquelchStateProvider, IAttributeChangeRequestListener, + IDecoderStateEventProvider, ISourceEventListener, ISquelchStateProvider, IAttributeChangeRequestListener, IOverflowListener { private final static Logger mLog = LoggerFactory.getLogger(ChannelState.class); @@ -66,7 +66,7 @@ public class ChannelState extends Module implements ICallEventProvider, IDecoder private ChannelType mChannelType; private TrafficChannelManager mTrafficChannelEndListener; private CallEvent mTrafficChannelCallEvent; - private FrequencyChangeListener mFrequencyChangeListener; + private SourceChangeListener mSourceChangeListener; private boolean mSquelchLocked = false; private boolean mSelected = false; @@ -196,14 +196,14 @@ public class ChannelState extends Module implements ICallEventProvider, IDecoder } @Override - public Listener getFrequencyChangeListener() + public Listener getSourceEventListener() { - if(mFrequencyChangeListener == null) + if(mSourceChangeListener == null) { - mFrequencyChangeListener = new FrequencyChangeListener(); + mSourceChangeListener = new SourceChangeListener(); } - return mFrequencyChangeListener; + return mSourceChangeListener; } private boolean isStandardChannel() @@ -552,14 +552,14 @@ public class ChannelState extends Module implements ICallEventProvider, IDecoder /** * Listener to receive frequency change events and rebroadcast them to the decoder states */ - public class FrequencyChangeListener implements Listener + public class SourceChangeListener implements Listener { @Override - public void receive(FrequencyChangeEvent frequencyChangeEvent) + public void receive(SourceEvent sourceEvent) { - if(frequencyChangeEvent.getEvent() == NOTIFICATION_FREQUENCY_CHANGE) + if(sourceEvent.getEvent() == NOTIFICATION_FREQUENCY_CHANGE) { - long frequency = frequencyChangeEvent.getValue().longValue(); + long frequency = sourceEvent.getValue().longValue(); broadcast(new DecoderStateEvent(this, Event.SOURCE_FREQUENCY, getState(), frequency)); } diff --git a/src/channel/traffic/TrafficChannelManager.java b/src/channel/traffic/TrafficChannelManager.java index c2568de8..d7fac5a2 100644 --- a/src/channel/traffic/TrafficChannelManager.java +++ b/src/channel/traffic/TrafficChannelManager.java @@ -18,8 +18,6 @@ ******************************************************************************/ package channel.traffic; -import alias.Alias; -import alias.id.priority.Priority; import channel.state.DecoderStateEvent; import channel.state.IDecoderStateEventListener; import controller.channel.Channel; @@ -27,7 +25,6 @@ import controller.channel.Channel.ChannelType; import controller.channel.ChannelEvent; import controller.channel.ChannelEvent.Event; import controller.channel.ChannelModel; -import controller.channel.ChannelProcessingManager; import controller.channel.TrafficChannelEvent; import module.Module; import module.decode.config.DecodeConfiguration; @@ -40,7 +37,6 @@ import record.config.RecordConfiguration; import sample.Listener; import source.config.SourceConfigTuner; import source.tuner.TunerChannel; -import source.tuner.TunerChannel.Type; import java.util.ArrayList; import java.util.List; @@ -193,8 +189,8 @@ public class TrafficChannelManager extends Module implements ICallEventProvider, if(frequency > 0) { - Channel channel = getChannel(callEvent.getChannel(), new TunerChannel(Type.TRAFFIC, - frequency, mDecodeConfiguration.getDecoderType().getChannelBandwidth())); + Channel channel = getChannel(callEvent.getChannel(), new TunerChannel(frequency, + mDecodeConfiguration.getDecoderType().getChannelBandwidth())); if(channel != null) { diff --git a/src/controller/channel/Channel.java b/src/controller/channel/Channel.java index 79be174b..b3e49224 100644 --- a/src/controller/channel/Channel.java +++ b/src/controller/channel/Channel.java @@ -34,9 +34,8 @@ import source.config.SourceConfigRecording; import source.config.SourceConfigTuner; import source.config.SourceConfiguration; import source.tuner.TunerChannel; -import source.tuner.TunerChannel.Type; -import source.tuner.frequency.FrequencyChangeEvent; -import source.tuner.frequency.FrequencyChangeEvent.Event; +import source.SourceEvent; +import source.SourceEvent.Event; import javax.xml.bind.annotation.XmlAttribute; import javax.xml.bind.annotation.XmlElement; @@ -46,7 +45,7 @@ import javax.xml.bind.annotation.XmlTransient; @XmlSeeAlso({Configuration.class}) @XmlRootElement(name = "channel") -public class Channel extends Configuration implements Listener +public class Channel extends Configuration implements Listener { // Standard channels are persisted and traffic channels are temporary public enum ChannelType @@ -418,15 +417,14 @@ public class Channel extends Configuration implements Listener + * + ******************************************************************************/ +package dsp.filter.channelizer; + +import org.slf4j.Logger; +import org.slf4j.LoggerFactory; +import sample.Broadcaster; +import sample.Listener; +import sample.complex.ComplexBuffer; +import source.ISourceEventListener; +import source.SourceEvent; + +import java.util.List; +import java.util.concurrent.CopyOnWriteArrayList; + +public abstract class AbstractComplexPolyphaseChannelizer implements Listener, ISourceEventListener +{ + private final static Logger mLog = LoggerFactory.getLogger(AbstractComplexPolyphaseChannelizer.class); + + private Broadcaster mSourceChangeBroadcaster = new Broadcaster(); + private List mChannels = new CopyOnWriteArrayList<>(); + private int mChannelCount; + private int mChannelSampleRate; + + /** + * Complex sample polyphase channelizer + * + * @param channelCount + */ + public AbstractComplexPolyphaseChannelizer(int channelCount, int channelSampleRate) + { + mChannelCount = channelCount; + mChannelSampleRate = channelSampleRate; + } + + /** + * Number of channels being processed by this channelizer. + */ + public int getChannelCount() + { + return mChannelCount; + } + + /** + * Output sample rate for each channel + * @return sample rate in hertz + */ + public int getChannelSampleRate() + { + return mChannelSampleRate; + } + + /** + * Filters the complex sample pair + * + * @param inphase sample + * @param quadrature sample + */ + protected abstract void filter(float inphase, float quadrature); + + @Override + public void receive(ComplexBuffer complexBuffer) + { + float[] samples = complexBuffer.getSamples(); + + for(int x = 0; x < samples.length; x += 2) + { + filter(samples[x], samples[x + 1]); + } + } + + /** + * Dispatches the processed channel samples to any registered polyphase channel outputs. + * + * @param channels containing an array of I/Q samples per channel + */ + protected void dispatch(float[] channels) + { + for(PolyphaseChannelSource channel : mChannels) + { + channel.processChannels(channels); + } + } + + /** + * Adds the polyphase channel source to receive processed output channel samples + * + * @param polyphaseChannelSource + */ + public void addChannel(PolyphaseChannelSource polyphaseChannelSource) + { + mChannels.add(polyphaseChannelSource); + mSourceChangeBroadcaster.addListener(polyphaseChannelSource.getSourceEventListener()); + } + + /** + * Removes the polyphase channel source from receiving output channel samples. + * + * @param polyphaseChannelSource + */ + public void removeChannel(PolyphaseChannelSource polyphaseChannelSource) + { + mChannels.remove(polyphaseChannelSource); + mSourceChangeBroadcaster.removeListener(polyphaseChannelSource.getSourceEventListener()); + } + + @Override + public Listener getSourceEventListener() + { + return mSourceChangeBroadcaster; + } +} diff --git a/src/dsp/filter/channelizer/Channelizer.java b/src/dsp/filter/channelizer/Channelizer.java new file mode 100644 index 00000000..a66e32d7 --- /dev/null +++ b/src/dsp/filter/channelizer/Channelizer.java @@ -0,0 +1,135 @@ +/******************************************************************************* + * 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.Listener; +import source.ISourceEventProcessor; +import source.Source; +import source.SourceEvent; +import source.tuner.Tuner; +import source.tuner.TunerChannel; + +import java.util.List; +import java.util.concurrent.CopyOnWriteArrayList; + +public class Channelizer implements ISourceEventProcessor, Listener +{ + private final static Logger mLog = LoggerFactory.getLogger(Channelizer.class); + + private Tuner mTuner; + private List mChannelSources = new CopyOnWriteArrayList<>(); + + /** + * Provides access to Digital Drop Channel (DDC) sources from the tuner. Incorporates a polyphase channelizer + * with downstream channel and upstream tuner management. + * + * @param tuner providing broadband sample buffers + */ + public Channelizer(Tuner tuner) + { + mTuner = tuner; + mTuner.addSourceEventListener(this); + } + + public Source getChannel(TunerChannel tunerChannel) + { + //TODO: return a polyphase channel here + return null; + } + + /** + * Starts/adds the channel source to receive channelized sample buffers, registering with the tuner to receive + * sample buffers when this is the first channel. + * + * @param channelSource to start + */ + private void startChannelSource(PolyphaseChannelSource channelSource) + { + if(mChannelSources.isEmpty()) + { + //TODO: start the tuner + } + + mChannelSources.add(channelSource); + } + + /** + * Stops/removes the channel source from receiving channelized sample buffers and deregisters from the tuner + * if this is the last channel being sourced. + * + * @param channelSource to stop + */ + private void stopChannelSource(PolyphaseChannelSource channelSource) + { + mChannelSources.remove(channelSource); + + if(mChannelSources.isEmpty()) + { + //TODO: stop the tuner + } + + if(mTuner != null) + { + //TODO: change the tuner to accept a remove(TunerChannel) method +// mTuner.remove(channel.getTunerChannel()); + } + } + + @Override + public void process(SourceEvent sourceEvent) + { + switch(sourceEvent.getEvent()) + { + case REQUEST_START_SAMPLE_STREAM: + if(sourceEvent.hasSource() && sourceEvent.getSource() instanceof PolyphaseChannelSource) + { + startChannelSource((PolyphaseChannelSource)sourceEvent.getSource()); + } + else + { + mLog.error("Request to stop sample stream for unrecognized source: " + + (sourceEvent.hasSource() ? sourceEvent.getSource().getClass() : "null source")); + } + //TODO: add the channel and register for samples from the tuner + break; + case REQUEST_STOP_SAMPLE_STREAM: + if(sourceEvent.hasSource() && sourceEvent.getSource() instanceof PolyphaseChannelSource) + { + stopChannelSource((PolyphaseChannelSource)sourceEvent.getSource()); + } + else + { + mLog.error("Request to stop sample stream for unrecognized source: " + + (sourceEvent.hasSource() ? sourceEvent.getSource().getClass() : "null source")); + } + break; + } + + + + } + + @Override + public void receive(SourceEvent sourceEvent) + { + + } +} diff --git a/src/dsp/filter/channelizer/ChannelizerViewer.java b/src/dsp/filter/channelizer/ChannelizerViewer.java index b6064c62..09d80c3f 100644 --- a/src/dsp/filter/channelizer/ChannelizerViewer.java +++ b/src/dsp/filter/channelizer/ChannelizerViewer.java @@ -20,7 +20,6 @@ package dsp.filter.channelizer; import dsp.filter.FilterFactory; import dsp.filter.Window; -import dsp.filter.correction.IQCorrectionFilter; import dsp.filter.design.FilterDesignException; import dsp.mixer.LowPhaseNoiseOscillator; import net.miginfocom.swing.MigLayout; @@ -31,7 +30,7 @@ import sample.SampleType; import sample.complex.ComplexBuffer; import settings.SettingsManager; import source.tuner.configuration.TunerConfigurationModel; -import source.tuner.frequency.FrequencyChangeEvent; +import source.SourceEvent; import spectrum.DFTProcessor; import spectrum.DFTSize; import spectrum.SpectrumPanel; @@ -86,7 +85,7 @@ public class ChannelizerViewer extends JFrame private void init() { - setTitle("Channelizer Viewer"); + setTitle("TunerChannelizer Viewer"); setSize(1000, 800); setDefaultCloseOperation(JFrame.EXIT_ON_CLOSE); setLayout(new MigLayout("insets 0 0 0 0", "[grow,fill]", "[grow,fill]")); @@ -212,7 +211,7 @@ public class ChannelizerViewer extends JFrame public class ChannelArrayPanel extends JPanel implements Listener { - private ComplexPolyphaseChannelizerM2 mPolyphaseChannelizer; + private ComplexPolyphaseChannelizer mPolyphaseChannelizer; private ChannelDistributor mChannelDistributor; public ChannelArrayPanel(float[] taps) @@ -223,7 +222,7 @@ public class ChannelizerViewer extends JFrame bufferSize++; } - mPolyphaseChannelizer = new ComplexPolyphaseChannelizerM2(taps, mChannelCount); + mPolyphaseChannelizer = new ComplexPolyphaseChannelizer(taps, mChannelCount, CHANNEL_BANDWIDTH); mChannelDistributor = new ChannelDistributor(bufferSize, mChannelCount); mPolyphaseChannelizer.setChannelDistributor(mChannelDistributor); @@ -272,8 +271,7 @@ public class ChannelizerViewer extends JFrame add(mSpectrumPanel); mDFTProcessor.addConverter(mComplexDecibelConverter); - mDFTProcessor.frequencyChanged(new FrequencyChangeEvent( - FrequencyChangeEvent.Event.NOTIFICATION_SAMPLE_RATE_CHANGE, sampleRate)); + mDFTProcessor.process(SourceEvent.sampleRateChange(sampleRate)); mComplexDecibelConverter.addListener(mSpectrumPanel); } diff --git a/src/dsp/filter/channelizer/ComplexPolyphaseChannelizer.java b/src/dsp/filter/channelizer/ComplexPolyphaseChannelizer.java index 408f844d..c4977b1e 100644 --- a/src/dsp/filter/channelizer/ComplexPolyphaseChannelizer.java +++ b/src/dsp/filter/channelizer/ComplexPolyphaseChannelizer.java @@ -25,44 +25,40 @@ import dsp.mixer.Oscillator; import org.jtransforms.fft.FloatFFT_1D; import org.slf4j.Logger; import org.slf4j.LoggerFactory; -import sample.Listener; -import sample.complex.ComplexBuffer; -import java.text.DecimalFormat; - -public class ComplexPolyphaseChannelizer implements Listener +public class ComplexPolyphaseChannelizer extends AbstractComplexPolyphaseChannelizer { private final static Logger mLog = LoggerFactory.getLogger(ComplexPolyphaseChannelizer.class); private RealFIRFilter[] mInphaseFilters; private RealFIRFilter[] mQuadratureFilters; private int mFilterPointer = 0; - private int mChannelCount; private float[] mFilteredSamples; private FloatFFT_1D mFFT; - private ChannelDistributor mChannelDistributor; /** * Maximally Decimated Polyphase Filter Bank (MDPFB) channelizer that divides the input frequency band into * equal bandwidth channels. * - * @param taps of a low-pass filter designed for the inbound sample rate with a cutoff frequency + * @param coefficients of a prototype low-pass filter designed for the inbound sample rate with a cutoff frequency * equal to the channel bandwidth (sample rate / filters). If you need to synthesize (combine two or more - * filte outputs) a new bandwidth signal from the outputs of this filter, then the filter should be designed + * filter outputs) a new bandwidth signal from the outputs of this filter, then the filter should be designed * as a nyquist filter with -6 dB attenuation at the channel bandwidth cutoff frequency * - * @param channels - number of channels to output. + * @param channelCount - number of channels to output. + * @param channelSampleRate - channel sample rate (after decimation) */ - public ComplexPolyphaseChannelizer(float[] taps, int channels) + public ComplexPolyphaseChannelizer(float[] coefficients, int channelCount, int channelSampleRate) { - mChannelCount = channels; + super(channelCount, channelSampleRate); - initFilters(taps); + initFilters(coefficients); } - public void filter(float inphase, float quadrature) + @Override + protected void filter(float inphase, float quadrature) { - int index = (mChannelCount - mFilterPointer - 1) * 2; + int index = (getChannelCount() - mFilterPointer - 1) * 2; mFilteredSamples[index] = mInphaseFilters[mFilterPointer].filter(inphase); mFilteredSamples[index + 1] = mQuadratureFilters[mFilterPointer].filter(quadrature); @@ -70,7 +66,7 @@ public class ComplexPolyphaseChannelizer implements Listener if(mFilterPointer == 0) { calculate(); - mFilterPointer = mChannelCount - 1; + mFilterPointer = getChannelCount() - 1; } else { @@ -78,20 +74,9 @@ public class ComplexPolyphaseChannelizer implements Listener } } - @Override - public void receive(ComplexBuffer complexBuffer) - { - float[] samples = complexBuffer.getSamples(); - - for(int x = 0; x < samples.length; x += 2) - { - filter(samples[x], samples[x + 1]); - } - } - private void calculate() { - float[] samples = new float[mChannelCount * 2]; + float[] samples = new float[getChannelCount() * 2]; System.arraycopy(mFilteredSamples, 0, samples, 0, mFilteredSamples.length); //FFT is executed in-place where the output overwrites the input @@ -100,24 +85,6 @@ public class ComplexPolyphaseChannelizer implements Listener dispatch(samples); } - private void dispatch(float[] channels) - { - if(mChannelDistributor != null) - { - mChannelDistributor.receive(channels); - } - } - - public void setChannelDistributor(ChannelDistributor channelDistributor) - { - mChannelDistributor = channelDistributor; - } - - private double magnitude(float real, float imaginary, int fftSize) - { - return Math.sqrt(Math.pow(real, 2.0) + Math.pow(imaginary, 2.0)); - } - /** * Distributes the filter taps to each of the polyphase filters * @@ -125,15 +92,15 @@ public class ComplexPolyphaseChannelizer implements Listener */ private void initFilters(float[] taps) { - int tapCount = (int)Math.ceil((double)taps.length / (double)mChannelCount); + int tapCount = (int)Math.ceil((double)taps.length / (double)getChannelCount()); - float[][] coefficients = new float[mChannelCount][tapCount]; + float[][] coefficients = new float[getChannelCount()][tapCount]; for(int tap = 0; tap < tapCount; tap++) { - for(int filter = 0; filter < mChannelCount; filter++) + for(int filter = 0; filter < getChannelCount(); filter++) { - int index = tap * mChannelCount + filter; + int index = tap * getChannelCount() + filter; if(index < taps.length) { @@ -142,18 +109,18 @@ public class ComplexPolyphaseChannelizer implements Listener } } - mInphaseFilters = new RealFIRFilter[mChannelCount]; - mQuadratureFilters = new RealFIRFilter[mChannelCount]; + mInphaseFilters = new RealFIRFilter[getChannelCount()]; + mQuadratureFilters = new RealFIRFilter[getChannelCount()]; - for(int filter = 0; filter < mChannelCount; filter++) + for(int filter = 0; filter < getChannelCount(); filter++) { mInphaseFilters[filter] = new RealFIRFilter(coefficients[filter], 1.0f); mQuadratureFilters[filter] = new RealFIRFilter(coefficients[filter], 1.0f); } - mFilteredSamples = new float[mChannelCount * 2]; - mFFT = new FloatFFT_1D(mChannelCount); - mFilterPointer = mChannelCount -1; + mFilteredSamples = new float[getChannelCount() * 2]; + mFFT = new FloatFFT_1D(getChannelCount()); + mFilterPointer = getChannelCount() -1; } public static void main(String[] args) @@ -171,14 +138,14 @@ public class ComplexPolyphaseChannelizer implements Listener int channelCenter = (int)(12500 * 1.5); StringBuilder sb = new StringBuilder(); - sb.append("\nPolyphase Channelizer\n"); + sb.append("\nPolyphase TunerChannelizer\n"); sb.append("Sample Rate:" + sampleRate + " Channels:" + channels + " Channel Rate:" + channelBandwidth + "\n"); sb.append("Tap Count:" + taps.length + "\n"); sb.append("Channel:" + channelCenter); mLog.debug(sb.toString()); - ComplexPolyphaseChannelizer channelizer = new ComplexPolyphaseChannelizer(taps, 8); + ComplexPolyphaseChannelizer channelizer = new ComplexPolyphaseChannelizer(taps, 8, channelBandwidth); Oscillator oscillator = new Oscillator(channelCenter, 2 * sampleRate); diff --git a/src/dsp/filter/channelizer/ComplexPolyphaseChannelizerM2.java b/src/dsp/filter/channelizer/ComplexPolyphaseChannelizerM2.java index 4ee0f32d..3b8673b8 100644 --- a/src/dsp/filter/channelizer/ComplexPolyphaseChannelizerM2.java +++ b/src/dsp/filter/channelizer/ComplexPolyphaseChannelizerM2.java @@ -25,21 +25,18 @@ import dsp.mixer.Oscillator; import org.jtransforms.fft.FloatFFT_1D; import org.slf4j.Logger; import org.slf4j.LoggerFactory; -import sample.Listener; import sample.complex.ComplexBuffer; import java.text.DecimalFormat; -public class ComplexPolyphaseChannelizerM2 implements Listener +public class ComplexPolyphaseChannelizerM2 extends AbstractComplexPolyphaseChannelizer { private final static Logger mLog = LoggerFactory.getLogger(ComplexPolyphaseChannelizerM2.class); private ReverseFloatCircularBuffer[] mDataBuffers; private float[][] mFilterCoefficients; private int mFilterPointer = 0; - private int mChannelCount; private FloatFFT_1D mFFT; - private ChannelDistributor mChannelDistributor; private DecimalFormat DECIMAL_FORMAT = new DecimalFormat("0.0000"); @@ -51,23 +48,23 @@ public class ComplexPolyphaseChannelizerM2 implements Listener * equal to the channel bandwidth (sample rate / filters). If you need to synthesize (combine two or more * filte outputs) a new bandwidth signal from the outputs of this filter, then the filter should be designed * as a nyquist filter with -6 dB attenuation at the channel bandwidth cutoff frequency - * * @param channels - number of filters/channels to output. Since this filter bank oversamples each filter * output, this number must be even (divisible by the oversample rate). + * @param channelSampleRate for each decimated, oversampled channel */ - public ComplexPolyphaseChannelizerM2(float[] taps, int channels) + public ComplexPolyphaseChannelizerM2(float[] taps, int channels, int channelSampleRate) { + super(channels, channelSampleRate); + if(channels % 2 != 0) { throw new IllegalArgumentException("Channel count must be a multiple of 2"); } - mChannelCount = channels; - initFilters(taps); } - public void filter(float inphase, float quadrature) + protected void filter(float inphase, float quadrature) { try { @@ -77,9 +74,9 @@ public class ComplexPolyphaseChannelizerM2 implements Listener if(mFilterPointer == 0) { calculate(); - mFilterPointer = mChannelCount * 2; + mFilterPointer = getChannelCount() * 2; } - else if(mFilterPointer == mChannelCount) + else if(mFilterPointer == getChannelCount()) { calculate(); } @@ -92,20 +89,9 @@ public class ComplexPolyphaseChannelizerM2 implements Listener } } - @Override - public void receive(ComplexBuffer complexBuffer) - { - float[] samples = complexBuffer.getSamples(); - - for(int x = 0; x < samples.length; x += 2) - { - filter(samples[x], samples[x + 1]); - } - } - private void calculate() { - float[] samples = new float[mChannelCount * 2]; + float[] samples = new float[getChannelCount() * 2]; if(mFilterPointer == 0) { @@ -158,6 +144,7 @@ public class ComplexPolyphaseChannelizerM2 implements Listener /** * Filters the samples contained in the buffer against the filter coefficients + * * @param coefficients of the sub filter * @param buffer containing data samples * @return @@ -166,32 +153,14 @@ public class ComplexPolyphaseChannelizerM2 implements Listener { float accumulator = 0.0f; - for( int x = 0; x < coefficients.length; x++ ) + for(int x = 0; x < coefficients.length; x++) { - accumulator += coefficients[ x ] * buffer.get(x); + accumulator += coefficients[x] * buffer.get(x); } return accumulator; } - private void dispatch(float[] channels) - { - if(mChannelDistributor != null) - { - mChannelDistributor.receive(channels); - } - } - - public void setChannelDistributor(ChannelDistributor channelDistributor) - { - mChannelDistributor = channelDistributor; - } - - private double magnitude(float real, float imaginary, int fftSize) - { - return Math.sqrt(Math.pow(real, 2.0) + Math.pow(imaginary, 2.0)); - } - /** * Distributes the filter taps to each of the polyphase filters * @@ -199,22 +168,22 @@ public class ComplexPolyphaseChannelizerM2 implements Listener */ private void initFilters(float[] taps) { - int tapCount = (int)Math.ceil((double)taps.length / (double)mChannelCount); + int tapCount = (int)Math.ceil((double)taps.length / (double)getChannelCount()); - mDataBuffers = new ReverseFloatCircularBuffer[mChannelCount * 2]; + mDataBuffers = new ReverseFloatCircularBuffer[getChannelCount() * 2]; - for(int x = 0; x < mChannelCount * 2; x++) + for(int x = 0; x < getChannelCount() * 2; x++) { mDataBuffers[x] = new ReverseFloatCircularBuffer(tapCount); } - mFilterCoefficients = new float[mChannelCount][tapCount]; + mFilterCoefficients = new float[getChannelCount()][tapCount]; for(int tap = 0; tap < tapCount; tap++) { - for(int filter = 0; filter < mChannelCount; filter++) + for(int filter = 0; filter < getChannelCount(); filter++) { - int index = tap * mChannelCount + filter; + int index = tap * getChannelCount() + filter; if(index < taps.length) { @@ -223,8 +192,8 @@ public class ComplexPolyphaseChannelizerM2 implements Listener } } - mFFT = new FloatFFT_1D(mChannelCount); - mFilterPointer = mChannelCount - 1; //Start at M/2 + mFFT = new FloatFFT_1D(getChannelCount()); + mFilterPointer = getChannelCount() - 1; //Start at M/2 } public static void main(String[] args) @@ -237,19 +206,19 @@ public class ComplexPolyphaseChannelizerM2 implements Listener int channelBandwidth = 12500; int sampleRate = channels * channelBandwidth; - float[] taps = FilterFactory.getLowPass( 10000000, 12500, 5600, Window.WindowType.BLACKMAN_HARRIS_7 ); + float[] taps = FilterFactory.getLowPass(10000000, 12500, 5600, Window.WindowType.BLACKMAN_HARRIS_7); int channelCenter = (int)(12500 * 1.5); StringBuilder sb = new StringBuilder(); - sb.append("\nPolyphase Channelizer\n"); + sb.append("\nPolyphase TunerChannelizer\n"); sb.append("Sample Rate:" + sampleRate + " Channels:" + channels + " Channel Rate:" + channelBandwidth + "\n"); sb.append("Tap Count:" + taps.length + "\n"); sb.append("Channel:" + channelCenter); mLog.debug(sb.toString()); - ComplexPolyphaseChannelizerM2 channelizer = new ComplexPolyphaseChannelizerM2(taps, 8); + ComplexPolyphaseChannelizerM2 channelizer = new ComplexPolyphaseChannelizerM2(taps, 8, channelBandwidth * 2); Oscillator oscillator = new Oscillator(channelCenter, 2 * sampleRate); diff --git a/src/dsp/filter/channelizer/PolyphaseChannelSource.java b/src/dsp/filter/channelizer/PolyphaseChannelSource.java new file mode 100644 index 00000000..6050491b --- /dev/null +++ b/src/dsp/filter/channelizer/PolyphaseChannelSource.java @@ -0,0 +1,201 @@ +/******************************************************************************* + * 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.Listener; +import sample.complex.ComplexBuffer; +import source.ISourceEventProcessor; +import source.SourceEvent; +import source.SourceException; +import source.tuner.ComplexChannelSource; +import source.tuner.TunerChannel; + +import java.util.concurrent.ScheduledExecutorService; + +public class PolyphaseChannelSource extends ComplexChannelSource +{ + private final static Logger mLog = LoggerFactory.getLogger(PolyphaseChannelSource.class); + + private TunerChannel mTunerChannel; + private long mTunerCenterFrequency; + private int mUpstreamSampleRate; + private int mChannelSampleRate; + private int mChannelCount; + private int mOutputFrequencyCorrection; + + /** + * Polyphase channelizer channel implementation. Adapts the channel array output samples from the polyphase + * channelizer into a single channel, or a channel synthesized from two adjacent channels that is frequency + * translated and decimated to a single channel. + * + * @param tunerChannel - requested output tuner channel frequency and bandwidth. + */ + public PolyphaseChannelSource(ISourceEventProcessor upstreamSourceEventProcessor, TunerChannel tunerChannel) + { + super(upstreamSourceEventProcessor); + + mTunerChannel = tunerChannel; + } + + /** + * Process source event notifications received from upstream source provider and source event requests + * received from downstream consumers. + * + * @param sourceEvent containing a request or notification + * @throws SourceException if there are any errors while processing the source event + */ + @Override + public void process(SourceEvent sourceEvent) throws SourceException + { + switch(sourceEvent.getEvent()) + { + case NOTIFICATION_CHANNEL_SAMPLE_RATE_CHANGE: + setChannelSampleRate(sourceEvent.getValue().intValue()); + break; + case NOTIFICATION_FREQUENCY_CHANGE: + mTunerCenterFrequency = sourceEvent.getValue().longValue(); + updateInputConfiguration(); + break; + case NOTIFICATION_SAMPLE_RATE_CHANGE: + setUpstreamSampleRate(sourceEvent.getValue().intValue()); + updateInputConfiguration(); + break; + case REQUEST_CHANNEL_FREQUENCY_CORRECTION_CHANGE: + setFrequencyCorrection(sourceEvent.getValue().intValue()); + break; + } + } + + /** + * Process the array samples for each of the output channels from the polyphase channelizer. + * @param channels + */ + public void processChannels(float[] channels) + { + + //TODO we have to dispatch full buffers on a separate thread so that we don't bog down the channelizer + //TODO thread that is delivering the channel samples to us. + } + + /** + * Updates current processing configuration any time there is a change in tuner center frequency + * or tuner sample rate. + */ + private void updateInputConfiguration() + { + + } + + /** + * Downstream sample rate + * + * @return sample rate in Hertz + * @throws SourceException if this method is accessed before the channel count is established + */ + @Override + public int getSampleRate() throws SourceException + { + if(mChannelSampleRate > 0) + { + return mChannelSampleRate; + } + + throw new SourceException("Channel sample rate has not been set."); + } + + /** + * Sets the downstream channel sample rate. Since oversampling may occur in the upstream channelizer, we can't + * rely on simply dividing the upstream sample rate by the number of channels. Thus, we required the upstream + * channelizer to tell us the exact channel sample rate. + * + * @param channelSampleRate in hertz + */ + private void setChannelSampleRate(int channelSampleRate) + { + mChannelSampleRate = channelSampleRate; + + //Translate the upstream channel sample rate event into a downstream sample rate change event + broadcast(SourceEvent.sampleRateChange(mChannelSampleRate)); + } + + /** + * Sets the input sample rate and broadcasts a source change event to the downstream listener + * @param sampleRate from the upstream source + */ + private void setUpstreamSampleRate(int sampleRate) + { + mUpstreamSampleRate = sampleRate; + + try + { + broadcast(SourceEvent.sampleRateChange(getSampleRate())); + } + catch(SourceException se) + { + mLog.error("Upstream sample rate changed - couldn't broadcast channel sample rate change event " + + "to downstream channel", se); + } + } + + /** + * Sets the polyphase channel count. + * @param channelCount number of channels being processed by the polyphase channelizer + */ + private void setChannelCount(int channelCount) + { + mChannelCount = channelCount; + + try + { + broadcast(SourceEvent.sampleRateChange(getSampleRate())); + } + catch(SourceException se) + { + mLog.error("Polyphase channel count changed - couldn't broadcast channel sample rate change event " + + "to downstream channel", se); + } + } + + @Override + public long getFrequency() throws SourceException + { + return mTunerChannel.getFrequency(); + } + + private void setFrequencyCorrection(int correction) + { + //TODO: + mLog.debug("Request for frequency correction: " + correction + " -- not yet implemented"); + } + + @Override + public void reset() + { + + } + + + @Override + public void dispose() + { + + } +} diff --git a/src/dsp/filter/fir/FIRFilterSpecification.java b/src/dsp/filter/fir/FIRFilterSpecification.java index 2ef0d820..2ffc10a0 100644 --- a/src/dsp/filter/fir/FIRFilterSpecification.java +++ b/src/dsp/filter/fir/FIRFilterSpecification.java @@ -151,7 +151,7 @@ public class FIRFilterSpecification } /** - * Creates a Polyphase Channelizer filter specification builder that allows you to define the filter parameters and + * Creates a Polyphase TunerChannelizer filter specification builder that allows you to define the filter parameters and * create a filter specification */ public static ChannelizerBuilder channelizerBuilder() diff --git a/src/dsp/filter/fir/remez/PolyphaseChannelizerDesigner.java b/src/dsp/filter/fir/remez/PolyphaseChannelizerDesigner.java index f7436798..01d8a8ff 100644 --- a/src/dsp/filter/fir/remez/PolyphaseChannelizerDesigner.java +++ b/src/dsp/filter/fir/remez/PolyphaseChannelizerDesigner.java @@ -23,7 +23,7 @@ import dsp.filter.fir.FIRFilterSpecification; public class PolyphaseChannelizerDesigner extends FIRFilterSpecification { /** - * Constructs a Polyphase Channelizer Remez filter specification. + * Constructs a Polyphase TunerChannelizer Remez filter specification. * * Constructor is package private -- use the PolyphaseChannelizerFilterFactory to create a filter * diff --git a/src/dsp/psk/LSMDemodulator.java b/src/dsp/psk/LSMDemodulator.java index 5142d0d9..11c5de0d 100644 --- a/src/dsp/psk/LSMDemodulator.java +++ b/src/dsp/psk/LSMDemodulator.java @@ -43,7 +43,7 @@ import java.util.List; import sample.Listener; import sample.complex.Complex; import sample.complex.ComplexSampleListener; -import source.tuner.frequency.IFrequencyChangeListener; +import source.ISourceEventListener; import buffer.FloatAveragingBuffer; import dsp.filter.interpolator.RealInterpolator; import dsp.symbol.Dibit; @@ -73,7 +73,7 @@ public class LSMDemodulator implements Instrumentable, ComplexSampleListener private CostasLoop mCostasLoop = new CostasLoop(); - private IFrequencyChangeListener mFrequencyChangeListener; + private ISourceEventListener mFrequencyChangeListener; private EyeDiagramDataTap mEyeDiagramDataTap; @@ -147,12 +147,12 @@ public class LSMDemodulator implements Instrumentable, ComplexSampleListener } } - public void addListener( IFrequencyChangeListener listener ) + public void addListener( ISourceEventListener listener ) { mFrequencyChangeListener = listener; } - public void removeListener( IFrequencyChangeListener listener ) + public void removeListener( ISourceEventListener listener ) { mFrequencyChangeListener = null; } @@ -202,8 +202,8 @@ public class LSMDemodulator implements Instrumentable, ComplexSampleListener // if( correctionNeeded && mFrequencyChangeListener != null ) // { // mLog.debug( "Issuing Frequency Correction: " + mFrequencyError ); -// mFrequencyChangeListener.frequencyChanged( -// new FrequencyChangeEvent( Attribute.FREQUENCY_ERROR, +// mFrequencyChangeListener.process( +// new SourceEvent( Attribute.FREQUENCY_ERROR, // mFrequencyError ) ); // } // diff --git a/src/gui/control/JFrequencyControl.java b/src/gui/control/JFrequencyControl.java index ca2053aa..aed75ab0 100644 --- a/src/gui/control/JFrequencyControl.java +++ b/src/gui/control/JFrequencyControl.java @@ -1,6 +1,6 @@ /******************************************************************************* - * SDR Trunk - * Copyright (C) 2014 Dennis Sheirer + * 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 @@ -14,15 +14,21 @@ * * You should have received a copy of the GNU General Public License * along with this program. If not, see + * ******************************************************************************/ package gui.control; -import java.awt.Color; -import java.awt.Cursor; -import java.awt.Font; -import java.awt.Image; -import java.awt.Point; -import java.awt.Toolkit; +import net.miginfocom.swing.MigLayout; +import org.slf4j.Logger; +import org.slf4j.LoggerFactory; +import source.ISourceEventProcessor; +import source.InvalidFrequencyException; +import source.SourceEvent; +import source.SourceEvent.Event; +import source.SourceException; + +import javax.swing.*; +import java.awt.*; import java.awt.event.KeyEvent; import java.awt.event.KeyListener; import java.awt.event.MouseEvent; @@ -35,26 +41,12 @@ import java.util.HashMap; import java.util.Iterator; import java.util.List; -import javax.swing.*; - -import net.miginfocom.swing.MigLayout; - -import org.slf4j.Logger; -import org.slf4j.LoggerFactory; - -import source.InvalidFrequencyException; -import source.SourceException; -import source.tuner.frequency.FrequencyChangeEvent; -import source.tuner.frequency.FrequencyChangeEvent.Event; -import source.tuner.frequency.IFrequencyChangeProcessor; - -public class JFrequencyControl extends JPanel implements IFrequencyChangeProcessor +public class JFrequencyControl extends JPanel implements ISourceEventProcessor { private static final long serialVersionUID = 1L; - private final static Logger mLog = - LoggerFactory.getLogger(JFrequencyControl.class); + private final static Logger mLog = LoggerFactory.getLogger(JFrequencyControl.class); - private List mProcessors = new ArrayList<>(); + private List mProcessors = new ArrayList<>(); private Color mHighlightColor = Color.YELLOW; @@ -62,7 +54,7 @@ public class JFrequencyControl extends JPanel implements IFrequencyChangeProcess private Cursor mBlankCursor; - private HashMap mDigits = new HashMap(); + private HashMap mDigits = new HashMap(); public JFrequencyControl() { @@ -81,7 +73,7 @@ public class JFrequencyControl extends JPanel implements IFrequencyChangeProcess Font font = new Font(Font.MONOSPACED, Font.BOLD, 30); - for (int x = 9; x >= 0; x--) + for(int x = 9; x >= 0; x--) { Digit digit = null; @@ -89,13 +81,13 @@ public class JFrequencyControl extends JPanel implements IFrequencyChangeProcess { digit = new Digit(x); } - catch (ParseException e) + catch(ParseException e) { mLog.error("JFrequencyControl - parse exception " - + "constructing a digit - " + e); + + "constructing a digit - " + e); } - if (digit != null) + if(digit != null) { mDigits.put(x, digit); @@ -103,7 +95,7 @@ public class JFrequencyControl extends JPanel implements IFrequencyChangeProcess digit.setFont(font); - if (x == 6) + if(x == 6) { JLabel period = new JLabel("."); @@ -124,7 +116,7 @@ public class JFrequencyControl extends JPanel implements IFrequencyChangeProcess Image cursorImage = Toolkit.getDefaultToolkit().createImage(imageByte); mBlankCursor = Toolkit.getDefaultToolkit() - .createCustomCursor(cursorImage, new Point(0, 0), "cursor"); + .createCustomCursor(cursorImage, new Point(0, 0), "cursor"); revalidate(); } @@ -134,7 +126,7 @@ public class JFrequencyControl extends JPanel implements IFrequencyChangeProcess { super.setEnabled(enabled); - for (Digit digit : mDigits.values()) + for(Digit digit : mDigits.values()) { digit.setEnabled(enabled); } @@ -145,9 +137,9 @@ public class JFrequencyControl extends JPanel implements IFrequencyChangeProcess * rebroadcast this event, just set the control to indicate the new frequency. */ @Override - public void frequencyChanged(FrequencyChangeEvent event) + public void process(SourceEvent event) { - if (event.getEvent() == Event.NOTIFICATION_FREQUENCY_CHANGE) + if(event.getEvent() == Event.NOTIFICATION_FREQUENCY_CHANGE) { setFrequency(event.getValue().longValue(), false); } @@ -161,7 +153,7 @@ public class JFrequencyControl extends JPanel implements IFrequencyChangeProcess { mFrequency = frequency; - for (Digit digit : mDigits.values()) + for(Digit digit : mDigits.values()) { digit.setFrequency(frequency, fireChangeEvent); } @@ -176,7 +168,7 @@ public class JFrequencyControl extends JPanel implements IFrequencyChangeProcess { long frequency = 0; - for (Digit digit : mDigits.values()) + for(Digit digit : mDigits.values()) { frequency += digit.getFrequency(); } @@ -184,27 +176,26 @@ public class JFrequencyControl extends JPanel implements IFrequencyChangeProcess mFrequency = frequency; } - private void fireFrequencyChanged() throws SourceException + private void fireSourceChanged() throws SourceException { updateFrequency(); - Iterator it = mProcessors.iterator(); + Iterator it = mProcessors.iterator(); - FrequencyChangeEvent event = new FrequencyChangeEvent( - Event.REQUEST_FREQUENCY_CHANGE, mFrequency); + SourceEvent event = SourceEvent.frequencyRequest(mFrequency); - while (it.hasNext()) + while(it.hasNext()) { - it.next().frequencyChanged(event); + it.next().process(event); } } - public void addListener(IFrequencyChangeProcessor processor) + public void addListener(ISourceEventProcessor processor) { mProcessors.add(processor); } - public void removeListener(IFrequencyChangeProcessor processor) + public void removeListener(ISourceEventProcessor processor) { mProcessors.remove(processor); } @@ -236,17 +227,17 @@ public class JFrequencyControl extends JPanel implements IFrequencyChangeProcess public void setFrequency(long frequency, boolean fireChangeEvent) { //Strip the digits higher than this one - long lower = frequency % (long) (Math.pow(10, mPower + 1)); + long lower = frequency % (long)(Math.pow(10, mPower + 1)); //Set the value to int value of dividing by 10 to this power - long value = (long) (lower / (long) (Math.pow(10, mPower))); + long value = (long)(lower / (long)(Math.pow(10, mPower))); set(value, fireChangeEvent); } public long getFrequency() { - return mValue * (long) Math.pow(10, mPower); + return mValue * (long)Math.pow(10, mPower); } public void increment() @@ -256,7 +247,7 @@ public class JFrequencyControl extends JPanel implements IFrequencyChangeProcess public void increment(boolean fireChangeEvent) { - if (isEnabled()) + if(isEnabled()) { set(mValue + 1, fireChangeEvent); } @@ -269,7 +260,7 @@ public class JFrequencyControl extends JPanel implements IFrequencyChangeProcess public void decrement(boolean fireChangeEvent) { - if (isEnabled()) + if(isEnabled()) { set(mValue - 1, fireChangeEvent); } @@ -295,13 +286,13 @@ public class JFrequencyControl extends JPanel implements IFrequencyChangeProcess mValue = amount; - while (mValue < 0) + while(mValue < 0) { mValue += 10; Digit nextHigherDigit = mDigits.get(mPower + 1); - if (nextHigherDigit != null) + if(nextHigherDigit != null) { nextHigherDigit.decrement(false); higherDigitDecremented = true; @@ -309,55 +300,55 @@ public class JFrequencyControl extends JPanel implements IFrequencyChangeProcess } - while (mValue > 9) + while(mValue > 9) { mValue -= 10; Digit nextHigherDigit = mDigits.get(mPower + 1); - if (nextHigherDigit != null) + if(nextHigherDigit != null) { nextHigherDigit.increment(false); higherDigitIncremented = true; } } - if (fireChangeEvent) + if(fireChangeEvent) { try { - fireFrequencyChanged(); + fireSourceChanged(); } - catch (SourceException se) + catch(SourceException se) { mValue = previous; - if (higherDigitIncremented) + if(higherDigitIncremented) { Digit nextHigherDigit = mDigits.get(mPower + 1); - if (nextHigherDigit != null) + if(nextHigherDigit != null) { nextHigherDigit.decrement(false); } } - if (higherDigitDecremented) + if(higherDigitDecremented) { Digit nextHigherDigit = mDigits.get(mPower + 1); - if (nextHigherDigit != null) + if(nextHigherDigit != null) { nextHigherDigit.increment(false); } } - if (se instanceof InvalidFrequencyException) + if(se instanceof InvalidFrequencyException) { - InvalidFrequencyException ife = (InvalidFrequencyException) se; + InvalidFrequencyException ife = (InvalidFrequencyException)se; JOptionPane.showMessageDialog(this, "Frequency [" + ife.getInvalidFrequency() + - "] exceeds the frequency limit [" + ife.getValidFrequency() + "] for this tuner."); + "] exceeds the frequency limit [" + ife.getValidFrequency() + "] for this tuner."); } } } @@ -369,15 +360,15 @@ public class JFrequencyControl extends JPanel implements IFrequencyChangeProcess } private class Listener implements KeyListener, - MouseListener, - MouseWheelListener + MouseListener, + MouseWheelListener { @Override public void keyReleased(KeyEvent e) { int key = e.getKeyCode(); - switch (key) + switch(key) { case KeyEvent.VK_0: case KeyEvent.VK_NUMPAD0: @@ -464,7 +455,7 @@ public class JFrequencyControl extends JPanel implements IFrequencyChangeProcess @Override public void mouseClicked(MouseEvent e) { - switch (e.getButton()) + switch(e.getButton()) { case MouseEvent.BUTTON1: increment(); diff --git a/src/module/ProcessingChain.java b/src/module/ProcessingChain.java index a018e360..f820df09 100644 --- a/src/module/ProcessingChain.java +++ b/src/module/ProcessingChain.java @@ -30,10 +30,8 @@ import channel.metadata.IAttributeChangeRequestProvider; import channel.state.ChannelState; import channel.state.DecoderState; import channel.state.DecoderStateEvent; -import channel.state.DecoderStateEvent.Event; import channel.state.IDecoderStateEventListener; import channel.state.IDecoderStateEventProvider; -import channel.state.State; import controller.channel.Channel.ChannelType; import controller.channel.ChannelEvent; import controller.channel.IChannelEventListener; @@ -63,11 +61,9 @@ import sample.real.RealBuffer; import source.ComplexSource; import source.RealSource; import source.Source; -import source.SourceException; -import source.tuner.TunerChannelSource; -import source.tuner.frequency.FrequencyChangeEvent; -import source.tuner.frequency.IFrequencyChangeListener; -import source.tuner.frequency.IFrequencyChangeProvider; +import source.ISourceEventListener; +import source.ISourceEventProvider; +import source.SourceEvent; import util.ThreadPool; import java.util.ArrayList; @@ -102,7 +98,7 @@ public class ProcessingChain implements IChannelEventListener private Broadcaster mChannelEventBroadcaster = new Broadcaster<>(); private Broadcaster mComplexBufferBroadcaster = new Broadcaster<>(); private Broadcaster mDecoderStateEventBroadcaster = new Broadcaster<>(); - private Broadcaster mFrequencyChangeEventBroadcaster = new Broadcaster<>(); + private Broadcaster mFrequencyChangeEventBroadcaster = new Broadcaster<>(); private Broadcaster mMessageBroadcaster = new Broadcaster<>(); private Broadcaster mFilteredRealBufferBroadcaster = new Broadcaster<>(); private Broadcaster mUnFilteredRealBufferBroadcaster = new Broadcaster<>(); @@ -318,9 +314,9 @@ public class ProcessingChain implements IChannelEventListener mDecoderStateEventBroadcaster.addListener(((IDecoderStateEventListener) module).getDecoderStateListener()); } - if(module instanceof IFrequencyChangeListener) + if(module instanceof ISourceEventListener) { - mFrequencyChangeEventBroadcaster.addListener(((IFrequencyChangeListener) module).getFrequencyChangeListener()); + mFrequencyChangeEventBroadcaster.addListener(((ISourceEventListener) module).getSourceEventListener()); } if(module instanceof IMessageListener) @@ -380,9 +376,9 @@ public class ProcessingChain implements IChannelEventListener mDecoderStateEventBroadcaster.removeListener(((IDecoderStateEventListener) module).getDecoderStateListener()); } - if(module instanceof IFrequencyChangeListener) + if(module instanceof ISourceEventListener) { - mFrequencyChangeEventBroadcaster.removeListener(((IFrequencyChangeListener) module).getFrequencyChangeListener()); + mFrequencyChangeEventBroadcaster.removeListener(((ISourceEventListener) module).getSourceEventListener()); } if(module instanceof IMessageListener) @@ -437,9 +433,9 @@ public class ProcessingChain implements IChannelEventListener ((IDecoderStateEventProvider) module).setDecoderStateListener(mDecoderStateEventBroadcaster); } - if(module instanceof IFrequencyChangeProvider) + if(module instanceof IFilteredRealBufferProvider) { - ((IFrequencyChangeProvider) module).setFrequencyChangeListener(mFrequencyChangeEventBroadcaster); + ((IFilteredRealBufferProvider) module).setFilteredRealBufferListener(mFilteredRealBufferBroadcaster); } if(module instanceof IMessageProvider) @@ -447,9 +443,9 @@ public class ProcessingChain implements IChannelEventListener ((IMessageProvider) module).setMessageListener(mMessageBroadcaster); } - if(module instanceof IFilteredRealBufferProvider) + if(module instanceof ISourceEventProvider) { - ((IFilteredRealBufferProvider) module).setFilteredRealBufferListener(mFilteredRealBufferBroadcaster); + ((ISourceEventProvider) module).setSourceEventListener(mFrequencyChangeEventBroadcaster); } if(module instanceof ISquelchStateProvider) @@ -494,9 +490,9 @@ public class ProcessingChain implements IChannelEventListener ((IDecoderStateEventProvider) module).setDecoderStateListener(null); } - if(module instanceof IFrequencyChangeProvider) + if(module instanceof IFilteredRealBufferProvider) { - ((IFrequencyChangeProvider) module).setFrequencyChangeListener(null); + ((IFilteredRealBufferProvider) module).setFilteredRealBufferListener(null); } if(module instanceof IMessageProvider) @@ -504,9 +500,9 @@ public class ProcessingChain implements IChannelEventListener ((IMessageProvider) module).setMessageListener(null); } - if(module instanceof IFilteredRealBufferProvider) + if(module instanceof ISourceEventProvider) { - ((IFilteredRealBufferProvider) module).setFilteredRealBufferListener(null); + ((ISourceEventProvider) module).setSourceEventListener(null); } if(module instanceof ISquelchStateProvider) @@ -743,7 +739,7 @@ public class ProcessingChain implements IChannelEventListener * Adds the listener to receive frequency change events from the processing chain * @param listener to receive events */ - public void addFrequencyChangeListener(Listener listener) + public void addFrequencyChangeListener(Listener listener) { mFrequencyChangeEventBroadcaster.addListener(listener); } @@ -752,7 +748,7 @@ public class ProcessingChain implements IChannelEventListener * Removes the listener from receiving frequency change events * @param listener to remove */ - public void removeFrequencyChangeListener(Listener listener) + public void removeFrequencyChangeListener(Listener listener) { mFrequencyChangeEventBroadcaster.removeListener(listener); } diff --git a/src/module/decode/p25/C4FMSymbolFilter.java b/src/module/decode/p25/C4FMSymbolFilter.java index 51e7b78d..70a772e0 100644 --- a/src/module/decode/p25/C4FMSymbolFilter.java +++ b/src/module/decode/p25/C4FMSymbolFilter.java @@ -1,512 +1,508 @@ package module.decode.p25; +import dsp.gain.DirectGainControl; import instrument.Instrumentable; import instrument.tap.Tap; import instrument.tap.TapGroup; import instrument.tap.stream.AdditiveFloatTap; import instrument.tap.stream.FloatTap; - -import java.util.ArrayList; -import java.util.List; - -import org.slf4j.Logger; -import org.slf4j.LoggerFactory; import sample.Listener; import sample.real.RealBuffer; import sample.real.RealSampleListener; import sample.real.RealSampleProvider; -import source.tuner.TunerChannelSource; -import source.tuner.frequency.FrequencyChangeEvent; -import source.tuner.frequency.FrequencyChangeEvent.Event; -import source.tuner.frequency.IFrequencyChangeListener; -import dsp.gain.DirectGainControl; +import source.ISourceEventListener; +import source.SourceEvent; -public class C4FMSymbolFilter implements Listener, IFrequencyChangeListener, RealSampleProvider, Instrumentable +import java.util.ArrayList; +import java.util.List; + +public class C4FMSymbolFilter implements Listener, ISourceEventListener, RealSampleProvider, Instrumentable { - private static final float TAPS[][] = - { - { 0.00000e+00f, 0.00000e+00f, 0.00000e+00f, 0.00000e+00f, 1.00000e+00f, 0.00000e+00f, 0.00000e+00f, 0.00000e+00f }, // 0/128 - { -1.54700e-04f, 8.53777e-04f, -2.76968e-03f, 7.89295e-03f, 9.98534e-01f, -5.41054e-03f, 1.24642e-03f, -1.98993e-04f }, // 1/128 - { -3.09412e-04f, 1.70888e-03f, -5.55134e-03f, 1.58840e-02f, 9.96891e-01f, -1.07209e-02f, 2.47942e-03f, -3.96391e-04f }, // 2/128 - { -4.64053e-04f, 2.56486e-03f, -8.34364e-03f, 2.39714e-02f, 9.95074e-01f, -1.59305e-02f, 3.69852e-03f, -5.92100e-04f }, // 3/128 - { -6.18544e-04f, 3.42130e-03f, -1.11453e-02f, 3.21531e-02f, 9.93082e-01f, -2.10389e-02f, 4.90322e-03f, -7.86031e-04f }, // 4/128 - { -7.72802e-04f, 4.27773e-03f, -1.39548e-02f, 4.04274e-02f, 9.90917e-01f, -2.60456e-02f, 6.09305e-03f, -9.78093e-04f }, // 5/128 - { -9.26747e-04f, 5.13372e-03f, -1.67710e-02f, 4.87921e-02f, 9.88580e-01f, -3.09503e-02f, 7.26755e-03f, -1.16820e-03f }, // 6/128 - { -1.08030e-03f, 5.98883e-03f, -1.95925e-02f, 5.72454e-02f, 9.86071e-01f, -3.57525e-02f, 8.42626e-03f, -1.35627e-03f }, // 7/128 - { -1.23337e-03f, 6.84261e-03f, -2.24178e-02f, 6.57852e-02f, 9.83392e-01f, -4.04519e-02f, 9.56876e-03f, -1.54221e-03f }, // 8/128 - { -1.38589e-03f, 7.69462e-03f, -2.52457e-02f, 7.44095e-02f, 9.80543e-01f, -4.50483e-02f, 1.06946e-02f, -1.72594e-03f }, // 9/128 - { -1.53777e-03f, 8.54441e-03f, -2.80746e-02f, 8.31162e-02f, 9.77526e-01f, -4.95412e-02f, 1.18034e-02f, -1.90738e-03f }, // 10/128 - { -1.68894e-03f, 9.39154e-03f, -3.09033e-02f, 9.19033e-02f, 9.74342e-01f, -5.39305e-02f, 1.28947e-02f, -2.08645e-03f }, // 11/128 - { -1.83931e-03f, 1.02356e-02f, -3.37303e-02f, 1.00769e-01f, 9.70992e-01f, -5.82159e-02f, 1.39681e-02f, -2.26307e-03f }, // 12/128 - { -1.98880e-03f, 1.10760e-02f, -3.65541e-02f, 1.09710e-01f, 9.67477e-01f, -6.23972e-02f, 1.50233e-02f, -2.43718e-03f }, // 13/128 - { -2.13733e-03f, 1.19125e-02f, -3.93735e-02f, 1.18725e-01f, 9.63798e-01f, -6.64743e-02f, 1.60599e-02f, -2.60868e-03f }, // 14/128 - { -2.28483e-03f, 1.27445e-02f, -4.21869e-02f, 1.27812e-01f, 9.59958e-01f, -7.04471e-02f, 1.70776e-02f, -2.77751e-03f }, // 15/128 - { -2.43121e-03f, 1.35716e-02f, -4.49929e-02f, 1.36968e-01f, 9.55956e-01f, -7.43154e-02f, 1.80759e-02f, -2.94361e-03f }, // 16/128 - { -2.57640e-03f, 1.43934e-02f, -4.77900e-02f, 1.46192e-01f, 9.51795e-01f, -7.80792e-02f, 1.90545e-02f, -3.10689e-03f }, // 17/128 - { -2.72032e-03f, 1.52095e-02f, -5.05770e-02f, 1.55480e-01f, 9.47477e-01f, -8.17385e-02f, 2.00132e-02f, -3.26730e-03f }, // 18/128 - { -2.86289e-03f, 1.60193e-02f, -5.33522e-02f, 1.64831e-01f, 9.43001e-01f, -8.52933e-02f, 2.09516e-02f, -3.42477e-03f }, // 19/128 - { -3.00403e-03f, 1.68225e-02f, -5.61142e-02f, 1.74242e-01f, 9.38371e-01f, -8.87435e-02f, 2.18695e-02f, -3.57923e-03f }, // 20/128 - { -3.14367e-03f, 1.76185e-02f, -5.88617e-02f, 1.83711e-01f, 9.33586e-01f, -9.20893e-02f, 2.27664e-02f, -3.73062e-03f }, // 21/128 - { -3.28174e-03f, 1.84071e-02f, -6.15931e-02f, 1.93236e-01f, 9.28650e-01f, -9.53307e-02f, 2.36423e-02f, -3.87888e-03f }, // 22/128 - { -3.41815e-03f, 1.91877e-02f, -6.43069e-02f, 2.02814e-01f, 9.23564e-01f, -9.84679e-02f, 2.44967e-02f, -4.02397e-03f }, // 23/128 - { -3.55283e-03f, 1.99599e-02f, -6.70018e-02f, 2.12443e-01f, 9.18329e-01f, -1.01501e-01f, 2.53295e-02f, -4.16581e-03f }, // 24/128 - { -3.68570e-03f, 2.07233e-02f, -6.96762e-02f, 2.22120e-01f, 9.12947e-01f, -1.04430e-01f, 2.61404e-02f, -4.30435e-03f }, // 25/128 - { -3.81671e-03f, 2.14774e-02f, -7.23286e-02f, 2.31843e-01f, 9.07420e-01f, -1.07256e-01f, 2.69293e-02f, -4.43955e-03f }, // 26/128 - { -3.94576e-03f, 2.22218e-02f, -7.49577e-02f, 2.41609e-01f, 9.01749e-01f, -1.09978e-01f, 2.76957e-02f, -4.57135e-03f }, // 27/128 - { -4.07279e-03f, 2.29562e-02f, -7.75620e-02f, 2.51417e-01f, 8.95936e-01f, -1.12597e-01f, 2.84397e-02f, -4.69970e-03f }, // 28/128 - { -4.19774e-03f, 2.36801e-02f, -8.01399e-02f, 2.61263e-01f, 8.89984e-01f, -1.15113e-01f, 2.91609e-02f, -4.82456e-03f }, // 29/128 - { -4.32052e-03f, 2.43930e-02f, -8.26900e-02f, 2.71144e-01f, 8.83893e-01f, -1.17526e-01f, 2.98593e-02f, -4.94589e-03f }, // 30/128 - { -4.44107e-03f, 2.50946e-02f, -8.52109e-02f, 2.81060e-01f, 8.77666e-01f, -1.19837e-01f, 3.05345e-02f, -5.06363e-03f }, // 31/128 - { -4.55932e-03f, 2.57844e-02f, -8.77011e-02f, 2.91006e-01f, 8.71305e-01f, -1.22047e-01f, 3.11866e-02f, -5.17776e-03f }, // 32/128 - { -4.67520e-03f, 2.64621e-02f, -9.01591e-02f, 3.00980e-01f, 8.64812e-01f, -1.24154e-01f, 3.18153e-02f, -5.28823e-03f }, // 33/128 - { -4.78866e-03f, 2.71272e-02f, -9.25834e-02f, 3.10980e-01f, 8.58189e-01f, -1.26161e-01f, 3.24205e-02f, -5.39500e-03f }, // 34/128 - { -4.89961e-03f, 2.77794e-02f, -9.49727e-02f, 3.21004e-01f, 8.51437e-01f, -1.28068e-01f, 3.30021e-02f, -5.49804e-03f }, // 35/128 - { -5.00800e-03f, 2.84182e-02f, -9.73254e-02f, 3.31048e-01f, 8.44559e-01f, -1.29874e-01f, 3.35600e-02f, -5.59731e-03f }, // 36/128 - { -5.11376e-03f, 2.90433e-02f, -9.96402e-02f, 3.41109e-01f, 8.37557e-01f, -1.31581e-01f, 3.40940e-02f, -5.69280e-03f }, // 37/128 - { -5.21683e-03f, 2.96543e-02f, -1.01915e-01f, 3.51186e-01f, 8.30432e-01f, -1.33189e-01f, 3.46042e-02f, -5.78446e-03f }, // 38/128 - { -5.31716e-03f, 3.02507e-02f, -1.04150e-01f, 3.61276e-01f, 8.23188e-01f, -1.34699e-01f, 3.50903e-02f, -5.87227e-03f }, // 39/128 - { -5.41467e-03f, 3.08323e-02f, -1.06342e-01f, 3.71376e-01f, 8.15826e-01f, -1.36111e-01f, 3.55525e-02f, -5.95620e-03f }, // 40/128 - { -5.50931e-03f, 3.13987e-02f, -1.08490e-01f, 3.81484e-01f, 8.08348e-01f, -1.37426e-01f, 3.59905e-02f, -6.03624e-03f }, // 41/128 - { -5.60103e-03f, 3.19495e-02f, -1.10593e-01f, 3.91596e-01f, 8.00757e-01f, -1.38644e-01f, 3.64044e-02f, -6.11236e-03f }, // 42/128 - { -5.68976e-03f, 3.24843e-02f, -1.12650e-01f, 4.01710e-01f, 7.93055e-01f, -1.39767e-01f, 3.67941e-02f, -6.18454e-03f }, // 43/128 - { -5.77544e-03f, 3.30027e-02f, -1.14659e-01f, 4.11823e-01f, 7.85244e-01f, -1.40794e-01f, 3.71596e-02f, -6.25277e-03f }, // 44/128 - { -5.85804e-03f, 3.35046e-02f, -1.16618e-01f, 4.21934e-01f, 7.77327e-01f, -1.41727e-01f, 3.75010e-02f, -6.31703e-03f }, // 45/128 - { -5.93749e-03f, 3.39894e-02f, -1.18526e-01f, 4.32038e-01f, 7.69305e-01f, -1.42566e-01f, 3.78182e-02f, -6.37730e-03f }, // 46/128 - { -6.01374e-03f, 3.44568e-02f, -1.20382e-01f, 4.42134e-01f, 7.61181e-01f, -1.43313e-01f, 3.81111e-02f, -6.43358e-03f }, // 47/128 - { -6.08674e-03f, 3.49066e-02f, -1.22185e-01f, 4.52218e-01f, 7.52958e-01f, -1.43968e-01f, 3.83800e-02f, -6.48585e-03f }, // 48/128 - { -6.15644e-03f, 3.53384e-02f, -1.23933e-01f, 4.62289e-01f, 7.44637e-01f, -1.44531e-01f, 3.86247e-02f, -6.53412e-03f }, // 49/128 - { -6.22280e-03f, 3.57519e-02f, -1.25624e-01f, 4.72342e-01f, 7.36222e-01f, -1.45004e-01f, 3.88454e-02f, -6.57836e-03f }, // 50/128 - { -6.28577e-03f, 3.61468e-02f, -1.27258e-01f, 4.82377e-01f, 7.27714e-01f, -1.45387e-01f, 3.90420e-02f, -6.61859e-03f }, // 51/128 - { -6.34530e-03f, 3.65227e-02f, -1.28832e-01f, 4.92389e-01f, 7.19116e-01f, -1.45682e-01f, 3.92147e-02f, -6.65479e-03f }, // 52/128 - { -6.40135e-03f, 3.68795e-02f, -1.30347e-01f, 5.02377e-01f, 7.10431e-01f, -1.45889e-01f, 3.93636e-02f, -6.68698e-03f }, // 53/128 - { -6.45388e-03f, 3.72167e-02f, -1.31800e-01f, 5.12337e-01f, 7.01661e-01f, -1.46009e-01f, 3.94886e-02f, -6.71514e-03f }, // 54/128 - { -6.50285e-03f, 3.75341e-02f, -1.33190e-01f, 5.22267e-01f, 6.92808e-01f, -1.46043e-01f, 3.95900e-02f, -6.73929e-03f }, // 55/128 - { -6.54823e-03f, 3.78315e-02f, -1.34515e-01f, 5.32164e-01f, 6.83875e-01f, -1.45993e-01f, 3.96678e-02f, -6.75943e-03f }, // 56/128 - { -6.58996e-03f, 3.81085e-02f, -1.35775e-01f, 5.42025e-01f, 6.74865e-01f, -1.45859e-01f, 3.97222e-02f, -6.77557e-03f }, // 57/128 - { -6.62802e-03f, 3.83650e-02f, -1.36969e-01f, 5.51849e-01f, 6.65779e-01f, -1.45641e-01f, 3.97532e-02f, -6.78771e-03f }, // 58/128 - { -6.66238e-03f, 3.86006e-02f, -1.38094e-01f, 5.61631e-01f, 6.56621e-01f, -1.45343e-01f, 3.97610e-02f, -6.79588e-03f }, // 59/128 - { -6.69300e-03f, 3.88151e-02f, -1.39150e-01f, 5.71370e-01f, 6.47394e-01f, -1.44963e-01f, 3.97458e-02f, -6.80007e-03f }, // 60/128 - { -6.71985e-03f, 3.90083e-02f, -1.40136e-01f, 5.81063e-01f, 6.38099e-01f, -1.44503e-01f, 3.97077e-02f, -6.80032e-03f }, // 61/128 - { -6.74291e-03f, 3.91800e-02f, -1.41050e-01f, 5.90706e-01f, 6.28739e-01f, -1.43965e-01f, 3.96469e-02f, -6.79662e-03f }, // 62/128 - { -6.76214e-03f, 3.93299e-02f, -1.41891e-01f, 6.00298e-01f, 6.19318e-01f, -1.43350e-01f, 3.95635e-02f, -6.78902e-03f }, // 63/128 - { -6.77751e-03f, 3.94578e-02f, -1.42658e-01f, 6.09836e-01f, 6.09836e-01f, -1.42658e-01f, 3.94578e-02f, -6.77751e-03f }, // 64/128 - { -6.78902e-03f, 3.95635e-02f, -1.43350e-01f, 6.19318e-01f, 6.00298e-01f, -1.41891e-01f, 3.93299e-02f, -6.76214e-03f }, // 65/128 - { -6.79662e-03f, 3.96469e-02f, -1.43965e-01f, 6.28739e-01f, 5.90706e-01f, -1.41050e-01f, 3.91800e-02f, -6.74291e-03f }, // 66/128 - { -6.80032e-03f, 3.97077e-02f, -1.44503e-01f, 6.38099e-01f, 5.81063e-01f, -1.40136e-01f, 3.90083e-02f, -6.71985e-03f }, // 67/128 - { -6.80007e-03f, 3.97458e-02f, -1.44963e-01f, 6.47394e-01f, 5.71370e-01f, -1.39150e-01f, 3.88151e-02f, -6.69300e-03f }, // 68/128 - { -6.79588e-03f, 3.97610e-02f, -1.45343e-01f, 6.56621e-01f, 5.61631e-01f, -1.38094e-01f, 3.86006e-02f, -6.66238e-03f }, // 69/128 - { -6.78771e-03f, 3.97532e-02f, -1.45641e-01f, 6.65779e-01f, 5.51849e-01f, -1.36969e-01f, 3.83650e-02f, -6.62802e-03f }, // 70/128 - { -6.77557e-03f, 3.97222e-02f, -1.45859e-01f, 6.74865e-01f, 5.42025e-01f, -1.35775e-01f, 3.81085e-02f, -6.58996e-03f }, // 71/128 - { -6.75943e-03f, 3.96678e-02f, -1.45993e-01f, 6.83875e-01f, 5.32164e-01f, -1.34515e-01f, 3.78315e-02f, -6.54823e-03f }, // 72/128 - { -6.73929e-03f, 3.95900e-02f, -1.46043e-01f, 6.92808e-01f, 5.22267e-01f, -1.33190e-01f, 3.75341e-02f, -6.50285e-03f }, // 73/128 - { -6.71514e-03f, 3.94886e-02f, -1.46009e-01f, 7.01661e-01f, 5.12337e-01f, -1.31800e-01f, 3.72167e-02f, -6.45388e-03f }, // 74/128 - { -6.68698e-03f, 3.93636e-02f, -1.45889e-01f, 7.10431e-01f, 5.02377e-01f, -1.30347e-01f, 3.68795e-02f, -6.40135e-03f }, // 75/128 - { -6.65479e-03f, 3.92147e-02f, -1.45682e-01f, 7.19116e-01f, 4.92389e-01f, -1.28832e-01f, 3.65227e-02f, -6.34530e-03f }, // 76/128 - { -6.61859e-03f, 3.90420e-02f, -1.45387e-01f, 7.27714e-01f, 4.82377e-01f, -1.27258e-01f, 3.61468e-02f, -6.28577e-03f }, // 77/128 - { -6.57836e-03f, 3.88454e-02f, -1.45004e-01f, 7.36222e-01f, 4.72342e-01f, -1.25624e-01f, 3.57519e-02f, -6.22280e-03f }, // 78/128 - { -6.53412e-03f, 3.86247e-02f, -1.44531e-01f, 7.44637e-01f, 4.62289e-01f, -1.23933e-01f, 3.53384e-02f, -6.15644e-03f }, // 79/128 - { -6.48585e-03f, 3.83800e-02f, -1.43968e-01f, 7.52958e-01f, 4.52218e-01f, -1.22185e-01f, 3.49066e-02f, -6.08674e-03f }, // 80/128 - { -6.43358e-03f, 3.81111e-02f, -1.43313e-01f, 7.61181e-01f, 4.42134e-01f, -1.20382e-01f, 3.44568e-02f, -6.01374e-03f }, // 81/128 - { -6.37730e-03f, 3.78182e-02f, -1.42566e-01f, 7.69305e-01f, 4.32038e-01f, -1.18526e-01f, 3.39894e-02f, -5.93749e-03f }, // 82/128 - { -6.31703e-03f, 3.75010e-02f, -1.41727e-01f, 7.77327e-01f, 4.21934e-01f, -1.16618e-01f, 3.35046e-02f, -5.85804e-03f }, // 83/128 - { -6.25277e-03f, 3.71596e-02f, -1.40794e-01f, 7.85244e-01f, 4.11823e-01f, -1.14659e-01f, 3.30027e-02f, -5.77544e-03f }, // 84/128 - { -6.18454e-03f, 3.67941e-02f, -1.39767e-01f, 7.93055e-01f, 4.01710e-01f, -1.12650e-01f, 3.24843e-02f, -5.68976e-03f }, // 85/128 - { -6.11236e-03f, 3.64044e-02f, -1.38644e-01f, 8.00757e-01f, 3.91596e-01f, -1.10593e-01f, 3.19495e-02f, -5.60103e-03f }, // 86/128 - { -6.03624e-03f, 3.59905e-02f, -1.37426e-01f, 8.08348e-01f, 3.81484e-01f, -1.08490e-01f, 3.13987e-02f, -5.50931e-03f }, // 87/128 - { -5.95620e-03f, 3.55525e-02f, -1.36111e-01f, 8.15826e-01f, 3.71376e-01f, -1.06342e-01f, 3.08323e-02f, -5.41467e-03f }, // 88/128 - { -5.87227e-03f, 3.50903e-02f, -1.34699e-01f, 8.23188e-01f, 3.61276e-01f, -1.04150e-01f, 3.02507e-02f, -5.31716e-03f }, // 89/128 - { -5.78446e-03f, 3.46042e-02f, -1.33189e-01f, 8.30432e-01f, 3.51186e-01f, -1.01915e-01f, 2.96543e-02f, -5.21683e-03f }, // 90/128 - { -5.69280e-03f, 3.40940e-02f, -1.31581e-01f, 8.37557e-01f, 3.41109e-01f, -9.96402e-02f, 2.90433e-02f, -5.11376e-03f }, // 91/128 - { -5.59731e-03f, 3.35600e-02f, -1.29874e-01f, 8.44559e-01f, 3.31048e-01f, -9.73254e-02f, 2.84182e-02f, -5.00800e-03f }, // 92/128 - { -5.49804e-03f, 3.30021e-02f, -1.28068e-01f, 8.51437e-01f, 3.21004e-01f, -9.49727e-02f, 2.77794e-02f, -4.89961e-03f }, // 93/128 - { -5.39500e-03f, 3.24205e-02f, -1.26161e-01f, 8.58189e-01f, 3.10980e-01f, -9.25834e-02f, 2.71272e-02f, -4.78866e-03f }, // 94/128 - { -5.28823e-03f, 3.18153e-02f, -1.24154e-01f, 8.64812e-01f, 3.00980e-01f, -9.01591e-02f, 2.64621e-02f, -4.67520e-03f }, // 95/128 - { -5.17776e-03f, 3.11866e-02f, -1.22047e-01f, 8.71305e-01f, 2.91006e-01f, -8.77011e-02f, 2.57844e-02f, -4.55932e-03f }, // 96/128 - { -5.06363e-03f, 3.05345e-02f, -1.19837e-01f, 8.77666e-01f, 2.81060e-01f, -8.52109e-02f, 2.50946e-02f, -4.44107e-03f }, // 97/128 - { -4.94589e-03f, 2.98593e-02f, -1.17526e-01f, 8.83893e-01f, 2.71144e-01f, -8.26900e-02f, 2.43930e-02f, -4.32052e-03f }, // 98/128 - { -4.82456e-03f, 2.91609e-02f, -1.15113e-01f, 8.89984e-01f, 2.61263e-01f, -8.01399e-02f, 2.36801e-02f, -4.19774e-03f }, // 99/128 - { -4.69970e-03f, 2.84397e-02f, -1.12597e-01f, 8.95936e-01f, 2.51417e-01f, -7.75620e-02f, 2.29562e-02f, -4.07279e-03f }, // 100/128 - { -4.57135e-03f, 2.76957e-02f, -1.09978e-01f, 9.01749e-01f, 2.41609e-01f, -7.49577e-02f, 2.22218e-02f, -3.94576e-03f }, // 101/128 - { -4.43955e-03f, 2.69293e-02f, -1.07256e-01f, 9.07420e-01f, 2.31843e-01f, -7.23286e-02f, 2.14774e-02f, -3.81671e-03f }, // 102/128 - { -4.30435e-03f, 2.61404e-02f, -1.04430e-01f, 9.12947e-01f, 2.22120e-01f, -6.96762e-02f, 2.07233e-02f, -3.68570e-03f }, // 103/128 - { -4.16581e-03f, 2.53295e-02f, -1.01501e-01f, 9.18329e-01f, 2.12443e-01f, -6.70018e-02f, 1.99599e-02f, -3.55283e-03f }, // 104/128 - { -4.02397e-03f, 2.44967e-02f, -9.84679e-02f, 9.23564e-01f, 2.02814e-01f, -6.43069e-02f, 1.91877e-02f, -3.41815e-03f }, // 105/128 - { -3.87888e-03f, 2.36423e-02f, -9.53307e-02f, 9.28650e-01f, 1.93236e-01f, -6.15931e-02f, 1.84071e-02f, -3.28174e-03f }, // 106/128 - { -3.73062e-03f, 2.27664e-02f, -9.20893e-02f, 9.33586e-01f, 1.83711e-01f, -5.88617e-02f, 1.76185e-02f, -3.14367e-03f }, // 107/128 - { -3.57923e-03f, 2.18695e-02f, -8.87435e-02f, 9.38371e-01f, 1.74242e-01f, -5.61142e-02f, 1.68225e-02f, -3.00403e-03f }, // 108/128 - { -3.42477e-03f, 2.09516e-02f, -8.52933e-02f, 9.43001e-01f, 1.64831e-01f, -5.33522e-02f, 1.60193e-02f, -2.86289e-03f }, // 109/128 - { -3.26730e-03f, 2.00132e-02f, -8.17385e-02f, 9.47477e-01f, 1.55480e-01f, -5.05770e-02f, 1.52095e-02f, -2.72032e-03f }, // 110/128 - { -3.10689e-03f, 1.90545e-02f, -7.80792e-02f, 9.51795e-01f, 1.46192e-01f, -4.77900e-02f, 1.43934e-02f, -2.57640e-03f }, // 111/128 - { -2.94361e-03f, 1.80759e-02f, -7.43154e-02f, 9.55956e-01f, 1.36968e-01f, -4.49929e-02f, 1.35716e-02f, -2.43121e-03f }, // 112/128 - { -2.77751e-03f, 1.70776e-02f, -7.04471e-02f, 9.59958e-01f, 1.27812e-01f, -4.21869e-02f, 1.27445e-02f, -2.28483e-03f }, // 113/128 - { -2.60868e-03f, 1.60599e-02f, -6.64743e-02f, 9.63798e-01f, 1.18725e-01f, -3.93735e-02f, 1.19125e-02f, -2.13733e-03f }, // 114/128 - { -2.43718e-03f, 1.50233e-02f, -6.23972e-02f, 9.67477e-01f, 1.09710e-01f, -3.65541e-02f, 1.10760e-02f, -1.98880e-03f }, // 115/128 - { -2.26307e-03f, 1.39681e-02f, -5.82159e-02f, 9.70992e-01f, 1.00769e-01f, -3.37303e-02f, 1.02356e-02f, -1.83931e-03f }, // 116/128 - { -2.08645e-03f, 1.28947e-02f, -5.39305e-02f, 9.74342e-01f, 9.19033e-02f, -3.09033e-02f, 9.39154e-03f, -1.68894e-03f }, // 117/128 - { -1.90738e-03f, 1.18034e-02f, -4.95412e-02f, 9.77526e-01f, 8.31162e-02f, -2.80746e-02f, 8.54441e-03f, -1.53777e-03f }, // 118/128 - { -1.72594e-03f, 1.06946e-02f, -4.50483e-02f, 9.80543e-01f, 7.44095e-02f, -2.52457e-02f, 7.69462e-03f, -1.38589e-03f }, // 119/128 - { -1.54221e-03f, 9.56876e-03f, -4.04519e-02f, 9.83392e-01f, 6.57852e-02f, -2.24178e-02f, 6.84261e-03f, -1.23337e-03f }, // 120/128 - { -1.35627e-03f, 8.42626e-03f, -3.57525e-02f, 9.86071e-01f, 5.72454e-02f, -1.95925e-02f, 5.98883e-03f, -1.08030e-03f }, // 121/128 - { -1.16820e-03f, 7.26755e-03f, -3.09503e-02f, 9.88580e-01f, 4.87921e-02f, -1.67710e-02f, 5.13372e-03f, -9.26747e-04f }, // 122/128 - { -9.78093e-04f, 6.09305e-03f, -2.60456e-02f, 9.90917e-01f, 4.04274e-02f, -1.39548e-02f, 4.27773e-03f, -7.72802e-04f }, // 123/128 - { -7.86031e-04f, 4.90322e-03f, -2.10389e-02f, 9.93082e-01f, 3.21531e-02f, -1.11453e-02f, 3.42130e-03f, -6.18544e-04f }, // 124/128 - { -5.92100e-04f, 3.69852e-03f, -1.59305e-02f, 9.95074e-01f, 2.39714e-02f, -8.34364e-03f, 2.56486e-03f, -4.64053e-04f }, // 125/128 - { -3.96391e-04f, 2.47942e-03f, -1.07209e-02f, 9.96891e-01f, 1.58840e-02f, -5.55134e-03f, 1.70888e-03f, -3.09412e-04f }, // 126/128 - { -1.98993e-04f, 1.24642e-03f, -5.41054e-03f, 9.98534e-01f, 7.89295e-03f, -2.76968e-03f, 8.53777e-04f, -1.54700e-04f }, // 127/128 - { 0.00000e+00f, 0.00000e+00f, 0.00000e+00f, 1.00000e+00f, 0.00000e+00f, 0.00000e+00f, 0.00000e+00f, 0.00000e+00f }, // 128/128 - }; - + private static final float TAPS[][] = + { + {0.00000e+00f, 0.00000e+00f, 0.00000e+00f, 0.00000e+00f, 1.00000e+00f, 0.00000e+00f, 0.00000e+00f, 0.00000e+00f}, // 0/128 + {-1.54700e-04f, 8.53777e-04f, -2.76968e-03f, 7.89295e-03f, 9.98534e-01f, -5.41054e-03f, 1.24642e-03f, -1.98993e-04f}, // 1/128 + {-3.09412e-04f, 1.70888e-03f, -5.55134e-03f, 1.58840e-02f, 9.96891e-01f, -1.07209e-02f, 2.47942e-03f, -3.96391e-04f}, // 2/128 + {-4.64053e-04f, 2.56486e-03f, -8.34364e-03f, 2.39714e-02f, 9.95074e-01f, -1.59305e-02f, 3.69852e-03f, -5.92100e-04f}, // 3/128 + {-6.18544e-04f, 3.42130e-03f, -1.11453e-02f, 3.21531e-02f, 9.93082e-01f, -2.10389e-02f, 4.90322e-03f, -7.86031e-04f}, // 4/128 + {-7.72802e-04f, 4.27773e-03f, -1.39548e-02f, 4.04274e-02f, 9.90917e-01f, -2.60456e-02f, 6.09305e-03f, -9.78093e-04f}, // 5/128 + {-9.26747e-04f, 5.13372e-03f, -1.67710e-02f, 4.87921e-02f, 9.88580e-01f, -3.09503e-02f, 7.26755e-03f, -1.16820e-03f}, // 6/128 + {-1.08030e-03f, 5.98883e-03f, -1.95925e-02f, 5.72454e-02f, 9.86071e-01f, -3.57525e-02f, 8.42626e-03f, -1.35627e-03f}, // 7/128 + {-1.23337e-03f, 6.84261e-03f, -2.24178e-02f, 6.57852e-02f, 9.83392e-01f, -4.04519e-02f, 9.56876e-03f, -1.54221e-03f}, // 8/128 + {-1.38589e-03f, 7.69462e-03f, -2.52457e-02f, 7.44095e-02f, 9.80543e-01f, -4.50483e-02f, 1.06946e-02f, -1.72594e-03f}, // 9/128 + {-1.53777e-03f, 8.54441e-03f, -2.80746e-02f, 8.31162e-02f, 9.77526e-01f, -4.95412e-02f, 1.18034e-02f, -1.90738e-03f}, // 10/128 + {-1.68894e-03f, 9.39154e-03f, -3.09033e-02f, 9.19033e-02f, 9.74342e-01f, -5.39305e-02f, 1.28947e-02f, -2.08645e-03f}, // 11/128 + {-1.83931e-03f, 1.02356e-02f, -3.37303e-02f, 1.00769e-01f, 9.70992e-01f, -5.82159e-02f, 1.39681e-02f, -2.26307e-03f}, // 12/128 + {-1.98880e-03f, 1.10760e-02f, -3.65541e-02f, 1.09710e-01f, 9.67477e-01f, -6.23972e-02f, 1.50233e-02f, -2.43718e-03f}, // 13/128 + {-2.13733e-03f, 1.19125e-02f, -3.93735e-02f, 1.18725e-01f, 9.63798e-01f, -6.64743e-02f, 1.60599e-02f, -2.60868e-03f}, // 14/128 + {-2.28483e-03f, 1.27445e-02f, -4.21869e-02f, 1.27812e-01f, 9.59958e-01f, -7.04471e-02f, 1.70776e-02f, -2.77751e-03f}, // 15/128 + {-2.43121e-03f, 1.35716e-02f, -4.49929e-02f, 1.36968e-01f, 9.55956e-01f, -7.43154e-02f, 1.80759e-02f, -2.94361e-03f}, // 16/128 + {-2.57640e-03f, 1.43934e-02f, -4.77900e-02f, 1.46192e-01f, 9.51795e-01f, -7.80792e-02f, 1.90545e-02f, -3.10689e-03f}, // 17/128 + {-2.72032e-03f, 1.52095e-02f, -5.05770e-02f, 1.55480e-01f, 9.47477e-01f, -8.17385e-02f, 2.00132e-02f, -3.26730e-03f}, // 18/128 + {-2.86289e-03f, 1.60193e-02f, -5.33522e-02f, 1.64831e-01f, 9.43001e-01f, -8.52933e-02f, 2.09516e-02f, -3.42477e-03f}, // 19/128 + {-3.00403e-03f, 1.68225e-02f, -5.61142e-02f, 1.74242e-01f, 9.38371e-01f, -8.87435e-02f, 2.18695e-02f, -3.57923e-03f}, // 20/128 + {-3.14367e-03f, 1.76185e-02f, -5.88617e-02f, 1.83711e-01f, 9.33586e-01f, -9.20893e-02f, 2.27664e-02f, -3.73062e-03f}, // 21/128 + {-3.28174e-03f, 1.84071e-02f, -6.15931e-02f, 1.93236e-01f, 9.28650e-01f, -9.53307e-02f, 2.36423e-02f, -3.87888e-03f}, // 22/128 + {-3.41815e-03f, 1.91877e-02f, -6.43069e-02f, 2.02814e-01f, 9.23564e-01f, -9.84679e-02f, 2.44967e-02f, -4.02397e-03f}, // 23/128 + {-3.55283e-03f, 1.99599e-02f, -6.70018e-02f, 2.12443e-01f, 9.18329e-01f, -1.01501e-01f, 2.53295e-02f, -4.16581e-03f}, // 24/128 + {-3.68570e-03f, 2.07233e-02f, -6.96762e-02f, 2.22120e-01f, 9.12947e-01f, -1.04430e-01f, 2.61404e-02f, -4.30435e-03f}, // 25/128 + {-3.81671e-03f, 2.14774e-02f, -7.23286e-02f, 2.31843e-01f, 9.07420e-01f, -1.07256e-01f, 2.69293e-02f, -4.43955e-03f}, // 26/128 + {-3.94576e-03f, 2.22218e-02f, -7.49577e-02f, 2.41609e-01f, 9.01749e-01f, -1.09978e-01f, 2.76957e-02f, -4.57135e-03f}, // 27/128 + {-4.07279e-03f, 2.29562e-02f, -7.75620e-02f, 2.51417e-01f, 8.95936e-01f, -1.12597e-01f, 2.84397e-02f, -4.69970e-03f}, // 28/128 + {-4.19774e-03f, 2.36801e-02f, -8.01399e-02f, 2.61263e-01f, 8.89984e-01f, -1.15113e-01f, 2.91609e-02f, -4.82456e-03f}, // 29/128 + {-4.32052e-03f, 2.43930e-02f, -8.26900e-02f, 2.71144e-01f, 8.83893e-01f, -1.17526e-01f, 2.98593e-02f, -4.94589e-03f}, // 30/128 + {-4.44107e-03f, 2.50946e-02f, -8.52109e-02f, 2.81060e-01f, 8.77666e-01f, -1.19837e-01f, 3.05345e-02f, -5.06363e-03f}, // 31/128 + {-4.55932e-03f, 2.57844e-02f, -8.77011e-02f, 2.91006e-01f, 8.71305e-01f, -1.22047e-01f, 3.11866e-02f, -5.17776e-03f}, // 32/128 + {-4.67520e-03f, 2.64621e-02f, -9.01591e-02f, 3.00980e-01f, 8.64812e-01f, -1.24154e-01f, 3.18153e-02f, -5.28823e-03f}, // 33/128 + {-4.78866e-03f, 2.71272e-02f, -9.25834e-02f, 3.10980e-01f, 8.58189e-01f, -1.26161e-01f, 3.24205e-02f, -5.39500e-03f}, // 34/128 + {-4.89961e-03f, 2.77794e-02f, -9.49727e-02f, 3.21004e-01f, 8.51437e-01f, -1.28068e-01f, 3.30021e-02f, -5.49804e-03f}, // 35/128 + {-5.00800e-03f, 2.84182e-02f, -9.73254e-02f, 3.31048e-01f, 8.44559e-01f, -1.29874e-01f, 3.35600e-02f, -5.59731e-03f}, // 36/128 + {-5.11376e-03f, 2.90433e-02f, -9.96402e-02f, 3.41109e-01f, 8.37557e-01f, -1.31581e-01f, 3.40940e-02f, -5.69280e-03f}, // 37/128 + {-5.21683e-03f, 2.96543e-02f, -1.01915e-01f, 3.51186e-01f, 8.30432e-01f, -1.33189e-01f, 3.46042e-02f, -5.78446e-03f}, // 38/128 + {-5.31716e-03f, 3.02507e-02f, -1.04150e-01f, 3.61276e-01f, 8.23188e-01f, -1.34699e-01f, 3.50903e-02f, -5.87227e-03f}, // 39/128 + {-5.41467e-03f, 3.08323e-02f, -1.06342e-01f, 3.71376e-01f, 8.15826e-01f, -1.36111e-01f, 3.55525e-02f, -5.95620e-03f}, // 40/128 + {-5.50931e-03f, 3.13987e-02f, -1.08490e-01f, 3.81484e-01f, 8.08348e-01f, -1.37426e-01f, 3.59905e-02f, -6.03624e-03f}, // 41/128 + {-5.60103e-03f, 3.19495e-02f, -1.10593e-01f, 3.91596e-01f, 8.00757e-01f, -1.38644e-01f, 3.64044e-02f, -6.11236e-03f}, // 42/128 + {-5.68976e-03f, 3.24843e-02f, -1.12650e-01f, 4.01710e-01f, 7.93055e-01f, -1.39767e-01f, 3.67941e-02f, -6.18454e-03f}, // 43/128 + {-5.77544e-03f, 3.30027e-02f, -1.14659e-01f, 4.11823e-01f, 7.85244e-01f, -1.40794e-01f, 3.71596e-02f, -6.25277e-03f}, // 44/128 + {-5.85804e-03f, 3.35046e-02f, -1.16618e-01f, 4.21934e-01f, 7.77327e-01f, -1.41727e-01f, 3.75010e-02f, -6.31703e-03f}, // 45/128 + {-5.93749e-03f, 3.39894e-02f, -1.18526e-01f, 4.32038e-01f, 7.69305e-01f, -1.42566e-01f, 3.78182e-02f, -6.37730e-03f}, // 46/128 + {-6.01374e-03f, 3.44568e-02f, -1.20382e-01f, 4.42134e-01f, 7.61181e-01f, -1.43313e-01f, 3.81111e-02f, -6.43358e-03f}, // 47/128 + {-6.08674e-03f, 3.49066e-02f, -1.22185e-01f, 4.52218e-01f, 7.52958e-01f, -1.43968e-01f, 3.83800e-02f, -6.48585e-03f}, // 48/128 + {-6.15644e-03f, 3.53384e-02f, -1.23933e-01f, 4.62289e-01f, 7.44637e-01f, -1.44531e-01f, 3.86247e-02f, -6.53412e-03f}, // 49/128 + {-6.22280e-03f, 3.57519e-02f, -1.25624e-01f, 4.72342e-01f, 7.36222e-01f, -1.45004e-01f, 3.88454e-02f, -6.57836e-03f}, // 50/128 + {-6.28577e-03f, 3.61468e-02f, -1.27258e-01f, 4.82377e-01f, 7.27714e-01f, -1.45387e-01f, 3.90420e-02f, -6.61859e-03f}, // 51/128 + {-6.34530e-03f, 3.65227e-02f, -1.28832e-01f, 4.92389e-01f, 7.19116e-01f, -1.45682e-01f, 3.92147e-02f, -6.65479e-03f}, // 52/128 + {-6.40135e-03f, 3.68795e-02f, -1.30347e-01f, 5.02377e-01f, 7.10431e-01f, -1.45889e-01f, 3.93636e-02f, -6.68698e-03f}, // 53/128 + {-6.45388e-03f, 3.72167e-02f, -1.31800e-01f, 5.12337e-01f, 7.01661e-01f, -1.46009e-01f, 3.94886e-02f, -6.71514e-03f}, // 54/128 + {-6.50285e-03f, 3.75341e-02f, -1.33190e-01f, 5.22267e-01f, 6.92808e-01f, -1.46043e-01f, 3.95900e-02f, -6.73929e-03f}, // 55/128 + {-6.54823e-03f, 3.78315e-02f, -1.34515e-01f, 5.32164e-01f, 6.83875e-01f, -1.45993e-01f, 3.96678e-02f, -6.75943e-03f}, // 56/128 + {-6.58996e-03f, 3.81085e-02f, -1.35775e-01f, 5.42025e-01f, 6.74865e-01f, -1.45859e-01f, 3.97222e-02f, -6.77557e-03f}, // 57/128 + {-6.62802e-03f, 3.83650e-02f, -1.36969e-01f, 5.51849e-01f, 6.65779e-01f, -1.45641e-01f, 3.97532e-02f, -6.78771e-03f}, // 58/128 + {-6.66238e-03f, 3.86006e-02f, -1.38094e-01f, 5.61631e-01f, 6.56621e-01f, -1.45343e-01f, 3.97610e-02f, -6.79588e-03f}, // 59/128 + {-6.69300e-03f, 3.88151e-02f, -1.39150e-01f, 5.71370e-01f, 6.47394e-01f, -1.44963e-01f, 3.97458e-02f, -6.80007e-03f}, // 60/128 + {-6.71985e-03f, 3.90083e-02f, -1.40136e-01f, 5.81063e-01f, 6.38099e-01f, -1.44503e-01f, 3.97077e-02f, -6.80032e-03f}, // 61/128 + {-6.74291e-03f, 3.91800e-02f, -1.41050e-01f, 5.90706e-01f, 6.28739e-01f, -1.43965e-01f, 3.96469e-02f, -6.79662e-03f}, // 62/128 + {-6.76214e-03f, 3.93299e-02f, -1.41891e-01f, 6.00298e-01f, 6.19318e-01f, -1.43350e-01f, 3.95635e-02f, -6.78902e-03f}, // 63/128 + {-6.77751e-03f, 3.94578e-02f, -1.42658e-01f, 6.09836e-01f, 6.09836e-01f, -1.42658e-01f, 3.94578e-02f, -6.77751e-03f}, // 64/128 + {-6.78902e-03f, 3.95635e-02f, -1.43350e-01f, 6.19318e-01f, 6.00298e-01f, -1.41891e-01f, 3.93299e-02f, -6.76214e-03f}, // 65/128 + {-6.79662e-03f, 3.96469e-02f, -1.43965e-01f, 6.28739e-01f, 5.90706e-01f, -1.41050e-01f, 3.91800e-02f, -6.74291e-03f}, // 66/128 + {-6.80032e-03f, 3.97077e-02f, -1.44503e-01f, 6.38099e-01f, 5.81063e-01f, -1.40136e-01f, 3.90083e-02f, -6.71985e-03f}, // 67/128 + {-6.80007e-03f, 3.97458e-02f, -1.44963e-01f, 6.47394e-01f, 5.71370e-01f, -1.39150e-01f, 3.88151e-02f, -6.69300e-03f}, // 68/128 + {-6.79588e-03f, 3.97610e-02f, -1.45343e-01f, 6.56621e-01f, 5.61631e-01f, -1.38094e-01f, 3.86006e-02f, -6.66238e-03f}, // 69/128 + {-6.78771e-03f, 3.97532e-02f, -1.45641e-01f, 6.65779e-01f, 5.51849e-01f, -1.36969e-01f, 3.83650e-02f, -6.62802e-03f}, // 70/128 + {-6.77557e-03f, 3.97222e-02f, -1.45859e-01f, 6.74865e-01f, 5.42025e-01f, -1.35775e-01f, 3.81085e-02f, -6.58996e-03f}, // 71/128 + {-6.75943e-03f, 3.96678e-02f, -1.45993e-01f, 6.83875e-01f, 5.32164e-01f, -1.34515e-01f, 3.78315e-02f, -6.54823e-03f}, // 72/128 + {-6.73929e-03f, 3.95900e-02f, -1.46043e-01f, 6.92808e-01f, 5.22267e-01f, -1.33190e-01f, 3.75341e-02f, -6.50285e-03f}, // 73/128 + {-6.71514e-03f, 3.94886e-02f, -1.46009e-01f, 7.01661e-01f, 5.12337e-01f, -1.31800e-01f, 3.72167e-02f, -6.45388e-03f}, // 74/128 + {-6.68698e-03f, 3.93636e-02f, -1.45889e-01f, 7.10431e-01f, 5.02377e-01f, -1.30347e-01f, 3.68795e-02f, -6.40135e-03f}, // 75/128 + {-6.65479e-03f, 3.92147e-02f, -1.45682e-01f, 7.19116e-01f, 4.92389e-01f, -1.28832e-01f, 3.65227e-02f, -6.34530e-03f}, // 76/128 + {-6.61859e-03f, 3.90420e-02f, -1.45387e-01f, 7.27714e-01f, 4.82377e-01f, -1.27258e-01f, 3.61468e-02f, -6.28577e-03f}, // 77/128 + {-6.57836e-03f, 3.88454e-02f, -1.45004e-01f, 7.36222e-01f, 4.72342e-01f, -1.25624e-01f, 3.57519e-02f, -6.22280e-03f}, // 78/128 + {-6.53412e-03f, 3.86247e-02f, -1.44531e-01f, 7.44637e-01f, 4.62289e-01f, -1.23933e-01f, 3.53384e-02f, -6.15644e-03f}, // 79/128 + {-6.48585e-03f, 3.83800e-02f, -1.43968e-01f, 7.52958e-01f, 4.52218e-01f, -1.22185e-01f, 3.49066e-02f, -6.08674e-03f}, // 80/128 + {-6.43358e-03f, 3.81111e-02f, -1.43313e-01f, 7.61181e-01f, 4.42134e-01f, -1.20382e-01f, 3.44568e-02f, -6.01374e-03f}, // 81/128 + {-6.37730e-03f, 3.78182e-02f, -1.42566e-01f, 7.69305e-01f, 4.32038e-01f, -1.18526e-01f, 3.39894e-02f, -5.93749e-03f}, // 82/128 + {-6.31703e-03f, 3.75010e-02f, -1.41727e-01f, 7.77327e-01f, 4.21934e-01f, -1.16618e-01f, 3.35046e-02f, -5.85804e-03f}, // 83/128 + {-6.25277e-03f, 3.71596e-02f, -1.40794e-01f, 7.85244e-01f, 4.11823e-01f, -1.14659e-01f, 3.30027e-02f, -5.77544e-03f}, // 84/128 + {-6.18454e-03f, 3.67941e-02f, -1.39767e-01f, 7.93055e-01f, 4.01710e-01f, -1.12650e-01f, 3.24843e-02f, -5.68976e-03f}, // 85/128 + {-6.11236e-03f, 3.64044e-02f, -1.38644e-01f, 8.00757e-01f, 3.91596e-01f, -1.10593e-01f, 3.19495e-02f, -5.60103e-03f}, // 86/128 + {-6.03624e-03f, 3.59905e-02f, -1.37426e-01f, 8.08348e-01f, 3.81484e-01f, -1.08490e-01f, 3.13987e-02f, -5.50931e-03f}, // 87/128 + {-5.95620e-03f, 3.55525e-02f, -1.36111e-01f, 8.15826e-01f, 3.71376e-01f, -1.06342e-01f, 3.08323e-02f, -5.41467e-03f}, // 88/128 + {-5.87227e-03f, 3.50903e-02f, -1.34699e-01f, 8.23188e-01f, 3.61276e-01f, -1.04150e-01f, 3.02507e-02f, -5.31716e-03f}, // 89/128 + {-5.78446e-03f, 3.46042e-02f, -1.33189e-01f, 8.30432e-01f, 3.51186e-01f, -1.01915e-01f, 2.96543e-02f, -5.21683e-03f}, // 90/128 + {-5.69280e-03f, 3.40940e-02f, -1.31581e-01f, 8.37557e-01f, 3.41109e-01f, -9.96402e-02f, 2.90433e-02f, -5.11376e-03f}, // 91/128 + {-5.59731e-03f, 3.35600e-02f, -1.29874e-01f, 8.44559e-01f, 3.31048e-01f, -9.73254e-02f, 2.84182e-02f, -5.00800e-03f}, // 92/128 + {-5.49804e-03f, 3.30021e-02f, -1.28068e-01f, 8.51437e-01f, 3.21004e-01f, -9.49727e-02f, 2.77794e-02f, -4.89961e-03f}, // 93/128 + {-5.39500e-03f, 3.24205e-02f, -1.26161e-01f, 8.58189e-01f, 3.10980e-01f, -9.25834e-02f, 2.71272e-02f, -4.78866e-03f}, // 94/128 + {-5.28823e-03f, 3.18153e-02f, -1.24154e-01f, 8.64812e-01f, 3.00980e-01f, -9.01591e-02f, 2.64621e-02f, -4.67520e-03f}, // 95/128 + {-5.17776e-03f, 3.11866e-02f, -1.22047e-01f, 8.71305e-01f, 2.91006e-01f, -8.77011e-02f, 2.57844e-02f, -4.55932e-03f}, // 96/128 + {-5.06363e-03f, 3.05345e-02f, -1.19837e-01f, 8.77666e-01f, 2.81060e-01f, -8.52109e-02f, 2.50946e-02f, -4.44107e-03f}, // 97/128 + {-4.94589e-03f, 2.98593e-02f, -1.17526e-01f, 8.83893e-01f, 2.71144e-01f, -8.26900e-02f, 2.43930e-02f, -4.32052e-03f}, // 98/128 + {-4.82456e-03f, 2.91609e-02f, -1.15113e-01f, 8.89984e-01f, 2.61263e-01f, -8.01399e-02f, 2.36801e-02f, -4.19774e-03f}, // 99/128 + {-4.69970e-03f, 2.84397e-02f, -1.12597e-01f, 8.95936e-01f, 2.51417e-01f, -7.75620e-02f, 2.29562e-02f, -4.07279e-03f}, // 100/128 + {-4.57135e-03f, 2.76957e-02f, -1.09978e-01f, 9.01749e-01f, 2.41609e-01f, -7.49577e-02f, 2.22218e-02f, -3.94576e-03f}, // 101/128 + {-4.43955e-03f, 2.69293e-02f, -1.07256e-01f, 9.07420e-01f, 2.31843e-01f, -7.23286e-02f, 2.14774e-02f, -3.81671e-03f}, // 102/128 + {-4.30435e-03f, 2.61404e-02f, -1.04430e-01f, 9.12947e-01f, 2.22120e-01f, -6.96762e-02f, 2.07233e-02f, -3.68570e-03f}, // 103/128 + {-4.16581e-03f, 2.53295e-02f, -1.01501e-01f, 9.18329e-01f, 2.12443e-01f, -6.70018e-02f, 1.99599e-02f, -3.55283e-03f}, // 104/128 + {-4.02397e-03f, 2.44967e-02f, -9.84679e-02f, 9.23564e-01f, 2.02814e-01f, -6.43069e-02f, 1.91877e-02f, -3.41815e-03f}, // 105/128 + {-3.87888e-03f, 2.36423e-02f, -9.53307e-02f, 9.28650e-01f, 1.93236e-01f, -6.15931e-02f, 1.84071e-02f, -3.28174e-03f}, // 106/128 + {-3.73062e-03f, 2.27664e-02f, -9.20893e-02f, 9.33586e-01f, 1.83711e-01f, -5.88617e-02f, 1.76185e-02f, -3.14367e-03f}, // 107/128 + {-3.57923e-03f, 2.18695e-02f, -8.87435e-02f, 9.38371e-01f, 1.74242e-01f, -5.61142e-02f, 1.68225e-02f, -3.00403e-03f}, // 108/128 + {-3.42477e-03f, 2.09516e-02f, -8.52933e-02f, 9.43001e-01f, 1.64831e-01f, -5.33522e-02f, 1.60193e-02f, -2.86289e-03f}, // 109/128 + {-3.26730e-03f, 2.00132e-02f, -8.17385e-02f, 9.47477e-01f, 1.55480e-01f, -5.05770e-02f, 1.52095e-02f, -2.72032e-03f}, // 110/128 + {-3.10689e-03f, 1.90545e-02f, -7.80792e-02f, 9.51795e-01f, 1.46192e-01f, -4.77900e-02f, 1.43934e-02f, -2.57640e-03f}, // 111/128 + {-2.94361e-03f, 1.80759e-02f, -7.43154e-02f, 9.55956e-01f, 1.36968e-01f, -4.49929e-02f, 1.35716e-02f, -2.43121e-03f}, // 112/128 + {-2.77751e-03f, 1.70776e-02f, -7.04471e-02f, 9.59958e-01f, 1.27812e-01f, -4.21869e-02f, 1.27445e-02f, -2.28483e-03f}, // 113/128 + {-2.60868e-03f, 1.60599e-02f, -6.64743e-02f, 9.63798e-01f, 1.18725e-01f, -3.93735e-02f, 1.19125e-02f, -2.13733e-03f}, // 114/128 + {-2.43718e-03f, 1.50233e-02f, -6.23972e-02f, 9.67477e-01f, 1.09710e-01f, -3.65541e-02f, 1.10760e-02f, -1.98880e-03f}, // 115/128 + {-2.26307e-03f, 1.39681e-02f, -5.82159e-02f, 9.70992e-01f, 1.00769e-01f, -3.37303e-02f, 1.02356e-02f, -1.83931e-03f}, // 116/128 + {-2.08645e-03f, 1.28947e-02f, -5.39305e-02f, 9.74342e-01f, 9.19033e-02f, -3.09033e-02f, 9.39154e-03f, -1.68894e-03f}, // 117/128 + {-1.90738e-03f, 1.18034e-02f, -4.95412e-02f, 9.77526e-01f, 8.31162e-02f, -2.80746e-02f, 8.54441e-03f, -1.53777e-03f}, // 118/128 + {-1.72594e-03f, 1.06946e-02f, -4.50483e-02f, 9.80543e-01f, 7.44095e-02f, -2.52457e-02f, 7.69462e-03f, -1.38589e-03f}, // 119/128 + {-1.54221e-03f, 9.56876e-03f, -4.04519e-02f, 9.83392e-01f, 6.57852e-02f, -2.24178e-02f, 6.84261e-03f, -1.23337e-03f}, // 120/128 + {-1.35627e-03f, 8.42626e-03f, -3.57525e-02f, 9.86071e-01f, 5.72454e-02f, -1.95925e-02f, 5.98883e-03f, -1.08030e-03f}, // 121/128 + {-1.16820e-03f, 7.26755e-03f, -3.09503e-02f, 9.88580e-01f, 4.87921e-02f, -1.67710e-02f, 5.13372e-03f, -9.26747e-04f}, // 122/128 + {-9.78093e-04f, 6.09305e-03f, -2.60456e-02f, 9.90917e-01f, 4.04274e-02f, -1.39548e-02f, 4.27773e-03f, -7.72802e-04f}, // 123/128 + {-7.86031e-04f, 4.90322e-03f, -2.10389e-02f, 9.93082e-01f, 3.21531e-02f, -1.11453e-02f, 3.42130e-03f, -6.18544e-04f}, // 124/128 + {-5.92100e-04f, 3.69852e-03f, -1.59305e-02f, 9.95074e-01f, 2.39714e-02f, -8.34364e-03f, 2.56486e-03f, -4.64053e-04f}, // 125/128 + {-3.96391e-04f, 2.47942e-03f, -1.07209e-02f, 9.96891e-01f, 1.58840e-02f, -5.55134e-03f, 1.70888e-03f, -3.09412e-04f}, // 126/128 + {-1.98993e-04f, 1.24642e-03f, -5.41054e-03f, 9.98534e-01f, 7.89295e-03f, -2.76968e-03f, 8.53777e-04f, -1.54700e-04f}, // 127/128 + {0.00000e+00f, 0.00000e+00f, 0.00000e+00f, 1.00000e+00f, 0.00000e+00f, 0.00000e+00f, 0.00000e+00f, 0.00000e+00f}, // 128/128 + }; + /* Instrumentation Taps */ - private static final String INSTRUMENT_SYMBOL_SPREAD = "Tap Point: Symbol Spread (Goal=2.0)"; - private FloatTap mSymbolSpreadTap; + private static final String INSTRUMENT_SYMBOL_SPREAD = "Tap Point: Symbol Spread (Goal=2.0)"; + private FloatTap mSymbolSpreadTap; private List mAvailableTaps; - - private static final int NUMBER_FILTER_TAPS = 8; - private static final int NUMBER_FILTER_STEPS = 128; - - private static final int SAMPLE_RATE = 48000; - private static final int SYMBOL_RATE = 4800; - - /* Tracking loop gain constant */ - private static final double K_SYMBOL_SPREAD = 0.0100; - /* Constraints on symbol spreading */ - private static final float SYMBOL_SPREAD_MAX = 2.4f; // upper range limit: +20% - private static final float SYMBOL_SPREAD_MIN = 1.6f; // lower range limit: -20% + private static final int NUMBER_FILTER_TAPS = 8; + private static final int NUMBER_FILTER_STEPS = 128; - /* Symbol clock tracking loop gain */ - private static final double K_SYMBOL_TIMING = 0.025; - - /* Coarse and fine frequency tracking constants */ - private static final double K_COARSE_FREQUENCY = 0.00125; - private static final double K_FINE_FREQUENCY = 0.125; - - /* Frequency correction broadcast threshold */ + private static final int SAMPLE_RATE = 48000; + private static final int SYMBOL_RATE = 4800; + + /* Tracking loop gain constant */ + private static final double K_SYMBOL_SPREAD = 0.0100; + + /* Constraints on symbol spreading */ + private static final float SYMBOL_SPREAD_MAX = 2.4f; // upper range limit: +20% + private static final float SYMBOL_SPREAD_MIN = 1.6f; // lower range limit: -20% + + /* Symbol clock tracking loop gain */ + private static final double K_SYMBOL_TIMING = 0.025; + + /* Coarse and fine frequency tracking constants */ + private static final double K_COARSE_FREQUENCY = 0.00125; + private static final double K_FINE_FREQUENCY = 0.125; + + /* Frequency correction broadcast threshold */ // private static final double COARSE_FREQUENCY_DEADBAND = 1.66; - private static final double COARSE_FREQUENCY_THRESHOLD = 1.20; - - /* 2.0 symbol spread gives -3, -1, 1, 3 */ - private float mSymbolSpread = 2.0f; - private float mSymbolClock = 0.0f; - private float mSymbolTime = (float)SYMBOL_RATE / (float)SAMPLE_RATE; - - private float mFineFrequencyCorrection = 0.0f; - private float mCoarseFrequencyCorrection = 0.0f; + private static final double COARSE_FREQUENCY_THRESHOLD = 1.20; - private float mHistory[] = new float[ NUMBER_FILTER_TAPS ]; - private int mHistoryLast = 0; - - private RealSampleListener mListener; - - private DirectGainControl mGainController = new DirectGainControl( 15.0f, 0.1f, 35.0f, 0.3f ); + /* 2.0 symbol spread gives -3, -1, 1, 3 */ + private float mSymbolSpread = 2.0f; + private float mSymbolClock = 0.0f; + private float mSymbolTime = (float)SYMBOL_RATE / (float)SAMPLE_RATE; - private int mFrequencyAdjustmentRequested = 0; - private int mFrequencyCorrection = 0; - private int mFrequencyCorrectionMaximum = 3000; - private boolean mResetFrequencyTracker = false; - private FrequencyCorrectionProcessor mFrequencyCorrectionProcessor; - private Listener mFrequencyChangeListener; - - /** - * C4FM Symbol Filter - * - * Sample Gain values - the gain of incoming sample values will critically - * impact the performance of this filter. Gain should be adjusted to - * optimize the value of mSymbolSpread toward a value of 2.0. If the value - * is less than 2.0, then increase gain. If the value is over 2.0, then - * decrease gain. - * - * When preceded by the RealAutomaticGainFilter class, an optimal gain - * setting for this filter is 15.4 and that yields a symbol spread that - * is centered on 2.0, ranging 1.92 to 2.08. - */ - public C4FMSymbolFilter( int frequencyCorrectionMaximum ) - { - mFrequencyCorrectionMaximum = frequencyCorrectionMaximum; - } - - public void dispose() - { - mGainController = null; + private float mFineFrequencyCorrection = 0.0f; + private float mCoarseFrequencyCorrection = 0.0f; + + private float mHistory[] = new float[NUMBER_FILTER_TAPS]; + private int mHistoryLast = 0; + + private RealSampleListener mListener; + + private DirectGainControl mGainController = new DirectGainControl(15.0f, 0.1f, 35.0f, 0.3f); + + private int mFrequencyAdjustmentRequested = 0; + private int mFrequencyCorrection = 0; + private int mFrequencyCorrectionMaximum = 3000; + private boolean mResetFrequencyTracker = false; + private FrequencyCorrectionProcessor mFrequencyCorrectionProcessor; + private Listener mFrequencyChangeListener; + + /** + * C4FM Symbol Filter + * + * Sample Gain values - the gain of incoming sample values will critically + * impact the performance of this filter. Gain should be adjusted to + * optimize the value of mSymbolSpread toward a value of 2.0. If the value + * is less than 2.0, then increase gain. If the value is over 2.0, then + * decrease gain. + * + * When preceded by the RealAutomaticGainFilter class, an optimal gain + * setting for this filter is 15.4 and that yields a symbol spread that + * is centered on 2.0, ranging 1.92 to 2.08. + */ + public C4FMSymbolFilter(int frequencyCorrectionMaximum) + { + mFrequencyCorrectionMaximum = frequencyCorrectionMaximum; + } + + public void dispose() + { + mGainController = null; // mFrequencyCorrectionControl = null; - mListener = null; - } - - @Override - public void receive( RealBuffer buffer ) - { - for( float sample: buffer.getSamples() ) - { - receive( sample ); - } + mListener = null; + } + + @Override + public void receive(RealBuffer buffer) + { + for(float sample : buffer.getSamples()) + { + receive(sample); + } /* If a frequency correction was requested during the processing of this - * buffer, we'll apply the change and it will be reflected in the next + * buffer, we'll apply the change and it will be reflected in the next * arriving buffer. Reset the lock on frequency correction and reset * the internal frequency correction tracker */ - if( mFrequencyAdjustmentRequested != 0 ) - { - int correction = mFrequencyCorrection + mFrequencyAdjustmentRequested; - - if( correction > mFrequencyCorrectionMaximum ) - { - correction = mFrequencyCorrectionMaximum; - } - else if( correction < -mFrequencyCorrectionMaximum ) - { - correction = -mFrequencyCorrectionMaximum; - } + if(mFrequencyAdjustmentRequested != 0) + { + int correction = mFrequencyCorrection + mFrequencyAdjustmentRequested; - broadcast( new FrequencyChangeEvent(Event.REQUEST_CHANNEL_FREQUENCY_CORRECTION_CHANGE, correction ) ); + if(correction > mFrequencyCorrectionMaximum) + { + correction = mFrequencyCorrectionMaximum; + } + else if(correction < -mFrequencyCorrectionMaximum) + { + correction = -mFrequencyCorrectionMaximum; + } - mFrequencyAdjustmentRequested = 0; - } - } + broadcast(SourceEvent.channelFrequencyCorrectionRequest(correction)); - public void receive( float sample ) + mFrequencyAdjustmentRequested = 0; + } + } + + public void receive(float sample) { - sample = mGainController.correct( sample ); - - if( mResetFrequencyTracker ) - { - mCoarseFrequencyCorrection = 0.0f; - mFineFrequencyCorrection = 0.0f; - - broadcast( new FrequencyChangeEvent( - Event.REQUEST_CHANNEL_FREQUENCY_CORRECTION_CHANGE, 0 )); - - mResetFrequencyTracker = false; - } - - mSymbolClock += mSymbolTime; - - mHistory[ mHistoryLast++ ] = sample; - - mHistoryLast %= NUMBER_FILTER_TAPS; - - if( mSymbolClock > 1.0f ) - { - mSymbolClock -= 1.0f; - - int imu = (int)Math.floor( 0.5 + - ( (float)NUMBER_FILTER_STEPS * ( mSymbolClock / mSymbolTime ) ) ); - - if( imu >= NUMBER_FILTER_STEPS ) - { - imu = NUMBER_FILTER_STEPS - 1; - } + sample = mGainController.correct(sample); - int imu_p1 = imu + 1; + if(mResetFrequencyTracker) + { + mCoarseFrequencyCorrection = 0.0f; + mFineFrequencyCorrection = 0.0f; - int j = mHistoryLast; - - double interp = 0.0; - double interp_p1 = 0.0; - - for( int i = 0; i < NUMBER_FILTER_TAPS; i++ ) - { - interp += TAPS[ imu][ i ] * mHistory[ j ]; - interp_p1 += TAPS[ imu_p1 ][ i ] * mHistory[ j ]; - - j = ( j + 1 ) % NUMBER_FILTER_TAPS; - } + broadcast(SourceEvent.channelFrequencyCorrectionRequest(0l)); + + mResetFrequencyTracker = false; + } + + mSymbolClock += mSymbolTime; + + mHistory[mHistoryLast++] = sample; + + mHistoryLast %= NUMBER_FILTER_TAPS; + + if(mSymbolClock > 1.0f) + { + mSymbolClock -= 1.0f; + + int imu = (int)Math.floor(0.5 + + ((float)NUMBER_FILTER_STEPS * (mSymbolClock / mSymbolTime))); + + if(imu >= NUMBER_FILTER_STEPS) + { + imu = NUMBER_FILTER_STEPS - 1; + } + + int imu_p1 = imu + 1; + + int j = mHistoryLast; + + double interp = 0.0; + double interp_p1 = 0.0; + + for(int i = 0; i < NUMBER_FILTER_TAPS; i++) + { + interp += TAPS[imu][i] * mHistory[j]; + interp_p1 += TAPS[imu_p1][i] * mHistory[j]; + + j = (j + 1) % NUMBER_FILTER_TAPS; + } /* Output symbol will be interpolated value corrected for symbol * spread and frequency offset */ - interp -= mFineFrequencyCorrection; - interp_p1 -= mFineFrequencyCorrection; + interp -= mFineFrequencyCorrection; + interp_p1 -= mFineFrequencyCorrection; /* Correct output for symbol deviation (spread) */ - float output = (float)( 2.0 * interp / mSymbolSpread ); + float output = (float)(2.0 * interp / mSymbolSpread); /* Detect received symbol error: basically use a hard decision and * subtract off expected position nominal symbol level which will be * +/- 0.5 * symbol spread and +/- 1.5 symbol spread. Remember that * nominal symbol spread will be 2.0 */ - double symbolError; - - if( interp < -mSymbolSpread ) - { + double symbolError; + + if(interp < -mSymbolSpread) + { /* symbol is -3: Expected at -1.5 * symbol spread */ - symbolError = interp + ( 1.5 * mSymbolSpread ); - mSymbolSpread -= ( symbolError * 0.5 * K_SYMBOL_SPREAD ); - } - else if( interp < 0.0 ) - { + symbolError = interp + (1.5 * mSymbolSpread); + mSymbolSpread -= (symbolError * 0.5 * K_SYMBOL_SPREAD); + } + else if(interp < 0.0) + { /* symbol is -1: Expected at -0.5 * symbol_spread */ - symbolError = interp + (0.5 * mSymbolSpread ); - mSymbolSpread -= ( symbolError * K_SYMBOL_SPREAD ); - } - else if( interp < mSymbolSpread ) - { + symbolError = interp + (0.5 * mSymbolSpread); + mSymbolSpread -= (symbolError * K_SYMBOL_SPREAD); + } + else if(interp < mSymbolSpread) + { /* symbol is +1: Expected at +0.5 * symbol_spread */ - symbolError = interp - ( 0.5 * mSymbolSpread ); - mSymbolSpread += ( symbolError * K_SYMBOL_SPREAD ); - } - else - { + symbolError = interp - (0.5 * mSymbolSpread); + mSymbolSpread += (symbolError * K_SYMBOL_SPREAD); + } + else + { /* symbol is +3: Expected at +1.5 * symbol_spread */ - symbolError = interp - ( 1.5 * mSymbolSpread ); - mSymbolSpread += ( symbolError * 0.5 * K_SYMBOL_SPREAD ); - } + symbolError = interp - (1.5 * mSymbolSpread); + mSymbolSpread += (symbolError * 0.5 * K_SYMBOL_SPREAD); + } /* Symbol clock tracking loop adjustment */ - if( interp_p1 < interp ) - { - mSymbolClock += symbolError * K_SYMBOL_TIMING; - } - else - { - mSymbolClock -= symbolError * K_SYMBOL_TIMING; - } - - if( mSymbolSpread < SYMBOL_SPREAD_MIN ) - { - mGainController.increase(); - - mSymbolSpread = SYMBOL_SPREAD_MIN; - } - else if( mSymbolSpread > SYMBOL_SPREAD_MAX ) - { - mGainController.decrease(); + if(interp_p1 < interp) + { + mSymbolClock += symbolError * K_SYMBOL_TIMING; + } + else + { + mSymbolClock -= symbolError * K_SYMBOL_TIMING; + } - mSymbolSpread = SYMBOL_SPREAD_MAX; - } + if(mSymbolSpread < SYMBOL_SPREAD_MIN) + { + mGainController.increase(); - mCoarseFrequencyCorrection += ( ( mFineFrequencyCorrection - - mCoarseFrequencyCorrection ) * K_COARSE_FREQUENCY ); - - mFineFrequencyCorrection += ( symbolError * K_FINE_FREQUENCY ); + mSymbolSpread = SYMBOL_SPREAD_MIN; + } + else if(mSymbolSpread > SYMBOL_SPREAD_MAX) + { + mGainController.decrease(); + + mSymbolSpread = SYMBOL_SPREAD_MAX; + } + + mCoarseFrequencyCorrection += ((mFineFrequencyCorrection - + mCoarseFrequencyCorrection) * K_COARSE_FREQUENCY); + + mFineFrequencyCorrection += (symbolError * K_FINE_FREQUENCY); /* Queue a frequency adjustment (once per buffer) as needed */ - if( Math.abs( mCoarseFrequencyCorrection ) > COARSE_FREQUENCY_THRESHOLD ) - { - mFrequencyAdjustmentRequested = - 500 * ( mCoarseFrequencyCorrection > 0 ? 1 : -1 ); - } - - if( mSymbolSpreadTap != null ) - { - mSymbolSpreadTap.receive( mSymbolSpread ); - } + if(Math.abs(mCoarseFrequencyCorrection) > COARSE_FREQUENCY_THRESHOLD) + { + mFrequencyAdjustmentRequested = + 500 * (mCoarseFrequencyCorrection > 0 ? 1 : -1); + } + + if(mSymbolSpreadTap != null) + { + mSymbolSpreadTap.receive(mSymbolSpread); + } /* dispatch the interpolated value to the listener */ - if( mListener != null ) - { - mListener.receive( output ); - } - } + if(mListener != null) + { + mListener.receive(output); + } + } } - @Override - public void setListener( RealSampleListener listener ) + @Override + public void setListener(RealSampleListener listener) { - mListener = listener; + mListener = listener; } - @Override - public void removeListener( RealSampleListener listener ) + @Override + public void removeListener(RealSampleListener listener) { - mListener = null; + mListener = null; } - - @Override + + @Override public List getTapGroups() { - if( mAvailableTaps == null ) - { - mAvailableTaps = new ArrayList<>(); + if(mAvailableTaps == null) + { + mAvailableTaps = new ArrayList<>(); /* * Since the target is 2.0, we subtract 2.0 from the value so that * a good value is plotted right on the zero center line */ - TapGroup group = new TapGroup( "C4FM Symbol Filter" ); - - group.add( - new AdditiveFloatTap( INSTRUMENT_SYMBOL_SPREAD, 0, 0.1f, -2.0f ) ); - - mAvailableTaps.add( group ); - } - - return mAvailableTaps; + TapGroup group = new TapGroup("C4FM Symbol Filter"); + + group.add( + new AdditiveFloatTap(INSTRUMENT_SYMBOL_SPREAD, 0, 0.1f, -2.0f)); + + mAvailableTaps.add(group); + } + + return mAvailableTaps; } - @Override - public void registerTap( Tap tap ) + @Override + public void registerTap(Tap tap) { - if( tap.getName().contentEquals( INSTRUMENT_SYMBOL_SPREAD ) ) - { - mSymbolSpreadTap = (FloatTap)tap; - } + if(tap.getName().contentEquals(INSTRUMENT_SYMBOL_SPREAD)) + { + mSymbolSpreadTap = (FloatTap)tap; + } } - @Override - public void unregisterTap( Tap tap ) + @Override + public void unregisterTap(Tap tap) { - if( tap.getName().contentEquals( INSTRUMENT_SYMBOL_SPREAD ) ) - { - mSymbolSpreadTap = null; - } + if(tap.getName().contentEquals(INSTRUMENT_SYMBOL_SPREAD)) + { + mSymbolSpreadTap = null; + } } /** * Broadcasts a frequency change event to the registered listener + * * @param event */ - public void broadcast( FrequencyChangeEvent event ) - { - if( mFrequencyChangeListener != null ) - { - mFrequencyChangeListener.receive( event ); - } - } + public void broadcast(SourceEvent event) + { + if(mFrequencyChangeListener != null) + { + mFrequencyChangeListener.receive(event); + } + } /** * Sets the listener for frequency change events. + * * @param listener */ - public void setFrequencyChangeListener( Listener listener ) - { - mFrequencyChangeListener = listener; - } - - @Override - public Listener getFrequencyChangeListener() - { - if( mFrequencyCorrectionProcessor == null ) - { - mFrequencyCorrectionProcessor = new FrequencyCorrectionProcessor(); - } - - return mFrequencyCorrectionProcessor; - } + public void setFrequencyChangeListener(Listener listener) + { + mFrequencyChangeListener = listener; + } - /** - * Receives notifications that the channel frequency correction has been - * applied and updates internal tracking value. - */ - public class FrequencyCorrectionProcessor implements Listener - { - @Override - public void receive( FrequencyChangeEvent event ) - { - switch( event.getEvent() ) - { - case NOTIFICATION_CHANNEL_FREQUENCY_CORRECTION_CHANGE: - mFrequencyCorrection = event.getValue().intValue(); + @Override + public Listener getSourceEventListener() + { + if(mFrequencyCorrectionProcessor == null) + { + mFrequencyCorrectionProcessor = new FrequencyCorrectionProcessor(); + } - //Reset internal frequency tracking - mCoarseFrequencyCorrection = 0.0f; - mFineFrequencyCorrection = 0.0f; - break; - case NOTIFICATION_FREQUENCY_CORRECTION_CHANGE: - case NOTIFICATION_SAMPLE_RATE_CHANGE: - mResetFrequencyTracker = true; - break; - default: - break; - } - } - } + return mFrequencyCorrectionProcessor; + } + + /** + * Receives notifications that the channel frequency correction has been + * applied and updates internal tracking value. + */ + public class FrequencyCorrectionProcessor implements Listener + { + @Override + public void receive(SourceEvent event) + { + switch(event.getEvent()) + { + case NOTIFICATION_CHANNEL_FREQUENCY_CORRECTION_CHANGE: + mFrequencyCorrection = event.getValue().intValue(); + + //Reset internal frequency tracking + mCoarseFrequencyCorrection = 0.0f; + mFineFrequencyCorrection = 0.0f; + break; + case NOTIFICATION_FREQUENCY_CORRECTION_CHANGE: + case NOTIFICATION_SAMPLE_RATE_CHANGE: + mResetFrequencyTracker = true; + break; + default: + break; + } + } + } } diff --git a/src/module/decode/p25/P25Decoder.java b/src/module/decode/p25/P25Decoder.java index 0ca1f8a9..79ab8f7b 100644 --- a/src/module/decode/p25/P25Decoder.java +++ b/src/module/decode/p25/P25Decoder.java @@ -4,10 +4,10 @@ import alias.AliasList; import instrument.Instrumentable; import module.decode.Decoder; import module.decode.DecoderType; -import source.tuner.frequency.IFrequencyChangeListener; -import source.tuner.frequency.IFrequencyChangeProvider; +import source.ISourceEventListener; +import source.ISourceEventProvider; -public abstract class P25Decoder extends Decoder implements IFrequencyChangeListener, IFrequencyChangeProvider, Instrumentable +public abstract class P25Decoder extends Decoder implements ISourceEventListener, ISourceEventProvider, Instrumentable { private P25MessageProcessor mMessageProcessor; diff --git a/src/module/decode/p25/P25_C4FMDecoder.java b/src/module/decode/p25/P25_C4FMDecoder.java index 2e7b1114..cbae323d 100644 --- a/src/module/decode/p25/P25_C4FMDecoder.java +++ b/src/module/decode/p25/P25_C4FMDecoder.java @@ -21,7 +21,6 @@ package module.decode.p25; import instrument.tap.Tap; import instrument.tap.TapGroup; import instrument.tap.stream.DibitTap; -import instrument.tap.stream.FloatBufferTap; import instrument.tap.stream.FloatTap; import java.util.ArrayList; @@ -35,12 +34,7 @@ import alias.AliasList; import sample.Listener; import sample.real.IFilteredRealBufferListener; import sample.real.RealBuffer; -import source.tuner.frequency.FrequencyChangeEvent; -import source.tuner.frequency.IFrequencyChangeListener; -import source.tuner.frequency.IFrequencyChangeProvider; -import dsp.filter.FilterFactory; -import dsp.filter.Window.WindowType; -import dsp.filter.fir.real.RealFIRFilter_RB_RB; +import source.SourceEvent; public class P25_C4FMDecoder extends P25Decoder implements IFilteredRealBufferListener { @@ -222,7 +216,7 @@ public class P25_C4FMDecoder extends P25Decoder implements IFilteredRealBufferLi } @Override - public void setFrequencyChangeListener( Listener listener ) + public void setSourceEventListener(Listener listener ) { if( mSymbolFilter != null ) { @@ -231,7 +225,7 @@ public class P25_C4FMDecoder extends P25Decoder implements IFilteredRealBufferLi } @Override - public void removeFrequencyChangeListener() + public void removeSourceEventListener() { if( mSymbolFilter != null ) { @@ -240,11 +234,11 @@ public class P25_C4FMDecoder extends P25Decoder implements IFilteredRealBufferLi } @Override - public Listener getFrequencyChangeListener() + public Listener getSourceEventListener() { if( mSymbolFilter != null ) { - return mSymbolFilter.getFrequencyChangeListener(); + return mSymbolFilter.getSourceEventListener(); } return null; diff --git a/src/module/decode/p25/P25_LSMDecoder.java b/src/module/decode/p25/P25_LSMDecoder.java index 1b135eb5..3e30e9ae 100644 --- a/src/module/decode/p25/P25_LSMDecoder.java +++ b/src/module/decode/p25/P25_LSMDecoder.java @@ -42,7 +42,7 @@ import dsp.filter.fir.complex.ComplexFIRFilter_CB_CB; import dsp.gain.ComplexFeedForwardGainControl; import dsp.psk.LSMDemodulator; import dsp.psk.QPSKPolarSlicer; -import source.tuner.frequency.FrequencyChangeEvent; +import source.SourceEvent; public class P25_LSMDecoder extends P25Decoder implements IComplexBufferListener { @@ -109,22 +109,22 @@ public class P25_LSMDecoder extends P25Decoder implements IComplexBufferListener } @Override - public void setFrequencyChangeListener(Listener listener) + public void setSourceEventListener(Listener listener) { } @Override - public void removeFrequencyChangeListener() + public void removeSourceEventListener() { } @Override - public Listener getFrequencyChangeListener() + public Listener getSourceEventListener() { - return new Listener() + return new Listener() { @Override - public void receive(FrequencyChangeEvent frequencyChangeEvent) + public void receive(SourceEvent sourceEvent) { //Ignored } diff --git a/src/sample/SampleType.java b/src/sample/SampleType.java index 09e9196b..8aedd8e2 100644 --- a/src/sample/SampleType.java +++ b/src/sample/SampleType.java @@ -2,7 +2,6 @@ package sample; public enum SampleType { - COMPLEX, //Baseband - REAL, //Demodulated - COMPLEX_AND_REAL; //Baseband and Demodulated + COMPLEX, // I/Q + REAL; // Real } diff --git a/src/source/ComplexSource.java b/src/source/ComplexSource.java index e7e1939c..b864aae8 100644 --- a/src/source/ComplexSource.java +++ b/src/source/ComplexSource.java @@ -1,3 +1,21 @@ +/******************************************************************************* + * 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 source; import sample.Provider; @@ -6,8 +24,9 @@ import sample.complex.ComplexBuffer; public abstract class ComplexSource extends Source implements Provider { - public ComplexSource() - { - super( SampleType.COMPLEX ); - } + @Override + public SampleType getSampleType() + { + return SampleType.COMPLEX; + } } diff --git a/src/source/ISourceEventListener.java b/src/source/ISourceEventListener.java new file mode 100644 index 00000000..feb65ddb --- /dev/null +++ b/src/source/ISourceEventListener.java @@ -0,0 +1,35 @@ +/******************************************************************************* + * 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 source; + +import sample.Listener; + + +/** + * Interface for broadcasting changes to frequency, bandwidth, sample rate, + * and actual sample rate values to all registered listeners. + * + * Note: broadcasted frequency should be the uncorrected, or displayable + * frequency value. All frequency correction aspects should be handled within + * the device that implements the tuning of the frequency. + */ +public interface ISourceEventListener +{ + Listener getSourceEventListener(); +} diff --git a/src/source/ISourceEventProcessor.java b/src/source/ISourceEventProcessor.java new file mode 100644 index 00000000..45f74e20 --- /dev/null +++ b/src/source/ISourceEventProcessor.java @@ -0,0 +1,24 @@ +/******************************************************************************* + * 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 source; + +public interface ISourceEventProcessor +{ + void process(SourceEvent event) throws SourceException; +} diff --git a/src/source/tuner/frequency/IFrequencyChangeProvider.java b/src/source/ISourceEventProvider.java similarity index 79% rename from src/source/tuner/frequency/IFrequencyChangeProvider.java rename to src/source/ISourceEventProvider.java index 354e9af9..6e6a1bed 100644 --- a/src/source/tuner/frequency/IFrequencyChangeProvider.java +++ b/src/source/ISourceEventProvider.java @@ -16,15 +16,15 @@ * along with this program. If not, see * ******************************************************************************/ -package source.tuner.frequency; +package source; import sample.Listener; /** - * Provider of Frequency Change Events + * Provider of Source Change Events */ -public interface IFrequencyChangeProvider +public interface ISourceEventProvider { - void setFrequencyChangeListener( Listener listener ); - void removeFrequencyChangeListener(); + void setSourceEventListener(Listener listener ); + void removeSourceEventListener(); } diff --git a/src/source/RealSource.java b/src/source/RealSource.java index b512bfbc..8b08ebca 100644 --- a/src/source/RealSource.java +++ b/src/source/RealSource.java @@ -1,14 +1,32 @@ +/******************************************************************************* + * 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 source; import sample.Provider; import sample.SampleType; import sample.real.RealBuffer; - public abstract class RealSource extends Source implements Provider { - public RealSource() - { - super( SampleType.REAL ); - } + @Override + public SampleType getSampleType() + { + return SampleType.REAL; + } } diff --git a/src/source/Source.java b/src/source/Source.java index 932c2749..8df74669 100644 --- a/src/source/Source.java +++ b/src/source/Source.java @@ -20,38 +20,38 @@ package source; import module.Module; import sample.SampleType; import sample.real.IOverflowListener; -import source.tuner.frequency.IFrequencyChangeListener; -import source.tuner.frequency.IFrequencyChangeProvider; /** * Abstract class to define the minimum functionality of a sample data provider. */ -public abstract class Source extends Module implements IFrequencyChangeListener, IFrequencyChangeProvider +public abstract class Source extends Module implements ISourceEventListener, ISourceEventProvider { - protected SampleType mSampleType; protected IOverflowListener mOverflowListener; - public Source( SampleType sampleType ) - { - mSampleType = sampleType; - } - + /** + * Indicates the type of samples provided by this source: real or complex + */ + public abstract SampleType getSampleType(); + + /** + * Sample rate provided by this source + * + * @throws SourceException if there is an issue determining the sample rate for this source + */ public abstract int getSampleRate() throws SourceException; - + + /** + * Center frequency for this source in Hertz + * + * @throws SourceException if there is an issue in determining the center frequency for this source + */ public abstract long getFrequency() throws SourceException; - + + /** + * Process any cleanup actions to prepare for garbage collection of this source + */ public abstract void dispose(); - public SampleType getSampleType() - { - return mSampleType; - } - - public void setSampleType( SampleType sampleType ) - { - mSampleType = sampleType; - } - /** * Registers the listener to receive overflow state changes. Use null argument to clear the listener */ @@ -63,7 +63,7 @@ public abstract class Source extends Module implements IFrequencyChangeListener, /** * Broadcasts an overflow state * - * @param overflow true if overlow, false if normal + * @param overflow true if overflow, false if normal */ protected void broadcastOverflowState(boolean overflow) { diff --git a/src/source/SourceEvent.java b/src/source/SourceEvent.java new file mode 100644 index 00000000..5a9dd3b7 --- /dev/null +++ b/src/source/SourceEvent.java @@ -0,0 +1,192 @@ +/******************************************************************************* + * 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 source; + +public class SourceEvent +{ + public enum Event + { + NOTIFICATION_CHANNEL_FREQUENCY_CORRECTION_CHANGE, + NOTIFICATION_CHANNEL_SAMPLE_RATE_CHANGE, + NOTIFICATION_FREQUENCY_CHANGE, + NOTIFICATION_FREQUENCY_CORRECTION_CHANGE, + NOTIFICATION_POLYPHASE_CHANNEL_COUNT_CHANGE, + NOTIFICATION_SAMPLE_RATE_CHANGE, + + REQUEST_CHANNEL_FREQUENCY_CORRECTION_CHANGE, + REQUEST_FREQUENCY_CHANGE, + REQUEST_START_SAMPLE_STREAM, + REQUEST_STOP_SAMPLE_STREAM; + } + + private Event mEvent; + private Number mValue; + private Source mSource; + + /** + * Private constructor. Use the static constructor methods to create an event. + */ + private SourceEvent(Event event, Number value) + { + mEvent = event; + mValue = value; + } + + /** + * Private constructor. Use the static constructor methods to create an event. + */ + private SourceEvent(Event event, Source source) + { + mEvent = event; + mSource = source; + } + + /** + * Event description. + */ + public Event getEvent() + { + return mEvent; + } + + /** + * Value associated with the event. + */ + public Number getValue() + { + return mValue; + } + + public boolean hasValue() + { + return mValue != null; + } + + /** + * Source associated with the event + */ + public Source getSource() + { + return mSource; + } + + /** + * Indicates if this event contains an optional source + */ + public boolean hasSource() + { + return mSource != null; + } + + /** + * Creates a new frequency change event + * + * @param frequency in hertz + */ + public static SourceEvent frequencyChange(long frequency) + { + return new SourceEvent(Event.NOTIFICATION_FREQUENCY_CHANGE, frequency); + } + + /** + * Creates a new frequency correction change event + * + * @param frequencyCorrection in hertz + */ + public static SourceEvent frequencyCorrectionChange(long frequencyCorrection) + { + return new SourceEvent(Event.NOTIFICATION_FREQUENCY_CORRECTION_CHANGE, frequencyCorrection); + } + + /** + * Creates a new channel frequency correction change event + * + * @param frequencyCorrection in hertz + */ + public static SourceEvent channelFrequencyCorrectionChange(long frequencyCorrection) + { + return new SourceEvent(Event.NOTIFICATION_CHANNEL_FREQUENCY_CORRECTION_CHANGE, frequencyCorrection); + } + + /** + * Creates a new sample rate change event + * + * @param sampleRate in hertz + */ + public static SourceEvent sampleRateChange(int sampleRate) + { + return new SourceEvent(Event.NOTIFICATION_SAMPLE_RATE_CHANGE, sampleRate); + } + + /** + * Creates a new channel sample rate change event + * + * @param sampleRate in hertz + */ + public static SourceEvent channelSampleRateChange(int sampleRate) + { + return new SourceEvent(Event.NOTIFICATION_CHANNEL_SAMPLE_RATE_CHANGE, sampleRate); + } + + /** + * Creates a new channel count change event + * + * @param channelCount + */ + public static SourceEvent channelCountChange(int channelCount) + { + return new SourceEvent(Event.NOTIFICATION_POLYPHASE_CHANNEL_COUNT_CHANGE, channelCount); + } + + /** + * Creates a new frequency change request event + * + * @param frequency requested + */ + public static SourceEvent frequencyRequest(long frequency) + { + return new SourceEvent(Event.REQUEST_FREQUENCY_CHANGE, frequency); + } + + /** + * Creates a new channel frequency correction change request event + * + * @param frequency requested + */ + public static SourceEvent channelFrequencyCorrectionRequest(long frequency) + { + return new SourceEvent(Event.REQUEST_CHANNEL_FREQUENCY_CORRECTION_CHANGE, frequency); + } + + /** + * Creates a new start sample stream request event + */ + public static SourceEvent startSampleStream(Source source) + { + return new SourceEvent(Event.REQUEST_START_SAMPLE_STREAM, source); + } + + /** + * Creates a new stop sample stream request event + */ + public static SourceEvent stopSampleStream(Source source) + { + return new SourceEvent(Event.REQUEST_STOP_SAMPLE_STREAM, source); + } +} diff --git a/src/source/config/SourceConfigRecording.java b/src/source/config/SourceConfigRecording.java index 801fb211..f1b7c1c3 100644 --- a/src/source/config/SourceConfigRecording.java +++ b/src/source/config/SourceConfigRecording.java @@ -1,70 +1,70 @@ /******************************************************************************* - * SDR Trunk - * Copyright (C) 2014 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 + * 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 source.config; -import javax.xml.bind.annotation.XmlAttribute; - import source.SourceType; import source.tuner.TunerChannel; -import source.tuner.TunerChannel.Type; + +import javax.xml.bind.annotation.XmlAttribute; public class SourceConfigRecording extends SourceConfiguration { - private String mRecordingAlias; - private long mFrequency; - - public SourceConfigRecording() + private String mRecordingAlias; + private long mFrequency; + + public SourceConfigRecording() { - super( SourceType.RECORDING ); + super(SourceType.RECORDING); } - - @XmlAttribute( name = "recording_alias" ) - public String getRecordingAlias() - { - return mRecordingAlias; - } - - public void setRecordingAlias( String alias ) - { - mRecordingAlias = alias; - } - @XmlAttribute( name = "frequency" ) - public long getFrequency() - { - return mFrequency; - } - - public void setFrequency( long frequency ) - { - mFrequency = frequency; - } + @XmlAttribute(name = "recording_alias") + public String getRecordingAlias() + { + return mRecordingAlias; + } - @Override + public void setRecordingAlias(String alias) + { + mRecordingAlias = alias; + } + + @XmlAttribute(name = "frequency") + public long getFrequency() + { + return mFrequency; + } + + public void setFrequency(long frequency) + { + mFrequency = frequency; + } + + @Override public String getDescription() { - return getRecordingAlias(); + return getRecordingAlias(); } - - public TunerChannel getTunerChannel() - { - /* Create a tuner channel with a default bandwidth of 15 kHz */ - return new TunerChannel( Type.LOCKED, mFrequency, 15000 ); - } - + + public TunerChannel getTunerChannel() + { + /* Create a tuner channel with a default bandwidth of 12.5 kHz */ + return new TunerChannel(mFrequency, 12500); + } + } diff --git a/src/source/config/SourceConfigTuner.java b/src/source/config/SourceConfigTuner.java index d6d9a8d4..2f31457d 100644 --- a/src/source/config/SourceConfigTuner.java +++ b/src/source/config/SourceConfigTuner.java @@ -1,69 +1,68 @@ /******************************************************************************* - * SDR Trunk - * Copyright (C) 2014-2016 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 + * 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 source.config; -import java.text.DecimalFormat; - -import javax.xml.bind.annotation.XmlAttribute; - import source.SourceType; import source.tuner.TunerChannel; -import source.tuner.TunerChannel.Type; + +import javax.xml.bind.annotation.XmlAttribute; +import java.text.DecimalFormat; public class SourceConfigTuner extends SourceConfiguration { - private static DecimalFormat sFORMAT = new DecimalFormat( "0.00000" ); + private static DecimalFormat FREQUENCY_FORMAT = new DecimalFormat("0.00000"); - private long mFrequency = 0; - private int mBandwidth = 12500; - - public SourceConfigTuner() + private long mFrequency = 0; + private int mBandwidth = 12500; + + public SourceConfigTuner() { - super( SourceType.TUNER ); + super(SourceType.TUNER); } - - public SourceConfigTuner( TunerChannel tunerChannel ) - { - this(); - - mFrequency = tunerChannel.getFrequency(); - mBandwidth = tunerChannel.getBandwidth(); - } - - @XmlAttribute( name = "frequency" ) - public long getFrequency() - { - return mFrequency; - } - - public void setFrequency( long frequency ) - { - mFrequency = frequency; - } - @Override + public SourceConfigTuner(TunerChannel tunerChannel) + { + this(); + + mFrequency = tunerChannel.getFrequency(); + mBandwidth = tunerChannel.getBandwidth(); + } + + @XmlAttribute(name = "frequency") + public long getFrequency() + { + return mFrequency; + } + + public void setFrequency(long frequency) + { + mFrequency = frequency; + } + + @Override public String getDescription() { - return sFORMAT.format( (double)mFrequency / 1000000.0d ) + " MHz"; + return FREQUENCY_FORMAT.format((double)mFrequency / 1000000.0d) + " MHz"; + } + + public TunerChannel getTunerChannel() + { + return new TunerChannel(mFrequency, mBandwidth); } - - public TunerChannel getTunerChannel() - { - return new TunerChannel( Type.LOCKED, mFrequency, mBandwidth ); - } } diff --git a/src/source/mixer/RealMixerSource.java b/src/source/mixer/RealMixerSource.java index cfc96997..c92a9a5a 100644 --- a/src/source/mixer/RealMixerSource.java +++ b/src/source/mixer/RealMixerSource.java @@ -36,7 +36,7 @@ import sample.real.RealBuffer; import source.RealSource; import source.SourceException; import dsp.gain.AutomaticGainControl; -import source.tuner.frequency.FrequencyChangeEvent; +import source.SourceEvent; public class RealMixerSource extends RealSource { @@ -80,19 +80,19 @@ public class RealMixerSource extends RealSource } @Override - public void setFrequencyChangeListener(Listener listener) + public void setSourceEventListener(Listener listener) { //Not implemented } @Override - public void removeFrequencyChangeListener() + public void removeSourceEventListener() { //Not implemented } @Override - public Listener getFrequencyChangeListener() + public Listener getSourceEventListener() { //Not implemented return null; diff --git a/src/source/recording/Recording.java b/src/source/recording/Recording.java index 9a228fc0..a89f77eb 100644 --- a/src/source/recording/Recording.java +++ b/src/source/recording/Recording.java @@ -9,12 +9,12 @@ import source.SourceException; import source.tuner.ITunerChannelProvider; import source.tuner.TunerChannel; import source.tuner.TunerChannelSource; -import source.tuner.frequency.FrequencyChangeEvent; -import source.tuner.frequency.FrequencyChangeEvent.Event; -import source.tuner.frequency.IFrequencyChangeProcessor; +import source.SourceEvent; +import source.SourceEvent.Event; +import source.ISourceEventProcessor; public class Recording implements Comparable, - IFrequencyChangeProcessor, + ISourceEventProcessor, ITunerChannelProvider { private RecordingConfiguration mConfiguration; @@ -89,7 +89,7 @@ public class Recording implements Comparable, } @Override - public void frequencyChanged( FrequencyChangeEvent event ) throws SourceException + public void process(SourceEvent event ) throws SourceException { if( event.getEvent() == Event.NOTIFICATION_FREQUENCY_CHANGE ) { @@ -103,7 +103,7 @@ public class Recording implements Comparable, for( TunerChannelSource channel: mTunerChannels ) { - channel.frequencyChanged( event ); + channel.process( event ); } } } diff --git a/src/source/tuner/ComplexChannelSource.java b/src/source/tuner/ComplexChannelSource.java new file mode 100644 index 00000000..f505980f --- /dev/null +++ b/src/source/tuner/ComplexChannelSource.java @@ -0,0 +1,169 @@ +/******************************************************************************* + * 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 source.tuner; + +import org.slf4j.Logger; +import org.slf4j.LoggerFactory; +import sample.Listener; +import sample.complex.ComplexBuffer; +import source.ComplexSource; +import source.ISourceEventProcessor; +import source.SourceEvent; +import source.SourceException; + +import java.util.concurrent.ScheduledExecutorService; + +/** + * Complex source that provides a complex sample stream at the channel's required sample rate + */ +public abstract class ComplexChannelSource extends ComplexSource implements ISourceEventProcessor +{ + private final static Logger mLog = LoggerFactory.getLogger(ComplexChannelSource.class); + private SourceEventProcessor mSourceEventProcessor = new SourceEventProcessor(); + private Listener mSourceEventListener; + private Listener mComplexBufferListener; + private ISourceEventProcessor mUpstreamSourceEventProcessor; + + public ComplexChannelSource(ISourceEventProcessor upstreamSourceEventProcessor) + { + mUpstreamSourceEventProcessor = upstreamSourceEventProcessor; + } + + /** + * Sets the source event listener to receive source events from this source + */ + @Override + public void setSourceEventListener(Listener listener) + { + mSourceEventListener = listener; + } + + /** + * Removes the source event listener from receiving source events from this source + */ + @Override + public void removeSourceEventListener() + { + mSourceEventListener = null; + } + + /** + * Broadcasts the source event to the registered source event listener + */ + protected void broadcast(SourceEvent sourceEvent) + { + if(mSourceEventListener != null) + { + mSourceEventListener.receive(sourceEvent); + } + } + + /** + * Registers the listener to receive complex sample buffers from this channel source + */ + @Override + public void setListener(Listener listener) + { + mComplexBufferListener = listener; + } + + @Override + public void removeListener(Listener listener) + { + mComplexBufferListener = null; + } + + /** + * Dispatches the processed complex buffer to the downstream consumer. + * @param complexBuffer containing complex samples + */ + protected void broadcast(ComplexBuffer complexBuffer) + { + if(mComplexBufferListener != null) + { + mComplexBufferListener.receive(complexBuffer); + } + } + + @Override + public void start(ScheduledExecutorService executor) + { + //TODO: start a runnable to process downstream buffer dispatching + + if(mUpstreamSourceEventProcessor != null) + { + try + { + mUpstreamSourceEventProcessor.process(SourceEvent.startSampleStream(ComplexChannelSource.this)); + } + catch(SourceException se) + { + mLog.error("Error starting complex channel source"); + } + } + } + + @Override + public void stop() + { + if(mUpstreamSourceEventProcessor != null) + { + try + { + mUpstreamSourceEventProcessor.process(SourceEvent.stopSampleStream(ComplexChannelSource.this)); + } + catch(SourceException se) + { + mLog.error("Error starting complex channel source"); + } + } + + //TODO: cancel a runnable to process downstream buffer dispatching + } + + + /** + * Source Event processor interface for this source to receive source events + */ + @Override + public Listener getSourceEventListener() + { + return mSourceEventProcessor; + } + + /** + * Adapter for implementing ISourceEventProcessor and dispatching source events to subclasses via the + * ISourceEventProcessor.process() method. + */ + public class SourceEventProcessor implements Listener + { + @Override + public void receive(SourceEvent sourceEvent) + { + try + { + process(sourceEvent); + } + catch(SourceException se) + { + mLog.error("Error processing source event", se); + } + } + } +} diff --git a/src/source/tuner/Tuner.java b/src/source/tuner/Tuner.java index cd6a3e4a..730a0bab 100644 --- a/src/source/tuner/Tuner.java +++ b/src/source/tuner/Tuner.java @@ -1,213 +1,239 @@ /******************************************************************************* - * SDR Trunk - * Copyright (C) 2014-2016 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 + * 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 source.tuner; +import org.slf4j.Logger; +import org.slf4j.LoggerFactory; +import sample.Broadcaster; +import sample.Listener; +import sample.complex.ComplexBuffer; +import source.ISourceEventProcessor; +import source.SourceEvent; +import source.SourceException; + import java.util.ArrayList; import java.util.List; import java.util.concurrent.CopyOnWriteArrayList; import java.util.concurrent.RejectedExecutionException; -import org.slf4j.Logger; -import org.slf4j.LoggerFactory; - -import sample.Listener; -import sample.complex.ComplexBuffer; -import source.SourceException; -import source.tuner.frequency.FrequencyChangeEvent; -import source.tuner.frequency.IFrequencyChangeProcessor; - /** * Tuner - provides tuner channel sources, representing a channel frequency */ public abstract class Tuner implements ITunerChannelProvider { - private final static Logger mLog = LoggerFactory.getLogger( Tuner.class ); - - private String mName; - private TunerController mTunerController; - /** - * Sample Listeners - these will typically be the DFT processor for spectral - * display, or it will be one or more tuner channel sources - */ - protected List> mSampleListeners = - new CopyOnWriteArrayList<>(); - - protected List> mTunerChangeListeners = - new ArrayList<>(); - - public Tuner( String name, TunerController tunerController ) - { - mName = name; - mTunerController = tunerController; + private final static Logger mLog = LoggerFactory.getLogger(Tuner.class); - //Rebroadcast frequency and sample rate change events as tuner events - mTunerController.addListener( new IFrequencyChangeProcessor() - { - @Override - public void frequencyChanged( FrequencyChangeEvent event ) - { - switch( event.getEvent() ) - { - case NOTIFICATION_FREQUENCY_CHANGE: - broadcast( new TunerEvent( Tuner.this, - source.tuner.TunerEvent.Event.FREQUENCY )); - break; - case NOTIFICATION_SAMPLE_RATE_CHANGE: - broadcast( new TunerEvent( Tuner.this, - source.tuner.TunerEvent.Event.SAMPLE_RATE )); - break; - default: - break; - } - } - } ); - } - - public TunerController getTunerController() - { - return mTunerController; - } - - public String toString() - { - return mName; - } - - public void dispose() - { - mSampleListeners.clear(); - } - - public void setName( String name ) - { - mName = name; - } - - /** - * Unique identifier for this tuner, used to lookup a tuner configuration - * from the settings manager. - * - * @return - unique identifier like a serial number, or a usb bus location - * or ip address and port. Return some form of unique identification that - * allows this tuner to be identified from among several of the same types - * of tuners. - */ - public abstract String getUniqueID(); + private String mName; + private TunerController mTunerController; + /** + * Sample Listeners - these will typically be the DFT processor for spectral + * display, or it will be one or more tuner channel sources + */ + protected List> mSampleListeners = new CopyOnWriteArrayList<>(); + protected List> mTunerChangeListeners = new ArrayList<>(); + protected Broadcaster mSourceEventBroadcaster = new Broadcaster<>(); - /** - * @return - tuner class enum entry - */ - public abstract TunerClass getTunerClass(); - - /** - * @return - tuner type enum entry - */ - public abstract TunerType getTunerType(); - - /** - * Name of this tuner object - * @return - string name of this tuner object - */ - public String getName() - { - return mName; - } - - /** - * Sample size in bits - */ - public abstract double getSampleSize(); - - /** - * Returns a tuner frequency channel source, tuned to the correct frequency - * - * Note: ensure the concrete implementation registers the returned - * tuner channel source as a listener on the tuner, to receive frequency - * change events. - * - * @param frequency - desired frequency - * - * @return - source for 48k sample rate - */ - public abstract TunerChannelSource getChannel( TunerChannel channel ) - throws RejectedExecutionException, SourceException; - - /** - * Releases the tuned channel resources - * - * Note: ensure the concrete implementation unregisters the tuner channel - * source from the tuner as a frequency change listener - * - * @param channel - previously obtained tuner channel - */ - public abstract void releaseChannel( TunerChannelSource source ); - - /** - * Registers the listener to receive complex float sample arrays - */ - public void addListener( Listener listener ) + public Tuner(String name, TunerController tunerController) { - mSampleListeners.add( listener ); + mName = name; + mTunerController = tunerController; + + //Rebroadcast frequency and sample rate change events as tuner events + mTunerController.addListener(new ISourceEventProcessor() + { + @Override + public void process(SourceEvent event) + { + switch(event.getEvent()) + { + case NOTIFICATION_FREQUENCY_CHANGE: + broadcast(new TunerEvent(Tuner.this, + source.tuner.TunerEvent.Event.FREQUENCY)); + break; + case NOTIFICATION_SAMPLE_RATE_CHANGE: + broadcast(new TunerEvent(Tuner.this, + source.tuner.TunerEvent.Event.SAMPLE_RATE)); + break; + default: + break; + } + } + }); } - /** - * Removes the registered listener - */ - public void removeListener( Listener listener ) + public TunerController getTunerController() { - mSampleListeners.remove( listener ); + return mTunerController; + } + + public String toString() + { + return mName; + } + + public void dispose() + { + mSampleListeners.clear(); + } + + public void setName(String name) + { + mName = name; + } + + /** + * Unique identifier for this tuner, used to lookup a tuner configuration + * from the settings manager. + * + * @return - unique identifier like a serial number, or a usb bus location + * or ip address and port. Return some form of unique identification that + * allows this tuner to be identified from among several of the same types + * of tuners. + */ + public abstract String getUniqueID(); + + /** + * @return - tuner class enum entry + */ + public abstract TunerClass getTunerClass(); + + /** + * @return - tuner type enum entry + */ + public abstract TunerType getTunerType(); + + /** + * Name of this tuner object + * + * @return - string name of this tuner object + */ + public String getName() + { + return mName; + } + + /** + * Sample size in bits + */ + public abstract double getSampleSize(); + + /** + * Returns a tuner frequency channel source, tuned to the correct frequency + * + * Note: ensure the concrete implementation registers the returned + * tuner channel source as a listener on the tuner, to receive frequency + * change events. + * + * @param channel - desired frequency and bandwidth + * @return - source for 48k sample rate + */ + public abstract TunerChannelSource getChannel(TunerChannel channel) + throws RejectedExecutionException, SourceException; + + /** + * Releases the tuned channel resources + * + * Note: ensure the concrete implementation unregisters the tuner channel + * source from the tuner as a frequency change listener + * + * @param source - previously obtained tuner channel + */ + public abstract void releaseChannel(TunerChannelSource source); + + /** + * Registers the listener to receive complex float sample arrays + */ + public void addListener(Listener listener) + { + mSampleListeners.add(listener); + } + + /** + * Removes the registered listener + */ + public void removeListener(Listener listener) + { + mSampleListeners.remove(listener); } /** * Broadcasts the samples to all registered listeners */ - public void broadcast( ComplexBuffer sampleBuffer ) + public void broadcast(ComplexBuffer sampleBuffer) { - for( Listener listener: mSampleListeners ) - { - listener.receive( sampleBuffer ); - } + for(Listener listener : mSampleListeners) + { + listener.receive(sampleBuffer); + } } - /** - * Registers the listener - */ - public void addTunerChangeListener( Listener listener ) + /** + * Registers the listener + */ + public void addTunerChangeListener(Listener listener) { - mTunerChangeListeners.add( listener ); + mTunerChangeListeners.add(listener); } - /** - * Removes the registered listener - */ - public void removeTunerChangeListener( Listener listener ) + /** + * Removes the registered listener + */ + public void removeTunerChangeListener(Listener listener) { - mTunerChangeListeners.remove( listener ); + mTunerChangeListeners.remove(listener); } /** * Broadcasts the tuner change event */ - public void broadcast( TunerEvent event ) + public void broadcast(TunerEvent tunerEvent) { - for( Listener listener: mTunerChangeListeners ) - { - listener.receive( event ); - } + for(Listener listener : mTunerChangeListeners) + { + listener.receive(tunerEvent); + } + + //Re-broadcast frequency and sample rate change events as source events + switch(tunerEvent.getEvent()) + { + case FREQUENCY: + mSourceEventBroadcaster.broadcast(SourceEvent.frequencyChange(getTunerController().getFrequency())); + break; + case SAMPLE_RATE: + mSourceEventBroadcaster.broadcast(SourceEvent.sampleRateChange(getTunerController().getSampleRate())); + break; + } + } + + /** + * Adds the listener to receive source events + */ + public void addSourceEventListener(Listener listener) + { + mSourceEventBroadcaster.addListener(listener); + } + + /** + * Removes the listener from receiving source events + */ + public void removeSourceEventListener(Listener listener) + { + mSourceEventBroadcaster.removeListener(listener); } } diff --git a/src/source/tuner/TunerChannel.java b/src/source/tuner/TunerChannel.java index 568e5424..f2b383b3 100644 --- a/src/source/tuner/TunerChannel.java +++ b/src/source/tuner/TunerChannel.java @@ -1,122 +1,93 @@ /******************************************************************************* - * SDR Trunk - * Copyright (C) 2014,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 + * 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 source.tuner; public class TunerChannel implements Comparable { - private Type mType; - private long mFrequency; - private int mBandwidth; - - public TunerChannel( Type type, long frequency, int bandwidth ) - { - mType = type; - mFrequency = frequency; - mBandwidth = bandwidth; - } - - public Type getType() - { - return mType; - } - - public void setType( Type type ) - { - mType = type; - } + private long mFrequency; + private int mBandwidth; - public long getFrequency() - { - return mFrequency; - } - - public void setFrequency( long frequency ) - { - mFrequency = frequency; - } - - public int getBandwidth() - { - return mBandwidth; - } - - public void setBandwidth( int bandwidth ) - { - mBandwidth = bandwidth; - } - - public long getMinFrequency() - { - return mFrequency - ( mBandwidth / 2 ); - } - - public long getMaxFrequency() - { - return mFrequency + ( mBandwidth / 2 ); - } - - /** - * Indicates if any part of this tuner channel overlaps the specified range - */ - public boolean overlaps( long minimum, long maximum ) - { - long channelMin = getMinFrequency(); - long channelMax = getMaxFrequency(); - - return ( ( minimum <= channelMin && channelMin <= maximum ) || - ( minimum <= channelMax && channelMax <= maximum ) || - ( minimum <= channelMin && channelMax <= maximum ) || - ( channelMin <= minimum && maximum <= channelMax ) ); - } - - public String toString() - { - StringBuilder sb = new StringBuilder(); - - sb.append( "Channel: " ); - sb.append( getMinFrequency() ); - sb.append( "-" ); - sb.append( getMaxFrequency() ); - - return sb.toString(); - } - - @Override - public int compareTo( TunerChannel otherLock ) + public TunerChannel(long frequency, int bandwidth) { - if( this.equals( otherLock ) ) - { - return 0; - } - else if( mFrequency < otherLock.getFrequency() ) - { - return -1; - } - else - { - return 1; - } + mFrequency = frequency; + mBandwidth = bandwidth; + } + + public long getFrequency() + { + return mFrequency; + } + + public void setFrequency(long frequency) + { + mFrequency = frequency; + } + + public int getBandwidth() + { + return mBandwidth; + } + + public void setBandwidth(int bandwidth) + { + mBandwidth = bandwidth; + } + + public long getMinFrequency() + { + return mFrequency - (mBandwidth / 2); + } + + public long getMaxFrequency() + { + return mFrequency + (mBandwidth / 2); + } + + /** + * Indicates if any part of this tuner channel overlaps the specified range + */ + public boolean overlaps(long minimum, long maximum) + { + long channelMin = getMinFrequency(); + long channelMax = getMaxFrequency(); + + return ((minimum <= channelMin && channelMin <= maximum) || + (minimum <= channelMax && channelMax <= maximum) || + (minimum <= channelMin && channelMax <= maximum) || + (channelMin <= minimum && maximum <= channelMax)); + } + + public String toString() + { + StringBuilder sb = new StringBuilder(); + + sb.append("Channel: "); + sb.append(getMinFrequency()); + sb.append("-"); + sb.append(getMaxFrequency()); + + return sb.toString(); + } + + @Override + public int compareTo(TunerChannel otherLock) + { + return Long.compare(getFrequency(), otherLock.getFrequency()); } - - public enum Type - { - LOCKED, //Use for decoding channels, so that they cannot be tuned out of range - TRAFFIC, //Like locked, but for temporary allocated traffic channels - TUNABLE; //Tunable -- can be tuned out of range - } } diff --git a/src/source/tuner/TunerChannelSource.java b/src/source/tuner/TunerChannelSource.java index 7556e7b5..a4d403de 100644 --- a/src/source/tuner/TunerChannelSource.java +++ b/src/source/tuner/TunerChannelSource.java @@ -32,11 +32,10 @@ import sample.complex.ComplexBuffer; import sample.real.IOverflowListener; import source.ComplexSource; import source.SourceException; -import source.tuner.frequency.FrequencyChangeEvent; -import source.tuner.frequency.FrequencyChangeEvent.Event; -import source.tuner.frequency.IFrequencyChangeListener; -import source.tuner.frequency.IFrequencyChangeProcessor; -import source.tuner.frequency.IFrequencyChangeProvider; +import source.ISourceEventListener; +import source.ISourceEventProvider; +import source.SourceEvent; +import source.ISourceEventProcessor; import java.util.ArrayList; import java.util.List; @@ -46,7 +45,7 @@ import java.util.concurrent.ScheduledFuture; import java.util.concurrent.TimeUnit; import java.util.concurrent.atomic.AtomicBoolean; -public class TunerChannelSource extends ComplexSource implements IFrequencyChangeProcessor, Listener +public class TunerChannelSource extends ComplexSource implements ISourceEventProcessor, Listener { private final static Logger mLog = LoggerFactory.getLogger(TunerChannelSource.class); @@ -66,7 +65,7 @@ public class TunerChannelSource extends ComplexSource implements IFrequencyChang private Oscillator mMixer; private ComplexPrimeCICDecimate mDecimationFilter; private Listener mListener; - private IFrequencyChangeProcessor mFrequencyChangeProcessor; + private ISourceEventProcessor mFrequencyChangeProcessor; private DownstreamProcessor mDownstreamFrequencyEventProcessor = new DownstreamProcessor(); private ScheduledFuture mTaskHandle; @@ -100,7 +99,7 @@ public class TunerChannelSource extends ComplexSource implements IFrequencyChang { mTuner = tuner; mTunerChannel = tunerChannel; - mTuner.getTunerController().addListener((IFrequencyChangeProcessor) this); + mTuner.getTunerController().addListener((ISourceEventProcessor) this); mTunerFrequency = mTuner.getTunerController().getFrequency(); mBuffer = new OverflowableTransferQueue<>(BUFFER_MAX_CAPACITY, BUFFER_OVERFLOW_RESET_THRESHOLD); @@ -111,8 +110,7 @@ public class TunerChannelSource extends ComplexSource implements IFrequencyChang mMixer = new Oscillator(frequencyOffset, mTuner.getTunerController().getSampleRate()); /* Fire a sample rate change event to setup the decimation chain */ - frequencyChanged(new FrequencyChangeEvent(Event.NOTIFICATION_SAMPLE_RATE_CHANGE, - mTuner.getTunerController().getSampleRate())); + process(SourceEvent.sampleRateChange(mTuner.getTunerController().getSampleRate())); } /** @@ -202,7 +200,7 @@ public class TunerChannelSource extends ComplexSource implements IFrequencyChang updateMixerFrequencyOffset(); mDownstreamFrequencyEventProcessor.broadcast( - new FrequencyChangeEvent( Event.NOTIFICATION_CHANNEL_FREQUENCY_CORRECTION_CHANGE, mChannelFrequencyCorrection)); + SourceEvent.channelFrequencyCorrectionChange(mChannelFrequencyCorrection)); } public Tuner getTuner() @@ -224,7 +222,7 @@ public class TunerChannelSource extends ComplexSource implements IFrequencyChang } } - public void setFrequencyChangeListener(IFrequencyChangeProcessor processor) + public void setFrequencyChangeListener(ISourceEventProcessor processor) { mFrequencyChangeProcessor = processor; } @@ -250,12 +248,12 @@ public class TunerChannelSource extends ComplexSource implements IFrequencyChang * frequency correction events received from the channel consumer/listener */ @Override - public void frequencyChanged(FrequencyChangeEvent event) throws SourceException + public void process(SourceEvent event) throws SourceException { // Echo the event to the registered event listener if(mFrequencyChangeProcessor != null) { - mFrequencyChangeProcessor.frequencyChanged(event); + mFrequencyChangeProcessor.process(event); } switch(event.getEvent()) @@ -320,36 +318,36 @@ public class TunerChannelSource extends ComplexSource implements IFrequencyChang } /** - * Implements IFrequencyChangeProvider to enable this source to broadcast frequency change events to downstream + * Implements ISourceEventProvider to enable this source to broadcast frequency change events to downstream * listeners. * * @param listener to receive downstream events */ @Override - public void setFrequencyChangeListener(Listener listener) + public void setSourceEventListener(Listener listener) { - mDownstreamFrequencyEventProcessor.setFrequencyChangeListener(listener); + mDownstreamFrequencyEventProcessor.setSourceEventListener(listener); } /** - * Implements IFrequencyChangeProvider to remove the frequency change listener from receiving down-stream frequency + * Implements ISourceEventProvider to remove the frequency change listener from receiving down-stream frequency * change events. */ @Override - public void removeFrequencyChangeListener() + public void removeSourceEventListener() { - mDownstreamFrequencyEventProcessor.removeFrequencyChangeListener(); + mDownstreamFrequencyEventProcessor.removeSourceEventListener(); } /** - * Implements IFrequencyChangeListener to receive frequency change events containing requests from downstream + * Implements ISourceEventListener to receive frequency change events containing requests from downstream * listeners to change frequency values. * @return listener */ @Override - public Listener getFrequencyChangeListener() + public Listener getSourceEventListener() { - return mDownstreamFrequencyEventProcessor.getFrequencyChangeListener(); + return mDownstreamFrequencyEventProcessor.getSourceEventListener(); } /** @@ -358,17 +356,17 @@ public class TunerChannelSource extends ComplexSource implements IFrequencyChang * notified of any frequency or sample rate change events and will also be able to request frequency correction * updates. */ - public class DownstreamProcessor implements IFrequencyChangeListener, IFrequencyChangeProvider, - Listener + public class DownstreamProcessor implements ISourceEventListener, ISourceEventProvider, + Listener { //Listener to receive downstream events - private Listener mListener; + private Listener mListener; /** * Broadcasts the frequency change event to the downstream frequency change listener * @param event to broadcast */ - public void broadcast(FrequencyChangeEvent event) + public void broadcast(SourceEvent event) { if(mListener != null) { @@ -384,7 +382,7 @@ public class TunerChannelSource extends ComplexSource implements IFrequencyChang try { long frequency = getFrequency(); - broadcast(new FrequencyChangeEvent(Event.NOTIFICATION_FREQUENCY_CHANGE, frequency)); + broadcast(SourceEvent.frequencyChange(frequency)); } catch(SourceException se) { @@ -400,7 +398,7 @@ public class TunerChannelSource extends ComplexSource implements IFrequencyChang try { //Note: downstream sample rate is currently a fixed value -- it will change in the future - broadcast(new FrequencyChangeEvent(Event.NOTIFICATION_SAMPLE_RATE_CHANGE, getSampleRate())); + broadcast(SourceEvent.sampleRateChange(getSampleRate())); } catch(SourceException se) { @@ -413,7 +411,7 @@ public class TunerChannelSource extends ComplexSource implements IFrequencyChang * @param listener to receive events */ @Override - public void setFrequencyChangeListener(Listener listener) + public void setSourceEventListener(Listener listener) { mListener = listener; } @@ -422,7 +420,7 @@ public class TunerChannelSource extends ComplexSource implements IFrequencyChang * Removes the downstream listener from receiving frequency change events. */ @Override - public void removeFrequencyChangeListener() + public void removeSourceEventListener() { mListener = null; } @@ -431,7 +429,7 @@ public class TunerChannelSource extends ComplexSource implements IFrequencyChang * Listener for receiving frequency change events from downstream components */ @Override - public Listener getFrequencyChangeListener() + public Listener getSourceEventListener() { return this; } @@ -441,7 +439,7 @@ public class TunerChannelSource extends ComplexSource implements IFrequencyChang * @param event to process */ @Override - public void receive(FrequencyChangeEvent event) + public void receive(SourceEvent event) { switch(event.getEvent()) { diff --git a/src/source/tuner/TunerController.java b/src/source/tuner/TunerController.java index 017231bd..23af0118 100644 --- a/src/source/tuner/TunerController.java +++ b/src/source/tuner/TunerController.java @@ -26,13 +26,13 @@ import org.slf4j.LoggerFactory; import source.SourceException; import source.tuner.configuration.TunerConfiguration; -import source.tuner.frequency.FrequencyChangeEvent; -import source.tuner.frequency.FrequencyChangeEvent.Event; +import source.SourceEvent; +import source.SourceEvent.Event; import source.tuner.frequency.FrequencyController; import source.tuner.frequency.FrequencyController.Tunable; -import source.tuner.frequency.IFrequencyChangeProcessor; +import source.ISourceEventProcessor; -public abstract class TunerController implements Tunable, IFrequencyChangeProcessor +public abstract class TunerController implements Tunable, ISourceEventProcessor { private final static Logger mLog = LoggerFactory.getLogger( TunerController.class ); @@ -79,7 +79,7 @@ public abstract class TunerController implements Tunable, IFrequencyChangeProces * Responds to requests to set the frequency */ @Override - public void frequencyChanged( FrequencyChangeEvent event ) throws SourceException + public void process(SourceEvent event ) throws SourceException { if( event.getEvent() == Event.REQUEST_FREQUENCY_CHANGE ) { @@ -381,7 +381,7 @@ public abstract class TunerController implements Tunable, IFrequencyChangeProces * Note: this is normally used by the Tuner. Any additional listeners can * be registered on the tuner. */ - public void addListener( IFrequencyChangeProcessor processor ) + public void addListener( ISourceEventProcessor processor ) { mFrequencyController.addListener( processor ); } @@ -389,7 +389,7 @@ public abstract class TunerController implements Tunable, IFrequencyChangeProces /** * Removes the frequency change listener */ - public void removeListener( IFrequencyChangeProcessor processor ) + public void removeListener( ISourceEventProcessor processor ) { mFrequencyController.removeFrequencyChangeProcessor( processor ); } diff --git a/src/source/tuner/TunerSourceEditor.java b/src/source/tuner/TunerSourceEditor.java index 4973e6b7..8d92360f 100644 --- a/src/source/tuner/TunerSourceEditor.java +++ b/src/source/tuner/TunerSourceEditor.java @@ -22,8 +22,8 @@ import gui.editor.Editor; import net.miginfocom.swing.MigLayout; import source.config.SourceConfigTuner; import source.config.SourceConfiguration; -import source.tuner.frequency.FrequencyChangeEvent; -import source.tuner.frequency.IFrequencyChangeProcessor; +import source.SourceEvent; +import source.ISourceEventProcessor; import controller.channel.Channel; public class TunerSourceEditor extends Editor @@ -41,10 +41,10 @@ public class TunerSourceEditor extends Editor setLayout( new MigLayout( "insets 0 0 0 0", "[left]", "" ) ); mFrequencyControl = new JFrequencyControl(); mFrequencyControl.setEnabled( false ); - mFrequencyControl.addListener( new IFrequencyChangeProcessor() + mFrequencyControl.addListener( new ISourceEventProcessor() { @Override - public void frequencyChanged( FrequencyChangeEvent event ) + public void process(SourceEvent event ) { setModified( true ); } diff --git a/src/source/tuner/frequency/DCTrackingFrequencyControl.java b/src/source/tuner/frequency/DCTrackingFrequencyControl.java deleted file mode 100644 index 7cfe5648..00000000 --- a/src/source/tuner/frequency/DCTrackingFrequencyControl.java +++ /dev/null @@ -1,128 +0,0 @@ -package source.tuner.frequency; -/******************************************************************************* - * SDR Trunk - * Copyright (C) 2016 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 - ******************************************************************************/ -import java.util.concurrent.ScheduledExecutorService; - -import module.Module; -import sample.Listener; -import sample.real.RealBuffer; -import source.tuner.frequency.FrequencyChangeEvent.Event; - -public class DCTrackingFrequencyControl extends Module - implements Listener, IFrequencyChangeProvider -{ - private static float COARSE_THRESHOLD = 0.1f; - private static float LOOP_GAIN = 0.1f; - private static float COARSE_GAIN = 1000.0f; - private static float FINE_GAIN = 100.0f; - private int mErrorCorrection = 0; - private int mMaximumFrequencyCorrection = 0; - private Listener mListener; - - /** - * DC offset Automatic Frequency Control. Monitors DC bias present in an FM - * demodulated audio stream and issues automatic frequency corrections to - * maintain the DC bias close to zero. - * - * @param maximum - maximum allowable (+/-) frequency correction value. - */ - public DCTrackingFrequencyControl( int maximum ) - { - mMaximumFrequencyCorrection = maximum; - } - - public void dispose() - { - mListener = null; - } - - @Override - public void receive( RealBuffer buffer ) - { - double sum = 0.0d; - - for( float sample: buffer.getSamples() ) - { - sum += sample; - } - - float mean = (float)( sum / (double)buffer.getSamples().length ); - - float adjustment = 0.0f; - - if( mean > COARSE_THRESHOLD || mean < - COARSE_THRESHOLD ) - { - adjustment = mean * COARSE_GAIN; - } - else - { - adjustment = mean * FINE_GAIN; - } - - setErrorCorrection( mErrorCorrection + (int)( (float)adjustment * LOOP_GAIN ) ); - } - - private void setErrorCorrection( int correction ) - { - if( mListener != null ) - { - if( correction > mMaximumFrequencyCorrection ) - { - correction = mMaximumFrequencyCorrection; - } - else if( correction < -mMaximumFrequencyCorrection ) - { - correction = -mMaximumFrequencyCorrection; - } - - if( correction != mErrorCorrection ) - { - mListener.receive( new FrequencyChangeEvent( - Event.REQUEST_CHANNEL_FREQUENCY_CORRECTION_CHANGE, correction ) ); - } - } - } - - @Override - public void setFrequencyChangeListener( Listener listener ) - { - mListener = listener; - } - - @Override - public void removeFrequencyChangeListener() - { - mListener = null; - } - - @Override - public void start( ScheduledExecutorService executor ) - { - } - - @Override - public void stop() - { - } - - @Override - public void reset() - { - setErrorCorrection( 0 ); - } -} diff --git a/src/source/tuner/frequency/DCTrackingSourceControl.java b/src/source/tuner/frequency/DCTrackingSourceControl.java new file mode 100644 index 00000000..09b48831 --- /dev/null +++ b/src/source/tuner/frequency/DCTrackingSourceControl.java @@ -0,0 +1,130 @@ +/******************************************************************************* + * 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 source.tuner.frequency; + +import module.Module; +import sample.Listener; +import sample.real.RealBuffer; +import source.ISourceEventProvider; +import source.SourceEvent; + +import java.util.concurrent.ScheduledExecutorService; + +public class DCTrackingSourceControl extends Module + implements Listener, ISourceEventProvider +{ + private static float COARSE_THRESHOLD = 0.1f; + private static float LOOP_GAIN = 0.1f; + private static float COARSE_GAIN = 1000.0f; + private static float FINE_GAIN = 100.0f; + private int mErrorCorrection = 0; + private int mMaximumFrequencyCorrection = 0; + private Listener mListener; + + /** + * DC offset Automatic Frequency Control. Monitors DC bias present in an FM + * demodulated audio stream and issues automatic frequency corrections to + * maintain the DC bias close to zero. + * + * @param maximum - maximum allowable (+/-) frequency correction value. + */ + public DCTrackingSourceControl(int maximum) + { + mMaximumFrequencyCorrection = maximum; + } + + public void dispose() + { + mListener = null; + } + + @Override + public void receive(RealBuffer buffer) + { + double sum = 0.0d; + + for(float sample : buffer.getSamples()) + { + sum += sample; + } + + float mean = (float)(sum / (double)buffer.getSamples().length); + + float adjustment = 0.0f; + + if(mean > COARSE_THRESHOLD || mean < -COARSE_THRESHOLD) + { + adjustment = mean * COARSE_GAIN; + } + else + { + adjustment = mean * FINE_GAIN; + } + + setErrorCorrection(mErrorCorrection + (int)((float)adjustment * LOOP_GAIN)); + } + + private void setErrorCorrection(int correction) + { + if(mListener != null) + { + if(correction > mMaximumFrequencyCorrection) + { + correction = mMaximumFrequencyCorrection; + } + else if(correction < -mMaximumFrequencyCorrection) + { + correction = -mMaximumFrequencyCorrection; + } + + if(correction != mErrorCorrection) + { + mListener.receive(SourceEvent.channelFrequencyCorrectionRequest(correction)); + } + } + } + + @Override + public void setSourceEventListener(Listener listener) + { + mListener = listener; + } + + @Override + public void removeSourceEventListener() + { + mListener = null; + } + + @Override + public void start(ScheduledExecutorService executor) + { + } + + @Override + public void stop() + { + } + + @Override + public void reset() + { + setErrorCorrection(0); + } +} diff --git a/src/source/tuner/frequency/FrequencyChangeEvent.java b/src/source/tuner/frequency/FrequencyChangeEvent.java deleted file mode 100644 index 9ceb3685..00000000 --- a/src/source/tuner/frequency/FrequencyChangeEvent.java +++ /dev/null @@ -1,34 +0,0 @@ -package source.tuner.frequency; - -public class FrequencyChangeEvent -{ - public enum Event - { - NOTIFICATION_FREQUENCY_CHANGE, - NOTIFICATION_FREQUENCY_CORRECTION_CHANGE, - NOTIFICATION_CHANNEL_FREQUENCY_CORRECTION_CHANGE, - NOTIFICATION_SAMPLE_RATE_CHANGE, - - REQUEST_FREQUENCY_CHANGE, - REQUEST_CHANNEL_FREQUENCY_CORRECTION_CHANGE; - } - - private Event mEvent; - private Number mValue; - - public FrequencyChangeEvent( Event event, Number value ) - { - mEvent = event; - mValue = value; - } - - public Event getEvent() - { - return mEvent; - } - - public Number getValue() - { - return mValue; - } -} diff --git a/src/source/tuner/frequency/FrequencyChangeProcessorWrapper.java b/src/source/tuner/frequency/FrequencyChangeProcessorWrapper.java deleted file mode 100644 index 8aa66445..00000000 --- a/src/source/tuner/frequency/FrequencyChangeProcessorWrapper.java +++ /dev/null @@ -1,45 +0,0 @@ -package source.tuner.frequency; - -import org.slf4j.Logger; -import org.slf4j.LoggerFactory; -import sample.Listener; -import source.SourceException; -import source.tuner.hackrf.HackRFTunerEditor; - -public class FrequencyChangeProcessorWrapper - implements IFrequencyChangeListener, Listener -{ - private final static Logger mLog = LoggerFactory.getLogger( FrequencyChangeProcessorWrapper.class ); - - private IFrequencyChangeProcessor mProcessor; - - /** - * Provides IFrequencyChangeListener wrapper for an IFrequencyChangeProcessor - */ - public FrequencyChangeProcessorWrapper( IFrequencyChangeProcessor handler ) - { - mProcessor = handler; - } - - @Override - public Listener getFrequencyChangeListener() - { - return this; - } - - @Override - public void receive( FrequencyChangeEvent event ) - { - if( mProcessor != null ) - { - try - { - mProcessor.frequencyChanged( event ); - } - catch( SourceException se ) - { - mLog.error("Error sending frequency change event to embedded processor", se); - } - } - } -} diff --git a/src/source/tuner/frequency/FrequencyController.java b/src/source/tuner/frequency/FrequencyController.java index 7262317b..7a4d83de 100644 --- a/src/source/tuner/frequency/FrequencyController.java +++ b/src/source/tuner/frequency/FrequencyController.java @@ -1,257 +1,253 @@ /******************************************************************************* - * SDR Trunk - * Copyright (C) 2014 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 + * 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 source.tuner.frequency; -import java.util.ArrayList; - import org.slf4j.Logger; import org.slf4j.LoggerFactory; - +import source.ISourceEventProcessor; import source.InvalidFrequencyException; -import source.Source; +import source.SourceEvent; import source.SourceException; -import source.tuner.frequency.FrequencyChangeEvent.Event; + +import java.util.ArrayList; public class FrequencyController { - private final static Logger mLog = LoggerFactory.getLogger( FrequencyController.class ); + private final static Logger mLog = LoggerFactory.getLogger(FrequencyController.class); - private Tunable mTunable; - private long mFrequency = 101100000; - private long mTunedFrequency = 101100000; - private long mMinimumFrequency; - private long mMaximumFrequency; - private double mFrequencyCorrection = 0d; - private int mSampleRate = 0; - - private ArrayList mProcessors = new ArrayList<>(); - - public FrequencyController( Tunable tunable, - long minFrequency, - long maxFrequency, - double frequencyCorrection ) - { - mTunable = tunable; - mMinimumFrequency = minFrequency; - mMaximumFrequency = maxFrequency; - mFrequencyCorrection = frequencyCorrection; - } + private Tunable mTunable; + private long mFrequency = 101100000; + private long mTunedFrequency = 101100000; + private long mMinimumFrequency; + private long mMaximumFrequency; + private double mFrequencyCorrection = 0d; + private int mSampleRate = 0; - /** - * Get bandwidth/sample rate in hertz - */ - public int getBandwidth() - { - return mSampleRate; - } + private ArrayList mProcessors = new ArrayList<>(); - /** - * Set sample rate in hertz - */ - public void setSampleRate( int sampleRate ) throws SourceException - { - mSampleRate = sampleRate; - - broadcastSampleRateChange(); - } + public FrequencyController(Tunable tunable, long minFrequency, long maxFrequency, double frequencyCorrection) + { + mTunable = tunable; + mMinimumFrequency = minFrequency; + mMaximumFrequency = maxFrequency; + mFrequencyCorrection = frequencyCorrection; + } - /** - * Get frequency in hertz - */ - public long getFrequency() - { - return mFrequency; - } + /** + * Get bandwidth/sample rate in hertz + */ + public int getBandwidth() + { + return mSampleRate; + } - /** - * Set frequency - * @param frequency in hertz - * @throws SourceException if tunable doesn't support tuning a corrected - * version of the requested frequency - */ - public void setFrequency( long frequency ) throws SourceException - { - setFrequency( frequency, true ); - } + /** + * Set sample rate in hertz + */ + public void setSampleRate(int sampleRate) throws SourceException + { + mSampleRate = sampleRate; - /** - * Set frequency with optional broadcast of frequency change event. This - * method supports changing the frequency correction value without - * broadcasting a frequency change event. - */ - private void setFrequency( long frequency, boolean broadcastChange ) throws SourceException - { - long tunedFrequency = getTunedFrequency( frequency ); - - if( tunedFrequency < mMinimumFrequency ) - { - throw new InvalidFrequencyException( "Requested frequency not valid", frequency, mMinimumFrequency ); - } + broadcastSampleRateChange(); + } - if( tunedFrequency > mMaximumFrequency ) - { - throw new InvalidFrequencyException( "Requested frequency not valid", frequency, mMaximumFrequency ); - } + /** + * Get frequency in hertz + */ + public long getFrequency() + { + return mFrequency; + } - mFrequency = frequency; + /** + * Set frequency + * + * @param frequency in hertz + * @throws SourceException if tunable doesn't support tuning a corrected + * version of the requested frequency + */ + public void setFrequency(long frequency) throws SourceException + { + setFrequency(frequency, true); + } - mTunedFrequency = tunedFrequency; + /** + * Set frequency with optional broadcast of frequency change event. This + * method supports changing the frequency correction value without + * broadcasting a frequency change event. + */ + private void setFrequency(long frequency, boolean broadcastChange) throws SourceException + { + long tunedFrequency = getTunedFrequency(frequency); - if( mTunable != null ) - { - mTunable.setTunedFrequency( mTunedFrequency ); - } + if(tunedFrequency < mMinimumFrequency) + { + throw new InvalidFrequencyException("Requested frequency not valid", frequency, mMinimumFrequency); + } + + if(tunedFrequency > mMaximumFrequency) + { + throw new InvalidFrequencyException("Requested frequency not valid", frequency, mMaximumFrequency); + } + + mFrequency = frequency; + + mTunedFrequency = tunedFrequency; + + if(mTunable != null) + { + mTunable.setTunedFrequency(mTunedFrequency); + } /* Broadcast to all listeners that the frequency has changed */ - if( broadcastChange ) - { - broadcastFrequencyChange(); - } - } - - public long getTunedFrequency() - { - return mTunedFrequency; - } - - public long getMinimumFrequency() - { - return mMinimumFrequency; - } - - public long getMaximumFrequency() - { - return mMaximumFrequency; - } - + if(broadcastChange) + { + broadcastFrequencyChange(); + } + } + + public long getTunedFrequency() + { + return mTunedFrequency; + } + + public long getMinimumFrequency() + { + return mMinimumFrequency; + } + + public long getMaximumFrequency() + { + return mMaximumFrequency; + } + /** * Calculate the tuned frequency by adding frequency correction to the * corrected frequency. + * * @param correctedFrequency */ - private long getTunedFrequency( long correctedFrequency ) + private long getTunedFrequency(long correctedFrequency) { - return (long)( (double)correctedFrequency / - ( 1.0 + ( mFrequencyCorrection / 1000000.0 ) ) ); + return (long)((double)correctedFrequency / + (1.0 + (mFrequencyCorrection / 1000000.0))); } /** - * Calculate the corrected frequency by subtracting frequency correction + * Calculate the corrected frequency by subtracting frequency correction * from the tuned frequency. + * * @param tunedFrequency */ @SuppressWarnings("unused") - private long getCorrectedFrequency( long tunedFrequency ) + private long getCorrectedFrequency(long tunedFrequency) { - return (long)( (double)tunedFrequency / - ( 1.0 - ( mFrequencyCorrection / 1000000.0 ) ) ); + return (long)((double)tunedFrequency / + (1.0 - (mFrequencyCorrection / 1000000.0))); } - + public double getFrequencyCorrection() { - return mFrequencyCorrection; + return mFrequencyCorrection; } - - public void setFrequencyCorrection( double correction ) throws SourceException + + public void setFrequencyCorrection(double correction) throws SourceException { - mFrequencyCorrection = correction; + mFrequencyCorrection = correction; - if( mFrequency > 0 ) - { - setFrequency( mFrequency, true ); - } + if(mFrequency > 0) + { + setFrequency(mFrequency, true); + } - broadcastFrequencyErrorChange(); + broadcastFrequencyCorrectionChange(); } - - /** - * Adds listener to receive frequency change events - */ -//TODO: rename this to addFrequencyChangeProcessor() - public void addListener( IFrequencyChangeProcessor processor ) - { - if( !mProcessors.contains( processor ) ) - { - mProcessors.add( processor ); - } - } - /** - * Removes listener from receiving frequency change events - */ - public void removeFrequencyChangeProcessor( IFrequencyChangeProcessor processor ) - { - mProcessors.remove( processor ); - } - - /** - * Broadcasts a change/update to the current (uncorrected) frequency or the + * Adds listener to receive frequency change events + */ + public void addListener(ISourceEventProcessor processor) + { + if(!mProcessors.contains(processor)) + { + mProcessors.add(processor); + } + } + + /** + * Removes listener from receiving frequency change events + */ + public void removeFrequencyChangeProcessor(ISourceEventProcessor processor) + { + mProcessors.remove(processor); + } + + + /** + * Broadcasts a change/update to the current (uncorrected) frequency or the * bandwidth/sample rate value. */ protected void broadcastFrequencyChange() throws SourceException { - broadcastFrequencyChangeEvent( - new FrequencyChangeEvent( Event.NOTIFICATION_FREQUENCY_CHANGE, mFrequency ) ); + broadcast(SourceEvent.frequencyChange(mFrequency)); } - + /** * Broadcast a frequency error/correction value change */ - protected void broadcastFrequencyErrorChange() throws SourceException + protected void broadcastFrequencyCorrectionChange() throws SourceException { - broadcastFrequencyChangeEvent( - new FrequencyChangeEvent( Event.NOTIFICATION_FREQUENCY_CORRECTION_CHANGE, mFrequencyCorrection ) ); + broadcast(SourceEvent.frequencyCorrectionChange((int)mFrequencyCorrection)); } - + /** * Broadcasts a sample rate change */ protected void broadcastSampleRateChange() throws SourceException { - broadcastFrequencyChangeEvent( - new FrequencyChangeEvent( Event.NOTIFICATION_SAMPLE_RATE_CHANGE, mSampleRate ) ); + broadcast(SourceEvent.sampleRateChange(mSampleRate)); } - - public void broadcastFrequencyChangeEvent( FrequencyChangeEvent event ) throws SourceException + + public void broadcast(SourceEvent event) throws SourceException { - for( IFrequencyChangeProcessor processor: mProcessors ) - { - processor.frequencyChanged( event ); - } + for(ISourceEventProcessor processor : mProcessors) + { + processor.process(event); + } } - - - public interface Tunable - { - /** - * Gets the tuned frequency of the device - */ - public long getTunedFrequency() throws SourceException; - /** - * Sets the tuned frequency of the device - */ - public void setTunedFrequency( long frequency ) throws SourceException; - /** - * Gets the current bandwidth setting of the device - */ - public int getCurrentSampleRate() throws SourceException; - } + public interface Tunable + { + /** + * Gets the tuned frequency of the device + */ + public long getTunedFrequency() throws SourceException; + + /** + * Sets the tuned frequency of the device + */ + public void setTunedFrequency(long frequency) throws SourceException; + + /** + * Gets the current bandwidth setting of the device + */ + public int getCurrentSampleRate() throws SourceException; + } } diff --git a/src/source/tuner/frequency/IFrequencyChangeListener.java b/src/source/tuner/frequency/IFrequencyChangeListener.java deleted file mode 100644 index 99b75804..00000000 --- a/src/source/tuner/frequency/IFrequencyChangeListener.java +++ /dev/null @@ -1,34 +0,0 @@ -/******************************************************************************* - * SDR Trunk - * Copyright (C) 2014 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 source.tuner.frequency; - -import sample.Listener; - - -/** - * Interface for broadcasting changes to frequency, bandwidth, sample rate, - * and actual sample rate values to all registered listeners. - * - * Note: broadcasted frequency should be the uncorrected, or displayable - * frequency value. All frequency correction aspects should be handled within - * the device that implements the tuning of the frequency. - */ -public interface IFrequencyChangeListener -{ - public Listener getFrequencyChangeListener(); -} diff --git a/src/source/tuner/frequency/IFrequencyChangeProcessor.java b/src/source/tuner/frequency/IFrequencyChangeProcessor.java deleted file mode 100644 index 788c46ac..00000000 --- a/src/source/tuner/frequency/IFrequencyChangeProcessor.java +++ /dev/null @@ -1,8 +0,0 @@ -package source.tuner.frequency; - -import source.SourceException; - -public interface IFrequencyChangeProcessor -{ - public void frequencyChanged( FrequencyChangeEvent event ) throws SourceException; -} diff --git a/src/source/wave/ComplexWaveSource.java b/src/source/wave/ComplexWaveSource.java index 8d80ce1d..b99289e9 100644 --- a/src/source/wave/ComplexWaveSource.java +++ b/src/source/wave/ComplexWaveSource.java @@ -37,7 +37,7 @@ import sample.complex.ComplexBuffer; import source.ComplexSource; import source.IControllableFileSource; import source.IFrameLocationListener; -import source.tuner.frequency.FrequencyChangeEvent; +import source.SourceEvent; public class ComplexWaveSource extends ComplexSource implements IControllableFileSource { @@ -58,19 +58,19 @@ public class ComplexWaveSource extends ComplexSource implements IControllableFil } @Override - public void setFrequencyChangeListener(Listener listener) + public void setSourceEventListener(Listener listener) { //Not implemented } @Override - public void removeFrequencyChangeListener() + public void removeSourceEventListener() { //Not implemented } @Override - public Listener getFrequencyChangeListener() + public Listener getSourceEventListener() { //Not implemented return null; diff --git a/src/source/wave/RealWaveSource.java b/src/source/wave/RealWaveSource.java index 4b9b120b..ec038471 100644 --- a/src/source/wave/RealWaveSource.java +++ b/src/source/wave/RealWaveSource.java @@ -36,7 +36,7 @@ import sample.real.RealBuffer; import source.IControllableFileSource; import source.IFrameLocationListener; import source.RealSource; -import source.tuner.frequency.FrequencyChangeEvent; +import source.SourceEvent; public class RealWaveSource extends RealSource implements IControllableFileSource, AutoCloseable @@ -58,19 +58,19 @@ public class RealWaveSource extends RealSource } @Override - public void setFrequencyChangeListener(Listener listener) + public void setSourceEventListener(Listener listener) { //Not implemented } @Override - public void removeFrequencyChangeListener() + public void removeSourceEventListener() { //Not implemented } @Override - public Listener getFrequencyChangeListener() + public Listener getSourceEventListener() { //Not implemented return null; diff --git a/src/spectrum/ChannelSpectrumPanel.java b/src/spectrum/ChannelSpectrumPanel.java index 397c3604..87fabc1d 100644 --- a/src/spectrum/ChannelSpectrumPanel.java +++ b/src/spectrum/ChannelSpectrumPanel.java @@ -26,8 +26,8 @@ import settings.ColorSettingMenuItem; import settings.Setting; import settings.SettingChangeListener; import settings.SettingsManager; -import source.tuner.frequency.FrequencyChangeEvent; -import source.tuner.frequency.FrequencyChangeEvent.Event; +import source.SourceEvent; +import source.SourceEvent.Event; import spectrum.converter.DFTResultsConverter; import spectrum.converter.RealDecibelConverter; import spectrum.menu.AveragingItem; @@ -90,8 +90,7 @@ public class ChannelSpectrumPanel extends JPanel mDFTConverter.addListener( mSpectrumPanel ); /* Set the DFTProcessor to the decimated 24kHz sample rate */ - mDFTProcessor.frequencyChanged( - new FrequencyChangeEvent( Event.NOTIFICATION_SAMPLE_RATE_CHANGE, 24000 ) ); + mDFTProcessor.process(SourceEvent.sampleRateChange(24000)); initGui(); } diff --git a/src/spectrum/DFTProcessor.java b/src/spectrum/DFTProcessor.java index 0f9e066c..73515b87 100644 --- a/src/spectrum/DFTProcessor.java +++ b/src/spectrum/DFTProcessor.java @@ -28,8 +28,8 @@ import sample.Buffer; import sample.Listener; import sample.SampleType; import sample.complex.ComplexBuffer; -import source.tuner.frequency.FrequencyChangeEvent; -import source.tuner.frequency.IFrequencyChangeProcessor; +import source.SourceEvent; +import source.ISourceEventProcessor; import spectrum.converter.DFTResultsConverter; import util.ThreadPool; @@ -46,7 +46,7 @@ import java.util.concurrent.atomic.AtomicBoolean; * Processes both complex samples or float samples and dispatches a float array * of DFT results, using configurable fft size and output dispatch timelines. */ -public class DFTProcessor implements Listener, IFrequencyChangeProcessor, IDFTWidthChangeProcessor +public class DFTProcessor implements Listener, ISourceEventProcessor, IDFTWidthChangeProcessor { private final static Logger mLog = LoggerFactory.getLogger(DFTProcessor.class); @@ -452,7 +452,7 @@ public class DFTProcessor implements Listener, IFrequencyChangePr } @Override - public void frequencyChanged(FrequencyChangeEvent event) + public void process(SourceEvent event) { switch(event.getEvent()) { diff --git a/src/spectrum/OverlayPanel.java b/src/spectrum/OverlayPanel.java index b064b804..d78101e2 100644 --- a/src/spectrum/OverlayPanel.java +++ b/src/spectrum/OverlayPanel.java @@ -31,8 +31,8 @@ import settings.Setting; import settings.SettingChangeListener; import settings.SettingsManager; import source.tuner.TunerChannel; -import source.tuner.frequency.FrequencyChangeEvent; -import source.tuner.frequency.IFrequencyChangeProcessor; +import source.SourceEvent; +import source.ISourceEventProcessor; import javax.swing.*; import java.awt.*; @@ -44,7 +44,7 @@ import java.text.DecimalFormat; import java.util.ArrayList; import java.util.concurrent.CopyOnWriteArrayList; -public class OverlayPanel extends JPanel implements ChannelEventListener, IFrequencyChangeProcessor, +public class OverlayPanel extends JPanel implements ChannelEventListener, ISourceEventProcessor, SettingChangeListener { private static final long serialVersionUID = 1L; @@ -635,7 +635,7 @@ public class OverlayPanel extends JPanel implements ChannelEventListener, IFrequ * Frequency change event handler */ @Override - public void frequencyChanged(FrequencyChangeEvent event) + public void process(SourceEvent event) { switch(event.getEvent()) { diff --git a/src/spectrum/SpectralDisplayPanel.java b/src/spectrum/SpectralDisplayPanel.java index 505c17e0..9c871aea 100644 --- a/src/spectrum/SpectralDisplayPanel.java +++ b/src/spectrum/SpectralDisplayPanel.java @@ -36,10 +36,9 @@ import sample.complex.ComplexBuffer; import settings.ColorSetting.ColorSettingName; import settings.ColorSettingMenuItem; import settings.SettingsManager; +import source.ISourceEventProcessor; +import source.SourceEvent; import source.tuner.Tuner; -import source.tuner.frequency.FrequencyChangeEvent; -import source.tuner.frequency.FrequencyChangeEvent.Event; -import source.tuner.frequency.IFrequencyChangeProcessor; import spectrum.OverlayPanel.ChannelDisplay; import spectrum.converter.ComplexDecibelConverter; import spectrum.converter.DFTResultsConverter; @@ -65,7 +64,7 @@ import java.text.DecimalFormat; import java.util.ArrayList; import java.util.Hashtable; -public class SpectralDisplayPanel extends JPanel implements Listener, IFrequencyChangeProcessor, IDFTWidthChangeProcessor +public class SpectralDisplayPanel extends JPanel implements Listener, ISourceEventProcessor, IDFTWidthChangeProcessor { private static final long serialVersionUID = 1L; @@ -223,7 +222,7 @@ public class SpectralDisplayPanel extends JPanel implements Listener) mDFTProcessor); + mTuner.addListener((Listener)mDFTProcessor); mSpectrumPanel.setSampleSize(mTuner.getSampleSize()); //Fire frequency and sample rate change events so that the spectrum //and overlay panels can synchronize - frequencyChanged(new FrequencyChangeEvent( - Event.NOTIFICATION_FREQUENCY_CHANGE, - mTuner.getTunerController().getFrequency())); + process(SourceEvent.frequencyChange(mTuner.getTunerController().getFrequency())); - frequencyChanged(new FrequencyChangeEvent( - Event.NOTIFICATION_SAMPLE_RATE_CHANGE, - mTuner.getTunerController().getSampleRate())); + process(SourceEvent.sampleRateChange(mTuner.getTunerController().getSampleRate())); } } @@ -452,7 +447,7 @@ public class SpectralDisplayPanel extends JPanel implements Listener) mDFTProcessor); + mTuner.removeListener((Listener)mDFTProcessor); mTuner = null; } } @@ -508,7 +503,7 @@ public class SpectralDisplayPanel extends JPanel implements Listener Date: Sat, 13 May 2017 10:16:18 -0400 Subject: [PATCH 18/87] wip-channelizer --- .../AbstractComplexPolyphaseChannelizer.java | 4 +- src/dsp/filter/channelizer/Channelizer.java | 55 +++- .../channelizer/ChannelizerConfiguration.java | 310 ++++++++++++++++++ .../filter/channelizer/ChannelizerViewer.java | 2 +- .../channelizer/PolyphaseChannelSource.java | 56 +++- .../output/ChannelOutputProcessorFactory.java | 31 ++ .../IPolyphaseChannelOutputProcessor.java | 39 +++ .../output/SingleChannelOutputProcessor.java | 105 ++++++ src/dsp/mixer/Oscillator.java | 237 ++++++------- src/source/Source.java | 31 +- 10 files changed, 734 insertions(+), 136 deletions(-) create mode 100644 src/dsp/filter/channelizer/ChannelizerConfiguration.java create mode 100644 src/dsp/filter/channelizer/output/ChannelOutputProcessorFactory.java create mode 100644 src/dsp/filter/channelizer/output/IPolyphaseChannelOutputProcessor.java create mode 100644 src/dsp/filter/channelizer/output/SingleChannelOutputProcessor.java diff --git a/src/dsp/filter/channelizer/AbstractComplexPolyphaseChannelizer.java b/src/dsp/filter/channelizer/AbstractComplexPolyphaseChannelizer.java index fc7a0f06..273bf23e 100644 --- a/src/dsp/filter/channelizer/AbstractComplexPolyphaseChannelizer.java +++ b/src/dsp/filter/channelizer/AbstractComplexPolyphaseChannelizer.java @@ -92,9 +92,11 @@ public abstract class AbstractComplexPolyphaseChannelizer implements Listener { private final static Logger mLog = LoggerFactory.getLogger(Channelizer.class); + private static final int CHANNEL_BANDWIDTH = 12500; + private static final double CHANNEL_OVERSAMPLING = 2.0; private Tuner mTuner; private List mChannelSources = new CopyOnWriteArrayList<>(); + private ChannelizerConfiguration mChannelizerConfiguration; /** * Provides access to Digital Drop Channel (DDC) sources from the tuner. Incorporates a polyphase channelizer @@ -46,12 +49,47 @@ public class Channelizer implements ISourceEventProcessor, Listener public Channelizer(Tuner tuner) { mTuner = tuner; + + int sampleRate = mTuner.getTunerController().getSampleRate(); + + if(sampleRate % 25000 != 0) + { + throw new IllegalArgumentException("Tuner sample rate [" + sampleRate + "] must be a multiple of 25 kHz"); + } + + int channelCount = sampleRate / CHANNEL_BANDWIDTH; + + long frequency = mTuner.getTunerController().getFrequency(); + + mChannelizerConfiguration = new ChannelizerConfiguration(frequency, channelCount, + CHANNEL_BANDWIDTH, CHANNEL_OVERSAMPLING); + mTuner.addSourceEventListener(this); } + /** + * Provides a Digital Drop Channel (DDC) for the specified tuner channel or returns null if the channel can't be + * sourced due to the current center frequency and/or sample rate. + * @param tunerChannel specifying center frequency and bandwidth. + * @return source or null. + */ public Source getChannel(TunerChannel tunerChannel) { - //TODO: return a polyphase channel here + try + { + List polyphaseIndexes = mChannelizerConfiguration.getPolyphaseChannelIndexes(tunerChannel); + + + + } + catch(IllegalArgumentException iae) + { + mLog.info("Can't provide DDC for " + tunerChannel.toString() + " due to channelizer frequency [" + + " due to channelizer frequency [" + mChannelizerConfiguration.getCenterFrequency() + + "] and sample rate [" + (mChannelizerConfiguration.getChannelCount() * + mChannelizerConfiguration.getChannelBandwidth()) + "]"); + } + return null; } @@ -121,6 +159,16 @@ public class Channelizer implements ISourceEventProcessor, Listener (sourceEvent.hasSource() ? sourceEvent.getSource().getClass() : "null source")); } break; + case NOTIFICATION_FREQUENCY_CHANGE: + mChannelizerConfiguration.setCenterFrequency(sourceEvent.getValue().longValue()); + updateChannelResultsProcessors(); + break; + case NOTIFICATION_SAMPLE_RATE_CHANGE: + int sampleRate = sourceEvent.getValue().intValue(); + int channelCount = sampleRate / mChannelizerConfiguration.getChannelBandwidth(); + mChannelizerConfiguration.setChannelCount(channelCount); + updateChannelResultsProcessors(); + break; } @@ -130,6 +178,11 @@ public class Channelizer implements ISourceEventProcessor, Listener @Override public void receive(SourceEvent sourceEvent) { + process(sourceEvent); + } + private void updateChannelResultsProcessors() + { + //Update the processors anytime that the center frequency or sample rate changes } } diff --git a/src/dsp/filter/channelizer/ChannelizerConfiguration.java b/src/dsp/filter/channelizer/ChannelizerConfiguration.java new file mode 100644 index 00000000..be89ac42 --- /dev/null +++ b/src/dsp/filter/channelizer/ChannelizerConfiguration.java @@ -0,0 +1,310 @@ +/******************************************************************************* + * 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 source.tuner.TunerChannel; + +import java.util.ArrayList; +import java.util.List; + +public class ChannelizerConfiguration +{ + private final static Logger mLog = LoggerFactory.getLogger(ChannelizerConfiguration.class); + + private long mCenterFrequency; + private int mChannelCount; + private int mChannelBandwidth; + private double mOversampling = 1.0; + + /** + * Configuration details for a polyphase channelizer that can be used to identify which polyphase + * channelizer output channel(s) should be processed to produce an output channel of a specified + * center frequency and channel sample rate/bandwidth. + * + * @param centerFrequency + * @param channelCount + * @param channelBandwidth + * @param oversampling set to 1.0 for no oversample, or greater than 1.0 for oversampling + */ + public ChannelizerConfiguration(long centerFrequency, int channelCount, int channelBandwidth, double oversampling) + { + mCenterFrequency = centerFrequency; + mChannelCount = channelCount; + mChannelBandwidth = channelBandwidth; + mOversampling = oversampling; + } + + /** + * Center frequency for the channelizer + */ + public long getCenterFrequency() + { + return mCenterFrequency; + } + + /** + * Sets or changes the center frequency for the channelizer + */ + public void setCenterFrequency(long frequency) + { + mCenterFrequency = frequency; + } + + /** + * Number of channels being channelized by the polyphase channelizer + */ + public int getChannelCount() + { + return mChannelCount; + } + + /** + * Sets the number of channels being channelized by the polyphase channelizer + */ + public void setChannelCount(int channelCount) + { + mChannelCount = channelCount; + } + + /** + * Channel bandwidth. Note: this value may be smaller than the channel sample rate when oversampling + * is being used. + */ + public int getChannelBandwidth() + { + return mChannelBandwidth; + } + + /** + * Half of the channel bandwidth. + */ + public int getHalfChannelBandwidth() + { + return mChannelBandwidth / 2; + } + + /** + * Sets the channel bandwidth in hertz. + */ + public void setChannelBandwidth(int channelBandwidth) + { + mChannelBandwidth = channelBandwidth; + } + + /** + * Oversampling rate produced by the channelizer. + * + * @return 1.0 for no oversampling, or a value greater than 1.0 when oversampling is applied. + */ + public double getOversampling() + { + return mOversampling; + } + + /** + * Sets the oversampling rate being applied by the channelizer + * @param oversampling rate where 1.0 indicates no oversampling, and a value greater than 1.0 indicates + * that the polyphase channelizer is oversampling each channel. + */ + public void setOversampling(double oversampling) + { + mOversampling = oversampling; + } + + /** + * Channel sample rate. This value is the product of the filtered channel bandwidth times the + * oversampling rate. + */ + public double getChannelSampleRate() + { + return (double)getChannelBandwidth() * getOversampling(); + } + + /** + * Channelizer input sample rate + * @return input sample rate in hertz + */ + public int getSampleRate() + { + return getChannelBandwidth() * getChannelCount(); + } + + /** + * Channelizer input minimum frequency + * @return minimum frequency in hertz + */ + public long getMinimumFrequency() + { + return getCenterFrequency() - (getSampleRate() / 2); + } + + /** + * Channelizer input maximum frequency + * @return maximum frequency in hertz + */ + public long getMaximumFrequency() + { + return getCenterFrequency() + (getSampleRate() / 2); + } + + /** + * Identifies the channelizer output channel(s) required to produce a tuner channel with the specified center + * frequency and channel bandwidth. + * + * @param tunerChannel specifying the desired channel frequency and bandwidth + * @return an array of one or more polyphase channelizer channels to target in order to produce an output channel + * for the desired frequency and bandwidth. + * @throws IllegalArgumentException if the tuner channel's minimum or maximum frequency is not contained + * within the channelizer's minimum or maximum frequency range + */ + public List getPolyphaseChannelIndexes(TunerChannel tunerChannel) throws IllegalArgumentException + { + if(tunerChannel.getMinFrequency() < getMinimumFrequency() || + tunerChannel.getMaxFrequency() > getMaximumFrequency()) + { + throw new IllegalArgumentException("Requested channel cannot be provided by this " + + "channelizer. Requested channel [" + tunerChannel.getMinFrequency() + " - " + + tunerChannel.getMaxFrequency() + "] exceeds current channelizer frequency range [" + + getMinimumFrequency() + " - " + getMaximumFrequency() + "]"); + } + + List indices = new ArrayList<>(); + + long startFrequencyOffset = (tunerChannel.getMinFrequency() - getMinimumFrequency()); + int startIndex = (int)(startFrequencyOffset / getChannelBandwidth()); + + long endFrequencyOffset = (tunerChannel.getMaxFrequency() - getMinimumFrequency()); + int endIndex = (int)(endFrequencyOffset / getChannelBandwidth()); + + int currentIndex = startIndex; + + while(currentIndex <= endIndex) + { + long currentIndexMin = getMinimumFrequencyForChannelIndex(currentIndex); + long currentIndexMax = getMaximumFrequencyForChannelIndex(currentIndex); + + //Case 1: tuner channel completely aligns with indexed channel + if(currentIndexMin <= tunerChannel.getMinFrequency() && tunerChannel.getMaxFrequency() <= currentIndexMax) + { + indices.add(currentIndex); + } + //Case 2: tuner channel min frequency aligns and max frequency exceeds + else if(currentIndexMin <= tunerChannel.getMinFrequency() && + tunerChannel.getMinFrequency() < currentIndexMax && + tunerChannel.getMaxFrequency() > currentIndexMax) + { + indices.add(currentIndex); + } + //Case 3: tuner channel min frequency exceeds and max frequency aligns + else if(tunerChannel.getMinFrequency() < currentIndexMin && + tunerChannel.getMaxFrequency() > currentIndexMin && + tunerChannel.getMaxFrequency() <= currentIndexMax) + { + indices.add(currentIndex); + } + //Case 4: tuner channel encapsulates the indexed channel + else if(tunerChannel.getMinFrequency() < currentIndexMin && tunerChannel.getMaxFrequency() > currentIndexMax) + { + indices.add(currentIndex); + } + + currentIndex++; + } + + return indices; + } + + /** + * Calculates the center frequency for the channel identified by the index in the channel results array. + * + * @param index to determine the channel center frequency + * @return center frequency for the indexed channel + */ + public long getCenterFrequencyForChannelIndex(int index) + { + if(index < 0 || index >= getChannelCount()) + { + throw new IllegalArgumentException("Illegal channel index"); + } + + return getMinimumFrequency() + getHalfChannelBandwidth() + (index * getChannelBandwidth()); + } + + /** + * Calculates the minimum frequency band edge for the channel identified by the index in the channel results + * array. + * @param index to calculate the minimum band edge frequency for the channel. + * @return frequency of the minimum band edge + */ + public long getMinimumFrequencyForChannelIndex(int index) + { + if(index < 0 || index > getChannelCount()) + { + throw new IllegalArgumentException("Can't determine minimum frequency - illegal channel index [" + + index + "]"); + } + + return getMinimumFrequency() + (index * getChannelBandwidth()); + } + + /** + * Calculates the maximum frequency band edge for the channel identified by the index in the channel results + * array. + * @param index to calculate the maximum band edge frequency for the channel. + * @return frequency of the maximum band edge + */ + public long getMaximumFrequencyForChannelIndex(int index) + { + if(index < 0 || index > getChannelCount()) + { + throw new IllegalArgumentException("Can't determine maximum frequency - illegal channel index [" + + index + "]"); + } + + //Maximum frequency is the same as the minimum frequency of the next higher indexed channel + return getMinimumFrequency() + ((index + 1) * getChannelBandwidth()); + } + + public static void main(String[] args) + { + mLog.debug("Starting ...."); + + int channelCount = 10; + + ChannelizerConfiguration channelizerConfiguration = + new ChannelizerConfiguration(100062500, channelCount, 12500, 2.0); + + for(int x = 0; x < channelCount; x++) + { + mLog.debug("Channel " + x + ": " + channelizerConfiguration.getMinimumFrequencyForChannelIndex(x) + " - " + + channelizerConfiguration.getMaximumFrequencyForChannelIndex(x) + + " Center: " + channelizerConfiguration.getCenterFrequencyForChannelIndex(x)); + } + + TunerChannel tunerChannel = new TunerChannel(100006250, 12500); + + List indices = channelizerConfiguration.getPolyphaseChannelIndexes(tunerChannel); + + mLog.debug("Indices: " + indices); + + mLog.debug("Finished!"); + } +} diff --git a/src/dsp/filter/channelizer/ChannelizerViewer.java b/src/dsp/filter/channelizer/ChannelizerViewer.java index 09d80c3f..92ba830b 100644 --- a/src/dsp/filter/channelizer/ChannelizerViewer.java +++ b/src/dsp/filter/channelizer/ChannelizerViewer.java @@ -224,7 +224,7 @@ public class ChannelizerViewer extends JFrame mPolyphaseChannelizer = new ComplexPolyphaseChannelizer(taps, mChannelCount, CHANNEL_BANDWIDTH); mChannelDistributor = new ChannelDistributor(bufferSize, mChannelCount); - mPolyphaseChannelizer.setChannelDistributor(mChannelDistributor); +// mPolyphaseChannelizer.setChannelDistributor(mChannelDistributor); init(); } diff --git a/src/dsp/filter/channelizer/PolyphaseChannelSource.java b/src/dsp/filter/channelizer/PolyphaseChannelSource.java index 6050491b..eb15f6cb 100644 --- a/src/dsp/filter/channelizer/PolyphaseChannelSource.java +++ b/src/dsp/filter/channelizer/PolyphaseChannelSource.java @@ -20,16 +20,15 @@ package dsp.filter.channelizer; import org.slf4j.Logger; import org.slf4j.LoggerFactory; -import sample.Listener; +import sample.OverflowableTransferQueue; import sample.complex.ComplexBuffer; +import sample.real.IOverflowListener; import source.ISourceEventProcessor; import source.SourceEvent; import source.SourceException; import source.tuner.ComplexChannelSource; import source.tuner.TunerChannel; -import java.util.concurrent.ScheduledExecutorService; - public class PolyphaseChannelSource extends ComplexChannelSource { private final static Logger mLog = LoggerFactory.getLogger(PolyphaseChannelSource.class); @@ -40,6 +39,7 @@ public class PolyphaseChannelSource extends ComplexChannelSource private int mChannelSampleRate; private int mChannelCount; private int mOutputFrequencyCorrection; + private ChannelBufferProcessor mProcessor = new ChannelBufferProcessor(); /** * Polyphase channelizer channel implementation. Adapts the channel array output samples from the polyphase @@ -86,13 +86,11 @@ public class PolyphaseChannelSource extends ComplexChannelSource /** * Process the array samples for each of the output channels from the polyphase channelizer. - * @param channels + * @param channelsBuffer */ - public void processChannels(float[] channels) + public void processChannels(ComplexBuffer channelsBuffer) { - - //TODO we have to dispatch full buffers on a separate thread so that we don't bog down the channelizer - //TODO thread that is delivering the channel samples to us. + mProcessor.process(channelsBuffer); } /** @@ -198,4 +196,46 @@ public class PolyphaseChannelSource extends ComplexChannelSource { } + + public class ChannelBufferProcessor implements Runnable + { + private OverflowableTransferQueue mComplexChannelBuffers; + + public ChannelBufferProcessor() + { + mComplexChannelBuffers = new OverflowableTransferQueue<>(500, 10); + mComplexChannelBuffers.setOverflowListener(new IOverflowListener() + { + @Override + public void sourceOverflow(boolean overflow) + { + broadcastOverflowState(overflow); + } + }); + } + + /** + * Enqueues the polyphase analysis filter's channel output for post channelizer mixing, filtering + * and resampling operations and buffers output for dispatch to downstream processing. + * + * @param channelsBuffer to enqueue for processing + */ + public void process(ComplexBuffer channelsBuffer) + { + mComplexChannelBuffers.offer(channelsBuffer); + } + + @Override + public void run() + { + try + { + + } + catch(Throwable throwable) + { + mLog.error("Error while processing polyphase channel samples", throwable); + } + } + } } diff --git a/src/dsp/filter/channelizer/output/ChannelOutputProcessorFactory.java b/src/dsp/filter/channelizer/output/ChannelOutputProcessorFactory.java new file mode 100644 index 00000000..323fd8a2 --- /dev/null +++ b/src/dsp/filter/channelizer/output/ChannelOutputProcessorFactory.java @@ -0,0 +1,31 @@ +/******************************************************************************* + * 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.output; + +public class ChannelOutputProcessorFactory +{ + /** + * Factory for obtaining polyphase channelizer results processors to perform any post-processing + * that may be required to perform frequency translation and/or resampling of channel results. + */ + public static IPolyphaseChannelOutputProcessor getProcessor(int sampleRate, long frequency) + { + return null; + } +} diff --git a/src/dsp/filter/channelizer/output/IPolyphaseChannelOutputProcessor.java b/src/dsp/filter/channelizer/output/IPolyphaseChannelOutputProcessor.java new file mode 100644 index 00000000..f1d1cd9f --- /dev/null +++ b/src/dsp/filter/channelizer/output/IPolyphaseChannelOutputProcessor.java @@ -0,0 +1,39 @@ +/******************************************************************************* + * 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.output; + +import sample.complex.IComplexSampleListener; + +public interface IPolyphaseChannelOutputProcessor +{ + /** + * Process the channel output channel results from the polyphase channelizer filter + * + * @param channelResults to process + * @param listener to receive the processed channel results + */ + void process(float[] channelResults, IComplexSampleListener listener); + + /** + * Specifies the positive or negative frequency correction that should be applied to samples extracted from + * the polyphase channelizer output results. + * @param frequency + */ + void setFrequencyCorrection(long frequency); +} diff --git a/src/dsp/filter/channelizer/output/SingleChannelOutputProcessor.java b/src/dsp/filter/channelizer/output/SingleChannelOutputProcessor.java new file mode 100644 index 00000000..c0e8eec5 --- /dev/null +++ b/src/dsp/filter/channelizer/output/SingleChannelOutputProcessor.java @@ -0,0 +1,105 @@ +/******************************************************************************* + * 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.output; + +import dsp.mixer.Oscillator; +import sample.complex.Complex; +import sample.complex.IComplexSampleListener; + +import java.util.List; + +public class SingleChannelOutputProcessor implements IPolyphaseChannelOutputProcessor +{ + private int mChannelOffset; + private Oscillator mFrequencyCorrectionMixer; + + /** + * Processor to extract a single channel from a polyphase channelizer and produce an output I/Q complex sample + * from each polyphase channelizer output results array. + * + * @param sampleRate of the output sample stream. + * @param channelIndexes containing a single channel index. + */ + public SingleChannelOutputProcessor(int sampleRate, List channelIndexes) + { + mFrequencyCorrectionMixer = new Oscillator(0, sampleRate); + setPolyphaseChannelIndices(channelIndexes); + } + + /** + * Updates this processor to extract a specified channel index. + * @param indexes containing a single channel index value. + * @throws IllegalArgumentException if the list of indexes does not contain a single channel index. + */ + public void setPolyphaseChannelIndices(List indexes) + { + if(indexes.size() != 1) + { + throw new IllegalArgumentException("Single channel output processor requires a single index to " + + "process - provided indexes " + indexes.toString()); + } + + //Set the channelized output results offset to twice the channel index to account for each channel having + //an I/Q pair + mChannelOffset = indexes.get(0) * 2; + } + + /** + * Extracts the channel from the channel results array, applies frequency translation, and delivers the + * extracted frequency-translated channel sample to the complex sample listener. + * + * @param channelResults to process containing an array of channel I/Q sample pairs (I0,Q0,I1,Q1...In,Qn) + * @param listener to receive the extracted, frequency-translated channel results + */ + @Override + public void process(float[] channelResults, IComplexSampleListener listener) + { + if(channelResults.length >= (mChannelOffset + 1)) + { + //Only perform frequency translation if the frequency correction mixer has a non-zero frequency value + if(mFrequencyCorrectionMixer.isEnabled()) + { + mFrequencyCorrectionMixer.rotate(); + + float i = Complex.multiplyInphase(channelResults[mChannelOffset], channelResults[mChannelOffset + 1], + mFrequencyCorrectionMixer.inphase(), mFrequencyCorrectionMixer.quadrature()); + + float q = Complex.multiplyQuadrature(channelResults[mChannelOffset], channelResults[mChannelOffset + 1], + mFrequencyCorrectionMixer.inphase(), mFrequencyCorrectionMixer.quadrature()); + + listener.receive(i, q); + } + else + { + listener.receive(channelResults[mChannelOffset], channelResults[mChannelOffset + 1]); + } + } + } + + /** + * Specifies a frequency correction/translation value to be applied to channel samples output from the + * polyphase channelizer. + * @param frequency + */ + @Override + public void setFrequencyCorrection(long frequency) + { + mFrequencyCorrectionMixer.setFrequency(frequency); + } +} diff --git a/src/dsp/mixer/Oscillator.java b/src/dsp/mixer/Oscillator.java index 26c6ac6a..70383bec 100644 --- a/src/dsp/mixer/Oscillator.java +++ b/src/dsp/mixer/Oscillator.java @@ -1,135 +1,152 @@ /******************************************************************************* - * SDR Trunk - * Copyright (C) 2014 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 + * 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 sample.complex.Complex; import sample.complex.ComplexBuffer; public class Oscillator { - private final static Logger mLog = LoggerFactory.getLogger( Oscillator.class ); + private final static Logger mLog = LoggerFactory.getLogger(Oscillator.class); - private double mFrequency; - private double mSampleRate; + private boolean mEnabled = true; + private double mFrequency; + private double mSampleRate; - private Complex mAnglePerSample; - private Complex mCurrentAngle = new Complex( 0.0f, -1.0f ); + 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 - * - * @param frequency - positive or negative frequency in hertz - * @param sampleRate - in hertz - */ - public Oscillator( long frequency, int sampleRate ) - { - mSampleRate = (double)sampleRate; - mFrequency = (double)frequency; - - update(); - } - - /** - * Updates the internal values after a frequency or sample rate change - */ - private void update() - { - float anglePerSample = - (float)( 2.0d * Math.PI * mFrequency / mSampleRate ); + /** + * Oscillator 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, int sampleRate) + { + mEnabled = frequency != 0; - mAnglePerSample = Complex.fromAngle( anglePerSample ); - } + mSampleRate = (double)sampleRate; + mFrequency = (double)frequency; - /** - * Sets or changes the frequency of this oscillator - */ - public void setFrequency( long frequency ) - { - mFrequency = (double)frequency; - update(); - } + update(); + } - /** - * Sets or changes the sample rate of this oscillator - */ - public void setSampleRate( int sampleRate ) - { - mSampleRate = (double)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. + */ + public boolean isEnabled() + { + return mEnabled; + } - /** - * Steps the current angle by the angle per sample amount - */ - public void rotate() - { - mCurrentAngle.multiply( mAnglePerSample ); - mCurrentAngle.fastNormalize(); - } + /** + * Updates the internal values after a frequency or sample rate change + */ + private void update() + { + float anglePerSample = + (float)(2.0d * Math.PI * mFrequency / mSampleRate); - public float inphase() - { - return mCurrentAngle.inphase(); - } - - public float quadrature() - { - return mCurrentAngle.quadrature(); - } - - /** - * Get next complex sample - */ - public Complex getComplex() - { - return mCurrentAngle.copy(); - } + mAnglePerSample = Complex.fromAngle(anglePerSample); + } - /** - * Get the next float sample - */ - public float getFloat() - { - return mCurrentAngle.real(); - } + /** + * Sets or changes the frequency of this oscillator + */ + public void setFrequency(long frequency) + { + mEnabled = frequency != 0; + mFrequency = (double)frequency; + update(); + } - /** - * 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]; + /** + * Sets or changes the sample rate of this oscillator + */ + public void setSampleRate(int sampleRate) + { + mSampleRate = (double)sampleRate; + update(); + } - for(int x = 0; x < sampleCount; x++) - { - int index = 2 * x; - buffer[index] = inphase(); - buffer[index + 1] = quadrature(); + /** + * Steps the current angle by the angle per sample amount + */ + public void rotate() + { + mCurrentAngle.multiply(mAnglePerSample); + mCurrentAngle.fastNormalize(); + } - rotate(); - } + public float inphase() + { + return mCurrentAngle.inphase(); + } - return new ComplexBuffer(buffer); - } + public float quadrature() + { + return mCurrentAngle.quadrature(); + } + + /** + * Get next complex sample + */ + public Complex getComplex() + { + 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); + } } diff --git a/src/source/Source.java b/src/source/Source.java index 8df74669..56b61b84 100644 --- a/src/source/Source.java +++ b/src/source/Source.java @@ -1,19 +1,20 @@ /******************************************************************************* - * SDR Trunk - * Copyright (C) 2014 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 + * 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 source; From 9e275b7d6206ff415ac17d5a32f404ce8379e8ef Mon Sep 17 00:00:00 2001 From: Denny Date: Sat, 2 Sep 2017 06:55:58 -0400 Subject: [PATCH 19/87] WIP --- .../AbstractComplexPolyphaseChannelizer.java | 4 +--- .../ComplexPolyphaseChannelizerM2.java | 2 +- .../channelizer/PolyphaseChannelSource.java | 8 +++---- .../IPolyphaseChannelOutputProcessor.java | 4 ++-- .../output/SingleChannelOutputProcessor.java | 12 +++++----- .../complex/IComplexSampleListener.java | 24 +++++++++++++++++++ 6 files changed, 38 insertions(+), 16 deletions(-) create mode 100644 src/sample/complex/IComplexSampleListener.java diff --git a/src/dsp/filter/channelizer/AbstractComplexPolyphaseChannelizer.java b/src/dsp/filter/channelizer/AbstractComplexPolyphaseChannelizer.java index 273bf23e..fc7a0f06 100644 --- a/src/dsp/filter/channelizer/AbstractComplexPolyphaseChannelizer.java +++ b/src/dsp/filter/channelizer/AbstractComplexPolyphaseChannelizer.java @@ -92,11 +92,9 @@ public abstract class AbstractComplexPolyphaseChannelizer implements Listener mComplexChannelBuffers; + private OverflowableTransferQueue mComplexChannelBuffers; public ChannelBufferProcessor() { @@ -220,7 +220,7 @@ public class PolyphaseChannelSource extends ComplexChannelSource * * @param channelsBuffer to enqueue for processing */ - public void process(ComplexBuffer channelsBuffer) + public void process(float[] channelsBuffer) { mComplexChannelBuffers.offer(channelsBuffer); } @@ -230,7 +230,7 @@ public class PolyphaseChannelSource extends ComplexChannelSource { try { - +//TODO: implement distribution to each channel } catch(Throwable throwable) { diff --git a/src/dsp/filter/channelizer/output/IPolyphaseChannelOutputProcessor.java b/src/dsp/filter/channelizer/output/IPolyphaseChannelOutputProcessor.java index f1d1cd9f..64f911f8 100644 --- a/src/dsp/filter/channelizer/output/IPolyphaseChannelOutputProcessor.java +++ b/src/dsp/filter/channelizer/output/IPolyphaseChannelOutputProcessor.java @@ -25,10 +25,10 @@ public interface IPolyphaseChannelOutputProcessor /** * Process the channel output channel results from the polyphase channelizer filter * - * @param channelResults to process + * @param channels to process * @param listener to receive the processed channel results */ - void process(float[] channelResults, IComplexSampleListener listener); + void process(float[] channels, IComplexSampleListener listener); /** * Specifies the positive or negative frequency correction that should be applied to samples extracted from diff --git a/src/dsp/filter/channelizer/output/SingleChannelOutputProcessor.java b/src/dsp/filter/channelizer/output/SingleChannelOutputProcessor.java index c0e8eec5..a3713158 100644 --- a/src/dsp/filter/channelizer/output/SingleChannelOutputProcessor.java +++ b/src/dsp/filter/channelizer/output/SingleChannelOutputProcessor.java @@ -64,30 +64,30 @@ public class SingleChannelOutputProcessor implements IPolyphaseChannelOutputProc * Extracts the channel from the channel results array, applies frequency translation, and delivers the * extracted frequency-translated channel sample to the complex sample listener. * - * @param channelResults to process containing an array of channel I/Q sample pairs (I0,Q0,I1,Q1...In,Qn) + * @param channels to process containing an array of channel I/Q sample pairs (I0,Q0,I1,Q1...In,Qn) * @param listener to receive the extracted, frequency-translated channel results */ @Override - public void process(float[] channelResults, IComplexSampleListener listener) + public void process(float[] channels, IComplexSampleListener listener) { - if(channelResults.length >= (mChannelOffset + 1)) + if(channels.length >= (mChannelOffset + 1)) { //Only perform frequency translation if the frequency correction mixer has a non-zero frequency value if(mFrequencyCorrectionMixer.isEnabled()) { mFrequencyCorrectionMixer.rotate(); - float i = Complex.multiplyInphase(channelResults[mChannelOffset], channelResults[mChannelOffset + 1], + float i = Complex.multiplyInphase(channels[mChannelOffset], channels[mChannelOffset + 1], mFrequencyCorrectionMixer.inphase(), mFrequencyCorrectionMixer.quadrature()); - float q = Complex.multiplyQuadrature(channelResults[mChannelOffset], channelResults[mChannelOffset + 1], + float q = Complex.multiplyQuadrature(channels[mChannelOffset], channels[mChannelOffset + 1], mFrequencyCorrectionMixer.inphase(), mFrequencyCorrectionMixer.quadrature()); listener.receive(i, q); } else { - listener.receive(channelResults[mChannelOffset], channelResults[mChannelOffset + 1]); + listener.receive(channels[mChannelOffset], channels[mChannelOffset + 1]); } } } diff --git a/src/sample/complex/IComplexSampleListener.java b/src/sample/complex/IComplexSampleListener.java new file mode 100644 index 00000000..8f783c45 --- /dev/null +++ b/src/sample/complex/IComplexSampleListener.java @@ -0,0 +1,24 @@ +/******************************************************************************* + * 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 sample.complex; + +public interface IComplexSampleListener +{ + public void receive(float i, float q); +} From 9983d2119d6c2c7996a3420ed55f37fbd3119da1 Mon Sep 17 00:00:00 2001 From: Denny Date: Sun, 10 Sep 2017 04:42:35 -0400 Subject: [PATCH 20/87] WIP --- .../output/ChannelOutputProcessor.java | 96 +++++++++++++++++++ .../output/ChannelOutputProcessorFactory.java | 2 +- .../output/DoubleChannelOutputProcessor.java | 84 ++++++++++++++++ .../IPolyphaseChannelOutputProcessor.java | 9 +- .../output/SingleChannelOutputProcessor.java | 48 +++------- src/dsp/mixer/Oscillator.java | 2 +- 6 files changed, 199 insertions(+), 42 deletions(-) create mode 100644 src/dsp/filter/channelizer/output/ChannelOutputProcessor.java create mode 100644 src/dsp/filter/channelizer/output/DoubleChannelOutputProcessor.java diff --git a/src/dsp/filter/channelizer/output/ChannelOutputProcessor.java b/src/dsp/filter/channelizer/output/ChannelOutputProcessor.java new file mode 100644 index 00000000..a4c84694 --- /dev/null +++ b/src/dsp/filter/channelizer/output/ChannelOutputProcessor.java @@ -0,0 +1,96 @@ +/******************************************************************************* + * 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.output; + +import dsp.mixer.Oscillator; +import sample.complex.Complex; + +public abstract class ChannelOutputProcessor implements IPolyphaseChannelOutputProcessor +{ + private Oscillator mFrequencyCorrectionMixer; + + /** + * Base class for polyphase channelizer output channel processing. Provides built-in frequency translation + * oscillator support to apply frequency correction to the channel sample stream as requested by sample consumer. + * + * @param sampleRate of the output channel. This is used to match the oscillator's sample rate to the output + * channel sample rate for frequency translation/correction. + */ + public ChannelOutputProcessor(int sampleRate) + { + mFrequencyCorrectionMixer = new Oscillator(0, sampleRate); + } + + /** + * Provides frequency correction of the inphase component or simply returns the inphase argument value when no + * frequency correction value has been specified. + * + * @param inphase value of the sample to translate/correct + * @param quadrature value of the sample to translate/correct + * @return corrected inphase value + */ + protected float getFrequencyCorrectedInphase(float inphase, float quadrature) + { + //Only apply frequency correction if the oscillator is set to a non-zero frequency offset + if(mFrequencyCorrectionMixer.enabled()) + { + return Complex.multiplyInphase(inphase, quadrature, mFrequencyCorrectionMixer.inphase(), + mFrequencyCorrectionMixer.quadrature()); + } + else + { + return inphase; + } + } + + /** + * Provides frequency correction of the quadrature component or simply returns the quadrature argument value when no + * frequency correction value has been applied, + * + * @param inphase value of the sample to translate/correct + * @param quadrature value of the sample to translate/correct + * @return translated inphase value + */ + protected float getFrequencyCorrectedQuadrature(float inphase, float quadrature) + { + //Only apply frequency correction if the oscillator is set to a non-zero frequency offset + if(mFrequencyCorrectionMixer.enabled()) + { + return Complex.multiplyQuadrature(inphase, quadrature, mFrequencyCorrectionMixer.inphase(), + mFrequencyCorrectionMixer.quadrature()); + } + else + { + return quadrature; + } + } + + /** + * Specifies a frequency correction value to be applied to channel samples output from this polyphase channelizer + * output processor. + * + * @param frequencyCorrection offset value + */ + @Override + public void setFrequencyCorrection(long frequencyCorrection) + { + mFrequencyCorrectionMixer.setFrequency(frequencyCorrection); + } + +} diff --git a/src/dsp/filter/channelizer/output/ChannelOutputProcessorFactory.java b/src/dsp/filter/channelizer/output/ChannelOutputProcessorFactory.java index 323fd8a2..4dc141c2 100644 --- a/src/dsp/filter/channelizer/output/ChannelOutputProcessorFactory.java +++ b/src/dsp/filter/channelizer/output/ChannelOutputProcessorFactory.java @@ -21,7 +21,7 @@ package dsp.filter.channelizer.output; public class ChannelOutputProcessorFactory { /** - * Factory for obtaining polyphase channelizer results processors to perform any post-processing + * Factory for obtaining polyphase channelizer output processors to perform any post-processing * that may be required to perform frequency translation and/or resampling of channel results. */ public static IPolyphaseChannelOutputProcessor getProcessor(int sampleRate, long frequency) diff --git a/src/dsp/filter/channelizer/output/DoubleChannelOutputProcessor.java b/src/dsp/filter/channelizer/output/DoubleChannelOutputProcessor.java new file mode 100644 index 00000000..40df765a --- /dev/null +++ b/src/dsp/filter/channelizer/output/DoubleChannelOutputProcessor.java @@ -0,0 +1,84 @@ +/******************************************************************************* + * 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.output; + +import sample.complex.IComplexSampleListener; + +import java.util.List; + +public class DoubleChannelOutputProcessor extends ChannelOutputProcessor +{ + private int mChannelOffset1; + private int mChannelOffset2; + + /** + * Processor to extract two channels from a polyphase channelizer, synthesize/recombine the channels, apply + * frequency translation and frequency correction, down-sample by a factor of two and output an I/Q complex sample. + * + * @param sampleRate of the output sample stream. + * @param channelIndexes containing two channel indices. + */ + public DoubleChannelOutputProcessor(int sampleRate, List channelIndexes) + { + super(sampleRate); + setPolyphaseChannelIndices(channelIndexes); + } + + /** + * Updates this processor to extract the two specified channel indexes. + * + * @param indexes containing a single channel index value. + * @throws IllegalArgumentException if the list of indexes does not contain a single channel index. + */ + public void setPolyphaseChannelIndices(List indexes) + { + if(indexes.size() != 2) + { + throw new IllegalArgumentException("Double channel output processor requires two indexes to " + + "process - provided indexes " + indexes.toString()); + } + + //Set the channelized output results offsets to twice the channel index to account for each channel having + //an I/Q pair + mChannelOffset1 = indexes.get(0) * 2; + mChannelOffset2 = indexes.get(1) * 2; + } + + /** + * Extract the channel from the channel results array, apply frequency translation, and deliver the + * extracted frequency-corrected channel I/Q sample set to the complex sample listener. + * + * @param channels to process containing an array of channel I/Q sample pairs (I0,Q0,I1,Q1...In,Qn) + * @param listener to receive the extracted, frequency-translated channel results + */ + @Override + public void process(float[] channels, IComplexSampleListener listener) + { +// if(channels.length < mChannelOffset + 1) +// { +// throw new IllegalArgumentException("Polyphase channelizer output channels array is not large enough to " + +// "cover this single channel output processor with channel offset [" + mChannelOffset + "]"); +// } +// +// float i = channels[mChannelOffset]; +// float q = channels[mChannelOffset + 1]; + + listener.receive(getFrequencyCorrectedInphase(i, q), getFrequencyCorrectedQuadrature(i, q)); + } +} diff --git a/src/dsp/filter/channelizer/output/IPolyphaseChannelOutputProcessor.java b/src/dsp/filter/channelizer/output/IPolyphaseChannelOutputProcessor.java index 64f911f8..d5473ff1 100644 --- a/src/dsp/filter/channelizer/output/IPolyphaseChannelOutputProcessor.java +++ b/src/dsp/filter/channelizer/output/IPolyphaseChannelOutputProcessor.java @@ -31,9 +31,10 @@ public interface IPolyphaseChannelOutputProcessor void process(float[] channels, IComplexSampleListener listener); /** - * Specifies the positive or negative frequency correction that should be applied to samples extracted from - * the polyphase channelizer output results. - * @param frequency + * Specifies the frequency correction (+/-) that should be applied to samples extracted from the polyphase + * channelizer output results. + * + * @param frequencyCorrection correction value */ - void setFrequencyCorrection(long frequency); + void setFrequencyCorrection(long frequencyCorrection); } diff --git a/src/dsp/filter/channelizer/output/SingleChannelOutputProcessor.java b/src/dsp/filter/channelizer/output/SingleChannelOutputProcessor.java index a3713158..289382a4 100644 --- a/src/dsp/filter/channelizer/output/SingleChannelOutputProcessor.java +++ b/src/dsp/filter/channelizer/output/SingleChannelOutputProcessor.java @@ -18,16 +18,13 @@ ******************************************************************************/ package dsp.filter.channelizer.output; -import dsp.mixer.Oscillator; -import sample.complex.Complex; import sample.complex.IComplexSampleListener; import java.util.List; -public class SingleChannelOutputProcessor implements IPolyphaseChannelOutputProcessor +public class SingleChannelOutputProcessor extends ChannelOutputProcessor { private int mChannelOffset; - private Oscillator mFrequencyCorrectionMixer; /** * Processor to extract a single channel from a polyphase channelizer and produce an output I/Q complex sample @@ -38,12 +35,12 @@ public class SingleChannelOutputProcessor implements IPolyphaseChannelOutputProc */ public SingleChannelOutputProcessor(int sampleRate, List channelIndexes) { - mFrequencyCorrectionMixer = new Oscillator(0, sampleRate); + super(sampleRate); setPolyphaseChannelIndices(channelIndexes); } /** - * Updates this processor to extract a specified channel index. + * Updates this processor to extract a single, specified channel index. * @param indexes containing a single channel index value. * @throws IllegalArgumentException if the list of indexes does not contain a single channel index. */ @@ -61,8 +58,8 @@ public class SingleChannelOutputProcessor implements IPolyphaseChannelOutputProc } /** - * Extracts the channel from the channel results array, applies frequency translation, and delivers the - * extracted frequency-translated channel sample to the complex sample listener. + * Extract the channel from the channel results array, apply frequency translation, and deliver the + * extracted frequency-corrected channel I/Q sample set to the complex sample listener. * * @param channels to process containing an array of channel I/Q sample pairs (I0,Q0,I1,Q1...In,Qn) * @param listener to receive the extracted, frequency-translated channel results @@ -70,36 +67,15 @@ public class SingleChannelOutputProcessor implements IPolyphaseChannelOutputProc @Override public void process(float[] channels, IComplexSampleListener listener) { - if(channels.length >= (mChannelOffset + 1)) + if(channels.length < mChannelOffset + 1) { - //Only perform frequency translation if the frequency correction mixer has a non-zero frequency value - if(mFrequencyCorrectionMixer.isEnabled()) - { - mFrequencyCorrectionMixer.rotate(); - - float i = Complex.multiplyInphase(channels[mChannelOffset], channels[mChannelOffset + 1], - mFrequencyCorrectionMixer.inphase(), mFrequencyCorrectionMixer.quadrature()); - - float q = Complex.multiplyQuadrature(channels[mChannelOffset], channels[mChannelOffset + 1], - mFrequencyCorrectionMixer.inphase(), mFrequencyCorrectionMixer.quadrature()); - - listener.receive(i, q); - } - else - { - listener.receive(channels[mChannelOffset], channels[mChannelOffset + 1]); - } + throw new IllegalArgumentException("Polyphase channelizer output channels array is not large enough to " + + "cover this single channel output processor with channel offset [" + mChannelOffset + "]"); } - } - /** - * Specifies a frequency correction/translation value to be applied to channel samples output from the - * polyphase channelizer. - * @param frequency - */ - @Override - public void setFrequencyCorrection(long frequency) - { - mFrequencyCorrectionMixer.setFrequency(frequency); + float i = channels[mChannelOffset]; + float q = channels[mChannelOffset + 1]; + + listener.receive(getFrequencyCorrectedInphase(i, q), getFrequencyCorrectedQuadrature(i, q)); } } diff --git a/src/dsp/mixer/Oscillator.java b/src/dsp/mixer/Oscillator.java index 70383bec..865f4bcf 100644 --- a/src/dsp/mixer/Oscillator.java +++ b/src/dsp/mixer/Oscillator.java @@ -58,7 +58,7 @@ public class Oscillator * * @return true if the current frequency value is non-zero. */ - public boolean isEnabled() + public boolean enabled() { return mEnabled; } From 27461df7f089c4e982b623a228bc92ba143533f0 Mon Sep 17 00:00:00 2001 From: Denny Date: Mon, 18 Sep 2017 04:15:04 -0400 Subject: [PATCH 21/87] WIP on 2 channel output processor --- ...nelOutputProcessor.java => OneChannelOutputProcessor.java} | 4 ++-- ...nelOutputProcessor.java => TwoChannelOutputProcessor.java} | 4 ++-- 2 files changed, 4 insertions(+), 4 deletions(-) rename src/dsp/filter/channelizer/output/{SingleChannelOutputProcessor.java => OneChannelOutputProcessor.java} (95%) rename src/dsp/filter/channelizer/output/{DoubleChannelOutputProcessor.java => TwoChannelOutputProcessor.java} (95%) diff --git a/src/dsp/filter/channelizer/output/SingleChannelOutputProcessor.java b/src/dsp/filter/channelizer/output/OneChannelOutputProcessor.java similarity index 95% rename from src/dsp/filter/channelizer/output/SingleChannelOutputProcessor.java rename to src/dsp/filter/channelizer/output/OneChannelOutputProcessor.java index 289382a4..f162bcc4 100644 --- a/src/dsp/filter/channelizer/output/SingleChannelOutputProcessor.java +++ b/src/dsp/filter/channelizer/output/OneChannelOutputProcessor.java @@ -22,7 +22,7 @@ import sample.complex.IComplexSampleListener; import java.util.List; -public class SingleChannelOutputProcessor extends ChannelOutputProcessor +public class OneChannelOutputProcessor extends ChannelOutputProcessor { private int mChannelOffset; @@ -33,7 +33,7 @@ public class SingleChannelOutputProcessor extends ChannelOutputProcessor * @param sampleRate of the output sample stream. * @param channelIndexes containing a single channel index. */ - public SingleChannelOutputProcessor(int sampleRate, List channelIndexes) + public OneChannelOutputProcessor(int sampleRate, List channelIndexes) { super(sampleRate); setPolyphaseChannelIndices(channelIndexes); diff --git a/src/dsp/filter/channelizer/output/DoubleChannelOutputProcessor.java b/src/dsp/filter/channelizer/output/TwoChannelOutputProcessor.java similarity index 95% rename from src/dsp/filter/channelizer/output/DoubleChannelOutputProcessor.java rename to src/dsp/filter/channelizer/output/TwoChannelOutputProcessor.java index 40df765a..970777f3 100644 --- a/src/dsp/filter/channelizer/output/DoubleChannelOutputProcessor.java +++ b/src/dsp/filter/channelizer/output/TwoChannelOutputProcessor.java @@ -22,7 +22,7 @@ import sample.complex.IComplexSampleListener; import java.util.List; -public class DoubleChannelOutputProcessor extends ChannelOutputProcessor +public class TwoChannelOutputProcessor extends ChannelOutputProcessor { private int mChannelOffset1; private int mChannelOffset2; @@ -34,7 +34,7 @@ public class DoubleChannelOutputProcessor extends ChannelOutputProcessor * @param sampleRate of the output sample stream. * @param channelIndexes containing two channel indices. */ - public DoubleChannelOutputProcessor(int sampleRate, List channelIndexes) + public TwoChannelOutputProcessor(int sampleRate, List channelIndexes) { super(sampleRate); setPolyphaseChannelIndices(channelIndexes); From c21f3fca238304530d77155a40822e24da711d46 Mon Sep 17 00:00:00 2001 From: Denny Date: Sun, 15 Oct 2017 07:19:36 -0400 Subject: [PATCH 22/87] Resolves issue from merging master onto branch. --- src/source/wave/RealWaveSource.java | 3 +-- 1 file changed, 1 insertion(+), 2 deletions(-) diff --git a/src/source/wave/RealWaveSource.java b/src/source/wave/RealWaveSource.java index 722feead..3545efc2 100644 --- a/src/source/wave/RealWaveSource.java +++ b/src/source/wave/RealWaveSource.java @@ -26,7 +26,7 @@ import sample.real.RealBuffer; import source.IControllableFileSource; import source.IFrameLocationListener; import source.RealSource; -import source.SourceEvent +import source.SourceEvent; import javax.sound.sampled.AudioFormat; import javax.sound.sampled.AudioInputStream; @@ -129,7 +129,6 @@ public class RealWaveSource extends RealSource implements IControllableFileSourc @Override public long getFrameCount() throws IOException { - // TODO Auto-generated method stub return 0; } From 3be65d5e9a31bb906daadd754060785662cb3cb1 Mon Sep 17 00:00:00 2001 From: Denny Date: Sun, 15 Oct 2017 07:26:17 -0400 Subject: [PATCH 23/87] Resolves issue from merging master branch into polyphase channelizer branch. --- .../filter/channelizer/PolyphaseChannelSource.java | 14 ++++++++++++++ .../output/TwoChannelOutputProcessor.java | 2 +- src/instrument/tap/stream/BinaryTapViewPanel.java | 2 +- src/module/ProcessingChain.java | 7 ++++++- 4 files changed, 22 insertions(+), 3 deletions(-) diff --git a/src/dsp/filter/channelizer/PolyphaseChannelSource.java b/src/dsp/filter/channelizer/PolyphaseChannelSource.java index 8a36d9be..cf6914d5 100644 --- a/src/dsp/filter/channelizer/PolyphaseChannelSource.java +++ b/src/dsp/filter/channelizer/PolyphaseChannelSource.java @@ -18,8 +18,10 @@ ******************************************************************************/ package dsp.filter.channelizer; +import channel.heartbeat.Heartbeat; import org.slf4j.Logger; import org.slf4j.LoggerFactory; +import sample.Listener; import sample.OverflowableTransferQueue; import sample.complex.ComplexBuffer; import sample.real.IOverflowListener; @@ -184,6 +186,18 @@ public class PolyphaseChannelSource extends ComplexChannelSource mLog.debug("Request for frequency correction: " + correction + " -- not yet implemented"); } + @Override + public void setHeartbeatListener(Listener listener) + { + //TODO: implement this + } + + @Override + public void removeHeartbeatListener() + { + //TODO: implement this + } + @Override public void reset() { diff --git a/src/dsp/filter/channelizer/output/TwoChannelOutputProcessor.java b/src/dsp/filter/channelizer/output/TwoChannelOutputProcessor.java index 970777f3..56d943ad 100644 --- a/src/dsp/filter/channelizer/output/TwoChannelOutputProcessor.java +++ b/src/dsp/filter/channelizer/output/TwoChannelOutputProcessor.java @@ -79,6 +79,6 @@ public class TwoChannelOutputProcessor extends ChannelOutputProcessor // float i = channels[mChannelOffset]; // float q = channels[mChannelOffset + 1]; - listener.receive(getFrequencyCorrectedInphase(i, q), getFrequencyCorrectedQuadrature(i, q)); +// listener.receive(getFrequencyCorrectedInphase(i, q), getFrequencyCorrectedQuadrature(i, q)); } } diff --git a/src/instrument/tap/stream/BinaryTapViewPanel.java b/src/instrument/tap/stream/BinaryTapViewPanel.java index 07b72667..03ae5fdf 100644 --- a/src/instrument/tap/stream/BinaryTapViewPanel.java +++ b/src/instrument/tap/stream/BinaryTapViewPanel.java @@ -93,7 +93,7 @@ public class BinaryTapViewPanel extends TapViewPanel * Paints a polyline representing a boolean value with a 0 or 1 label in * the middle of it * - * @param graphics + * @param graphics object * @param index - index of the sample int the sample buffer * @param value - bit value, 0 or 1 */ diff --git a/src/module/ProcessingChain.java b/src/module/ProcessingChain.java index 9047051d..6736aece 100644 --- a/src/module/ProcessingChain.java +++ b/src/module/ProcessingChain.java @@ -320,7 +320,12 @@ public class ProcessingChain implements IChannelEventListener if(module instanceof ISourceEventListener) { - mFrequencyChangeEventBroadcaster.addListener(frequencyChangeEventListener); + Listener listener = ((ISourceEventListener)module).getSourceEventListener(); + + if(listener != null) + { + mFrequencyChangeEventBroadcaster.addListener(listener); + } } if(module instanceof IHeartbeatListener) From a00ec42230e0e2c0ecbc199fc408e23f1d6c1bc0 Mon Sep 17 00:00:00 2001 From: Denny Date: Sun, 22 Oct 2017 06:37:24 -0400 Subject: [PATCH 24/87] Cleanup from master merge --- src/source/tuner/TunerController.java | 9 ++++----- 1 file changed, 4 insertions(+), 5 deletions(-) diff --git a/src/source/tuner/TunerController.java b/src/source/tuner/TunerController.java index e9d41a41..be32e591 100644 --- a/src/source/tuner/TunerController.java +++ b/src/source/tuner/TunerController.java @@ -19,20 +19,19 @@ package source.tuner; import org.slf4j.Logger; import org.slf4j.LoggerFactory; -import source.SourceException; -import source.tuner.configuration.TunerConfiguration; +import source.ISourceEventProcessor; import source.SourceEvent; import source.SourceEvent.Event; +import source.SourceException; +import source.tuner.configuration.TunerConfiguration; import source.tuner.frequency.FrequencyController; import source.tuner.frequency.FrequencyController.Tunable; -import source.ISourceEventProcessor; -public abstract class TunerController implements Tunable, ISourceEventProcessor import java.util.SortedSet; import java.util.concurrent.ConcurrentSkipListSet; import java.util.concurrent.RejectedExecutionException; -public abstract class TunerController implements Tunable, IFrequencyChangeProcessor +public abstract class TunerController implements Tunable, ISourceEventProcessor { private final static Logger mLog = LoggerFactory.getLogger(TunerController.class); From 2c3be02939ebfcb105b42514e43e7eff4543fc1b Mon Sep 17 00:00:00 2001 From: Denny Date: Mon, 23 Oct 2017 08:43:53 -0400 Subject: [PATCH 25/87] WIP - polyphase channelizer --- .../AbstractComplexPolyphaseChannelizer.java | 8 ++ .../channelizer/ChannelizerConfiguration.java | 6 +- .../filter/channelizer/ChannelizerViewer.java | 40 ++++++++-- .../ComplexPolyphaseChannelizerM2.java | 2 +- ...izer.java => PolyphaseChannelManager.java} | 80 ++++++++++++------- .../channelizer/PolyphaseChannelSource.java | 8 +- .../output/ChannelOutputProcessorFactory.java | 31 ------- .../output/TwoChannelOutputProcessor.java | 4 +- src/source/tuner/Tuner.java | 10 +++ 9 files changed, 119 insertions(+), 70 deletions(-) rename src/dsp/filter/channelizer/{Channelizer.java => PolyphaseChannelManager.java} (69%) delete mode 100644 src/dsp/filter/channelizer/output/ChannelOutputProcessorFactory.java diff --git a/src/dsp/filter/channelizer/AbstractComplexPolyphaseChannelizer.java b/src/dsp/filter/channelizer/AbstractComplexPolyphaseChannelizer.java index fc7a0f06..e2b3b74f 100644 --- a/src/dsp/filter/channelizer/AbstractComplexPolyphaseChannelizer.java +++ b/src/dsp/filter/channelizer/AbstractComplexPolyphaseChannelizer.java @@ -120,6 +120,14 @@ public abstract class AbstractComplexPolyphaseChannelizer implements Listener getSourceEventListener() { diff --git a/src/dsp/filter/channelizer/ChannelizerConfiguration.java b/src/dsp/filter/channelizer/ChannelizerConfiguration.java index be89ac42..b206da9a 100644 --- a/src/dsp/filter/channelizer/ChannelizerConfiguration.java +++ b/src/dsp/filter/channelizer/ChannelizerConfiguration.java @@ -139,7 +139,7 @@ public class ChannelizerConfiguration } /** - * Channelizer input sample rate + * PolyphaseChannelManager input sample rate * @return input sample rate in hertz */ public int getSampleRate() @@ -148,7 +148,7 @@ public class ChannelizerConfiguration } /** - * Channelizer input minimum frequency + * PolyphaseChannelManager input minimum frequency * @return minimum frequency in hertz */ public long getMinimumFrequency() @@ -157,7 +157,7 @@ public class ChannelizerConfiguration } /** - * Channelizer input maximum frequency + * PolyphaseChannelManager input maximum frequency * @return maximum frequency in hertz */ public long getMaximumFrequency() diff --git a/src/dsp/filter/channelizer/ChannelizerViewer.java b/src/dsp/filter/channelizer/ChannelizerViewer.java index 92ba830b..c81b06f2 100644 --- a/src/dsp/filter/channelizer/ChannelizerViewer.java +++ b/src/dsp/filter/channelizer/ChannelizerViewer.java @@ -29,12 +29,16 @@ import sample.Listener; import sample.SampleType; import sample.complex.ComplexBuffer; import settings.SettingsManager; +import source.ISourceEventProcessor; +import source.SourceException; +import source.tuner.TunerChannel; import source.tuner.configuration.TunerConfigurationModel; import source.SourceEvent; import spectrum.DFTProcessor; import spectrum.DFTSize; import spectrum.SpectrumPanel; import spectrum.converter.ComplexDecibelConverter; +import util.ThreadPool; import javax.swing.*; import javax.swing.event.ChangeEvent; @@ -137,7 +141,7 @@ public class ChannelizerViewer extends JFrame { if(mPrimarySpectrumPanel == null) { - mPrimarySpectrumPanel = new ChannelPanel(mSettingsManager, mSampleRate); +// mPrimarySpectrumPanel = new ChannelPanel(mSettingsManager, mSampleRate); mPrimarySpectrumPanel.setPreferredSize(new Dimension(1000, 200)); mPrimarySpectrumPanel.setDFTSize(mMainPanelDFTSize); } @@ -233,9 +237,14 @@ public class ChannelizerViewer extends JFrame { setLayout(new MigLayout("insets 0 0 0 0", "fill", "fill")); + long spectralBandwidth = mChannelCount * CHANNEL_BANDWIDTH; + long halfSpectralBandwidth = spectralBandwidth / 2; + for(int x = 0; x < mChannelCount; x++) { - ChannelPanel channelPanel = new ChannelPanel(mSettingsManager, CHANNEL_BANDWIDTH * 2); + //place the channels left and right of 10.0 MHz + long frequency = 10000000l + ((x * CHANNEL_BANDWIDTH) - halfSpectralBandwidth); + ChannelPanel channelPanel = new ChannelPanel(mSettingsManager, CHANNEL_BANDWIDTH * 2, frequency, CHANNEL_BANDWIDTH); channelPanel.setDFTSize(mChannelPanelDFTSize); if(x % mChannelsPerRow == mChannelsPerRow - 1) @@ -247,6 +256,8 @@ public class ChannelizerViewer extends JFrame add(channelPanel, "grow,push"); } +// channelPanel.getPolyphaseChannelSource() + mChannelDistributor.addListener(x, channelPanel); } } @@ -258,13 +269,14 @@ public class ChannelizerViewer extends JFrame } } - public class ChannelPanel extends JPanel implements Listener + public class ChannelPanel extends JPanel implements Listener, ISourceEventProcessor { + private PolyphaseChannelSource mPolyphaseChannelSource; private DFTProcessor mDFTProcessor = new DFTProcessor(SampleType.COMPLEX); private ComplexDecibelConverter mComplexDecibelConverter = new ComplexDecibelConverter(); private SpectrumPanel mSpectrumPanel; - public ChannelPanel(SettingsManager settingsManager, int sampleRate) + public ChannelPanel(SettingsManager settingsManager, int sampleRate, long frequency, int bandwidth) { setLayout(new MigLayout("insets 0 0 0 0", "[grow,fill]", "[grow,fill]")); mSpectrumPanel = new SpectrumPanel(settingsManager); @@ -273,6 +285,16 @@ public class ChannelizerViewer extends JFrame mDFTProcessor.addConverter(mComplexDecibelConverter); mDFTProcessor.process(SourceEvent.sampleRateChange(sampleRate)); mComplexDecibelConverter.addListener(mSpectrumPanel); + + TunerChannel tunerChannel = new TunerChannel(frequency, bandwidth); + mPolyphaseChannelSource = new PolyphaseChannelSource(this, tunerChannel); + mPolyphaseChannelSource.setListener(mDFTProcessor); + mPolyphaseChannelSource.start(ThreadPool.SCHEDULED); + } + + public PolyphaseChannelSource getPolyphaseChannelSource() + { + return mPolyphaseChannelSource; } public void setDFTSize(DFTSize dftSize) @@ -285,6 +307,12 @@ public class ChannelizerViewer extends JFrame { mDFTProcessor.receive(complexBuffer); } + + @Override + public void process(SourceEvent event) throws SourceException + { + mLog.debug("Source Event! Add handler support for this to channelizer viewer"); + } } public class SampleGenerationTask implements Runnable @@ -298,8 +326,8 @@ public class ChannelizerViewer extends JFrame public static void main(String[] args) { - int channelCount = 4; - int channelsPerRow = 4; + int channelCount = 160; + int channelsPerRow = 20; int tapsPerChannel = 17; final ChannelizerViewer frame = new ChannelizerViewer(channelCount, channelsPerRow, tapsPerChannel); diff --git a/src/dsp/filter/channelizer/ComplexPolyphaseChannelizerM2.java b/src/dsp/filter/channelizer/ComplexPolyphaseChannelizerM2.java index 12ba4832..2eaa64b6 100644 --- a/src/dsp/filter/channelizer/ComplexPolyphaseChannelizerM2.java +++ b/src/dsp/filter/channelizer/ComplexPolyphaseChannelizerM2.java @@ -46,7 +46,7 @@ public class ComplexPolyphaseChannelizerM2 extends AbstractComplexPolyphaseChann * * @param taps of a low-pass filter designed for the inbound sample rate with a cutoff frequency * equal to the channel bandwidth (sample rate / filters). If you need to synthesize (combine two or more - * filte outputs) a new bandwidth signal from the outputs of this filter, then the filter should be designed + * filter outputs) a new bandwidth signal from the outputs of this filter, then the filter should be designed * as a nyquist filter with -6 dB attenuation at the channel bandwidth cutoff frequency * @param channels - number of filters/channels to output. Since this filter bank oversamples each filter * output, this number must be even (divisible by the oversample rate). diff --git a/src/dsp/filter/channelizer/Channelizer.java b/src/dsp/filter/channelizer/PolyphaseChannelManager.java similarity index 69% rename from src/dsp/filter/channelizer/Channelizer.java rename to src/dsp/filter/channelizer/PolyphaseChannelManager.java index 1acb860d..2b1416c0 100644 --- a/src/dsp/filter/channelizer/Channelizer.java +++ b/src/dsp/filter/channelizer/PolyphaseChannelManager.java @@ -18,6 +18,9 @@ ******************************************************************************/ package dsp.filter.channelizer; +import dsp.filter.channelizer.output.IPolyphaseChannelOutputProcessor; +import dsp.filter.channelizer.output.OneChannelOutputProcessor; +import dsp.filter.channelizer.output.TwoChannelOutputProcessor; import org.slf4j.Logger; import org.slf4j.LoggerFactory; import sample.Listener; @@ -29,34 +32,44 @@ import source.tuner.TunerChannel; import java.util.List; import java.util.concurrent.CopyOnWriteArrayList; +import java.util.concurrent.atomic.AtomicInteger; -public class Channelizer implements ISourceEventProcessor, Listener +public class PolyphaseChannelManager implements ISourceEventProcessor, Listener { - private final static Logger mLog = LoggerFactory.getLogger(Channelizer.class); + private final static Logger mLog = LoggerFactory.getLogger(PolyphaseChannelManager.class); private static final int CHANNEL_BANDWIDTH = 12500; + private static final int CHANNEL_SAMPLE_RATE = CHANNEL_BANDWIDTH * 2; private static final double CHANNEL_OVERSAMPLING = 2.0; private Tuner mTuner; private List mChannelSources = new CopyOnWriteArrayList<>(); private ChannelizerConfiguration mChannelizerConfiguration; + private ComplexPolyphaseChannelizerM2 mPolyphaseChannelizer; /** - * Provides access to Digital Drop Channel (DDC) sources from the tuner. Incorporates a polyphase channelizer + * Provides access to Digital Drop Channel (DDC) sources from a tuner. Incorporates a polyphase channelizer * with downstream channel and upstream tuner management. * * @param tuner providing broadband sample buffers */ - public Channelizer(Tuner tuner) + public PolyphaseChannelManager(Tuner tuner) { - mTuner = tuner; + if(tuner == null) + { + throw new IllegalArgumentException("Tuner argument cannot be null"); + } int sampleRate = mTuner.getTunerController().getSampleRate(); - if(sampleRate % 25000 != 0) + //Ensure that tuner sample rate is a multiple of channel sample rate since polyphase channelizer is M2 + if(sampleRate % (CHANNEL_SAMPLE_RATE) != 0) { - throw new IllegalArgumentException("Tuner sample rate [" + sampleRate + "] must be a multiple of 25 kHz"); + throw new IllegalArgumentException("Tuner sample rate [" + sampleRate + "] must be a multiple of " + + CHANNEL_SAMPLE_RATE + " Hz"); } + mTuner = tuner; + int channelCount = sampleRate / CHANNEL_BANDWIDTH; long frequency = mTuner.getTunerController().getFrequency(); @@ -75,22 +88,41 @@ public class Channelizer implements ISourceEventProcessor, Listener */ public Source getChannel(TunerChannel tunerChannel) { + PolyphaseChannelSource channelSource = null; + + IPolyphaseChannelOutputProcessor outputProcessor = null; + try { List polyphaseIndexes = mChannelizerConfiguration.getPolyphaseChannelIndexes(tunerChannel); - - + switch(polyphaseIndexes.size()) + { + case 1: + outputProcessor = new OneChannelOutputProcessor(CHANNEL_SAMPLE_RATE, polyphaseIndexes); + break; + case 2: + outputProcessor = new TwoChannelOutputProcessor(CHANNEL_SAMPLE_RATE, polyphaseIndexes); + break; + default: + //TODO: create output processor for greater than 2 input channels + break; + } } catch(IllegalArgumentException iae) { mLog.info("Can't provide DDC for " + tunerChannel.toString() + " due to channelizer frequency [" + - " due to channelizer frequency [" + mChannelizerConfiguration.getCenterFrequency() + - "] and sample rate [" + (mChannelizerConfiguration.getChannelCount() * - mChannelizerConfiguration.getChannelBandwidth()) + "]"); + mChannelizerConfiguration.getCenterFrequency() + "] and sample rate [" + + (mChannelizerConfiguration.getChannelCount() * mChannelizerConfiguration.getChannelBandwidth()) + "]"); } - return null; + if(outputProcessor != null) + { + channelSource = new PolyphaseChannelSource(tunerChannel, outputProcessor, this); + mChannelSources.add(channelSource); + } + + return channelSource; } /** @@ -101,17 +133,17 @@ public class Channelizer implements ISourceEventProcessor, Listener */ private void startChannelSource(PolyphaseChannelSource channelSource) { - if(mChannelSources.isEmpty()) - { - //TODO: start the tuner - } + mPolyphaseChannelizer.addChannel(channelSource); - mChannelSources.add(channelSource); + if(mPolyphaseChannelizer.getRegisteredChannelCount() == 1) + { + mTuner.addListener(mPolyphaseChannelizer); + } } /** * Stops/removes the channel source from receiving channelized sample buffers and deregisters from the tuner - * if this is the last channel being sourced. + * when this is the last channel being sourced. * * @param channelSource to stop */ @@ -119,15 +151,9 @@ public class Channelizer implements ISourceEventProcessor, Listener { mChannelSources.remove(channelSource); - if(mChannelSources.isEmpty()) + if(mPolyphaseChannelizer.getRegisteredChannelCount() == 0) { - //TODO: stop the tuner - } - - if(mTuner != null) - { - //TODO: change the tuner to accept a remove(TunerChannel) method -// mTuner.remove(channel.getTunerChannel()); + mTuner.removeListener(mPolyphaseChannelizer); } } diff --git a/src/dsp/filter/channelizer/PolyphaseChannelSource.java b/src/dsp/filter/channelizer/PolyphaseChannelSource.java index cf6914d5..aa2a4767 100644 --- a/src/dsp/filter/channelizer/PolyphaseChannelSource.java +++ b/src/dsp/filter/channelizer/PolyphaseChannelSource.java @@ -19,6 +19,7 @@ package dsp.filter.channelizer; import channel.heartbeat.Heartbeat; +import dsp.filter.channelizer.output.IPolyphaseChannelOutputProcessor; import org.slf4j.Logger; import org.slf4j.LoggerFactory; import sample.Listener; @@ -36,6 +37,7 @@ public class PolyphaseChannelSource extends ComplexChannelSource private final static Logger mLog = LoggerFactory.getLogger(PolyphaseChannelSource.class); private TunerChannel mTunerChannel; + private IPolyphaseChannelOutputProcessor mPolyphaseChannelOutputProcessor; private long mTunerCenterFrequency; private int mUpstreamSampleRate; private int mChannelSampleRate; @@ -49,12 +51,16 @@ public class PolyphaseChannelSource extends ComplexChannelSource * translated and decimated to a single channel. * * @param tunerChannel - requested output tuner channel frequency and bandwidth. + * @param outputProcessor - to process polyphase channelizer channel results into a channel stream + * @param upstreamSourceEventProcessor to handle requests for start/stop sample flow and frequency corrections */ - public PolyphaseChannelSource(ISourceEventProcessor upstreamSourceEventProcessor, TunerChannel tunerChannel) + public PolyphaseChannelSource(TunerChannel tunerChannel, IPolyphaseChannelOutputProcessor outputProcessor, + ISourceEventProcessor upstreamSourceEventProcessor) { super(upstreamSourceEventProcessor); mTunerChannel = tunerChannel; + mPolyphaseChannelOutputProcessor = outputProcessor; } /** diff --git a/src/dsp/filter/channelizer/output/ChannelOutputProcessorFactory.java b/src/dsp/filter/channelizer/output/ChannelOutputProcessorFactory.java deleted file mode 100644 index 4dc141c2..00000000 --- a/src/dsp/filter/channelizer/output/ChannelOutputProcessorFactory.java +++ /dev/null @@ -1,31 +0,0 @@ -/******************************************************************************* - * 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.output; - -public class ChannelOutputProcessorFactory -{ - /** - * Factory for obtaining polyphase channelizer output processors to perform any post-processing - * that may be required to perform frequency translation and/or resampling of channel results. - */ - public static IPolyphaseChannelOutputProcessor getProcessor(int sampleRate, long frequency) - { - return null; - } -} diff --git a/src/dsp/filter/channelizer/output/TwoChannelOutputProcessor.java b/src/dsp/filter/channelizer/output/TwoChannelOutputProcessor.java index 56d943ad..b242ce29 100644 --- a/src/dsp/filter/channelizer/output/TwoChannelOutputProcessor.java +++ b/src/dsp/filter/channelizer/output/TwoChannelOutputProcessor.java @@ -36,7 +36,9 @@ public class TwoChannelOutputProcessor extends ChannelOutputProcessor */ public TwoChannelOutputProcessor(int sampleRate, List channelIndexes) { - super(sampleRate); + //Set the frequency correction oscillator to 2 x output sample rate since we'll be correcting the frequency + //after synthesizing both input channels + super(sampleRate * 2); setPolyphaseChannelIndices(channelIndexes); } diff --git a/src/source/tuner/Tuner.java b/src/source/tuner/Tuner.java index 730a0bab..41cf7d26 100644 --- a/src/source/tuner/Tuner.java +++ b/src/source/tuner/Tuner.java @@ -172,6 +172,16 @@ public abstract class Tuner implements ITunerChannelProvider mSampleListeners.remove(listener); } + /** + * Indicates if the listener is currently registered with this tuner + * @param listener of complex buffer samples + * @return true if the listener is registered + */ + public boolean hasListener(Listener listener) + { + return mSampleListeners.contains(listener); + } + /** * Broadcasts the samples to all registered listeners */ From 0181926fe3158e9fc2d49052e67612313362ab29 Mon Sep 17 00:00:00 2001 From: Denny Date: Mon, 30 Oct 2017 19:12:14 -0400 Subject: [PATCH 26/87] WIP - polyphase channelizer --- .idea/vcs.xml | 10 ++ ...ation.java => ChannelIndexCalculator.java} | 23 ++-- .../channelizer/PolyphaseChannelManager.java | 123 ++++++++++++------ .../channelizer/PolyphaseChannelSource.java | 27 ++++ .../output/ChannelOutputProcessor.java | 11 +- .../IPolyphaseChannelOutputProcessor.java | 14 ++ .../output/OneChannelOutputProcessor.java | 2 +- .../output/TwoChannelOutputProcessor.java | 2 +- 8 files changed, 159 insertions(+), 53 deletions(-) rename src/dsp/filter/channelizer/{ChannelizerConfiguration.java => ChannelIndexCalculator.java} (90%) diff --git a/.idea/vcs.xml b/.idea/vcs.xml index 35eb1ddf..0dc6f92c 100644 --- a/.idea/vcs.xml +++ b/.idea/vcs.xml @@ -1,5 +1,15 @@ + + + diff --git a/src/dsp/filter/channelizer/ChannelizerConfiguration.java b/src/dsp/filter/channelizer/ChannelIndexCalculator.java similarity index 90% rename from src/dsp/filter/channelizer/ChannelizerConfiguration.java rename to src/dsp/filter/channelizer/ChannelIndexCalculator.java index b206da9a..6714786c 100644 --- a/src/dsp/filter/channelizer/ChannelizerConfiguration.java +++ b/src/dsp/filter/channelizer/ChannelIndexCalculator.java @@ -25,9 +25,9 @@ import source.tuner.TunerChannel; import java.util.ArrayList; import java.util.List; -public class ChannelizerConfiguration +public class ChannelIndexCalculator { - private final static Logger mLog = LoggerFactory.getLogger(ChannelizerConfiguration.class); + private final static Logger mLog = LoggerFactory.getLogger(ChannelIndexCalculator.class); private long mCenterFrequency; private int mChannelCount; @@ -35,16 +35,15 @@ public class ChannelizerConfiguration private double mOversampling = 1.0; /** - * Configuration details for a polyphase channelizer that can be used to identify which polyphase - * channelizer output channel(s) should be processed to produce an output channel of a specified - * center frequency and channel sample rate/bandwidth. + * Calculates the channel index(es) from a polyphase channelizer output to source a DDC polyphase channel source + * with a specified channel center frequency and bandwidth. * * @param centerFrequency * @param channelCount * @param channelBandwidth * @param oversampling set to 1.0 for no oversample, or greater than 1.0 for oversampling */ - public ChannelizerConfiguration(long centerFrequency, int channelCount, int channelBandwidth, double oversampling) + public ChannelIndexCalculator(long centerFrequency, int channelCount, int channelBandwidth, double oversampling) { mCenterFrequency = centerFrequency; mChannelCount = channelCount; @@ -289,19 +288,19 @@ public class ChannelizerConfiguration int channelCount = 10; - ChannelizerConfiguration channelizerConfiguration = - new ChannelizerConfiguration(100062500, channelCount, 12500, 2.0); + ChannelIndexCalculator channelIndexCalculator = + new ChannelIndexCalculator(100062500, channelCount, 12500, 2.0); for(int x = 0; x < channelCount; x++) { - mLog.debug("Channel " + x + ": " + channelizerConfiguration.getMinimumFrequencyForChannelIndex(x) + " - " + - channelizerConfiguration.getMaximumFrequencyForChannelIndex(x) + - " Center: " + channelizerConfiguration.getCenterFrequencyForChannelIndex(x)); + mLog.debug("Channel " + x + ": " + channelIndexCalculator.getMinimumFrequencyForChannelIndex(x) + " - " + + channelIndexCalculator.getMaximumFrequencyForChannelIndex(x) + + " Center: " + channelIndexCalculator.getCenterFrequencyForChannelIndex(x)); } TunerChannel tunerChannel = new TunerChannel(100006250, 12500); - List indices = channelizerConfiguration.getPolyphaseChannelIndexes(tunerChannel); + List indices = channelIndexCalculator.getPolyphaseChannelIndexes(tunerChannel); mLog.debug("Indices: " + indices); diff --git a/src/dsp/filter/channelizer/PolyphaseChannelManager.java b/src/dsp/filter/channelizer/PolyphaseChannelManager.java index 2b1416c0..8765c7fd 100644 --- a/src/dsp/filter/channelizer/PolyphaseChannelManager.java +++ b/src/dsp/filter/channelizer/PolyphaseChannelManager.java @@ -32,7 +32,6 @@ import source.tuner.TunerChannel; import java.util.List; import java.util.concurrent.CopyOnWriteArrayList; -import java.util.concurrent.atomic.AtomicInteger; public class PolyphaseChannelManager implements ISourceEventProcessor, Listener { @@ -43,12 +42,16 @@ public class PolyphaseChannelManager implements ISourceEventProcessor, Listener< private Tuner mTuner; private List mChannelSources = new CopyOnWriteArrayList<>(); - private ChannelizerConfiguration mChannelizerConfiguration; + private ChannelIndexCalculator mChannelIndexCalculator; private ComplexPolyphaseChannelizerM2 mPolyphaseChannelizer; /** - * Provides access to Digital Drop Channel (DDC) sources from a tuner. Incorporates a polyphase channelizer - * with downstream channel and upstream tuner management. + * Polyphase Channel Manager is a DDC channel manager for a tuner. This class provides DDC polyphase channel + * sources and wraps a polyphase channelizer processing sample buffers produced by the tuner and distributing + * channelized sample buffers to each allocated DDC polyphase channel source. This class is responsible for + * monitoring the tuner for changes in center frequency and/or sample rate and updating active DDC polyphase channel + * sources accordingly. This class also monitors source event requests and notifications received from active DDC + * polyphase channel sources to adjust sample streams as required. * * @param tuner providing broadband sample buffers */ @@ -74,8 +77,8 @@ public class PolyphaseChannelManager implements ISourceEventProcessor, Listener< long frequency = mTuner.getTunerController().getFrequency(); - mChannelizerConfiguration = new ChannelizerConfiguration(frequency, channelCount, - CHANNEL_BANDWIDTH, CHANNEL_OVERSAMPLING); + mChannelIndexCalculator = new ChannelIndexCalculator(frequency, channelCount, CHANNEL_BANDWIDTH, + CHANNEL_OVERSAMPLING); mTuner.addSourceEventListener(this); } @@ -90,41 +93,42 @@ public class PolyphaseChannelManager implements ISourceEventProcessor, Listener< { PolyphaseChannelSource channelSource = null; - IPolyphaseChannelOutputProcessor outputProcessor = null; - try { - List polyphaseIndexes = mChannelizerConfiguration.getPolyphaseChannelIndexes(tunerChannel); + List polyphaseIndexes = mChannelIndexCalculator.getPolyphaseChannelIndexes(tunerChannel); - switch(polyphaseIndexes.size()) + IPolyphaseChannelOutputProcessor outputProcessor = getOutputProcessor(polyphaseIndexes); + + if(outputProcessor != null) { - case 1: - outputProcessor = new OneChannelOutputProcessor(CHANNEL_SAMPLE_RATE, polyphaseIndexes); - break; - case 2: - outputProcessor = new TwoChannelOutputProcessor(CHANNEL_SAMPLE_RATE, polyphaseIndexes); - break; - default: - //TODO: create output processor for greater than 2 input channels - break; + channelSource = new PolyphaseChannelSource(tunerChannel, outputProcessor, this); + mChannelSources.add(channelSource); } } catch(IllegalArgumentException iae) { mLog.info("Can't provide DDC for " + tunerChannel.toString() + " due to channelizer frequency [" + - mChannelizerConfiguration.getCenterFrequency() + "] and sample rate [" + - (mChannelizerConfiguration.getChannelCount() * mChannelizerConfiguration.getChannelBandwidth()) + "]"); - } - - if(outputProcessor != null) - { - channelSource = new PolyphaseChannelSource(tunerChannel, outputProcessor, this); - mChannelSources.add(channelSource); + mChannelIndexCalculator.getCenterFrequency() + "] and sample rate [" + + (mChannelIndexCalculator.getChannelCount() * mChannelIndexCalculator.getChannelBandwidth()) + "]"); } return channelSource; } + private IPolyphaseChannelOutputProcessor getOutputProcessor(List indexes) throws IllegalArgumentException + { + switch(indexes.size()) + { + case 1: + return new OneChannelOutputProcessor(CHANNEL_SAMPLE_RATE, indexes); + case 2: + return new TwoChannelOutputProcessor(CHANNEL_SAMPLE_RATE, indexes); + default: + //TODO: create output processor for greater than 2 input channels + return null; + } + } + /** * Starts/adds the channel source to receive channelized sample buffers, registering with the tuner to receive * sample buffers when this is the first channel. @@ -157,6 +161,9 @@ public class PolyphaseChannelManager implements ISourceEventProcessor, Listener< } } + /** + * Processes source events from the tuner or from any of the child polyphase channel sources. + */ @Override public void process(SourceEvent sourceEvent) { @@ -169,7 +176,7 @@ public class PolyphaseChannelManager implements ISourceEventProcessor, Listener< } else { - mLog.error("Request to stop sample stream for unrecognized source: " + + mLog.error("Request to start sample stream for unrecognized source: " + (sourceEvent.hasSource() ? sourceEvent.getSource().getClass() : "null source")); } //TODO: add the channel and register for samples from the tuner @@ -186,29 +193,69 @@ public class PolyphaseChannelManager implements ISourceEventProcessor, Listener< } break; case NOTIFICATION_FREQUENCY_CHANGE: - mChannelizerConfiguration.setCenterFrequency(sourceEvent.getValue().longValue()); - updateChannelResultsProcessors(); + mChannelIndexCalculator.setCenterFrequency(sourceEvent.getValue().longValue()); + updateOutputProcessors(); break; case NOTIFICATION_SAMPLE_RATE_CHANGE: int sampleRate = sourceEvent.getValue().intValue(); - int channelCount = sampleRate / mChannelizerConfiguration.getChannelBandwidth(); - mChannelizerConfiguration.setChannelCount(channelCount); - updateChannelResultsProcessors(); + int channelCount = sampleRate / mChannelIndexCalculator.getChannelBandwidth(); + mChannelIndexCalculator.setChannelCount(channelCount); + updateOutputProcessors(); break; } - - - } + /** + * Implements the Listener interface by delegating to the process(sourceEvent) method. + */ @Override public void receive(SourceEvent sourceEvent) { process(sourceEvent); } - private void updateChannelResultsProcessors() + /** + * Updates each of the output processors for any changes in the tuner's center frequency or sample rate, which + * would cause the output processors to change the polyphase channelizer results channel(s) that the processor is + * consuming + */ + private void updateOutputProcessors() { - //Update the processors anytime that the center frequency or sample rate changes + for(PolyphaseChannelSource channelSource: mChannelSources) + { + updateOutputProcessor(channelSource); + } + } + + /** + * Updates the polyphase channel source's output processor due to a change in the center frequency or sample + * rate for the source providing sample buffers to the polyphase channelizer, or whenever the DDC channel's + * center tuned frequency changes. + * + * @param channelSource that requires an update to its output processor + */ + private void updateOutputProcessor(PolyphaseChannelSource channelSource) + { + try + { + List indexes = mChannelIndexCalculator.getPolyphaseChannelIndexes(channelSource.getTunerChannel()); + + //If the indexes size is the same then update the current processor, otherwise create a new one + if(channelSource.getPolyphaseChannelOutputProcessor().getInputChannelCount() == indexes.size()) + { + channelSource.getPolyphaseChannelOutputProcessor().setPolyphaseChannelIndices(indexes); + } + else + { + channelSource.setPolyphaseChannelOutputProcessor(getOutputProcessor(indexes)); + } + } + catch(IllegalArgumentException iae) + { + mLog.error("Error updating polyphase channel source - can't determine output channel indexes for " + + "updated tuner center frequency and sample rate. Stopping channel source", iae); + + stopChannelSource(channelSource); + } } } diff --git a/src/dsp/filter/channelizer/PolyphaseChannelSource.java b/src/dsp/filter/channelizer/PolyphaseChannelSource.java index aa2a4767..f5e905a7 100644 --- a/src/dsp/filter/channelizer/PolyphaseChannelSource.java +++ b/src/dsp/filter/channelizer/PolyphaseChannelSource.java @@ -63,6 +63,33 @@ public class PolyphaseChannelSource extends ComplexChannelSource mPolyphaseChannelOutputProcessor = outputProcessor; } + /** + * Channel output processor used by this channel source. + */ + public IPolyphaseChannelOutputProcessor getPolyphaseChannelOutputProcessor() + { + return mPolyphaseChannelOutputProcessor; + } + + /** + * Updates the output processor for this channel source + */ + public void setPolyphaseChannelOutputProcessor(IPolyphaseChannelOutputProcessor outputProcessor) + { + synchronized(mPolyphaseChannelOutputProcessor) + { + mPolyphaseChannelOutputProcessor = outputProcessor; + } + } + + /** + * Tuner channel sourced by this channel source. + */ + public TunerChannel getTunerChannel() + { + return mTunerChannel; + } + /** * Process source event notifications received from upstream source provider and source event requests * received from downstream consumers. diff --git a/src/dsp/filter/channelizer/output/ChannelOutputProcessor.java b/src/dsp/filter/channelizer/output/ChannelOutputProcessor.java index a4c84694..e65ca444 100644 --- a/src/dsp/filter/channelizer/output/ChannelOutputProcessor.java +++ b/src/dsp/filter/channelizer/output/ChannelOutputProcessor.java @@ -23,20 +23,29 @@ import sample.complex.Complex; public abstract class ChannelOutputProcessor implements IPolyphaseChannelOutputProcessor { + private int mInputChannelCount; private Oscillator mFrequencyCorrectionMixer; /** * Base class for polyphase channelizer output channel processing. Provides built-in frequency translation * oscillator support to apply frequency correction to the channel sample stream as requested by sample consumer. * + * @param inputChannelCount is the number of input channels for this output processor * @param sampleRate of the output channel. This is used to match the oscillator's sample rate to the output * channel sample rate for frequency translation/correction. */ - public ChannelOutputProcessor(int sampleRate) + public ChannelOutputProcessor(int inputChannelCount, int sampleRate) { + mInputChannelCount = inputChannelCount; mFrequencyCorrectionMixer = new Oscillator(0, sampleRate); } + @Override + public int getInputChannelCount() + { + return mInputChannelCount; + } + /** * Provides frequency correction of the inphase component or simply returns the inphase argument value when no * frequency correction value has been specified. diff --git a/src/dsp/filter/channelizer/output/IPolyphaseChannelOutputProcessor.java b/src/dsp/filter/channelizer/output/IPolyphaseChannelOutputProcessor.java index d5473ff1..cc8644d1 100644 --- a/src/dsp/filter/channelizer/output/IPolyphaseChannelOutputProcessor.java +++ b/src/dsp/filter/channelizer/output/IPolyphaseChannelOutputProcessor.java @@ -20,6 +20,8 @@ package dsp.filter.channelizer.output; import sample.complex.IComplexSampleListener; +import java.util.List; + public interface IPolyphaseChannelOutputProcessor { /** @@ -37,4 +39,16 @@ public interface IPolyphaseChannelOutputProcessor * @param frequencyCorrection correction value */ void setFrequencyCorrection(long frequencyCorrection); + + /** + * Indicates the number of input channels processed by this output processor + * @return + */ + int getInputChannelCount(); + + /** + * Updates the input polyphase channel index(es) used by this output processor + * @param indexes + */ + void setPolyphaseChannelIndices(List indexes); } diff --git a/src/dsp/filter/channelizer/output/OneChannelOutputProcessor.java b/src/dsp/filter/channelizer/output/OneChannelOutputProcessor.java index f162bcc4..863b6454 100644 --- a/src/dsp/filter/channelizer/output/OneChannelOutputProcessor.java +++ b/src/dsp/filter/channelizer/output/OneChannelOutputProcessor.java @@ -35,7 +35,7 @@ public class OneChannelOutputProcessor extends ChannelOutputProcessor */ public OneChannelOutputProcessor(int sampleRate, List channelIndexes) { - super(sampleRate); + super(1, sampleRate); setPolyphaseChannelIndices(channelIndexes); } diff --git a/src/dsp/filter/channelizer/output/TwoChannelOutputProcessor.java b/src/dsp/filter/channelizer/output/TwoChannelOutputProcessor.java index b242ce29..e2ff98aa 100644 --- a/src/dsp/filter/channelizer/output/TwoChannelOutputProcessor.java +++ b/src/dsp/filter/channelizer/output/TwoChannelOutputProcessor.java @@ -38,7 +38,7 @@ public class TwoChannelOutputProcessor extends ChannelOutputProcessor { //Set the frequency correction oscillator to 2 x output sample rate since we'll be correcting the frequency //after synthesizing both input channels - super(sampleRate * 2); + super(2, sampleRate * 2); setPolyphaseChannelIndices(channelIndexes); } From a581da56a87b649631f3501712b315c440bded70 Mon Sep 17 00:00:00 2001 From: Denny Date: Sun, 5 Nov 2017 11:38:01 -0500 Subject: [PATCH 27/87] #39 WIP polyphase channelizers. --- .../AbstractComplexPolyphaseChannelizer.java | 2 +- .../channelizer/PolyphaseChannelManager.java | 91 ++-- .../channelizer/PolyphaseChannelSource.java | 405 ++++++++++++------ .../output/ChannelOutputProcessor.java | 77 ++++ .../IPolyphaseChannelOutputProcessor.java | 13 +- .../output/OneChannelOutputProcessor.java | 4 +- .../output/TwoChannelOutputProcessor.java | 4 +- .../complex/IComplexSampleListener.java | 24 -- 8 files changed, 420 insertions(+), 200 deletions(-) delete mode 100644 src/sample/complex/IComplexSampleListener.java diff --git a/src/dsp/filter/channelizer/AbstractComplexPolyphaseChannelizer.java b/src/dsp/filter/channelizer/AbstractComplexPolyphaseChannelizer.java index e2b3b74f..3f33436d 100644 --- a/src/dsp/filter/channelizer/AbstractComplexPolyphaseChannelizer.java +++ b/src/dsp/filter/channelizer/AbstractComplexPolyphaseChannelizer.java @@ -94,7 +94,7 @@ public abstract class AbstractComplexPolyphaseChannelizer implements Listener mChannelSources = new CopyOnWriteArrayList<>(); private ChannelIndexCalculator mChannelIndexCalculator; private ComplexPolyphaseChannelizerM2 mPolyphaseChannelizer; + private ChannelSourceEventListener mChannelSourceEventListener = new ChannelSourceEventListener(); /** * Polyphase Channel Manager is a DDC channel manager for a tuner. This class provides DDC polyphase channel @@ -101,7 +103,7 @@ public class PolyphaseChannelManager implements ISourceEventProcessor, Listener< if(outputProcessor != null) { - channelSource = new PolyphaseChannelSource(tunerChannel, outputProcessor, this); + channelSource = new PolyphaseChannelSource(tunerChannel, outputProcessor, this, CHANNEL_SAMPLE_RATE); mChannelSources.add(channelSource); } } @@ -162,45 +164,23 @@ public class PolyphaseChannelManager implements ISourceEventProcessor, Listener< } /** - * Processes source events from the tuner or from any of the child polyphase channel sources. + * Processes source event notifications from the tuner or source event requests from any of the child polyphase + * channel sources. */ @Override public void process(SourceEvent sourceEvent) { switch(sourceEvent.getEvent()) { - case REQUEST_START_SAMPLE_STREAM: - if(sourceEvent.hasSource() && sourceEvent.getSource() instanceof PolyphaseChannelSource) - { - startChannelSource((PolyphaseChannelSource)sourceEvent.getSource()); - } - else - { - mLog.error("Request to start sample stream for unrecognized source: " + - (sourceEvent.hasSource() ? sourceEvent.getSource().getClass() : "null source")); - } - //TODO: add the channel and register for samples from the tuner - break; - case REQUEST_STOP_SAMPLE_STREAM: - if(sourceEvent.hasSource() && sourceEvent.getSource() instanceof PolyphaseChannelSource) - { - stopChannelSource((PolyphaseChannelSource)sourceEvent.getSource()); - } - else - { - mLog.error("Request to stop sample stream for unrecognized source: " + - (sourceEvent.hasSource() ? sourceEvent.getSource().getClass() : "null source")); - } - break; case NOTIFICATION_FREQUENCY_CHANGE: mChannelIndexCalculator.setCenterFrequency(sourceEvent.getValue().longValue()); - updateOutputProcessors(); + updateOutputProcessors(sourceEvent); break; case NOTIFICATION_SAMPLE_RATE_CHANGE: int sampleRate = sourceEvent.getValue().intValue(); int channelCount = sampleRate / mChannelIndexCalculator.getChannelBandwidth(); mChannelIndexCalculator.setChannelCount(channelCount); - updateOutputProcessors(); + updateOutputProcessors(sourceEvent); break; } } @@ -218,12 +198,28 @@ public class PolyphaseChannelManager implements ISourceEventProcessor, Listener< * Updates each of the output processors for any changes in the tuner's center frequency or sample rate, which * would cause the output processors to change the polyphase channelizer results channel(s) that the processor is * consuming + * + * @param sourceEvent that requires an update to the output processors */ - private void updateOutputProcessors() + private void updateOutputProcessors(SourceEvent sourceEvent) { for(PolyphaseChannelSource channelSource: mChannelSources) { updateOutputProcessor(channelSource); + + //Notify the channel source that the frequency or sample rate has changed so that it can reset + //any frequency correction value. + if(sourceEvent != null) + { + try + { + channelSource.process(sourceEvent); + } + catch(SourceException se) + { + mLog.error("Error while notifying polyphase channel source of a source event", se); + } + } } } @@ -258,4 +254,43 @@ public class PolyphaseChannelManager implements ISourceEventProcessor, Listener< stopChannelSource(channelSource); } } + + /** + * Internal class for handling requests for start/stop sample stream from polyphase channel sources + */ + private class ChannelSourceEventListener implements Listener + { + @Override + public void receive(SourceEvent sourceEvent) + { + switch(sourceEvent.getEvent()) + { + case REQUEST_START_SAMPLE_STREAM: + if(sourceEvent.hasSource() && sourceEvent.getSource() instanceof PolyphaseChannelSource) + { + startChannelSource((PolyphaseChannelSource)sourceEvent.getSource()); + } + else + { + mLog.error("Request to start sample stream for unrecognized source: " + + (sourceEvent.hasSource() ? sourceEvent.getSource().getClass() : "null source")); + } + break; + case REQUEST_STOP_SAMPLE_STREAM: + if(sourceEvent.hasSource() && sourceEvent.getSource() instanceof PolyphaseChannelSource) + { + stopChannelSource((PolyphaseChannelSource)sourceEvent.getSource()); + } + else + { + mLog.error("Request to stop sample stream for unrecognized source: " + + (sourceEvent.hasSource() ? sourceEvent.getSource().getClass() : "null source")); + } + break; + default: + mLog.error("Received unrecognized source event from polyphase channel source [" + + sourceEvent.getEvent() + "]"); + } + } + } } diff --git a/src/dsp/filter/channelizer/PolyphaseChannelSource.java b/src/dsp/filter/channelizer/PolyphaseChannelSource.java index f5e905a7..993b2b87 100644 --- a/src/dsp/filter/channelizer/PolyphaseChannelSource.java +++ b/src/dsp/filter/channelizer/PolyphaseChannelSource.java @@ -23,27 +23,33 @@ import dsp.filter.channelizer.output.IPolyphaseChannelOutputProcessor; import org.slf4j.Logger; import org.slf4j.LoggerFactory; import sample.Listener; -import sample.OverflowableTransferQueue; import sample.complex.ComplexBuffer; -import sample.real.IOverflowListener; +import sample.complex.ComplexToComplexBufferAssembler; +import source.ComplexSource; import source.ISourceEventProcessor; +import source.Source; import source.SourceEvent; import source.SourceException; -import source.tuner.ComplexChannelSource; import source.tuner.TunerChannel; -public class PolyphaseChannelSource extends ComplexChannelSource +import java.util.concurrent.ScheduledExecutorService; +import java.util.concurrent.ScheduledFuture; +import java.util.concurrent.TimeUnit; + +public class PolyphaseChannelSource extends ComplexSource implements ISourceEventProcessor { private final static Logger mLog = LoggerFactory.getLogger(PolyphaseChannelSource.class); private TunerChannel mTunerChannel; - private IPolyphaseChannelOutputProcessor mPolyphaseChannelOutputProcessor; - private long mTunerCenterFrequency; - private int mUpstreamSampleRate; private int mChannelSampleRate; - private int mChannelCount; - private int mOutputFrequencyCorrection; - private ChannelBufferProcessor mProcessor = new ChannelBufferProcessor(); + + private DownstreamSourceEventHandler mDownstreamSourceEventHandler = new DownstreamSourceEventHandler(); + private UpstreamSourceEventHandler mUpstreamSourceEventHandler; + + private ComplexToComplexBufferAssembler mComplexBufferAssembler = new ComplexToComplexBufferAssembler(2500); + private IPolyphaseChannelOutputProcessor mPolyphaseChannelOutputProcessor; + private ChannelResultsProcessor mChannelResultsProcessor; + private ScheduledFuture mChannelResultsProcessorFuture; /** * Polyphase channelizer channel implementation. Adapts the channel array output samples from the polyphase @@ -52,47 +58,116 @@ public class PolyphaseChannelSource extends ComplexChannelSource * * @param tunerChannel - requested output tuner channel frequency and bandwidth. * @param outputProcessor - to process polyphase channelizer channel results into a channel stream - * @param upstreamSourceEventProcessor to handle requests for start/stop sample flow and frequency corrections + * @param externalEventListener to process sample stream start/stop requests */ public PolyphaseChannelSource(TunerChannel tunerChannel, IPolyphaseChannelOutputProcessor outputProcessor, - ISourceEventProcessor upstreamSourceEventProcessor) + Listener externalEventListener, int channelSampleRate) { - super(upstreamSourceEventProcessor); - mTunerChannel = tunerChannel; mPolyphaseChannelOutputProcessor = outputProcessor; + mUpstreamSourceEventHandler = new UpstreamSourceEventHandler(externalEventListener);; + mChannelSampleRate = channelSampleRate; } /** - * Channel output processor used by this channel source. + * Registers the listener to receive complex sample buffers from this channel source */ - public IPolyphaseChannelOutputProcessor getPolyphaseChannelOutputProcessor() + @Override + public void setListener(Listener listener) { - return mPolyphaseChannelOutputProcessor; + mComplexBufferAssembler.setListener(listener); } /** - * Updates the output processor for this channel source + * Removes the listener from receiving complex sample buffers from this channel source */ - public void setPolyphaseChannelOutputProcessor(IPolyphaseChannelOutputProcessor outputProcessor) + @Override + public void removeListener(Listener listener) { - synchronized(mPolyphaseChannelOutputProcessor) + mComplexBufferAssembler.setListener(null); + } + + /** + * Starts scheduled thread pool processing of the inbound polyphase channel results buffer to produce channelized + * complex sample buffer output for broadcast to a registered complex sample buffer listener. + * + * @param executor to use when scheduling a buffer processor + */ + @Override + public void start(ScheduledExecutorService executor) + { + if(mUpstreamSourceEventHandler != null) { - mPolyphaseChannelOutputProcessor = outputProcessor; + mUpstreamSourceEventHandler.broadcast(SourceEvent.startSampleStream(PolyphaseChannelSource.this)); } + + mChannelResultsProcessor = new ChannelResultsProcessor(); + + if(mChannelResultsProcessorFuture != null) + { + mChannelResultsProcessorFuture.cancel(true); + mLog.error("An existing channel results processor scheduled future was cancelled"); + } + + //Schedule the results processor to run every 100 milliseconds + mChannelResultsProcessorFuture = + executor.scheduleAtFixedRate(mChannelResultsProcessor, 0, 100, TimeUnit.MILLISECONDS); } /** - * Tuner channel sourced by this channel source. + * Stops scheduled thread pool processing of the inbound polyphase channel results buffer to produce channelized + * complex sample buffer output for broadcast to a registered complex sample buffer listener. */ - public TunerChannel getTunerChannel() + @Override + public void stop() { - return mTunerChannel; + if(mUpstreamSourceEventHandler != null) + { + mUpstreamSourceEventHandler.broadcast(SourceEvent.stopSampleStream(PolyphaseChannelSource.this)); + } + + if(mChannelResultsProcessorFuture != null) + { + mChannelResultsProcessorFuture.cancel(true); + mChannelResultsProcessorFuture = null; + } + + mChannelResultsProcessor = null; + } + + + /** + * Module-level ISourceEventListener interface implementation for processing downstream events + */ + @Override + public Listener getSourceEventListener() + { + return mDownstreamSourceEventHandler; } /** - * Process source event notifications received from upstream source provider and source event requests - * received from downstream consumers. + * Module-level ISourceEventProvider interface implementation, sets the listener to receive downstream module + * level source events from this source + */ + @Override + public void setSourceEventListener(Listener listener) + { + mDownstreamSourceEventHandler.setDownstreamEventListener(listener); + } + + /** + * Module-level ISourceEventProvider interface implementation, removes the listener from receiving downstream module + * level source events from this source + */ + @Override + public void removeSourceEventListener() + { + mDownstreamSourceEventHandler.removeDownstreamEventListener(); + } + + /** + * ISourceEventProcessor implementation for processing source events from the external polyphase channel manager + * and/or tuner providing samples to the polphase channelizer implementation. * * @param sourceEvent containing a request or notification * @throws SourceException if there are any errors while processing the source event @@ -100,184 +175,141 @@ public class PolyphaseChannelSource extends ComplexChannelSource @Override public void process(SourceEvent sourceEvent) throws SourceException { - switch(sourceEvent.getEvent()) + mUpstreamSourceEventHandler.receive(sourceEvent); + } + + /** + * Channel output processor used by this channel source to convert polyphase channel results into a specific + * channel complex buffer output stream. + */ + public IPolyphaseChannelOutputProcessor getPolyphaseChannelOutputProcessor() + { + return mPolyphaseChannelOutputProcessor; + } + + /** + * Sets/updates the output processor for this channel source, replacing the existing output processor. + */ + public void setPolyphaseChannelOutputProcessor(IPolyphaseChannelOutputProcessor outputProcessor) + { + //Lock on the channel output processor to block the channel results processor thread from servicing the + //channel output processor's buffer while we change out the processors. + synchronized(mPolyphaseChannelOutputProcessor) { - case NOTIFICATION_CHANNEL_SAMPLE_RATE_CHANGE: - setChannelSampleRate(sourceEvent.getValue().intValue()); - break; - case NOTIFICATION_FREQUENCY_CHANGE: - mTunerCenterFrequency = sourceEvent.getValue().longValue(); - updateInputConfiguration(); - break; - case NOTIFICATION_SAMPLE_RATE_CHANGE: - setUpstreamSampleRate(sourceEvent.getValue().intValue()); - updateInputConfiguration(); - break; - case REQUEST_CHANNEL_FREQUENCY_CORRECTION_CHANGE: - setFrequencyCorrection(sourceEvent.getValue().intValue()); - break; + IPolyphaseChannelOutputProcessor existingProcessor = mPolyphaseChannelOutputProcessor; + + //Swap out the processor so that incoming samples can accumulate in the new channel output processor + mPolyphaseChannelOutputProcessor = outputProcessor; + + //Fully process the residual channel results buffer of the previous channel output processor + if(existingProcessor != null) + { + existingProcessor.processChannelResults(mComplexBufferAssembler); + } } } /** - * Process the array samples for each of the output channels from the polyphase channelizer. - * @param channelsBuffer + * Tuner channel that is being sourced by this polyphase channel source. */ - public void processChannels(float[] channelsBuffer) + public TunerChannel getTunerChannel() { - mProcessor.process(channelsBuffer); + return mTunerChannel; } /** - * Updates current processing configuration any time there is a change in tuner center frequency - * or tuner sample rate. + * Primary method for receiving channel results output from a polyphase channelizer. The results array will be + * queued for processing to extract the target channel samples, process them for frequency correction and/or + * channel aggregation, and dispatch the results to the downstream sample listener/consumer. + * + * @param channelResultsBuffer containing the polyphase channelizer output for a single complex sample period. */ - private void updateInputConfiguration() + public void receiveChannelResults(float[] channelResultsBuffer) { - + mPolyphaseChannelOutputProcessor.receiveChannelResults(channelResultsBuffer); } /** * Downstream sample rate * * @return sample rate in Hertz - * @throws SourceException if this method is accessed before the channel count is established + * @throws SourceException never */ @Override public int getSampleRate() throws SourceException { - if(mChannelSampleRate > 0) - { - return mChannelSampleRate; - } - - throw new SourceException("Channel sample rate has not been set."); + return mChannelSampleRate; } /** - * Sets the downstream channel sample rate. Since oversampling may occur in the upstream channelizer, we can't - * rely on simply dividing the upstream sample rate by the number of channels. Thus, we required the upstream - * channelizer to tell us the exact channel sample rate. + * Center tuned frequency for this channel. * - * @param channelSampleRate in hertz + * @return frequency in hertz. + * @throws SourceException never */ - private void setChannelSampleRate(int channelSampleRate) - { - mChannelSampleRate = channelSampleRate; - - //Translate the upstream channel sample rate event into a downstream sample rate change event - broadcast(SourceEvent.sampleRateChange(mChannelSampleRate)); - } - - /** - * Sets the input sample rate and broadcasts a source change event to the downstream listener - * @param sampleRate from the upstream source - */ - private void setUpstreamSampleRate(int sampleRate) - { - mUpstreamSampleRate = sampleRate; - - try - { - broadcast(SourceEvent.sampleRateChange(getSampleRate())); - } - catch(SourceException se) - { - mLog.error("Upstream sample rate changed - couldn't broadcast channel sample rate change event " + - "to downstream channel", se); - } - } - - /** - * Sets the polyphase channel count. - * @param channelCount number of channels being processed by the polyphase channelizer - */ - private void setChannelCount(int channelCount) - { - mChannelCount = channelCount; - - try - { - broadcast(SourceEvent.sampleRateChange(getSampleRate())); - } - catch(SourceException se) - { - mLog.error("Polyphase channel count changed - couldn't broadcast channel sample rate change event " + - "to downstream channel", se); - } - } - @Override public long getFrequency() throws SourceException { return mTunerChannel.getFrequency(); } - private void setFrequencyCorrection(int correction) - { - //TODO: - mLog.debug("Request for frequency correction: " + correction + " -- not yet implemented"); - } - @Override public void setHeartbeatListener(Listener listener) { + mLog.info("Polyphase channel source received request to set heartbeat listener"); //TODO: implement this } @Override public void removeHeartbeatListener() { + mLog.info("Polyphase channel source received request to remove heartbeat listener"); //TODO: implement this } @Override public void reset() { - + mLog.info("Polyphase channel source received request to reset"); } @Override public void dispose() { - + mLog.info("Polyphase channel source received request to dispose"); } - public class ChannelBufferProcessor implements Runnable + /** + * Adjusts the frequency correction value that is being applied to the channelized output stream by the + * polyphase channel output processor. + * @param value to apply for frequency correction in hertz + */ + private void setFrequencyCorrection(long value) { - private OverflowableTransferQueue mComplexChannelBuffers; - - public ChannelBufferProcessor() + if(mPolyphaseChannelOutputProcessor != null) { - mComplexChannelBuffers = new OverflowableTransferQueue<>(500, 10); - mComplexChannelBuffers.setOverflowListener(new IOverflowListener() - { - @Override - public void sourceOverflow(boolean overflow) - { - broadcastOverflowState(overflow); - } - }); - } - - /** - * Enqueues the polyphase analysis filter's channel output for post channelizer mixing, filtering - * and resampling operations and buffers output for dispatch to downstream processing. - * - * @param channelsBuffer to enqueue for processing - */ - public void process(float[] channelsBuffer) - { - mComplexChannelBuffers.offer(channelsBuffer); + mPolyphaseChannelOutputProcessor.setFrequencyCorrection(value); + mDownstreamSourceEventHandler.broadcast(SourceEvent.frequencyCorrectionChange(value)); } + } + /** + * Runnable for processing the incoming polyphase channelizer results queue and distributing the channel + * results to the registered complex sample listener + */ + public class ChannelResultsProcessor implements Runnable + { @Override public void run() { try { -//TODO: implement distribution to each channel + //Lock on the output processor so that it can't be changed out in the middle of processing + synchronized(mPolyphaseChannelOutputProcessor) + { + mPolyphaseChannelOutputProcessor.processChannelResults(mComplexBufferAssembler); + } } catch(Throwable throwable) { @@ -285,4 +317,99 @@ public class PolyphaseChannelSource extends ComplexChannelSource } } } + + public class UpstreamSourceEventHandler implements Listener + { + private Listener mExternalSourceEventListener; + + /** + * Upstream source event processor for handling any source events generated by the polyphase channelizer that + * provides this polyphase channel source. + * + * @param externalSourceEventListener to receive requests for start/stop sample streams + */ + public UpstreamSourceEventHandler(Listener externalSourceEventListener) + { + mExternalSourceEventListener = externalSourceEventListener; + } + + /** + * Broadcasts the request source event to the external source event listener. This listener is normally the + * polyphase channel manager. + * + * @param sourceEvent to broadcast + */ + public void broadcast(SourceEvent sourceEvent) + { + if(mExternalSourceEventListener != null) + { + mExternalSourceEventListener.receive(sourceEvent); + } + } + + /** + * Process source events received from the upstream (ie tuner or channelizer) sample provider. + * @param sourceEvent to process + */ + @Override + public void receive(SourceEvent sourceEvent) + { + switch(sourceEvent.getEvent()) + { + //Reset frequency correction anytime the upstream frequency or sample rate changes so that the + //downstream modules can reset and recalculate any needed frequency correction + case NOTIFICATION_FREQUENCY_CHANGE: + case NOTIFICATION_SAMPLE_RATE_CHANGE: + setFrequencyCorrection(0); + break; + } + } + } + + /** + * Downstream source event handler implements the ISourceEventProvider and ISourceEventProcessor interfaces on + * for this module to handle broadcasting source events to other downstream modules and receiving requests from + * any downstream modules. + */ + public class DownstreamSourceEventHandler implements Listener + { + private Listener mDownstreamEventListener; + + public void broadcast(SourceEvent sourceEvent) + { + if(mDownstreamEventListener != null) + { + mDownstreamEventListener.receive(sourceEvent); + } + } + + /** + * Sets the downstream event listener to receive source events from this channel source module + * @param listener to receive events + */ + public void setDownstreamEventListener(Listener listener) + { + mDownstreamEventListener = listener; + } + + public void removeDownstreamEventListener() + { + mDownstreamEventListener = null; + } + + /** + * Process source events received from the downstream channel modules. + * @param sourceEvent to process + */ + @Override + public void receive(SourceEvent sourceEvent) + { + switch(sourceEvent.getEvent()) + { + case REQUEST_CHANNEL_FREQUENCY_CORRECTION_CHANGE: + setFrequencyCorrection(sourceEvent.getValue().intValue()); + break; + } + } + } } diff --git a/src/dsp/filter/channelizer/output/ChannelOutputProcessor.java b/src/dsp/filter/channelizer/output/ChannelOutputProcessor.java index e65ca444..cf04d434 100644 --- a/src/dsp/filter/channelizer/output/ChannelOutputProcessor.java +++ b/src/dsp/filter/channelizer/output/ChannelOutputProcessor.java @@ -19,10 +19,26 @@ package dsp.filter.channelizer.output; import dsp.mixer.Oscillator; +import org.slf4j.Logger; +import org.slf4j.LoggerFactory; +import sample.OverflowableTransferQueue; import sample.complex.Complex; +import sample.complex.ComplexSampleListener; +import sample.real.IOverflowListener; + +import java.util.ArrayList; +import java.util.List; public abstract class ChannelOutputProcessor implements IPolyphaseChannelOutputProcessor { + private final static Logger mLog = LoggerFactory.getLogger(ChannelOutputProcessor.class); + + private static final int OVERFLOW_THRESHOLD = 25000 * 3; //25 kHz * 3 seconds + private static final int RESET_THRESHOLD = (int)(OVERFLOW_THRESHOLD * .25); //reset at 25% + + private OverflowableTransferQueue mChannelResultsQueue; + private List mChannelResultsToProcess = new ArrayList<>(); + private int mInputChannelCount; private Oscillator mFrequencyCorrectionMixer; @@ -38,6 +54,67 @@ public abstract class ChannelOutputProcessor implements IPolyphaseChannelOutputP { mInputChannelCount = inputChannelCount; mFrequencyCorrectionMixer = new Oscillator(0, sampleRate); + mChannelResultsQueue = new OverflowableTransferQueue<>(OVERFLOW_THRESHOLD, RESET_THRESHOLD); + } + + @Override + public void receiveChannelResults(float[] channelResults) + { + mChannelResultsQueue.offer(channelResults); + } + + /** + * Processes all enqueued polyphase channelizer results until the internal queue is empty + * @param listener to receive the processed channel results + */ + @Override + public void processChannelResults(ComplexSampleListener listener) + { + try + { + mChannelResultsQueue.drainTo(mChannelResultsToProcess, 1000); + + while(!mChannelResultsToProcess.isEmpty()) + { + for(float[] channelResults: mChannelResultsToProcess) + { + process(channelResults, listener); + } + + mChannelResultsToProcess.clear(); + mChannelResultsQueue.drainTo(mChannelResultsToProcess, 1000); + } + } + catch(Throwable throwable) + { + mLog.error("Error while processing polyphase channel samples", throwable); + mChannelResultsToProcess.clear(); + } + + } + + /** + * Sub-class implementation to process one polyphase channelizer result array. + * @param channelResult to process + * @param listener to receive the channel complex sample + */ + public abstract void process(float[] channelResult, ComplexSampleListener listener); + + + /** + * Sets the overflow listener to monitor the internal channelizer channel results queue overflow state + */ + public void setOverflowListener(IOverflowListener listener) + { + mChannelResultsQueue.setOverflowListener(listener); + } + + /** + * Removes the overflow listener from monitoring the internal channelizer channel results queue overflow state + */ + public void removeOverflowListener(IOverflowListener listener) + { + mChannelResultsQueue.setOverflowListener(null); } @Override diff --git a/src/dsp/filter/channelizer/output/IPolyphaseChannelOutputProcessor.java b/src/dsp/filter/channelizer/output/IPolyphaseChannelOutputProcessor.java index cc8644d1..453e7bfd 100644 --- a/src/dsp/filter/channelizer/output/IPolyphaseChannelOutputProcessor.java +++ b/src/dsp/filter/channelizer/output/IPolyphaseChannelOutputProcessor.java @@ -18,19 +18,24 @@ ******************************************************************************/ package dsp.filter.channelizer.output; -import sample.complex.IComplexSampleListener; +import sample.complex.ComplexSampleListener; import java.util.List; public interface IPolyphaseChannelOutputProcessor { /** - * Process the channel output channel results from the polyphase channelizer filter + * Receive and enqueue output results from the polyphase analysis channelizer + * @param channelResults to enqueue + */ + void receiveChannelResults(float[] channelResults); + + /** + * Process the channel output channel results queue and deliver the output to the listener * - * @param channels to process * @param listener to receive the processed channel results */ - void process(float[] channels, IComplexSampleListener listener); + void processChannelResults(ComplexSampleListener listener); /** * Specifies the frequency correction (+/-) that should be applied to samples extracted from the polyphase diff --git a/src/dsp/filter/channelizer/output/OneChannelOutputProcessor.java b/src/dsp/filter/channelizer/output/OneChannelOutputProcessor.java index 863b6454..a03ee59a 100644 --- a/src/dsp/filter/channelizer/output/OneChannelOutputProcessor.java +++ b/src/dsp/filter/channelizer/output/OneChannelOutputProcessor.java @@ -18,7 +18,7 @@ ******************************************************************************/ package dsp.filter.channelizer.output; -import sample.complex.IComplexSampleListener; +import sample.complex.ComplexSampleListener; import java.util.List; @@ -65,7 +65,7 @@ public class OneChannelOutputProcessor extends ChannelOutputProcessor * @param listener to receive the extracted, frequency-translated channel results */ @Override - public void process(float[] channels, IComplexSampleListener listener) + public void process(float[] channels, ComplexSampleListener listener) { if(channels.length < mChannelOffset + 1) { diff --git a/src/dsp/filter/channelizer/output/TwoChannelOutputProcessor.java b/src/dsp/filter/channelizer/output/TwoChannelOutputProcessor.java index e2ff98aa..a9ffd404 100644 --- a/src/dsp/filter/channelizer/output/TwoChannelOutputProcessor.java +++ b/src/dsp/filter/channelizer/output/TwoChannelOutputProcessor.java @@ -18,7 +18,7 @@ ******************************************************************************/ package dsp.filter.channelizer.output; -import sample.complex.IComplexSampleListener; +import sample.complex.ComplexSampleListener; import java.util.List; @@ -70,7 +70,7 @@ public class TwoChannelOutputProcessor extends ChannelOutputProcessor * @param listener to receive the extracted, frequency-translated channel results */ @Override - public void process(float[] channels, IComplexSampleListener listener) + public void process(float[] channels, ComplexSampleListener listener) { // if(channels.length < mChannelOffset + 1) // { diff --git a/src/sample/complex/IComplexSampleListener.java b/src/sample/complex/IComplexSampleListener.java deleted file mode 100644 index 8f783c45..00000000 --- a/src/sample/complex/IComplexSampleListener.java +++ /dev/null @@ -1,24 +0,0 @@ -/******************************************************************************* - * 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 sample.complex; - -public interface IComplexSampleListener -{ - public void receive(float i, float q); -} From 5281679b7feb25978204cbcec290f8d80e7a6795 Mon Sep 17 00:00:00 2001 From: Denny Date: Sun, 12 Nov 2017 10:33:13 -0500 Subject: [PATCH 28/87] #39 WIP polyphase channelizers. Creates test tuner and controller to use in testing tuner and polyphase channel manager operations. --- .../AbstractComplexPolyphaseChannelizer.java | 14 +- .../channelizer/ChannelIndexCalculator.java | 11 ++ .../filter/channelizer/ChannelizerViewer.java | 181 ++++++++---------- .../ComplexPolyphaseChannelizerM2.java | 72 ++++++- .../channelizer/PolyphaseChannelManager.java | 163 +++++++++++----- .../channelizer/ScheduledBufferProcessor.java | 161 ++++++++++++++++ src/dsp/mixer/LowPhaseNoiseOscillator.java | 28 ++- src/sample/Broadcaster.java | 148 ++++++++------ src/source/InvalidFrequencyException.java | 7 + src/source/tuner/MixerTuner.java | 2 +- src/source/tuner/Tuner.java | 35 ++-- src/source/tuner/TunerClass.java | 3 +- src/source/tuner/TunerController.java | 2 +- src/source/tuner/TunerType.java | 1 + .../tuner/frequency/FrequencyController.java | 10 +- src/source/tuner/test/SampleGenerator.java | 147 ++++++++++++++ src/source/tuner/test/TestTuner.java | 180 +++++++++++++++++ .../tuner/test/TestTunerController.java | 143 ++++++++++++++ 18 files changed, 1081 insertions(+), 227 deletions(-) create mode 100644 src/dsp/filter/channelizer/ScheduledBufferProcessor.java create mode 100644 src/source/tuner/test/SampleGenerator.java create mode 100644 src/source/tuner/test/TestTuner.java create mode 100644 src/source/tuner/test/TestTunerController.java diff --git a/src/dsp/filter/channelizer/AbstractComplexPolyphaseChannelizer.java b/src/dsp/filter/channelizer/AbstractComplexPolyphaseChannelizer.java index 3f33436d..814a1b92 100644 --- a/src/dsp/filter/channelizer/AbstractComplexPolyphaseChannelizer.java +++ b/src/dsp/filter/channelizer/AbstractComplexPolyphaseChannelizer.java @@ -105,8 +105,11 @@ public abstract class AbstractComplexPolyphaseChannelizer implements Listener + public class ChannelArrayPanel extends JPanel { - private ComplexPolyphaseChannelizer mPolyphaseChannelizer; - private ChannelDistributor mChannelDistributor; - - public ChannelArrayPanel(float[] taps) + public ChannelArrayPanel() { int bufferSize = CHANNEL_BANDWIDTH / CHANNEL_FFT_FRAME_RATE; if(bufferSize % 2 == 1) @@ -226,10 +189,6 @@ public class ChannelizerViewer extends JFrame bufferSize++; } - mPolyphaseChannelizer = new ComplexPolyphaseChannelizer(taps, mChannelCount, CHANNEL_BANDWIDTH); - mChannelDistributor = new ChannelDistributor(bufferSize, mChannelCount); -// mPolyphaseChannelizer.setChannelDistributor(mChannelDistributor); - init(); } @@ -243,7 +202,8 @@ public class ChannelizerViewer extends JFrame for(int x = 0; x < mChannelCount; x++) { //place the channels left and right of 10.0 MHz - long frequency = 10000000l + ((x * CHANNEL_BANDWIDTH) - halfSpectralBandwidth); + long frequency = mTestTuner.getTunerController().getFrequency() + + ((x * CHANNEL_BANDWIDTH) - halfSpectralBandwidth); ChannelPanel channelPanel = new ChannelPanel(mSettingsManager, CHANNEL_BANDWIDTH * 2, frequency, CHANNEL_BANDWIDTH); channelPanel.setDFTSize(mChannelPanelDFTSize); @@ -255,28 +215,17 @@ public class ChannelizerViewer extends JFrame { add(channelPanel, "grow,push"); } - -// channelPanel.getPolyphaseChannelSource() - - mChannelDistributor.addListener(x, channelPanel); } } - - @Override - public void receive(ComplexBuffer complexBuffer) - { - mPolyphaseChannelizer.receive(complexBuffer); - } } - public class ChannelPanel extends JPanel implements Listener, ISourceEventProcessor + public class PrimarySpectrumPanel extends JPanel implements Listener, ISourceEventProcessor { - private PolyphaseChannelSource mPolyphaseChannelSource; private DFTProcessor mDFTProcessor = new DFTProcessor(SampleType.COMPLEX); private ComplexDecibelConverter mComplexDecibelConverter = new ComplexDecibelConverter(); private SpectrumPanel mSpectrumPanel; - public ChannelPanel(SettingsManager settingsManager, int sampleRate, long frequency, int bandwidth) + public PrimarySpectrumPanel(SettingsManager settingsManager, int sampleRate) { setLayout(new MigLayout("insets 0 0 0 0", "[grow,fill]", "[grow,fill]")); mSpectrumPanel = new SpectrumPanel(settingsManager); @@ -285,16 +234,6 @@ public class ChannelizerViewer extends JFrame mDFTProcessor.addConverter(mComplexDecibelConverter); mDFTProcessor.process(SourceEvent.sampleRateChange(sampleRate)); mComplexDecibelConverter.addListener(mSpectrumPanel); - - TunerChannel tunerChannel = new TunerChannel(frequency, bandwidth); - mPolyphaseChannelSource = new PolyphaseChannelSource(this, tunerChannel); - mPolyphaseChannelSource.setListener(mDFTProcessor); - mPolyphaseChannelSource.start(ThreadPool.SCHEDULED); - } - - public PolyphaseChannelSource getPolyphaseChannelSource() - { - return mPolyphaseChannelSource; } public void setDFTSize(DFTSize dftSize) @@ -315,12 +254,57 @@ public class ChannelizerViewer extends JFrame } } - public class SampleGenerationTask implements Runnable + public class ChannelPanel extends JPanel implements Listener, ISourceEventProcessor { - @Override - public void run() + private PolyphaseChannelSource mSource; + private DFTProcessor mDFTProcessor = new DFTProcessor(SampleType.COMPLEX); + private ComplexDecibelConverter mComplexDecibelConverter = new ComplexDecibelConverter(); + private SpectrumPanel mSpectrumPanel; + + public ChannelPanel(SettingsManager settingsManager, int sampleRate, long frequency, int bandwidth) { - generateSamples(); + setLayout(new MigLayout("insets 0 0 0 0", "[grow,fill]", "[grow,fill]")); + mSpectrumPanel = new SpectrumPanel(settingsManager); + add(mSpectrumPanel); + + mDFTProcessor.addConverter(mComplexDecibelConverter); + mDFTProcessor.process(SourceEvent.sampleRateChange(sampleRate)); + mComplexDecibelConverter.addListener(mSpectrumPanel); + + TunerChannel tunerChannel = new TunerChannel(frequency, bandwidth); + mSource = (PolyphaseChannelSource)mTestTuner.getChannelSource(tunerChannel); + + if(mSource != null) + { + mSource.setListener(mDFTProcessor); + mSource.start(ThreadPool.SCHEDULED); + } + else + { + mLog.error("Couldn't get a source from the tuner for frequency: " + frequency); + } + } + + public PolyphaseChannelSource getSource() + { + return mSource; + } + + public void setDFTSize(DFTSize dftSize) + { + mDFTProcessor.setDFTSize(dftSize); + } + + @Override + public void receive(ComplexBuffer complexBuffer) + { + mDFTProcessor.receive(complexBuffer); + } + + @Override + public void process(SourceEvent event) throws SourceException + { + mLog.debug("Source Event! Add handler support for this to channelizer viewer"); } } @@ -338,7 +322,6 @@ public class ChannelizerViewer extends JFrame public void run() { frame.setVisible(true); - frame.start(); } }); } diff --git a/src/dsp/filter/channelizer/ComplexPolyphaseChannelizerM2.java b/src/dsp/filter/channelizer/ComplexPolyphaseChannelizerM2.java index 2eaa64b6..dd9f32a1 100644 --- a/src/dsp/filter/channelizer/ComplexPolyphaseChannelizerM2.java +++ b/src/dsp/filter/channelizer/ComplexPolyphaseChannelizerM2.java @@ -21,11 +21,11 @@ package dsp.filter.channelizer; import buffer.ReverseFloatCircularBuffer; import dsp.filter.FilterFactory; import dsp.filter.Window; +import dsp.filter.design.FilterDesignException; import dsp.mixer.Oscillator; import org.jtransforms.fft.FloatFFT_1D; import org.slf4j.Logger; import org.slf4j.LoggerFactory; -import sample.complex.ComplexBuffer; import java.text.DecimalFormat; @@ -33,6 +33,8 @@ public class ComplexPolyphaseChannelizerM2 extends AbstractComplexPolyphaseChann { private final static Logger mLog = LoggerFactory.getLogger(ComplexPolyphaseChannelizerM2.class); + private static final int DEFAULT_CHANNEL_BANDWIDTH = 12500; + private ReverseFloatCircularBuffer[] mDataBuffers; private float[][] mFilterCoefficients; private int mFilterPointer = 0; @@ -48,8 +50,10 @@ public class ComplexPolyphaseChannelizerM2 extends AbstractComplexPolyphaseChann * equal to the channel bandwidth (sample rate / filters). If you need to synthesize (combine two or more * filter outputs) a new bandwidth signal from the outputs of this filter, then the filter should be designed * as a nyquist filter with -6 dB attenuation at the channel bandwidth cutoff frequency + * * @param channels - number of filters/channels to output. Since this filter bank oversamples each filter * output, this number must be even (divisible by the oversample rate). + * * @param channelSampleRate for each decimated, oversampled channel */ public ComplexPolyphaseChannelizerM2(float[] taps, int channels, int channelSampleRate) @@ -64,6 +68,72 @@ public class ComplexPolyphaseChannelizerM2 extends AbstractComplexPolyphaseChann initFilters(taps); } + /** + * Non-Maximally Decimated Polyphase Filter Bank (NMDPFB) channelizer that divides the input frequency band into + * equal bandwidth channels that are each oversampled by 2x for output. + * + * Note: this constructor uses a default channel bandwidth of 12.5 kHz and produces channels with an oversampled + * channel sample rate of 25.0 kHz. + * + * @param sampleRate to be channelized. + */ + private ComplexPolyphaseChannelizerM2(int sampleRate) throws FilterDesignException + { + super(sampleRate / DEFAULT_CHANNEL_BANDWIDTH, DEFAULT_CHANNEL_BANDWIDTH); + + mLog.debug("Sample Rate: " + sampleRate); + + float[] filterTaps = FilterFactory.getSincChannelizer(getChannelSampleRate(), getChannelCount(), + 15, Window.WindowType.BLACKMAN_HARRIS_7, true); + + initFilters(filterTaps); + } + + /** + * Non-Maximally Decimated Polyphase Filter Bank (NMDPFB) channelizer with a default channel bandwidth of 12.5 kHz + * and a channel output sample rate of 25.0 kHz. + * + * @param sampleRate that is an even multiple of 25.0 kHz + * @return channelizer + * @throws IllegalArgumentException if the sample rate argument is not an even multiple of 25.0 kHz + */ + public static ComplexPolyphaseChannelizerM2 create(int sampleRate) throws IllegalArgumentException, + FilterDesignException + { + if(sampleRate % (2 * DEFAULT_CHANNEL_BANDWIDTH) != 0) + { + throw new IllegalArgumentException("Sample rate must be an even multiple of 25 kHz"); + } + + return new ComplexPolyphaseChannelizerM2(sampleRate); + } + + /** + * Updates this channelizer to use the new sample rate. This method creates a new filter suitable for the + * sample rate and reinitializes all internal data structures to prepare for processing the new sample rate. + * @param sampleRate that is a multiple of 25.0 kHz + * @throws IllegalArgumentException if the sample rate is not a multiple of 25.0 kHz + * @throws FilterDesignException if a filter cannot be designed for the target sample rate + */ + public void updateSampleRate(int sampleRate) throws IllegalArgumentException, FilterDesignException + { + if(sampleRate % (2 * DEFAULT_CHANNEL_BANDWIDTH) != 0) + { + throw new IllegalArgumentException("Sample rate must be an even multiple of 25 kHz"); + } + + float[] filterTaps = FilterFactory.getSincChannelizer(getChannelSampleRate(), getChannelCount(), + 15, Window.WindowType.BLACKMAN_HARRIS_7, true); + + initFilters(filterTaps); + } + + /** + * Filters the incoming complex sample pair + * + * @param inphase sample + * @param quadrature sample + */ protected void filter(float inphase, float quadrature) { try diff --git a/src/dsp/filter/channelizer/PolyphaseChannelManager.java b/src/dsp/filter/channelizer/PolyphaseChannelManager.java index 8751cec4..b640ddec 100644 --- a/src/dsp/filter/channelizer/PolyphaseChannelManager.java +++ b/src/dsp/filter/channelizer/PolyphaseChannelManager.java @@ -21,6 +21,7 @@ package dsp.filter.channelizer; import dsp.filter.channelizer.output.IPolyphaseChannelOutputProcessor; import dsp.filter.channelizer.output.OneChannelOutputProcessor; import dsp.filter.channelizer.output.TwoChannelOutputProcessor; +import dsp.filter.design.FilterDesignException; import org.slf4j.Logger; import org.slf4j.LoggerFactory; import sample.Listener; @@ -34,28 +35,29 @@ import source.tuner.TunerChannel; import java.util.List; import java.util.concurrent.CopyOnWriteArrayList; -public class PolyphaseChannelManager implements ISourceEventProcessor, Listener +public class PolyphaseChannelManager implements Listener { private final static Logger mLog = LoggerFactory.getLogger(PolyphaseChannelManager.class); private static final int CHANNEL_BANDWIDTH = 12500; - private static final int CHANNEL_SAMPLE_RATE = CHANNEL_BANDWIDTH * 2; private static final double CHANNEL_OVERSAMPLING = 2.0; + private static final int CHANNEL_SAMPLE_RATE = (int)(CHANNEL_BANDWIDTH * CHANNEL_OVERSAMPLING); private Tuner mTuner; private List mChannelSources = new CopyOnWriteArrayList<>(); private ChannelIndexCalculator mChannelIndexCalculator; private ComplexPolyphaseChannelizerM2 mPolyphaseChannelizer; private ChannelSourceEventListener mChannelSourceEventListener = new ChannelSourceEventListener(); + private ScheduledBufferProcessor mBufferProcessor; /** - * Polyphase Channel Manager is a DDC channel manager for a tuner. This class provides DDC polyphase channel - * sources and wraps a polyphase channelizer processing sample buffers produced by the tuner and distributing - * channelized sample buffers to each allocated DDC polyphase channel source. This class is responsible for - * monitoring the tuner for changes in center frequency and/or sample rate and updating active DDC polyphase channel - * sources accordingly. This class also monitors source event requests and notifications received from active DDC - * polyphase channel sources to adjust sample streams as required. + * Polyphase Channel Manager is a DDC channel manager and complex buffer queue/processor for a tuner. This class + * provides DDC polyphase channel sources and wraps a polyphase channelizer processing sample buffers produced by + * the tuner and distributing channelized sample buffers to each allocated DDC polyphase channel source. This + * class is responsible for monitoring the tuner for changes in center frequency and/or sample rate and updating + * active DDC polyphase channel sources accordingly. This class also monitors source event requests and + * notifications received from active DDC polyphase channel sources to adjust sample streams as required. * - * @param tuner providing broadband sample buffers + * @param tuner providing complex sample buffers */ public PolyphaseChannelManager(Tuner tuner) { @@ -64,25 +66,29 @@ public class PolyphaseChannelManager implements ISourceEventProcessor, Listener< throw new IllegalArgumentException("Tuner argument cannot be null"); } + mTuner = tuner; + int sampleRate = mTuner.getTunerController().getSampleRate(); //Ensure that tuner sample rate is a multiple of channel sample rate since polyphase channelizer is M2 if(sampleRate % (CHANNEL_SAMPLE_RATE) != 0) { - throw new IllegalArgumentException("Tuner sample rate [" + sampleRate + "] must be a multiple of " + + throw new IllegalArgumentException("Tuner sample rate [" + sampleRate + "] must be an even multiple of " + CHANNEL_SAMPLE_RATE + " Hz"); } - mTuner = tuner; - - int channelCount = sampleRate / CHANNEL_BANDWIDTH; - long frequency = mTuner.getTunerController().getFrequency(); + int channelCount = sampleRate / CHANNEL_BANDWIDTH; mChannelIndexCalculator = new ChannelIndexCalculator(frequency, channelCount, CHANNEL_BANDWIDTH, CHANNEL_OVERSAMPLING); + mBufferProcessor = new ScheduledBufferProcessor(500, 100, 100, 50); + + updateChannelizer(sampleRate, null); + mTuner.addSourceEventListener(this); + } /** @@ -103,7 +109,9 @@ public class PolyphaseChannelManager implements ISourceEventProcessor, Listener< if(outputProcessor != null) { - channelSource = new PolyphaseChannelSource(tunerChannel, outputProcessor, this, CHANNEL_SAMPLE_RATE); + channelSource = new PolyphaseChannelSource(tunerChannel, outputProcessor, mChannelSourceEventListener, + CHANNEL_SAMPLE_RATE); + mChannelSources.add(channelSource); } } @@ -117,7 +125,13 @@ public class PolyphaseChannelManager implements ISourceEventProcessor, Listener< return channelSource; } - private IPolyphaseChannelOutputProcessor getOutputProcessor(List indexes) throws IllegalArgumentException + /** + * Creates a processor to process the channelizer channel indexes into a composite output stream providing + * channelized complex sample buffers to a registered source listener. + * @param indexes to target by the output processor + * @return output processor compatible with the number of indexes to monitor + */ + private IPolyphaseChannelOutputProcessor getOutputProcessor(List indexes) { switch(indexes.size()) { @@ -127,6 +141,7 @@ public class PolyphaseChannelManager implements ISourceEventProcessor, Listener< return new TwoChannelOutputProcessor(CHANNEL_SAMPLE_RATE, indexes); 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()); return null; } } @@ -139,11 +154,15 @@ public class PolyphaseChannelManager implements ISourceEventProcessor, Listener< */ private void startChannelSource(PolyphaseChannelSource channelSource) { - mPolyphaseChannelizer.addChannel(channelSource); - - if(mPolyphaseChannelizer.getRegisteredChannelCount() == 1) + synchronized(mBufferProcessor) { - mTuner.addListener(mPolyphaseChannelizer); + mPolyphaseChannelizer.addChannel(channelSource); + + if(mPolyphaseChannelizer.getRegisteredChannelCount() == 1) + { + mTuner.addListener(mBufferProcessor); + mBufferProcessor.start(); + } } } @@ -155,43 +174,91 @@ public class PolyphaseChannelManager implements ISourceEventProcessor, Listener< */ private void stopChannelSource(PolyphaseChannelSource channelSource) { - mChannelSources.remove(channelSource); - - if(mPolyphaseChannelizer.getRegisteredChannelCount() == 0) + synchronized(mBufferProcessor) { - mTuner.removeListener(mPolyphaseChannelizer); + mChannelSources.remove(channelSource); + + if(mPolyphaseChannelizer.getRegisteredChannelCount() == 0) + { + mTuner.removeListener(mBufferProcessor); + mBufferProcessor.stop(); + } } } /** - * Processes source event notifications from the tuner or source event requests from any of the child polyphase - * channel sources. - */ - @Override - public void process(SourceEvent sourceEvent) - { - switch(sourceEvent.getEvent()) - { - case NOTIFICATION_FREQUENCY_CHANGE: - mChannelIndexCalculator.setCenterFrequency(sourceEvent.getValue().longValue()); - updateOutputProcessors(sourceEvent); - break; - case NOTIFICATION_SAMPLE_RATE_CHANGE: - int sampleRate = sourceEvent.getValue().intValue(); - int channelCount = sampleRate / mChannelIndexCalculator.getChannelBandwidth(); - mChannelIndexCalculator.setChannelCount(channelCount); - updateOutputProcessors(sourceEvent); - break; - } - } - - /** - * Implements the Listener interface by delegating to the process(sourceEvent) method. + * Implements the Listener interface for receiving notifications of frequency and sample rate + * changes from the tuner. */ @Override public void receive(SourceEvent sourceEvent) { - process(sourceEvent); + switch(sourceEvent.getEvent()) + { + case NOTIFICATION_FREQUENCY_CHANGE: + //Update the index calculator and each of the output processors to update each channel frequency + mChannelIndexCalculator.setCenterFrequency(sourceEvent.getValue().longValue()); + updateOutputProcessors(sourceEvent); + break; + case NOTIFICATION_SAMPLE_RATE_CHANGE: + //Update the channelizer (and the output processors) for the new sample rate + updateChannelizer(sourceEvent.getValue().intValue(), sourceEvent); + break; + } + } + + /** + * Creates or updates the channelizer to process the incoming sample rate and updates any channel processors. + * + * @param sampleRate to use for the channelizer + * @param sourceEvent containing a notification of sample rate change (optional - can be null). This source + * event will be passed to all output processors to ensure the change is fully promulgated before restarting the + * buffer processor if it was already running. + */ + private void updateChannelizer(int sampleRate, SourceEvent sourceEvent) + { + mChannelIndexCalculator.setSampleRate(sampleRate); + + //Lock on the buffer processor to block any channel create/start/stop requests while we configure + synchronized(mBufferProcessor) + { + //Get current processor state so we can restart it after we update the channelizer + boolean bufferProcessorWasRunning = mBufferProcessor.isRunning(); + + //Stop will be ignored if it is not currently running + mBufferProcessor.stop(); + + try + { + if(mPolyphaseChannelizer == null) + { + mPolyphaseChannelizer = ComplexPolyphaseChannelizerM2.create(sampleRate); + mBufferProcessor.setListener(mPolyphaseChannelizer); + } + else + { + mPolyphaseChannelizer.updateSampleRate(sampleRate); + } + } + catch(IllegalArgumentException iae) + { + mLog.error("Could not create polyphase channelizer for sample rate [" + sampleRate + "]", iae); + } + catch(FilterDesignException fde) + { + mLog.error("Could not create filter for polyphase channelizer for sample rate [" + sampleRate + "]", fde); + } + + if(sourceEvent != null && sourceEvent.getEvent() == SourceEvent.Event.NOTIFICATION_SAMPLE_RATE_CHANGE) + { + updateOutputProcessors(sourceEvent); + } + + if(bufferProcessorWasRunning) + { + mBufferProcessor.start(); + } + } } /** diff --git a/src/dsp/filter/channelizer/ScheduledBufferProcessor.java b/src/dsp/filter/channelizer/ScheduledBufferProcessor.java new file mode 100644 index 00000000..d1513219 --- /dev/null +++ b/src/dsp/filter/channelizer/ScheduledBufferProcessor.java @@ -0,0 +1,161 @@ +/******************************************************************************* + * 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 sample.Listener; +import sample.OverflowableTransferQueue; +import sample.real.IOverflowListener; +import util.ThreadPool; + +import java.util.ArrayList; +import java.util.List; +import java.util.concurrent.ScheduledFuture; +import java.util.concurrent.TimeUnit; +import java.util.concurrent.atomic.AtomicBoolean; + +public class ScheduledBufferProcessor implements Listener +{ + private OverflowableTransferQueue mQueue; + private Listener mListener; + private ScheduledFuture mScheduledFuture; + private long mDistributionInterval; + private int mMaxBuffersPerInterval; + private AtomicBoolean mRunning = new AtomicBoolean(); + + /** + * Scheduled Buffer Processor combines an internal overflowable buffer with a scheduled runnable processing task + * for periodically distributing internally queued elements to the registered listener. This processor provides + * a convenient way to create a thread-safe buffer for receiving elements from one thread/runnable and then + * distributing those elements to a registered listener where distribution occurs on a separate scheduled thread + * pool runnable thread. This allows the calling input thread to quickly return without incurring any subsequent + * processing workload. + * + * The internal queue is an overflowable queue implementation that allows a listener to be registered to receive + * notifications of overflow and reset state. Queue sizing parameters are specified in the constructor. + * + * @param maximumSize of the internal queue (overflow happens when this is exceeded) + * @param resetThreshold of the internal queue (overflow reset happens once queue size falls below this threshold + * @param distributionInterval in milliseconds specifying how often the internal scheduled thread pool buffer + * distributor should run + * @param maxBuffersPerInterval maximum number of buffers to pull from the internal queue during each distribution + * interval + */ + public ScheduledBufferProcessor(int maximumSize, int resetThreshold, long distributionInterval, int maxBuffersPerInterval) + { + mQueue = new OverflowableTransferQueue<>(maximumSize, resetThreshold); + mDistributionInterval = distributionInterval; + mMaxBuffersPerInterval = maxBuffersPerInterval; + } + + /** + * Sets the listener to receive notifications of buffer overflow and/or reset. Note: this method can also be + * invoked with a null argument to clear the previously registered overflow listener. + * + * @param listener to receive overflow/reset notifications. + */ + public void setOverflowListener(IOverflowListener listener) + { + mQueue.setOverflowListener(listener); + } + + /** + * Sets or changes the listener to receive buffers from this processor. + * @param listener to receive buffers + */ + public void setListener(Listener listener) + { + mListener = listener; + } + + /** + * Primary input method for adding buffers to this processor. Note: incoming buffers will be ignored if this + * processor is in a stopped state. You must invoke start() to allow incoming buffers and initiate buffer + * processing. + * + * @param e to enqueue for distribution to a registered listener + */ + @Override + public void receive(E e) + { + if(mRunning.get()) + { + mQueue.offer(e); + } + } + + /** + * Starts this buffer processor and allows queuing of incoming buffers. + */ + public void start() + { + if(mRunning.compareAndSet(false, true)) + { + mScheduledFuture = ThreadPool.SCHEDULED.scheduleAtFixedRate(new Processor(), 0, mDistributionInterval, TimeUnit.MILLISECONDS); + } + } + + /** + * Stops this buffer processor, stops queuing of incoming buffers and clears the internal buffer queue. + */ + public void stop() + { + if(mRunning.compareAndSet(true, false)) + { + if(mScheduledFuture != null) + { + mScheduledFuture.cancel(true); + mScheduledFuture = null; + } + + mQueue.clear(); + } + } + + /** + * Indicates if this processor is currently running + */ + public boolean isRunning() + { + return mRunning.get(); + } + + /** + * Processor to service the buffer queue and distribute the buffers to the registered listener + */ + class Processor implements Runnable + { + private List mBuffers = new ArrayList<>(); + + @Override + public void run() + { + mQueue.drainTo(mBuffers, mMaxBuffersPerInterval); + + if(mListener != null) + { + for(E buffer: mBuffers) + { + mListener.receive(buffer); + } + } + + mBuffers.clear(); + } + } +} diff --git a/src/dsp/mixer/LowPhaseNoiseOscillator.java b/src/dsp/mixer/LowPhaseNoiseOscillator.java index ad190e2d..160d0e1e 100644 --- a/src/dsp/mixer/LowPhaseNoiseOscillator.java +++ b/src/dsp/mixer/LowPhaseNoiseOscillator.java @@ -31,7 +31,7 @@ public class LowPhaseNoiseOscillator 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 long mFrequency; private float mInphase = 1.0f; private float mQuadrature = 0.0f; private float mPreviousInphase = 1.0f; @@ -46,7 +46,7 @@ public class LowPhaseNoiseOscillator * @param sampleRate for the oscillator * @param frequency to generate */ - public LowPhaseNoiseOscillator(int sampleRate, int frequency) + public LowPhaseNoiseOscillator(int sampleRate, long frequency) { mSampleRate = sampleRate; mFrequency = frequency; @@ -54,18 +54,40 @@ public class LowPhaseNoiseOscillator update(); } + /** + * Sets the sample rate for this oscillator + */ public void setSampleRate(int sampleRate) { mSampleRate = sampleRate; update(); } - public void setFrequency(int frequency) + /** + * Sample rate for this oscillator + */ + public int getSampleRate() + { + 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; + } + /** * Updates the internal values after a frequency or sample rate change */ diff --git a/src/sample/Broadcaster.java b/src/sample/Broadcaster.java index d4f9cce1..b3bf951a 100644 --- a/src/sample/Broadcaster.java +++ b/src/sample/Broadcaster.java @@ -1,17 +1,17 @@ /******************************************************************************* * SDR Trunk * Copyright (C) 2014 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 ******************************************************************************/ @@ -25,62 +25,100 @@ import java.util.concurrent.CopyOnWriteArrayList; */ public class Broadcaster implements Listener { - private List> mListeners = new CopyOnWriteArrayList<>(); + private List> mListeners = new CopyOnWriteArrayList<>(); - @Override - public void receive( T t ) + /** + * Implements the Listener interface to receive an element and broadcast that element to all registered + * listeners. + * + * @param t element to broadcast + */ + @Override + public void receive(T t) { - broadcast( t ); + broadcast(t); } - - /** - * Clear listeners to prepare for garbage collection - */ - public void dispose() - { - mListeners.clear(); - } - - public boolean hasListeners() - { - return !mListeners.isEmpty(); - } - - public int getListenerCount() - { - return mListeners.size(); - } - - public List> getListeners() - { - return mListeners; - } - - public void addListener( Listener listener ) - { - if(listener == null) - { - throw new IllegalArgumentException("Listener cannot be null"); - } - mListeners.add( listener ); - } - - public void removeListener( Listener listener ) - { - mListeners.remove( listener ); - } - - public void clear() - { - mListeners.clear(); - } - - public void broadcast( T t ) + /** + * Clear listeners to prepare for garbage collection + */ + public void dispose() { - for( Listener listener: mListeners ) - { - listener.receive( t ); - } + mListeners.clear(); + } + + /** + * Indicates if this broadcaster has any listeners registered + */ + public boolean hasListeners() + { + return !mListeners.isEmpty(); + } + + /** + * Indicates if the listener is currently registered with this broadcaster + */ + public boolean hasListener(Listener listener) + { + return listener != null && mListeners.contains(listener); + } + + /** + * The count of listeners currently registered with this broadcaster + */ + public int getListenerCount() + { + return mListeners.size(); + } + + /** + * The list of listeners currently registered with this broadcaster + */ + public List> getListeners() + { + return mListeners; + } + + /** + * Registers the listener to receive elements from this broadcaster + * + * @param listener + */ + public void addListener(Listener listener) + { + if(listener != null && !mListeners.contains(listener)) + { + mListeners.add(listener); + } + } + + /** + * Deregisters the listener from receiving elements from this broadcaster + */ + public void removeListener(Listener listener) + { + if(listener != null && mListeners.contains(listener)) + { + mListeners.remove(listener); + } + } + + /** + * Deregisters all listeners from this broadcaster + */ + public void clear() + { + mListeners.clear(); + } + + /** + * Broadcasts the element to all registered listeners + */ + public void broadcast(T t) + { + for(Listener listener : mListeners) + { + listener.receive(t); + } } } diff --git a/src/source/InvalidFrequencyException.java b/src/source/InvalidFrequencyException.java index 77e1bee7..e60a8843 100644 --- a/src/source/InvalidFrequencyException.java +++ b/src/source/InvalidFrequencyException.java @@ -54,4 +54,11 @@ public class InvalidFrequencyException extends SourceException { return mValidFrequency; } + + @Override + public String getMessage() + { + return super.getMessage() + " Invalid Frequency: " + getInvalidFrequency() + + " - current valid frequency:" + getValidFrequency(); + } } diff --git a/src/source/tuner/MixerTuner.java b/src/source/tuner/MixerTuner.java index 37f79bed..c873aa8c 100644 --- a/src/source/tuner/MixerTuner.java +++ b/src/source/tuner/MixerTuner.java @@ -123,7 +123,7 @@ public abstract class MixerTuner extends Tuner implements Listener> mSampleListeners = new CopyOnWriteArrayList<>(); + private Broadcaster mSampleBroadcaster = new Broadcaster<>(); protected List> mTunerChangeListeners = new ArrayList<>(); protected Broadcaster mSourceEventBroadcaster = new Broadcaster<>(); @@ -89,7 +85,7 @@ public abstract class Tuner implements ITunerChannelProvider public void dispose() { - mSampleListeners.clear(); + mSampleBroadcaster.clear(); } public void setName(String name) @@ -161,7 +157,7 @@ public abstract class Tuner implements ITunerChannelProvider */ public void addListener(Listener listener) { - mSampleListeners.add(listener); + mSampleBroadcaster.addListener(listener); } /** @@ -169,7 +165,15 @@ public abstract class Tuner implements ITunerChannelProvider */ public void removeListener(Listener listener) { - mSampleListeners.remove(listener); + mSampleBroadcaster.removeListener(listener); + } + + /** + * Provides access to the internal sample broadcaster for sub-class use (ie protected). + */ + protected Broadcaster getSampleBroadcaster() + { + return mSampleBroadcaster; } /** @@ -179,7 +183,15 @@ public abstract class Tuner implements ITunerChannelProvider */ public boolean hasListener(Listener listener) { - return mSampleListeners.contains(listener); + return mSampleBroadcaster.hasListener(listener); + } + + /** + * Indicates if this tuner has any complex buffer listeners registered + */ + public boolean hasListeners() + { + return mSampleBroadcaster.hasListeners(); } /** @@ -187,10 +199,7 @@ public abstract class Tuner implements ITunerChannelProvider */ public void broadcast(ComplexBuffer sampleBuffer) { - for(Listener listener : mSampleListeners) - { - listener.receive(sampleBuffer); - } + mSampleBroadcaster.broadcast(sampleBuffer); } /** diff --git a/src/source/tuner/TunerClass.java b/src/source/tuner/TunerClass.java index 124aefe5..9d898cda 100644 --- a/src/source/tuner/TunerClass.java +++ b/src/source/tuner/TunerClass.java @@ -51,7 +51,8 @@ public enum TunerClass TERRATEC_NOXON_REV2( TunerType.ELONICS_E4000, "0CCD", "00E0", "Terratec", "NOXON R2" ), TERRATEC_T_STICK_PLUS( TunerType.ELONICS_E4000, "0CCD", "00D7", "Terratec", "T Stick Plus" ), TWINTECH_UT40( TunerType.FITIPOWER_FC0013, "1B80", "D3A4", "Twintech", "UT-40" ), - ZAAPA_ZTMINDVBZP( TunerType.FITIPOWER_FC0012, "1B80", "D398", "Zaapa", "ZT-MINDVBZP" ), + ZAAPA_ZTMINDVBZP( TunerType.FITIPOWER_FC0012, "1B80", "D398", "Zaapa", "ZT-MINDVBZP" ), + TEST_TUNER(TunerType.TEST, "0", "0", "ABC Tuners Inc.", "Model XYZ"), UNKNOWN( TunerType.UNKNOWN, "0", "0", "Unknown Manufacturer", "Unknown Device" ); private TunerType mTunerType; diff --git a/src/source/tuner/TunerController.java b/src/source/tuner/TunerController.java index be32e591..8fd2cc54 100644 --- a/src/source/tuner/TunerController.java +++ b/src/source/tuner/TunerController.java @@ -104,7 +104,7 @@ public abstract class TunerController implements Tunable, ISourceEventProcessor public int getSampleRate() { - return mFrequencyController.getBandwidth(); + return mFrequencyController.getSampleRate(); } public double getFrequencyCorrection() diff --git a/src/source/tuner/TunerType.java b/src/source/tuner/TunerType.java index 65b293f3..156c6dd6 100644 --- a/src/source/tuner/TunerType.java +++ b/src/source/tuner/TunerType.java @@ -32,6 +32,7 @@ public enum TunerType RAFAELMICRO_R820T( "R820T" ), RAFAELMICRO_R828D( "R828D" ), RTL2832_VARIOUS( "Generic" ), + TEST("Test"), UNKNOWN( "Unknown" ); private String mLabel; diff --git a/src/source/tuner/frequency/FrequencyController.java b/src/source/tuner/frequency/FrequencyController.java index 7a4d83de..7403ad64 100644 --- a/src/source/tuner/frequency/FrequencyController.java +++ b/src/source/tuner/frequency/FrequencyController.java @@ -50,7 +50,7 @@ public class FrequencyController } /** - * Get bandwidth/sample rate in hertz + * Get bandwidth in hertz */ public int getBandwidth() { @@ -67,6 +67,14 @@ public class FrequencyController broadcastSampleRateChange(); } + /** + * Get sample rate in hertz + */ + public int getSampleRate() + { + return mSampleRate; + } + /** * Get frequency in hertz */ diff --git a/src/source/tuner/test/SampleGenerator.java b/src/source/tuner/test/SampleGenerator.java new file mode 100644 index 00000000..723482c3 --- /dev/null +++ b/src/source/tuner/test/SampleGenerator.java @@ -0,0 +1,147 @@ +/******************************************************************************* + * 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 source.tuner.test; + +import dsp.mixer.LowPhaseNoiseOscillator; +import sample.Listener; +import sample.complex.ComplexBuffer; +import util.ThreadPool; + +import java.util.concurrent.ScheduledFuture; +import java.util.concurrent.TimeUnit; + +public class SampleGenerator +{ + private LowPhaseNoiseOscillator mOscillator; + private Listener mListener; + private long mInterval; + private int mSamplesPerInterval; + private ScheduledFuture mScheduledFuture; + + /** + * Generates complex sample buffers at the specified sample rate with a unity gain tone at the specified + * frequency. This generator runs via a scheduled thread pool and generates samples at the specified time + * interval. + * + * @param sampleRate of the complex samples + * @param frequency of the tone produced by the generator + * @param interval in milliseconds for generating samples + */ + public SampleGenerator(int sampleRate, long frequency, long interval) + { + mOscillator = new LowPhaseNoiseOscillator(sampleRate, frequency); + mInterval = interval; + + updateSamplesPerInterval(); + } + + /** + * Updates the number of complex samples generated per interval + */ + private void updateSamplesPerInterval() + { + mSamplesPerInterval = (int)((double)mOscillator.getSampleRate() * ((double)mInterval / 1000.0) * 2.0); + } + + /** + * Starts the generator producing samples + */ + public void start() + { + if(mScheduledFuture == null) + { + mScheduledFuture = ThreadPool.SCHEDULED.scheduleAtFixedRate(new Generator(), 0, mInterval, + TimeUnit.MILLISECONDS); + } + else + { + throw new IllegalStateException("Sample generator is already started"); + } + } + + /** + * Stops the generator from producing samples + */ + public void stop() + { + if(mScheduledFuture != null) + { + mScheduledFuture.cancel(true); + mScheduledFuture = null; + } + else + { + throw new IllegalStateException("Sample generator is already stopped"); + } + } + + /** + * Registers the listener to receive samples once the generator starts. Note: invoke this method with null + * argument to de-register the listener. + * + * @param listener to receive complex sample buffers + */ + public void setListener(Listener listener) + { + mListener = listener; + } + + /** + * Updates the tone frequency for this generator + * @param frequency to generate + */ + public void setFrequency(long frequency) + { + mOscillator.setFrequency(frequency); + } + + /** + * Current tone frequency for this generator + */ + public long getFrequency() + { + return mOscillator.getFrequency(); + } + + /** + * Updates the sample rate for this generator + * @param sampleRate in hertz for complex samples + */ + public void setSampleRate(int sampleRate) + { + mOscillator.setSampleRate(sampleRate); + updateSamplesPerInterval(); + } + + /** + * Generates a complex sample buffer and distributes the buffer to a registered listener + */ + public class Generator implements Runnable + { + @Override + public void run() + { + if(mListener != null) + { + float[] samples = mOscillator.generate(mSamplesPerInterval); + mListener.receive(new ComplexBuffer(samples)); + } + } + } +} diff --git a/src/source/tuner/test/TestTuner.java b/src/source/tuner/test/TestTuner.java new file mode 100644 index 00000000..e80657f2 --- /dev/null +++ b/src/source/tuner/test/TestTuner.java @@ -0,0 +1,180 @@ +/******************************************************************************* + * 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 source.tuner.test; + +import dsp.filter.channelizer.PolyphaseChannelManager; +import org.slf4j.Logger; +import org.slf4j.LoggerFactory; +import sample.Listener; +import sample.complex.ComplexBuffer; +import source.Source; +import source.SourceException; +import source.tuner.Tuner; +import source.tuner.TunerChannel; +import source.tuner.TunerChannelSource; +import source.tuner.TunerClass; +import source.tuner.TunerType; + +import java.util.concurrent.RejectedExecutionException; + +/** + * Testing tuner that implements an internal oscillator to output a unity gain tone at a specified frequency offset + * from a configurable center tune frequency and sample rate + */ +public class TestTuner extends Tuner +{ + private final static Logger mLog = LoggerFactory.getLogger(TestTuner.class); + private static int mInstanceCounter = 1; + private final int mInstanceID = mInstanceCounter++; + + private PolyphaseChannelManager mPolyphaseChannelManager; + + public TestTuner() + { + super("Test Tuner", new TestTunerController()); + + mLog.debug("Initializing test tuner. Tuner Controller's sample rate is:" + getTunerController().getSampleRate()); + mLog.debug("Initializing test tuner. Test Tuner Controller's sample rate is:" + getTestTunerController().getSampleRate()); + + mPolyphaseChannelManager = new PolyphaseChannelManager(this); + } + + /** + * Returns the tuner controller cast as a test tuner controller. + * + * Note: this is a temporary method until the tuner controller interface can be updated with the start(listener) + * and stop() methods. + */ + private TestTunerController getTestTunerController() + { + return (TestTunerController)getTunerController(); + } + + /** + * Sets the center tuned frequency for this tuner + * @param frequency + * @throws SourceException + */ + public void setFrequency(long frequency) throws SourceException + { + getTunerController().setFrequency(frequency); + } + + public void setToneFrequency(long frequency) + { + getTestTunerController().setToneFrequency(frequency); + } + + public long getToneFrequency() + { + return getTestTunerController().getToneFrequency(); + } + + public void setSampleRate(int sampleRate) throws SourceException + { + getTestTunerController().setSampleRate(sampleRate); + } + + public int getSampleRate() + { + return getTestTunerController().getSampleRate(); + } + + @Override + public String getUniqueID() + { + return getName() + "-" + mInstanceID; + } + + @Override + public TunerClass getTunerClass() + { + return TunerClass.TEST_TUNER; + } + + @Override + public TunerType getTunerType() + { + return TunerClass.TEST_TUNER.getTunerType(); + } + + @Override + public double getSampleSize() + { + return 16.0; + } + + /** + * Creates a polyphase channel source. The provided channel is a one-use channel that can be commanded to start + * and stop processing via the source event interface. Once stopped, the channel is deregistered from this tuner + * and is no longer usable -- a new channel source must be requested from the tuner. + * + * @param tunerChannel identifying the channel center frequency and bandwidth + * @return channel source or null if the channel source cannot be provided by this tuner + */ + public Source getChannelSource(TunerChannel tunerChannel) + { + return mPolyphaseChannelManager.getChannel(tunerChannel); + } + + @Override + public TunerChannelSource getChannel(TunerChannel channel) throws RejectedExecutionException, SourceException + { + mLog.error("This method is deprecated in this test tuner implementation"); + throw new IllegalStateException("Method deprecated - this test tuner cannot provide a TunerChannelSource"); + } + + @Override + public void releaseChannel(TunerChannelSource source) + { + mLog.error("This method is deprecated in this test tuner implementation"); + } + + /** + * Adds the listener to receive complex sample buffers and auto-starts the sample stream if this is the first + * registered listener. + */ + @Override + public void addListener(Listener listener) + { + boolean isRunning = hasListeners(); + + super.addListener(listener); + + if(!isRunning) + { + getTestTunerController().start(getSampleBroadcaster()); + } + } + + /** + * Removes the listener from receiving complex sample buffers and auto-stops the sample stream when there are + * no more listeners. + */ + @Override + public void removeListener(Listener listener) + { + super.removeListener(listener); + + if(!hasListeners()) + { + getTestTunerController().stop(); + } + } +} diff --git a/src/source/tuner/test/TestTunerController.java b/src/source/tuner/test/TestTunerController.java new file mode 100644 index 00000000..9878202e --- /dev/null +++ b/src/source/tuner/test/TestTunerController.java @@ -0,0 +1,143 @@ +/******************************************************************************* + * 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 source.tuner.test; + +import org.slf4j.Logger; +import org.slf4j.LoggerFactory; +import sample.Listener; +import sample.complex.ComplexBuffer; +import source.SourceException; +import source.tuner.TunerController; +import source.tuner.configuration.TunerConfiguration; + +public class TestTunerController extends TunerController +{ + private final static Logger mLog = LoggerFactory.getLogger(TestTunerController.class); + + public static final long MINIMUM_FREQUENCY = 1000000l; //1 MHz + public static final long MAXIMUM_FREQUENCY = 10000000000l; //10 GHz + public static final long DEFAULT_FREQUENCY = 100000000; //100 MHz + public static final int DEFAULT_SAMPLE_RATE = 1000000; //1 MHz + public static final int DC_NOISE_BANDWIDTH = 5000; // +/-5 kHz + public static final double USABLE_BANDWIDTH = 0.90; //90% usable bandwidth - 5% unusable on either end + + private SampleGenerator mSampleGenerator; + + /** + * Tuner controller testing implementation. + */ + public TestTunerController() + { + super(MINIMUM_FREQUENCY, MAXIMUM_FREQUENCY, DC_NOISE_BANDWIDTH, USABLE_BANDWIDTH); + + mSampleGenerator = new SampleGenerator(DEFAULT_SAMPLE_RATE, DEFAULT_FREQUENCY, 100); + + try + { + mLog.debug("Setting frequency ..."); + mFrequencyController.setFrequency(DEFAULT_FREQUENCY); + mLog.debug("Setting sample rate of freq controller to: " + DEFAULT_SAMPLE_RATE); + mFrequencyController.setSampleRate(DEFAULT_SAMPLE_RATE); + } + catch(Exception e) + { + mLog.error("Error!", e); + } + } + + /** + * Starts the tuner controller producing samples that will be delivered to the specified listener + */ + public void start(Listener listener) + { + mSampleGenerator.setListener(listener); + mSampleGenerator.start(); + } + + /** + * Stops the tuner controller sample generator and deregisters the listener + */ + public void stop() + { + mSampleGenerator.stop(); + mSampleGenerator.setListener(null); + } + + @Override + public void apply(TunerConfiguration config) throws SourceException + { + mLog.error("Request to apply tuner configuration was ignored"); + } + + /** + * Current center frequency for this tuner + * @throws SourceException + */ + @Override + public long getTunedFrequency() throws SourceException + { + return mSampleGenerator.getFrequency(); + } + + /** + * Sets the center frequency for this tuner + * @param frequency in hertz + * @throws SourceException + */ + @Override + public void setTunedFrequency(long frequency) throws SourceException + { + mSampleGenerator.setFrequency(frequency); + } + + /** + * Sets the tone frequency for the sample generator. The tone frequency is offset from the center tuned frequency. + * @param toneFrequency + */ + public void setToneFrequency(long toneFrequency) + { + mSampleGenerator.setFrequency(toneFrequency - getFrequency()); + } + + /** + * Current tone frequency for this test controller. + * @return + */ + public long getToneFrequency() + { + return getFrequency() + mSampleGenerator.getFrequency(); + } + + /** + * Current sample rate for this tuner controller + */ + @Override + public int getCurrentSampleRate() throws SourceException + { + return mFrequencyController.getSampleRate(); + } + + /** + * Sets the sample rate for this tuner controller + */ + public void setSampleRate(int sampleRate) throws SourceException + { + mFrequencyController.setSampleRate(sampleRate); + } +} From b3e4bd6a9c044570f97e3e2c884fb2edb85e050f Mon Sep 17 00:00:00 2001 From: Denny Date: Sat, 18 Nov 2017 05:50:00 -0500 Subject: [PATCH 29/87] #39 WIP polyphase channelizers. --- src/dsp/filter/FilterFactory.java | 5 +- .../AbstractComplexPolyphaseChannelizer.java | 1 + .../channelizer/ChannelIndexCalculator.java | 3 + .../filter/channelizer/ChannelizerViewer.java | 64 ++++++++++++------- .../ComplexPolyphaseChannelizerM2.java | 8 +-- .../channelizer/PolyphaseChannelManager.java | 5 +- .../channelizer/PolyphaseChannelSource.java | 19 +++++- .../channelizer/ScheduledBufferProcessor.java | 13 ++++ .../output/ChannelOutputProcessor.java | 62 ++++++++---------- .../output/OneChannelOutputProcessor.java | 36 ++++++++--- .../output/TwoChannelOutputProcessor.java | 9 ++- src/sample/OverflowableTransferQueue.java | 8 +++ src/source/tuner/Tuner.java | 8 +++ src/source/tuner/test/SampleGenerator.java | 57 +++++++++++++++-- src/source/tuner/test/TestTuner.java | 3 - .../tuner/test/TestTunerController.java | 6 +- 16 files changed, 217 insertions(+), 90 deletions(-) diff --git a/src/dsp/filter/FilterFactory.java b/src/dsp/filter/FilterFactory.java index eac69592..0dff4b73 100644 --- a/src/dsp/filter/FilterFactory.java +++ b/src/dsp/filter/FilterFactory.java @@ -834,9 +834,10 @@ public class FilterFactory if(logResults) { mLog.debug("Polyphase TunerChannelizer Filter Design Summary"); - mLog.debug("-------------------------------------------"); + mLog.debug("-----------------------------------------------------"); + mLog.debug("Window Type: " + windowType.name()); mLog.debug("Channels: " + channels); - mLog.debug("Taps Per Channel: " + tapsPerChannel); + mLog.debug("Taps Per Channel: " + tapsPerChannel + " (Requested:" + tapsPerChannel + ")"); mLog.debug("Filter Length: " + (taps.length + 1)); mLog.debug("Channel Bandwidth: " + channelBandwidth); mLog.debug("Cutoff Frequency: " + (sampleRate * cutoffFrequency)); diff --git a/src/dsp/filter/channelizer/AbstractComplexPolyphaseChannelizer.java b/src/dsp/filter/channelizer/AbstractComplexPolyphaseChannelizer.java index 814a1b92..41cce4d0 100644 --- a/src/dsp/filter/channelizer/AbstractComplexPolyphaseChannelizer.java +++ b/src/dsp/filter/channelizer/AbstractComplexPolyphaseChannelizer.java @@ -26,6 +26,7 @@ import sample.complex.ComplexBuffer; import source.ISourceEventListener; import source.SourceEvent; +import java.util.Arrays; import java.util.List; import java.util.concurrent.CopyOnWriteArrayList; diff --git a/src/dsp/filter/channelizer/ChannelIndexCalculator.java b/src/dsp/filter/channelizer/ChannelIndexCalculator.java index 9970bcdf..d7b6f28b 100644 --- a/src/dsp/filter/channelizer/ChannelIndexCalculator.java +++ b/src/dsp/filter/channelizer/ChannelIndexCalculator.java @@ -187,6 +187,9 @@ public class ChannelIndexCalculator */ public List getPolyphaseChannelIndexes(TunerChannel tunerChannel) throws IllegalArgumentException { + long min = getMinimumFrequency(); + long max = getMaximumFrequency(); + if(tunerChannel.getMinFrequency() < getMinimumFrequency() || tunerChannel.getMaxFrequency() > getMaximumFrequency()) { diff --git a/src/dsp/filter/channelizer/ChannelizerViewer.java b/src/dsp/filter/channelizer/ChannelizerViewer.java index e1f721f0..950a5913 100644 --- a/src/dsp/filter/channelizer/ChannelizerViewer.java +++ b/src/dsp/filter/channelizer/ChannelizerViewer.java @@ -32,6 +32,7 @@ import source.tuner.TunerChannel; import source.tuner.configuration.TunerConfigurationModel; import source.tuner.test.TestTuner; import spectrum.DFTProcessor; +import spectrum.DFTResultsListener; import spectrum.DFTSize; import spectrum.SpectrumPanel; import spectrum.converter.ComplexDecibelConverter; @@ -56,8 +57,6 @@ public class ChannelizerViewer extends JFrame private ChannelArrayPanel mChannelPanel; private int mChannelCount; private int mChannelsPerRow; - private int mFilterTapsPerChannel; - private int mSampleRate; private long mToneFrequency; private DFTSize mMainPanelDFTSize = DFTSize.FFT32768; private DFTSize mChannelPanelDFTSize = DFTSize.FFT00512; @@ -66,19 +65,15 @@ public class ChannelizerViewer extends JFrame /** * GUI Test utility for researching polyphase channelizers. * - * @param channelCount * @param channelsPerRow */ - public ChannelizerViewer(int channelCount, int channelsPerRow, int tapsPerChannel) + public ChannelizerViewer(int channelsPerRow) { - mChannelCount = channelCount; - mChannelsPerRow = channelsPerRow; - mFilterTapsPerChannel = tapsPerChannel; - - mSampleRate = mChannelCount * CHANNEL_BANDWIDTH; - mTestTuner = new TestTuner(); + mChannelCount = mTestTuner.getSampleRate() / CHANNEL_BANDWIDTH; + mChannelsPerRow = channelsPerRow; + mToneFrequency = mTestTuner.getTunerController().getFrequency() + (int)(CHANNEL_BANDWIDTH * 1); //Set to channel 1 as default @@ -115,7 +110,7 @@ public class ChannelizerViewer extends JFrame { if(mPrimarySpectrumPanel == null) { - mPrimarySpectrumPanel = new PrimarySpectrumPanel(mSettingsManager, mSampleRate); + mPrimarySpectrumPanel = new PrimarySpectrumPanel(mSettingsManager, mTestTuner.getSampleRate()); mPrimarySpectrumPanel.setPreferredSize(new Dimension(1000, 200)); mPrimarySpectrumPanel.setDFTSize(mMainPanelDFTSize); mTestTuner.addListener(mPrimarySpectrumPanel); @@ -132,7 +127,7 @@ public class ChannelizerViewer extends JFrame mControlPanel.setLayout(new MigLayout("insets 0 0 0 0", "", "")); mControlPanel.add(new JLabel("Tone:"), "align left"); - int maximumFrequencyOffset = mSampleRate / 2; + int maximumFrequencyOffset = mTestTuner.getSampleRate() / 2; long minimumFrequency = mTestTuner.getTunerController().getFrequency() - maximumFrequencyOffset; long maximumFrequency = mTestTuner.getTunerController().getFrequency() + maximumFrequencyOffset; @@ -154,9 +149,9 @@ public class ChannelizerViewer extends JFrame mControlPanel.add(spinner); mControlPanel.add(new JLabel("Hz")); - mControlPanel.add(new JLabel("Sample Rate: " + mSampleRate + " Hz"), "push,align right"); + mControlPanel.add(new JLabel("Sample Rate: " + mTestTuner.getSampleRate() + " Hz"), "push,align right"); - mControlPanel.add(new JLabel("Max: " + (mSampleRate / 2) + " Hz"), "push,align right"); + mControlPanel.add(new JLabel("Max: " + (mTestTuner.getSampleRate() / 2) + " Hz"), "push,align right"); mControlPanel.add(new JLabel("Channels: " + mChannelCount), "push,align right"); } @@ -196,15 +191,20 @@ public class ChannelizerViewer extends JFrame { setLayout(new MigLayout("insets 0 0 0 0", "fill", "fill")); - long spectralBandwidth = mChannelCount * CHANNEL_BANDWIDTH; + long spectralBandwidth = mTestTuner.getSampleRate(); long halfSpectralBandwidth = spectralBandwidth / 2; + int channelToLog = -1; + for(int x = 0; x < mChannelCount; x++) { //place the channels left and right of 10.0 MHz long frequency = mTestTuner.getTunerController().getFrequency() + - ((x * CHANNEL_BANDWIDTH) - halfSpectralBandwidth); - ChannelPanel channelPanel = new ChannelPanel(mSettingsManager, CHANNEL_BANDWIDTH * 2, frequency, CHANNEL_BANDWIDTH); + ((x * CHANNEL_BANDWIDTH) - halfSpectralBandwidth) + (CHANNEL_BANDWIDTH / 2); + + mLog.debug("Channel " + x + " Frequency: " + frequency); + + ChannelPanel channelPanel = new ChannelPanel(mSettingsManager, CHANNEL_BANDWIDTH * 2, frequency, CHANNEL_BANDWIDTH, (x == channelToLog)); channelPanel.setDFTSize(mChannelPanelDFTSize); if(x % mChannelsPerRow == mChannelsPerRow - 1) @@ -260,8 +260,9 @@ public class ChannelizerViewer extends JFrame private DFTProcessor mDFTProcessor = new DFTProcessor(SampleType.COMPLEX); private ComplexDecibelConverter mComplexDecibelConverter = new ComplexDecibelConverter(); private SpectrumPanel mSpectrumPanel; + private long mFrequency; - public ChannelPanel(SettingsManager settingsManager, int sampleRate, long frequency, int bandwidth) + public ChannelPanel(SettingsManager settingsManager, int sampleRate, long frequency, int bandwidth, boolean enableLogging) { setLayout(new MigLayout("insets 0 0 0 0", "[grow,fill]", "[grow,fill]")); mSpectrumPanel = new SpectrumPanel(settingsManager); @@ -269,14 +270,32 @@ public class ChannelizerViewer extends JFrame mDFTProcessor.addConverter(mComplexDecibelConverter); mDFTProcessor.process(SourceEvent.sampleRateChange(sampleRate)); + mComplexDecibelConverter.addListener(mSpectrumPanel); + mFrequency = frequency; + TunerChannel tunerChannel = new TunerChannel(frequency, bandwidth); mSource = (PolyphaseChannelSource)mTestTuner.getChannelSource(tunerChannel); if(mSource != null) { - mSource.setListener(mDFTProcessor); + if(enableLogging) + { + mSource.setListener(new Listener() + { + @Override + public void receive(ComplexBuffer complexBuffer) + { + mLog.debug("Sending samples to DFT processor: " + complexBuffer.getSamples().length); + mDFTProcessor.receive(complexBuffer); + } + }); + } + else + { + mSource.setListener(mDFTProcessor); + } mSource.start(ThreadPool.SCHEDULED); } else @@ -298,6 +317,7 @@ public class ChannelizerViewer extends JFrame @Override public void receive(ComplexBuffer complexBuffer) { + mLog.debug("Got a buffer for the channel dft processor"); mDFTProcessor.receive(complexBuffer); } @@ -310,11 +330,9 @@ public class ChannelizerViewer extends JFrame public static void main(String[] args) { - int channelCount = 160; - int channelsPerRow = 20; - int tapsPerChannel = 17; + int channelsPerRow = 4; - final ChannelizerViewer frame = new ChannelizerViewer(channelCount, channelsPerRow, tapsPerChannel); + final ChannelizerViewer frame = new ChannelizerViewer(channelsPerRow); EventQueue.invokeLater(new Runnable() { diff --git a/src/dsp/filter/channelizer/ComplexPolyphaseChannelizerM2.java b/src/dsp/filter/channelizer/ComplexPolyphaseChannelizerM2.java index dd9f32a1..114b3417 100644 --- a/src/dsp/filter/channelizer/ComplexPolyphaseChannelizerM2.java +++ b/src/dsp/filter/channelizer/ComplexPolyphaseChannelizerM2.java @@ -77,14 +77,14 @@ public class ComplexPolyphaseChannelizerM2 extends AbstractComplexPolyphaseChann * * @param sampleRate to be channelized. */ - private ComplexPolyphaseChannelizerM2(int sampleRate) throws FilterDesignException + private ComplexPolyphaseChannelizerM2(int sampleRate, int tapsPerFilter) throws FilterDesignException { super(sampleRate / DEFAULT_CHANNEL_BANDWIDTH, DEFAULT_CHANNEL_BANDWIDTH); mLog.debug("Sample Rate: " + sampleRate); float[] filterTaps = FilterFactory.getSincChannelizer(getChannelSampleRate(), getChannelCount(), - 15, Window.WindowType.BLACKMAN_HARRIS_7, true); + tapsPerFilter, Window.WindowType.BLACKMAN_HARRIS_7, true); initFilters(filterTaps); } @@ -97,7 +97,7 @@ public class ComplexPolyphaseChannelizerM2 extends AbstractComplexPolyphaseChann * @return channelizer * @throws IllegalArgumentException if the sample rate argument is not an even multiple of 25.0 kHz */ - public static ComplexPolyphaseChannelizerM2 create(int sampleRate) throws IllegalArgumentException, + public static ComplexPolyphaseChannelizerM2 create(int sampleRate, int tapsPerFilter) throws IllegalArgumentException, FilterDesignException { if(sampleRate % (2 * DEFAULT_CHANNEL_BANDWIDTH) != 0) @@ -105,7 +105,7 @@ public class ComplexPolyphaseChannelizerM2 extends AbstractComplexPolyphaseChann throw new IllegalArgumentException("Sample rate must be an even multiple of 25 kHz"); } - return new ComplexPolyphaseChannelizerM2(sampleRate); + return new ComplexPolyphaseChannelizerM2(sampleRate, tapsPerFilter); } /** diff --git a/src/dsp/filter/channelizer/PolyphaseChannelManager.java b/src/dsp/filter/channelizer/PolyphaseChannelManager.java index b640ddec..6bd797af 100644 --- a/src/dsp/filter/channelizer/PolyphaseChannelManager.java +++ b/src/dsp/filter/channelizer/PolyphaseChannelManager.java @@ -41,6 +41,7 @@ public class PolyphaseChannelManager implements Listener private static final int CHANNEL_BANDWIDTH = 12500; private static final double CHANNEL_OVERSAMPLING = 2.0; private static final int CHANNEL_SAMPLE_RATE = (int)(CHANNEL_BANDWIDTH * CHANNEL_OVERSAMPLING); + private static final int POLYPHASE_FILTER_TAPS_PER_CHANNEL = 17; private Tuner mTuner; private List mChannelSources = new CopyOnWriteArrayList<>(); @@ -232,7 +233,9 @@ public class PolyphaseChannelManager implements Listener { if(mPolyphaseChannelizer == null) { - mPolyphaseChannelizer = ComplexPolyphaseChannelizerM2.create(sampleRate); + mPolyphaseChannelizer = ComplexPolyphaseChannelizerM2.create(sampleRate, + POLYPHASE_FILTER_TAPS_PER_CHANNEL); + mBufferProcessor.setListener(mPolyphaseChannelizer); } else diff --git a/src/dsp/filter/channelizer/PolyphaseChannelSource.java b/src/dsp/filter/channelizer/PolyphaseChannelSource.java index 993b2b87..89a7e0df 100644 --- a/src/dsp/filter/channelizer/PolyphaseChannelSource.java +++ b/src/dsp/filter/channelizer/PolyphaseChannelSource.java @@ -32,6 +32,7 @@ import source.SourceEvent; import source.SourceException; import source.tuner.TunerChannel; +import java.util.Arrays; import java.util.concurrent.ScheduledExecutorService; import java.util.concurrent.ScheduledFuture; import java.util.concurrent.TimeUnit; @@ -67,15 +68,29 @@ public class PolyphaseChannelSource extends ComplexSource implements ISourceEven mPolyphaseChannelOutputProcessor = outputProcessor; mUpstreamSourceEventHandler = new UpstreamSourceEventHandler(externalEventListener);; mChannelSampleRate = channelSampleRate; + } /** * Registers the listener to receive complex sample buffers from this channel source */ @Override - public void setListener(Listener listener) + public void setListener(final Listener listener) { - mComplexBufferAssembler.setListener(listener); +// mComplexBufferAssembler.setListener(listener); + + mComplexBufferAssembler.setListener(new Listener() + { + @Override + public void receive(ComplexBuffer complexBuffer) + { + if(mTunerChannel.getFrequency() == 99981250) + { +// mLog.debug(Arrays.toString(complexBuffer.getSamples())); + } + listener.receive(complexBuffer); + } + }); } /** diff --git a/src/dsp/filter/channelizer/ScheduledBufferProcessor.java b/src/dsp/filter/channelizer/ScheduledBufferProcessor.java index d1513219..17635504 100644 --- a/src/dsp/filter/channelizer/ScheduledBufferProcessor.java +++ b/src/dsp/filter/channelizer/ScheduledBufferProcessor.java @@ -18,6 +18,8 @@ ******************************************************************************/ package dsp.filter.channelizer; +import org.slf4j.Logger; +import org.slf4j.LoggerFactory; import sample.Listener; import sample.OverflowableTransferQueue; import sample.real.IOverflowListener; @@ -31,6 +33,8 @@ import java.util.concurrent.atomic.AtomicBoolean; public class ScheduledBufferProcessor implements Listener { + private final static Logger mLog = LoggerFactory.getLogger(ScheduledBufferProcessor.class); + private OverflowableTransferQueue mQueue; private Listener mListener; private ScheduledFuture mScheduledFuture; @@ -59,6 +63,15 @@ public class ScheduledBufferProcessor implements Listener public ScheduledBufferProcessor(int maximumSize, int resetThreshold, long distributionInterval, int maxBuffersPerInterval) { mQueue = new OverflowableTransferQueue<>(maximumSize, resetThreshold); + + mQueue.setOverflowListener(new IOverflowListener() + { + @Override + public void sourceOverflow(boolean overflow) + { + mLog.error("Overflow state changed - overflow:" + overflow); + } + }); mDistributionInterval = distributionInterval; mMaxBuffersPerInterval = maxBuffersPerInterval; } diff --git a/src/dsp/filter/channelizer/output/ChannelOutputProcessor.java b/src/dsp/filter/channelizer/output/ChannelOutputProcessor.java index cf04d434..653be07a 100644 --- a/src/dsp/filter/channelizer/output/ChannelOutputProcessor.java +++ b/src/dsp/filter/channelizer/output/ChannelOutputProcessor.java @@ -33,14 +33,14 @@ public abstract class ChannelOutputProcessor implements IPolyphaseChannelOutputP { private final static Logger mLog = LoggerFactory.getLogger(ChannelOutputProcessor.class); - private static final int OVERFLOW_THRESHOLD = 25000 * 3; //25 kHz * 3 seconds - private static final int RESET_THRESHOLD = (int)(OVERFLOW_THRESHOLD * .25); //reset at 25% - private OverflowableTransferQueue mChannelResultsQueue; private List mChannelResultsToProcess = new ArrayList<>(); + private int mMaxResultsToProcess; private int mInputChannelCount; +//TODO: swap this out and use the LowPhaseNoiseOscillator private Oscillator mFrequencyCorrectionMixer; + private boolean mFrequencyCorrectionEnabled; /** * Base class for polyphase channelizer output channel processing. Provides built-in frequency translation @@ -54,7 +54,9 @@ public abstract class ChannelOutputProcessor implements IPolyphaseChannelOutputP { mInputChannelCount = inputChannelCount; mFrequencyCorrectionMixer = new Oscillator(0, sampleRate); - mChannelResultsQueue = new OverflowableTransferQueue<>(OVERFLOW_THRESHOLD, RESET_THRESHOLD); + mMaxResultsToProcess = sampleRate / 10 * 2; //process at 100 millis interval, twice the expected inflow rate + + mChannelResultsQueue = new OverflowableTransferQueue<>(sampleRate * 3, (int)(sampleRate * 0.5)); } @Override @@ -72,33 +74,27 @@ public abstract class ChannelOutputProcessor implements IPolyphaseChannelOutputP { try { - mChannelResultsQueue.drainTo(mChannelResultsToProcess, 1000); + int count = mChannelResultsQueue.drainTo(mChannelResultsToProcess, mMaxResultsToProcess); - while(!mChannelResultsToProcess.isEmpty()) + if(count > 0) { - for(float[] channelResults: mChannelResultsToProcess) - { - process(channelResults, listener); - } - - mChannelResultsToProcess.clear(); - mChannelResultsQueue.drainTo(mChannelResultsToProcess, 1000); + process(mChannelResultsToProcess, listener); } } catch(Throwable throwable) { mLog.error("Error while processing polyphase channel samples", throwable); - mChannelResultsToProcess.clear(); } + mChannelResultsToProcess.clear(); } /** * Sub-class implementation to process one polyphase channelizer result array. - * @param channelResult to process + * @param channelResults to process * @param listener to receive the channel complex sample */ - public abstract void process(float[] channelResult, ComplexSampleListener listener); + public abstract void process(List channelResults, ComplexSampleListener listener); /** @@ -133,16 +129,8 @@ public abstract class ChannelOutputProcessor implements IPolyphaseChannelOutputP */ protected float getFrequencyCorrectedInphase(float inphase, float quadrature) { - //Only apply frequency correction if the oscillator is set to a non-zero frequency offset - if(mFrequencyCorrectionMixer.enabled()) - { - return Complex.multiplyInphase(inphase, quadrature, mFrequencyCorrectionMixer.inphase(), - mFrequencyCorrectionMixer.quadrature()); - } - else - { - return inphase; - } + return Complex.multiplyInphase(inphase, quadrature, mFrequencyCorrectionMixer.inphase(), + mFrequencyCorrectionMixer.quadrature()); } /** @@ -155,16 +143,8 @@ public abstract class ChannelOutputProcessor implements IPolyphaseChannelOutputP */ protected float getFrequencyCorrectedQuadrature(float inphase, float quadrature) { - //Only apply frequency correction if the oscillator is set to a non-zero frequency offset - if(mFrequencyCorrectionMixer.enabled()) - { - return Complex.multiplyQuadrature(inphase, quadrature, mFrequencyCorrectionMixer.inphase(), - mFrequencyCorrectionMixer.quadrature()); - } - else - { - return quadrature; - } + return Complex.multiplyQuadrature(inphase, quadrature, mFrequencyCorrectionMixer.inphase(), + mFrequencyCorrectionMixer.quadrature()); } /** @@ -177,6 +157,16 @@ public abstract class ChannelOutputProcessor implements IPolyphaseChannelOutputP public void setFrequencyCorrection(long frequencyCorrection) { mFrequencyCorrectionMixer.setFrequency(frequencyCorrection); + + mFrequencyCorrectionEnabled = (frequencyCorrection != 0); + } + + /** + * Indicates if this channel has a frequency correction offset + */ + public boolean hasFrequencyCorrection() + { + return mFrequencyCorrectionEnabled; } } diff --git a/src/dsp/filter/channelizer/output/OneChannelOutputProcessor.java b/src/dsp/filter/channelizer/output/OneChannelOutputProcessor.java index a03ee59a..44f5f396 100644 --- a/src/dsp/filter/channelizer/output/OneChannelOutputProcessor.java +++ b/src/dsp/filter/channelizer/output/OneChannelOutputProcessor.java @@ -18,12 +18,17 @@ ******************************************************************************/ package dsp.filter.channelizer.output; +import org.slf4j.Logger; +import org.slf4j.LoggerFactory; import sample.complex.ComplexSampleListener; +import java.util.Arrays; import java.util.List; public class OneChannelOutputProcessor extends ChannelOutputProcessor { + private final static Logger mLog = LoggerFactory.getLogger(OneChannelOutputProcessor.class); + private int mChannelOffset; /** @@ -37,6 +42,7 @@ public class OneChannelOutputProcessor extends ChannelOutputProcessor { super(1, sampleRate); setPolyphaseChannelIndices(channelIndexes); + mLog.debug("Sample Rate:" + sampleRate + " PolyPhase Channel Indexes:" + channelIndexes); } /** @@ -61,21 +67,31 @@ public class OneChannelOutputProcessor extends ChannelOutputProcessor * Extract the channel from the channel results array, apply frequency translation, and deliver the * extracted frequency-corrected channel I/Q sample set to the complex sample listener. * - * @param channels to process containing an array of channel I/Q sample pairs (I0,Q0,I1,Q1...In,Qn) + * @param channelResults to process containing a list of channel array of I/Q sample pairs (I0,Q0,I1,Q1...In,Qn) * @param listener to receive the extracted, frequency-translated channel results */ @Override - public void process(float[] channels, ComplexSampleListener listener) + public void process(List channelResults, ComplexSampleListener listener) { - if(channels.length < mChannelOffset + 1) + for(float[] channels: channelResults) { - throw new IllegalArgumentException("Polyphase channelizer output channels array is not large enough to " + - "cover this single channel output processor with channel offset [" + mChannelOffset + "]"); + if(channels.length < mChannelOffset + 1) + { + throw new IllegalArgumentException("Polyphase channelizer output channels array is not large enough to " + + "cover this single channel output processor with channel offset [" + mChannelOffset + "]"); + } + + float i = channels[mChannelOffset]; + float q = channels[mChannelOffset + 1]; + + if(hasFrequencyCorrection()) + { + listener.receive(getFrequencyCorrectedInphase(i, q), getFrequencyCorrectedQuadrature(i, q)); + } + else + { + listener.receive(i, q); + } } - - float i = channels[mChannelOffset]; - float q = channels[mChannelOffset + 1]; - - listener.receive(getFrequencyCorrectedInphase(i, q), getFrequencyCorrectedQuadrature(i, q)); } } diff --git a/src/dsp/filter/channelizer/output/TwoChannelOutputProcessor.java b/src/dsp/filter/channelizer/output/TwoChannelOutputProcessor.java index a9ffd404..ba0c3e15 100644 --- a/src/dsp/filter/channelizer/output/TwoChannelOutputProcessor.java +++ b/src/dsp/filter/channelizer/output/TwoChannelOutputProcessor.java @@ -18,12 +18,16 @@ ******************************************************************************/ package dsp.filter.channelizer.output; +import org.slf4j.Logger; +import org.slf4j.LoggerFactory; import sample.complex.ComplexSampleListener; import java.util.List; public class TwoChannelOutputProcessor extends ChannelOutputProcessor { + private final static Logger mLog = LoggerFactory.getLogger(TwoChannelOutputProcessor.class); + private int mChannelOffset1; private int mChannelOffset2; @@ -66,12 +70,13 @@ public class TwoChannelOutputProcessor extends ChannelOutputProcessor * Extract the channel from the channel results array, apply frequency translation, and deliver the * extracted frequency-corrected channel I/Q sample set to the complex sample listener. * - * @param channels to process containing an array of channel I/Q sample pairs (I0,Q0,I1,Q1...In,Qn) + * @param channelResults to process containing an array of channel I/Q sample pairs (I0,Q0,I1,Q1...In,Qn) * @param listener to receive the extracted, frequency-translated channel results */ @Override - public void process(float[] channels, ComplexSampleListener listener) + public void process(List channelResults, ComplexSampleListener listener) { + mLog.debug("I got channel results - doing nothing !!"); // if(channels.length < mChannelOffset + 1) // { // throw new IllegalArgumentException("Polyphase channelizer output channels array is not large enough to " + diff --git a/src/sample/OverflowableTransferQueue.java b/src/sample/OverflowableTransferQueue.java index 2deeca07..f37314a4 100644 --- a/src/sample/OverflowableTransferQueue.java +++ b/src/sample/OverflowableTransferQueue.java @@ -74,6 +74,14 @@ public class OverflowableTransferQueue } } + /** + * Removes and returns a single element from the head of the queue or null if the queue is empty + */ + public E poll() + { + return mQueue.poll(); + } + /** * Retrieves elements from the queue into the collection up to the maximum number of elements specified */ diff --git a/src/source/tuner/Tuner.java b/src/source/tuner/Tuner.java index 8657ac27..aac3297e 100644 --- a/src/source/tuner/Tuner.java +++ b/src/source/tuner/Tuner.java @@ -168,6 +168,14 @@ public abstract class Tuner implements ITunerChannelProvider mSampleBroadcaster.removeListener(listener); } + /** + * Number of registered complex sample buffer listeners + */ + public int getListenerCount() + { + return mSampleBroadcaster.getListenerCount(); + } + /** * Provides access to the internal sample broadcaster for sub-class use (ie protected). */ diff --git a/src/source/tuner/test/SampleGenerator.java b/src/source/tuner/test/SampleGenerator.java index 723482c3..0c52a338 100644 --- a/src/source/tuner/test/SampleGenerator.java +++ b/src/source/tuner/test/SampleGenerator.java @@ -19,6 +19,8 @@ package source.tuner.test; import dsp.mixer.LowPhaseNoiseOscillator; +import org.slf4j.Logger; +import org.slf4j.LoggerFactory; import sample.Listener; import sample.complex.ComplexBuffer; import util.ThreadPool; @@ -28,12 +30,40 @@ import java.util.concurrent.TimeUnit; public class SampleGenerator { + private final static Logger mLog = LoggerFactory.getLogger(SampleGenerator.class); + private LowPhaseNoiseOscillator mOscillator; + private int mSweepUpdateInterval; private Listener mListener; private long mInterval; private int mSamplesPerInterval; private ScheduledFuture mScheduledFuture; + /** + * Generates complex sample buffers at the specified sample rate with a unity gain tone at the specified + * frequency. This generator runs via a scheduled thread pool and generates samples at the specified time + * interval. + * + * @param sampleRate of the complex samples + * @param frequency of the tone produced by the generator + * @param interval in milliseconds for generating samples + * @param sweepUpdateRate to turn on sweeping of the tone frequency where the frequency is incremented by the + * rate specified at each interval, resetting to 1 Hz after reaching/exceeding the highest frequency. + */ + public SampleGenerator(int sampleRate, long frequency, long interval, int sweepUpdateRate) + { + if(Math.abs(sweepUpdateRate) >= sampleRate) + { + throw new IllegalArgumentException("Sweep update rate cannot be greater than sample rate"); + } + + mOscillator = new LowPhaseNoiseOscillator(sampleRate, frequency); + mInterval = interval; + mSweepUpdateInterval = sweepUpdateRate; + + updateSamplesPerInterval(); + } + /** * Generates complex sample buffers at the specified sample rate with a unity gain tone at the specified * frequency. This generator runs via a scheduled thread pool and generates samples at the specified time @@ -45,10 +75,7 @@ public class SampleGenerator */ public SampleGenerator(int sampleRate, long frequency, long interval) { - mOscillator = new LowPhaseNoiseOscillator(sampleRate, frequency); - mInterval = interval; - - updateSamplesPerInterval(); + this(sampleRate, frequency, interval, 0); } /** @@ -56,7 +83,9 @@ public class SampleGenerator */ private void updateSamplesPerInterval() { - mSamplesPerInterval = (int)((double)mOscillator.getSampleRate() * ((double)mInterval / 1000.0) * 2.0); + mSamplesPerInterval = (int)((double)mOscillator.getSampleRate() * ((double)mInterval / 1000.0)); + + mLog.debug("Sample Generator: " + mSamplesPerInterval + " samples every " + mInterval + " ms."); } /** @@ -141,6 +170,24 @@ public class SampleGenerator { float[] samples = mOscillator.generate(mSamplesPerInterval); mListener.receive(new ComplexBuffer(samples)); + + if(mSweepUpdateInterval != 0) + { + long updatedFrequency = mOscillator.getFrequency() + mSweepUpdateInterval; + + if(updatedFrequency > mOscillator.getSampleRate() / 2) + { + mOscillator.setFrequency(mOscillator.getSampleRate() / -2); + } + else if(updatedFrequency < mOscillator.getSampleRate() / -2) + { + mOscillator.setFrequency(mOscillator.getSampleRate() / 2); + } + else + { + mOscillator.setFrequency(updatedFrequency); + } + } } } } diff --git a/src/source/tuner/test/TestTuner.java b/src/source/tuner/test/TestTuner.java index e80657f2..476c6174 100644 --- a/src/source/tuner/test/TestTuner.java +++ b/src/source/tuner/test/TestTuner.java @@ -49,9 +49,6 @@ public class TestTuner extends Tuner { super("Test Tuner", new TestTunerController()); - mLog.debug("Initializing test tuner. Tuner Controller's sample rate is:" + getTunerController().getSampleRate()); - mLog.debug("Initializing test tuner. Test Tuner Controller's sample rate is:" + getTestTunerController().getSampleRate()); - mPolyphaseChannelManager = new PolyphaseChannelManager(this); } diff --git a/src/source/tuner/test/TestTunerController.java b/src/source/tuner/test/TestTunerController.java index 9878202e..3f1c6093 100644 --- a/src/source/tuner/test/TestTunerController.java +++ b/src/source/tuner/test/TestTunerController.java @@ -33,7 +33,7 @@ public class TestTunerController extends TunerController public static final long MINIMUM_FREQUENCY = 1000000l; //1 MHz public static final long MAXIMUM_FREQUENCY = 10000000000l; //10 GHz public static final long DEFAULT_FREQUENCY = 100000000; //100 MHz - public static final int DEFAULT_SAMPLE_RATE = 1000000; //1 MHz + public static final int DEFAULT_SAMPLE_RATE = 50000; //25 kHz public static final int DC_NOISE_BANDWIDTH = 5000; // +/-5 kHz public static final double USABLE_BANDWIDTH = 0.90; //90% usable bandwidth - 5% unusable on either end @@ -46,7 +46,9 @@ public class TestTunerController extends TunerController { super(MINIMUM_FREQUENCY, MAXIMUM_FREQUENCY, DC_NOISE_BANDWIDTH, USABLE_BANDWIDTH); - mSampleGenerator = new SampleGenerator(DEFAULT_SAMPLE_RATE, DEFAULT_FREQUENCY, 100); + int sweepRate = 0; //Hz per interval + + mSampleGenerator = new SampleGenerator(DEFAULT_SAMPLE_RATE, DEFAULT_FREQUENCY, 100, sweepRate); try { From a12aaf7cf37fc20db6658eb12ec1db2ec4963249 Mon Sep 17 00:00:00 2001 From: Denny Date: Sat, 18 Nov 2017 07:33:28 -0500 Subject: [PATCH 30/87] #39 WIP polyphase channelizers. --- src/dsp/filter/FilterFactory.java | 14 ++++++++------ .../filter/channelizer/ChannelizerViewer.java | 7 ------- src/source/tuner/test/SampleGenerator.java | 3 ++- src/source/tuner/test/TestTunerController.java | 18 +++++++++++------- 4 files changed, 21 insertions(+), 21 deletions(-) diff --git a/src/dsp/filter/FilterFactory.java b/src/dsp/filter/FilterFactory.java index 0dff4b73..b90b7986 100644 --- a/src/dsp/filter/FilterFactory.java +++ b/src/dsp/filter/FilterFactory.java @@ -567,7 +567,7 @@ public class FilterFactory * For 40db attenuation, calculate the number of symbols based on the following formula: * Symbols = -44 * alpha + 33 * - * Polyphase TunerChannelizer notes: + * Polyphase Channelizer notes: * -Set samples Per Symbol at 2 (or more) * number of channels * -Set symbolCount to sufficient size to produce required attenuation * -Set symbol rate in hertz @@ -725,7 +725,7 @@ public class FilterFactory } /** - * Creates a windowed-sync (Nyquist) M/2 prototype FIR filter for use with a Polyphase TunerChannelizer/Synthesizer. + * 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 * adjusted to achieve a -6.02 dB attenuation at the channel band edge to enable perfect reconstruction of adjacent * channels. @@ -833,14 +833,16 @@ public class FilterFactory if(logResults) { - mLog.debug("Polyphase TunerChannelizer Filter Design Summary"); + mLog.debug("Polyphase Channelizer Filter Design Summary"); mLog.debug("-----------------------------------------------------"); - mLog.debug("Window Type: " + windowType.name()); + mLog.debug("Input Sample Rate: " + sampleRate); + mLog.debug("Channel Bandwidth: " + channelBandwidth); mLog.debug("Channels: " + channels); + mLog.debug("Window Type: " + windowType.name()); mLog.debug("Taps Per Channel: " + tapsPerChannel + " (Requested:" + tapsPerChannel + ")"); mLog.debug("Filter Length: " + (taps.length + 1)); - mLog.debug("Channel Bandwidth: " + channelBandwidth); - mLog.debug("Cutoff Frequency: " + (sampleRate * cutoffFrequency)); + mLog.debug("Requested Cutoff Frequency: " + (sampleRate * bandEdge)); + mLog.debug("Actual Cutoff Frequency: " + (sampleRate * cutoffFrequency)); mLog.debug("Attenuation at 1.00 OBJECTIVE: " + PERFECT_RECONSTRUCTION_GAIN_AT_BAND_EDGE); mLog.debug("Attenuation at 1.00 Channels: " + evaluate(taps, bandEdge * 1.00)); mLog.debug("Attenuation at 1.25 Channels: " + evaluate(taps, bandEdge * 1.25)); diff --git a/src/dsp/filter/channelizer/ChannelizerViewer.java b/src/dsp/filter/channelizer/ChannelizerViewer.java index 950a5913..482f8795 100644 --- a/src/dsp/filter/channelizer/ChannelizerViewer.java +++ b/src/dsp/filter/channelizer/ChannelizerViewer.java @@ -32,7 +32,6 @@ import source.tuner.TunerChannel; import source.tuner.configuration.TunerConfigurationModel; import source.tuner.test.TestTuner; import spectrum.DFTProcessor; -import spectrum.DFTResultsListener; import spectrum.DFTSize; import spectrum.SpectrumPanel; import spectrum.converter.ComplexDecibelConverter; @@ -168,12 +167,6 @@ public class ChannelizerViewer extends JFrame return mChannelPanel; } -// private float[] getFilter() throws FilterDesignException -// { -// return FilterFactory.getSincChannelizer(CHANNEL_BANDWIDTH, mChannelCount, mFilterTapsPerChannel, -// Window.WindowType.BLACKMAN_HARRIS_7, true); -// } - public class ChannelArrayPanel extends JPanel { public ChannelArrayPanel() diff --git a/src/source/tuner/test/SampleGenerator.java b/src/source/tuner/test/SampleGenerator.java index 0c52a338..15796caf 100644 --- a/src/source/tuner/test/SampleGenerator.java +++ b/src/source/tuner/test/SampleGenerator.java @@ -57,6 +57,7 @@ public class SampleGenerator throw new IllegalArgumentException("Sweep update rate cannot be greater than sample rate"); } + mLog.debug("Sample Generator - Sample Rate:" + sampleRate + " Frequency:" + frequency); mOscillator = new LowPhaseNoiseOscillator(sampleRate, frequency); mInterval = interval; mSweepUpdateInterval = sweepUpdateRate; @@ -85,7 +86,7 @@ public class SampleGenerator { mSamplesPerInterval = (int)((double)mOscillator.getSampleRate() * ((double)mInterval / 1000.0)); - mLog.debug("Sample Generator: " + mSamplesPerInterval + " samples every " + mInterval + " ms."); + mLog.debug("Sample Generator - " + mSamplesPerInterval + " samples every " + mInterval + "ms"); } /** diff --git a/src/source/tuner/test/TestTunerController.java b/src/source/tuner/test/TestTunerController.java index 3f1c6093..280f853a 100644 --- a/src/source/tuner/test/TestTunerController.java +++ b/src/source/tuner/test/TestTunerController.java @@ -32,12 +32,15 @@ public class TestTunerController extends TunerController public static final long MINIMUM_FREQUENCY = 1000000l; //1 MHz public static final long MAXIMUM_FREQUENCY = 10000000000l; //10 GHz - public static final long DEFAULT_FREQUENCY = 100000000; //100 MHz + public static final long DEFAULT_CENTER_FREQUENCY = 100000000; //100 MHz public static final int DEFAULT_SAMPLE_RATE = 50000; //25 kHz public static final int DC_NOISE_BANDWIDTH = 5000; // +/-5 kHz public static final double USABLE_BANDWIDTH = 0.90; //90% usable bandwidth - 5% unusable on either end + public static final int SPECTRAL_FRAME_RATE = 20; + public static final long SAMPLE_GENERATION_INTERVAL = 1000 / SPECTRAL_FRAME_RATE; private SampleGenerator mSampleGenerator; + private long mReferenceFrequency = DEFAULT_CENTER_FREQUENCY; /** * Tuner controller testing implementation. @@ -48,12 +51,12 @@ public class TestTunerController extends TunerController int sweepRate = 0; //Hz per interval - mSampleGenerator = new SampleGenerator(DEFAULT_SAMPLE_RATE, DEFAULT_FREQUENCY, 100, sweepRate); + mSampleGenerator = new SampleGenerator(DEFAULT_SAMPLE_RATE, 5000, SAMPLE_GENERATION_INTERVAL, sweepRate); try { mLog.debug("Setting frequency ..."); - mFrequencyController.setFrequency(DEFAULT_FREQUENCY); + mFrequencyController.setFrequency(DEFAULT_CENTER_FREQUENCY); mLog.debug("Setting sample rate of freq controller to: " + DEFAULT_SAMPLE_RATE); mFrequencyController.setSampleRate(DEFAULT_SAMPLE_RATE); } @@ -94,7 +97,7 @@ public class TestTunerController extends TunerController @Override public long getTunedFrequency() throws SourceException { - return mSampleGenerator.getFrequency(); + return mReferenceFrequency; } /** @@ -105,7 +108,7 @@ public class TestTunerController extends TunerController @Override public void setTunedFrequency(long frequency) throws SourceException { - mSampleGenerator.setFrequency(frequency); + mReferenceFrequency = frequency; } /** @@ -114,7 +117,8 @@ public class TestTunerController extends TunerController */ public void setToneFrequency(long toneFrequency) { - mSampleGenerator.setFrequency(toneFrequency - getFrequency()); + long frequency = toneFrequency - mReferenceFrequency; + mSampleGenerator.setFrequency(frequency); } /** @@ -123,7 +127,7 @@ public class TestTunerController extends TunerController */ public long getToneFrequency() { - return getFrequency() + mSampleGenerator.getFrequency(); + return mReferenceFrequency + mSampleGenerator.getFrequency(); } /** From bf65e102867ae9dbc0feb7c8e7fbb1dbdfc55166 Mon Sep 17 00:00:00 2001 From: Denny Date: Sat, 18 Nov 2017 16:32:11 -0500 Subject: [PATCH 31/87] #39 WIP polyphase channelizer Updates the M2 channelizer and correctly implements the ChannelCalculator --- src/dsp/filter/FilterFactory.java | 22 +- .../AbstractComplexPolyphaseChannelizer.java | 50 +- .../filter/channelizer/ChannelCalculator.java | 588 ++++++++++++++++++ .../channelizer/ChannelIndexCalculator.java | 323 ---------- .../filter/channelizer/ChannelizerViewer.java | 5 +- .../ComplexPolyphaseChannelizer.java | 11 + .../ComplexPolyphaseChannelizerM2.java | 414 ++++++------ .../channelizer/PolyphaseChannelManager.java | 24 +- .../channelizer/ScheduledBufferProcessor.java | 5 +- .../tuner/test/TestTunerController.java | 2 +- 10 files changed, 882 insertions(+), 562 deletions(-) create mode 100644 src/dsp/filter/channelizer/ChannelCalculator.java delete mode 100644 src/dsp/filter/channelizer/ChannelIndexCalculator.java diff --git a/src/dsp/filter/FilterFactory.java b/src/dsp/filter/FilterFactory.java index b90b7986..0a8103cc 100644 --- a/src/dsp/filter/FilterFactory.java +++ b/src/dsp/filter/FilterFactory.java @@ -749,14 +749,14 @@ public class FilterFactory * * @throws FilterDesignException if a filter cannot be designed with a band edge attenuation of -6.02 dB */ - public static float[] getSincChannelizer(int channelBandwidth, int channels, int tapsPerChannel, WindowType windowType, - boolean logResults) throws FilterDesignException + public static float[] getSincChannelizer(double channelBandwidth, int channels, int tapsPerChannel, + WindowType windowType, boolean logResults) throws FilterDesignException { int currentTapsPerChannel = tapsPerChannel; - int sampleRate = channelBandwidth * channels * 2; + int sampleRate = (int)(channelBandwidth * (double)channels * 2.0); int filterLength = (channels * currentTapsPerChannel) - 1; - double cutoffFrequency = ((double)channelBandwidth / (double)sampleRate); + double cutoffFrequency = channelBandwidth / (double)sampleRate; double bandEdge = cutoffFrequency; double increment = cutoffFrequency * 0.1; @@ -831,6 +831,10 @@ public class FilterFactory throw new FilterDesignException("Cannot design filter to specifications"); } + //This is an odd length filter - increase the length by 1 by pre-padding a zero coefficient + float[] extendedTaps = new float[taps.length + 1]; + System.arraycopy(taps, 0, extendedTaps, 1, taps.length); + if(logResults) { mLog.debug("Polyphase Channelizer Filter Design Summary"); @@ -839,11 +843,11 @@ public class FilterFactory mLog.debug("Channel Bandwidth: " + channelBandwidth); mLog.debug("Channels: " + channels); mLog.debug("Window Type: " + windowType.name()); - mLog.debug("Taps Per Channel: " + tapsPerChannel + " (Requested:" + tapsPerChannel + ")"); - mLog.debug("Filter Length: " + (taps.length + 1)); + mLog.debug("Taps Per Channel - Requested:" + tapsPerChannel + " Actual:" + ((double)extendedTaps.length / (double)channels)); + mLog.debug("Filter Length: " + (extendedTaps.length)); mLog.debug("Requested Cutoff Frequency: " + (sampleRate * bandEdge)); mLog.debug("Actual Cutoff Frequency: " + (sampleRate * cutoffFrequency)); - mLog.debug("Attenuation at 1.00 OBJECTIVE: " + PERFECT_RECONSTRUCTION_GAIN_AT_BAND_EDGE); + mLog.debug("Attenuation at 1.0 OBJECTIVE: " + PERFECT_RECONSTRUCTION_GAIN_AT_BAND_EDGE); mLog.debug("Attenuation at 1.00 Channels: " + evaluate(taps, bandEdge * 1.00)); mLog.debug("Attenuation at 1.25 Channels: " + evaluate(taps, bandEdge * 1.25)); mLog.debug("Attenuation at 1.50 Channels: " + evaluate(taps, bandEdge * 1.50)); @@ -851,10 +855,6 @@ public class FilterFactory mLog.debug("Attenuation at 2.00 Channels: " + evaluate(taps, bandEdge * 2.00)); } - //This is an odd length filter - increase the length by 1 by pre-padding a zero coefficient - float[] extendedTaps = new float[taps.length + 1]; - System.arraycopy(taps, 0, extendedTaps, 1, taps.length); - return extendedTaps; } diff --git a/src/dsp/filter/channelizer/AbstractComplexPolyphaseChannelizer.java b/src/dsp/filter/channelizer/AbstractComplexPolyphaseChannelizer.java index 41cce4d0..5aee3dcf 100644 --- a/src/dsp/filter/channelizer/AbstractComplexPolyphaseChannelizer.java +++ b/src/dsp/filter/channelizer/AbstractComplexPolyphaseChannelizer.java @@ -36,18 +36,39 @@ public abstract class AbstractComplexPolyphaseChannelizer implements Listener mSourceChangeBroadcaster = new Broadcaster(); private List mChannels = new CopyOnWriteArrayList<>(); + private int mSampleRate; private int mChannelCount; - private int mChannelSampleRate; + private double mChannelSampleRate; /** * Complex sample polyphase channelizer * * @param channelCount */ - public AbstractComplexPolyphaseChannelizer(int channelCount, int channelSampleRate) + public AbstractComplexPolyphaseChannelizer(int sampleRate, int channelCount) { mChannelCount = channelCount; - mChannelSampleRate = channelSampleRate; + mSampleRate = sampleRate; + mChannelSampleRate = (double)mSampleRate / (double)mChannelCount; + } + + /** + * Input sample rate for this channelizer + * @return sample rate in hertz + */ + public int getSampleRate() + { + return mSampleRate; + } + + /** + * Sets the input sample rate for for this channelizer + * @param sampleRate in hertz + */ + public void setSampleRate(int sampleRate) + { + mSampleRate = sampleRate; + mChannelSampleRate = (double)mSampleRate / (double)mChannelCount; } /** @@ -59,33 +80,14 @@ public abstract class AbstractComplexPolyphaseChannelizer implements Listener + * + ******************************************************************************/ +package dsp.filter.channelizer; + +import org.slf4j.Logger; +import org.slf4j.LoggerFactory; +import source.tuner.TunerChannel; + +import java.text.DecimalFormat; +import java.util.ArrayList; +import java.util.List; + +public class ChannelCalculator +{ + private final static Logger mLog = LoggerFactory.getLogger(ChannelCalculator.class); + + /** + * Policy to determine desired corrective action for methods that calculate an index for a frequency or a frequency + * from an index where the frequency may exist either at the overlapping boundary of two indexes, or in the middle + * of the wrap-around (N/2) index. + */ + private enum IndexBoundaryPolicy + { + ADJUST_POSITIVE, + ADJUST_NEGATIVE + } + + private int mSampleRate; + private int mChannelCount; + private double mCenterFrequency; + private double mChannelBandwidth; + private double mOversampling = 1.0; + + /** + * Calculates channel index(es) from a polyphase channelizer output to source a DDC polyphase channel source + * with a specified channel center frequency and bandwidth. + * + * Polyphase channel indexes are not linear/consecutive owing to the use of the FFT to rotate channels to their + * center frequencies following filtering in the polyphase filter bank. As such, this calculator provides methods + * to determine the min/center/max frequency of each channel that is adjusted for the current center frequency + * value and to determine the channel index(es) that need to be targeted to produce a requested output channel + * with a specific frequency and bandwdith. + * + * @param sampleRate in hertz for the input sample rate from the sample source (ie tuner) + * @param channelCount number of channels in the polyphase filter bank + * @param centerFrequency center frequency for the input sample stream + * @param oversampling set to 1.0 for no oversample, or greater than 1.0 for oversampling + */ + public ChannelCalculator(int sampleRate, int channelCount, double centerFrequency, double oversampling) + { + mSampleRate = sampleRate; + mChannelCount = channelCount; + mCenterFrequency = centerFrequency; + mOversampling = oversampling; + + updateChannelBandwidth();; + } + + /** + * Calculates the channel bandwidth from the sample rate and channel count. + */ + private void updateChannelBandwidth() + { + mChannelBandwidth = (double)mSampleRate / (double)mChannelCount; + } + + /** + * Center frequency for the channelizer + */ + public double getCenterFrequency() + { + return mCenterFrequency; + } + + /** + * Updates the center frequency for the channelizer + */ + public void setCenterFrequency(double frequency) + { + mCenterFrequency = frequency; + } + + /** + * Number of channels being channelized by the polyphase channelizer + */ + public int getChannelCount() + { + return mChannelCount; + } + + /** + * Updates the number of channels being channelized by the polyphase channelizer + */ + public void setChannelCount(int channelCount) + { + mChannelCount = channelCount; + updateChannelBandwidth(); + } + + /** + * Identifies the index of the wrap-around channel meaning that the lower half of the channel exists at the upper + * end of the spectrum and the upper half of the channel exists at the lower end of the spectrum + * + * @return wrap-around channel index + */ + public int getWrapAroundIndex() + { + return mChannelCount / 2; + } + + /** + * PolyphaseChannelManager input sample rate + * @return input sample rate in hertz + */ + public int getSampleRate() + { + return mSampleRate; + } + + /** + * Updates the channel count using the sample rate argument to calculate the number of channels from the + * already established channel bandwidth. + * + * @param sampleRate to use for channel count + */ + public void setSampleRate(int sampleRate) + { + setChannelCount((int)((double)sampleRate / getChannelBandwidth())); + } + + /** + * Channel bandwidth. Note: this value may be smaller than the channel sample rate when oversampling + * is being used. + */ + public double getChannelBandwidth() + { + return mChannelBandwidth; + } + + /** + * Half of the channel bandwidth. + */ + public double getHalfChannelBandwidth() + { + return mChannelBandwidth / 2.0; + } + + /** + * Oversampling rate produced by the channelizer. + * + * @return 1.0 for no oversampling, or a value greater than 1.0 when oversampling is applied. + */ + public double getOversampling() + { + return mOversampling; + } + + /** + * Sets the oversampling rate being applied by the polyphase channelizer + * @param oversampling rate where 1.0 indicates no oversampling, and a value greater than 1.0 indicates + * that the polyphase channelizer is oversampling each channel. + */ + public void setOversampling(double oversampling) + { + mOversampling = oversampling; + } + + /** + * Channel sample rate. This value is the product of the filtered channel bandwidth times the + * oversampling rate. + */ + public double getChannelSampleRate() + { + return (double)getChannelBandwidth() * getOversampling(); + } + + /** + * PolyphaseChannelManager input minimum frequency + * @return minimum frequency in hertz + */ + public long getMinimumFrequency() + { + return (long)(getCenterFrequency() - (getSampleRate() / 2.0)); + } + + /** + * PolyphaseChannelManager input maximum frequency + * @return maximum frequency in hertz + */ + public long getMaximumFrequency() + { + return (long)(getCenterFrequency() + (getSampleRate() / 2.0)); + } + + /** + * Identifies the channelizer channel index(es) required to produce a tuner channel with the specified center + * frequency and channel bandwidth. + * + * @param tunerChannel specifying the desired channel frequency and bandwidth + * + * @return an array of one or more polyphase channelizer index(es) to target in order to produce an output channel + * for the desired frequency and bandwidth. + * + * @throws IllegalArgumentException if the tuner channel's minimum or maximum frequency is not contained + * within the channelizer's minimum or maximum frequency range + */ + public List getChannelIndexes(TunerChannel tunerChannel) throws IllegalArgumentException + { + if(tunerChannel.getMinFrequency() < getMinimumFrequency() || + tunerChannel.getMaxFrequency() > getMaximumFrequency()) + { + throw new IllegalArgumentException("Requested channel cannot be provided by this " + + "channelizer. Requested channel [" + tunerChannel.getMinFrequency() + " - " + + tunerChannel.getMaxFrequency() + "] exceeds current channelizer frequency range [" + + getMinimumFrequency() + " - " + getMaximumFrequency() + "]"); + } + + int minIndex = getIndexForFrequency(tunerChannel.getMinFrequency(), IndexBoundaryPolicy.ADJUST_POSITIVE); + int maxIndex = getIndexForFrequency(tunerChannel.getMaxFrequency(), IndexBoundaryPolicy.ADJUST_NEGATIVE); + + //Detect the unsourceable situation where the wrap around index would be included in both the minimum index + //and the maximum index, meaning that the tuner channel bandwidth is nearly the same as the input sample rate. + if(minIndex == getWrapAroundIndex() && maxIndex == getWrapAroundIndex()) + { + throw new IllegalArgumentException("Requested tuner channel cannot be provided. Requested bandwidth is " + + "within two channel bandwidths of the sample rate."); + } + + List indexes = new ArrayList<>(); + + indexes.add(minIndex); + + if(minIndex != maxIndex) + { + //Ensure our min/max indexes are within the correct bounds 0 <> N-1 to safeguard the while loop below + if(minIndex < 0 || minIndex >= getChannelCount() || maxIndex < 0 || maxIndex >= getChannelCount()) + { + throw new IllegalArgumentException("Something went wrong while calculating the min and max polyphase " + + "channel indexes for tuner channel: " + tunerChannel); + } + + int pointer = minIndex + 1; + + if(pointer >= getChannelCount()) + { + pointer -= getChannelCount(); + } + + while(pointer != maxIndex) + { + indexes.add(pointer); + + pointer++; + + if(pointer >= getChannelCount()) + { + pointer -= getChannelCount(); + } + } + + indexes.add(maxIndex); + } + + return indexes; + } + + /** + * Calculates the index for the specified frequency. Certain frequencies can exist as both the maximum frequency + * of one channel and the minimum frequency of an adjacent channel. The index boundary policy identifies your + * choice to select the higher frequency channel index (ADJUST_POSITIVE) or the lower frequency channel + * index (ADJUST_NEGATIVE). + * + * @param frequency to determine channel index for + * @param indexBoundaryPolicy to determine the behavior when the frequency overlaps two channel boundaries + * @return index for the frequency + */ + public int getIndexForFrequency(long frequency, IndexBoundaryPolicy indexBoundaryPolicy) + { + double offset = frequency - getCenterFrequency(); + + if(Math.abs(offset) < getHalfChannelBandwidth()) + { + return 0; //Center index + } + + if(offset > 0) + { + offset += getHalfChannelBandwidth(); + } + else + { + offset -= getHalfChannelBandwidth(); + } + + int indexOffset = (int)(offset / getChannelBandwidth()); + + if(indexOffset < 0) + { + indexOffset += getChannelCount(); + } + + if(indexBoundaryPolicy == IndexBoundaryPolicy.ADJUST_POSITIVE && + isOverlapFrequency(frequency, indexOffset, indexOffset + 1)) + { + indexOffset = normalize(indexOffset + 1); + } + else if(indexBoundaryPolicy == IndexBoundaryPolicy.ADJUST_NEGATIVE && + isOverlapFrequency(frequency, indexOffset - 1, indexOffset)) + { + indexOffset = normalize(indexOffset - 1); + } + + return indexOffset; + } + + /** + * Indicates if the frequency is an overlapping frequency between channel index 1 and channel index 2 where the + * maximum frequency of index 1 is the same as the minimum frequency of index 2 and both of these frequencies are + * the same as the frequency argument. + * + * Note: both index 1 and index 2 will be normalized to the range of 0 <> N-1 prior to determining overlap + * + * @param frequency to test for overlap between index 1 and 2 + * @param index1 which is the lower channel index located next to index 2. + * @param index2 which is the higher channel index located next to index 1. + * @return true if frequency = index1 maximum frequency = index2 minimum frequency + */ + public boolean isOverlapFrequency(long frequency, int index1, int index2) + { + index1 = normalize(index1); + index2 = normalize(index2); + + //Check that these two indexes are side by side + int delta = normalize(index2 - index1); + + if(delta != 1) + { + return false; + } + + long index1MaximumFrequency = (long)getIndexMaximumFrequency(index1, IndexBoundaryPolicy.ADJUST_POSITIVE); + long index2MinimumFrequency = (long)getIndexMinimumFrequency(index2, IndexBoundaryPolicy.ADJUST_NEGATIVE); + + if(index1 == getWrapAroundIndex()) + { + index1MaximumFrequency = (long)getIndexMaximumFrequency(index1, IndexBoundaryPolicy.ADJUST_NEGATIVE); + } + + if(index2 == getWrapAroundIndex()) + { + index2MinimumFrequency = (long)getIndexMinimumFrequency(index2, IndexBoundaryPolicy.ADJUST_POSITIVE); + } + + return frequency == index1MaximumFrequency && frequency == index2MinimumFrequency; + } + + /** + * Normalizes the index to the range of 0 <> N-1 channel indexes + */ + private int normalize(int index) + { + while(index < 0) + { + index += getChannelCount(); + } + + while(index >= getChannelCount()) + { + index -= getChannelCount(); + } + + return index; + } + + /** + * Calculates the center frequency for the channel identified by the index in the channel results array. + * + * @param index to determine the channel center frequency + * @return center frequency for the indexed channel + */ + public double getIndexCenterFrequency(int index, IndexBoundaryPolicy indexBoundaryPolicy) + { + if(index < 0 || index >= getChannelCount()) + { + throw new IllegalArgumentException("Illegal channel index"); + } + + int wrapAroundIndex = getWrapAroundIndex(); + + if(index == wrapAroundIndex) + { + if(indexBoundaryPolicy == IndexBoundaryPolicy.ADJUST_POSITIVE) + { + return getCenterFrequency() + (index * getChannelBandwidth()); + } + else + { + return getCenterFrequency() - (index * getChannelBandwidth()); + } + } + else if(index < wrapAroundIndex) + { + return getCenterFrequency() + (index * getChannelBandwidth()); + } + else + { + return getCenterFrequency() - ((getChannelCount() - index) * getChannelBandwidth()); + } + } + + /** + * Calculates the minimum frequency band edge for the channel identified by the index in the channel results + * array. + * @param index to calculate the minimum band edge frequency for the channel. + * @param indexBoundaryPolicy for the wrap around index to indicate if this method should return the minimum + * frequency of the upper spectrum half of the wrap around channel (ADJUST_POSITIVE) or the minimum frequency of the + * lower spectrum half of the wrap around channel (ADJUST_NEGATIVE) + * @return frequency of the minimum band edge + */ + public double getIndexMinimumFrequency(int index, IndexBoundaryPolicy indexBoundaryPolicy) + { + if(index < 0 || index >= getChannelCount()) + { + throw new IllegalArgumentException("Can't determine minimum frequency - illegal channel index [" + + index + "]"); + } + + int wrapAroundIndex = getWrapAroundIndex(); + + if(index == wrapAroundIndex) + { + if(indexBoundaryPolicy == IndexBoundaryPolicy.ADJUST_POSITIVE) + { + return getCenterFrequency() + ((double)index * getChannelBandwidth()) - getHalfChannelBandwidth(); + } + else + { + return getIndexCenterFrequency(index, indexBoundaryPolicy); + } + } + else if(index <= wrapAroundIndex) + { + return getCenterFrequency() + ((double)index * getChannelBandwidth()) - getHalfChannelBandwidth(); + } + else + { + return getCenterFrequency() - ((double)(getChannelCount() - index) * getChannelBandwidth()) - + getHalfChannelBandwidth(); + } + } + + /** + * Calculates the maximum frequency band edge for the channel identified by the index in the channel results + * array. + * @param index to calculate the maximum band edge frequency for the channel. + * @param indexBoundaryPolicy for the wrap around index to indicate if this method should return the maximum + * frequency of the upper spectrum half of the wrap around channel (ADJUST_POSITIVE) or the maximum frequency of the + * lower spectrum half of the wrap around channel (ADJUST_NEGATIVE) + * @return frequency of the maximum band edge + */ + public double getIndexMaximumFrequency(int index, IndexBoundaryPolicy indexBoundaryPolicy) + { + if(index < 0 || index >= getChannelCount()) + { + throw new IllegalArgumentException("Can't determine maximum frequency - illegal channel index [" + + index + "]"); + } + + int wrapAroundIndex = getWrapAroundIndex(); + + if(index == wrapAroundIndex) + { + if(indexBoundaryPolicy == IndexBoundaryPolicy.ADJUST_POSITIVE) + { + return getIndexCenterFrequency(index, indexBoundaryPolicy); + } + else + { + return getCenterFrequency() - ((double)index * getChannelBandwidth() - getHalfChannelBandwidth()); + } + } + else if(index <= wrapAroundIndex) + { + return getCenterFrequency() + ((double)index * getChannelBandwidth()) + getHalfChannelBandwidth(); + } + else + { + return getCenterFrequency() - ((double)(getChannelCount() - index) * getChannelBandwidth()) + + getHalfChannelBandwidth(); + } + } + + /** + * Calculates the center frequency for a channel that will be synthesized from the channel indexes. + * @param indexes that are contiguous in the spectrum + * @return center frequency in hertz + */ + public long getCenterFrequencyForIndexes(List indexes) + { + if(indexes.isEmpty()) + { + throw new IllegalArgumentException("Indexes cannot be empty"); + } + + int middleIndex = indexes.get(indexes.size() / 2); + + if(indexes.size() % 2 == 0) + { + //Even number of indexes + return (long)getIndexMinimumFrequency(middleIndex, IndexBoundaryPolicy.ADJUST_POSITIVE); + } + else + { + //Odd number of indexes + return (long)getIndexCenterFrequency(middleIndex, IndexBoundaryPolicy.ADJUST_POSITIVE); + } + } + + /** + * String representation of the channel calculator settings + */ + public String toString() + { + StringBuilder sb = new StringBuilder(); + sb.append("Channels:").append(getChannelCount()); + sb.append(" Sample Rate:").append(getSampleRate()); + sb.append(" Channel Bandwidth:").append(getChannelBandwidth()); + sb.append(" Channel Rate:").append(getChannelSampleRate()); + sb.append(" Min:").append(getMinimumFrequency()); + sb.append(" Max:").append(getMaximumFrequency()); + return sb.toString(); + } + + + public static void main(String[] args) + { + mLog.debug("Starting ...."); + + int channelBandwidth = 12500; + int sampleRate = 100000; //Airspy + int channelCount = sampleRate / channelBandwidth; + double centerFrequency = 455000000; + + ChannelCalculator calculator = new ChannelCalculator(sampleRate, channelCount, centerFrequency, 2.0); + + mLog.debug(calculator.toString()); + + DecimalFormat decimalFormat = new DecimalFormat("000,000,000.0"); + + int wrapAroundIndex = calculator.getWrapAroundIndex(); + + IndexBoundaryPolicy indexBoundaryPolicy = IndexBoundaryPolicy.ADJUST_NEGATIVE; + + for(int index = wrapAroundIndex; index < calculator.getChannelCount(); index++) + { + mLog.debug("Index:" + index + " Min:" + decimalFormat.format(calculator.getIndexMinimumFrequency(index, indexBoundaryPolicy)) + + " Center:" + decimalFormat.format(calculator.getIndexCenterFrequency(index, indexBoundaryPolicy)) + + " Max:" + decimalFormat.format(calculator.getIndexMaximumFrequency(index, indexBoundaryPolicy))); + } + + indexBoundaryPolicy = IndexBoundaryPolicy.ADJUST_POSITIVE; + + for(int index = 0; index <= wrapAroundIndex; index++) + { + mLog.debug("Index:" + index + " Min:" + decimalFormat.format(calculator.getIndexMinimumFrequency(index, indexBoundaryPolicy)) + + " Center:" + decimalFormat.format(calculator.getIndexCenterFrequency(index, indexBoundaryPolicy)) + + " Max:" + decimalFormat.format(calculator.getIndexMaximumFrequency(index, indexBoundaryPolicy))); + } + + mLog.debug("Finished"); + } +} diff --git a/src/dsp/filter/channelizer/ChannelIndexCalculator.java b/src/dsp/filter/channelizer/ChannelIndexCalculator.java deleted file mode 100644 index d7b6f28b..00000000 --- a/src/dsp/filter/channelizer/ChannelIndexCalculator.java +++ /dev/null @@ -1,323 +0,0 @@ -/******************************************************************************* - * 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 source.tuner.TunerChannel; - -import java.util.ArrayList; -import java.util.List; - -public class ChannelIndexCalculator -{ - private final static Logger mLog = LoggerFactory.getLogger(ChannelIndexCalculator.class); - - private long mCenterFrequency; - private int mChannelCount; - private int mChannelBandwidth; - private double mOversampling = 1.0; - - /** - * Calculates the channel index(es) from a polyphase channelizer output to source a DDC polyphase channel source - * with a specified channel center frequency and bandwidth. - * - * @param centerFrequency - * @param channelCount - * @param channelBandwidth - * @param oversampling set to 1.0 for no oversample, or greater than 1.0 for oversampling - */ - public ChannelIndexCalculator(long centerFrequency, int channelCount, int channelBandwidth, double oversampling) - { - mCenterFrequency = centerFrequency; - mChannelCount = channelCount; - mChannelBandwidth = channelBandwidth; - mOversampling = oversampling; - } - - /** - * Center frequency for the channelizer - */ - public long getCenterFrequency() - { - return mCenterFrequency; - } - - /** - * Sets or changes the center frequency for the channelizer - */ - public void setCenterFrequency(long frequency) - { - mCenterFrequency = frequency; - } - - /** - * Number of channels being channelized by the polyphase channelizer - */ - public int getChannelCount() - { - return mChannelCount; - } - - /** - * Sets the number of channels being channelized by the polyphase channelizer - */ - public void setChannelCount(int channelCount) - { - mChannelCount = channelCount; - } - - /** - * Updates the channel count using the sample rate argument to calculate the number of channels from the - * already established channel bandwidth. - * - * @param sampleRate to use for channel count - */ - public void setSampleRate(int sampleRate) - { - setChannelCount(sampleRate / getChannelBandwidth()); - } - - /** - * Channel bandwidth. Note: this value may be smaller than the channel sample rate when oversampling - * is being used. - */ - public int getChannelBandwidth() - { - return mChannelBandwidth; - } - - /** - * Half of the channel bandwidth. - */ - public int getHalfChannelBandwidth() - { - return mChannelBandwidth / 2; - } - - /** - * Sets the channel bandwidth in hertz. - */ - public void setChannelBandwidth(int channelBandwidth) - { - mChannelBandwidth = channelBandwidth; - } - - /** - * Oversampling rate produced by the channelizer. - * - * @return 1.0 for no oversampling, or a value greater than 1.0 when oversampling is applied. - */ - public double getOversampling() - { - return mOversampling; - } - - /** - * Sets the oversampling rate being applied by the channelizer - * @param oversampling rate where 1.0 indicates no oversampling, and a value greater than 1.0 indicates - * that the polyphase channelizer is oversampling each channel. - */ - public void setOversampling(double oversampling) - { - mOversampling = oversampling; - } - - /** - * Channel sample rate. This value is the product of the filtered channel bandwidth times the - * oversampling rate. - */ - public double getChannelSampleRate() - { - return (double)getChannelBandwidth() * getOversampling(); - } - - /** - * PolyphaseChannelManager input sample rate - * @return input sample rate in hertz - */ - public int getSampleRate() - { - return getChannelBandwidth() * getChannelCount(); - } - - /** - * PolyphaseChannelManager input minimum frequency - * @return minimum frequency in hertz - */ - public long getMinimumFrequency() - { - return getCenterFrequency() - (getSampleRate() / 2); - } - - /** - * PolyphaseChannelManager input maximum frequency - * @return maximum frequency in hertz - */ - public long getMaximumFrequency() - { - return getCenterFrequency() + (getSampleRate() / 2); - } - - /** - * Identifies the channelizer output channel(s) required to produce a tuner channel with the specified center - * frequency and channel bandwidth. - * - * @param tunerChannel specifying the desired channel frequency and bandwidth - * @return an array of one or more polyphase channelizer channels to target in order to produce an output channel - * for the desired frequency and bandwidth. - * @throws IllegalArgumentException if the tuner channel's minimum or maximum frequency is not contained - * within the channelizer's minimum or maximum frequency range - */ - public List getPolyphaseChannelIndexes(TunerChannel tunerChannel) throws IllegalArgumentException - { - long min = getMinimumFrequency(); - long max = getMaximumFrequency(); - - if(tunerChannel.getMinFrequency() < getMinimumFrequency() || - tunerChannel.getMaxFrequency() > getMaximumFrequency()) - { - throw new IllegalArgumentException("Requested channel cannot be provided by this " + - "channelizer. Requested channel [" + tunerChannel.getMinFrequency() + " - " + - tunerChannel.getMaxFrequency() + "] exceeds current channelizer frequency range [" + - getMinimumFrequency() + " - " + getMaximumFrequency() + "]"); - } - - List indices = new ArrayList<>(); - - long startFrequencyOffset = (tunerChannel.getMinFrequency() - getMinimumFrequency()); - int startIndex = (int)(startFrequencyOffset / getChannelBandwidth()); - - long endFrequencyOffset = (tunerChannel.getMaxFrequency() - getMinimumFrequency()); - int endIndex = (int)(endFrequencyOffset / getChannelBandwidth()); - - int currentIndex = startIndex; - - while(currentIndex <= endIndex) - { - long currentIndexMin = getMinimumFrequencyForChannelIndex(currentIndex); - long currentIndexMax = getMaximumFrequencyForChannelIndex(currentIndex); - - //Case 1: tuner channel completely aligns with indexed channel - if(currentIndexMin <= tunerChannel.getMinFrequency() && tunerChannel.getMaxFrequency() <= currentIndexMax) - { - indices.add(currentIndex); - } - //Case 2: tuner channel min frequency aligns and max frequency exceeds - else if(currentIndexMin <= tunerChannel.getMinFrequency() && - tunerChannel.getMinFrequency() < currentIndexMax && - tunerChannel.getMaxFrequency() > currentIndexMax) - { - indices.add(currentIndex); - } - //Case 3: tuner channel min frequency exceeds and max frequency aligns - else if(tunerChannel.getMinFrequency() < currentIndexMin && - tunerChannel.getMaxFrequency() > currentIndexMin && - tunerChannel.getMaxFrequency() <= currentIndexMax) - { - indices.add(currentIndex); - } - //Case 4: tuner channel encapsulates the indexed channel - else if(tunerChannel.getMinFrequency() < currentIndexMin && tunerChannel.getMaxFrequency() > currentIndexMax) - { - indices.add(currentIndex); - } - - currentIndex++; - } - - return indices; - } - - /** - * Calculates the center frequency for the channel identified by the index in the channel results array. - * - * @param index to determine the channel center frequency - * @return center frequency for the indexed channel - */ - public long getCenterFrequencyForChannelIndex(int index) - { - if(index < 0 || index >= getChannelCount()) - { - throw new IllegalArgumentException("Illegal channel index"); - } - - return getMinimumFrequency() + getHalfChannelBandwidth() + (index * getChannelBandwidth()); - } - - /** - * Calculates the minimum frequency band edge for the channel identified by the index in the channel results - * array. - * @param index to calculate the minimum band edge frequency for the channel. - * @return frequency of the minimum band edge - */ - public long getMinimumFrequencyForChannelIndex(int index) - { - if(index < 0 || index > getChannelCount()) - { - throw new IllegalArgumentException("Can't determine minimum frequency - illegal channel index [" + - index + "]"); - } - - return getMinimumFrequency() + (index * getChannelBandwidth()); - } - - /** - * Calculates the maximum frequency band edge for the channel identified by the index in the channel results - * array. - * @param index to calculate the maximum band edge frequency for the channel. - * @return frequency of the maximum band edge - */ - public long getMaximumFrequencyForChannelIndex(int index) - { - if(index < 0 || index > getChannelCount()) - { - throw new IllegalArgumentException("Can't determine maximum frequency - illegal channel index [" + - index + "]"); - } - - //Maximum frequency is the same as the minimum frequency of the next higher indexed channel - return getMinimumFrequency() + ((index + 1) * getChannelBandwidth()); - } - - public static void main(String[] args) - { - mLog.debug("Starting ...."); - - int channelCount = 10; - - ChannelIndexCalculator channelIndexCalculator = - new ChannelIndexCalculator(100062500, channelCount, 12500, 2.0); - - for(int x = 0; x < channelCount; x++) - { - mLog.debug("Channel " + x + ": " + channelIndexCalculator.getMinimumFrequencyForChannelIndex(x) + " - " + - channelIndexCalculator.getMaximumFrequencyForChannelIndex(x) + - " Center: " + channelIndexCalculator.getCenterFrequencyForChannelIndex(x)); - } - - TunerChannel tunerChannel = new TunerChannel(100006250, 12500); - - List indices = channelIndexCalculator.getPolyphaseChannelIndexes(tunerChannel); - - mLog.debug("Indices: " + indices); - - mLog.debug("Finished!"); - } -} diff --git a/src/dsp/filter/channelizer/ChannelizerViewer.java b/src/dsp/filter/channelizer/ChannelizerViewer.java index 482f8795..8afc76b6 100644 --- a/src/dsp/filter/channelizer/ChannelizerViewer.java +++ b/src/dsp/filter/channelizer/ChannelizerViewer.java @@ -58,7 +58,7 @@ public class ChannelizerViewer extends JFrame private int mChannelsPerRow; private long mToneFrequency; private DFTSize mMainPanelDFTSize = DFTSize.FFT32768; - private DFTSize mChannelPanelDFTSize = DFTSize.FFT00512; + private DFTSize mChannelPanelDFTSize = DFTSize.FFT01024; private TestTuner mTestTuner; /** @@ -73,8 +73,7 @@ public class ChannelizerViewer extends JFrame mChannelCount = mTestTuner.getSampleRate() / CHANNEL_BANDWIDTH; mChannelsPerRow = channelsPerRow; - mToneFrequency = mTestTuner.getTunerController().getFrequency() + - (int)(CHANNEL_BANDWIDTH * 1); //Set to channel 1 as default + mToneFrequency = mTestTuner.getTunerController().getFrequency() + 19200; mTestTuner.setToneFrequency(mToneFrequency); diff --git a/src/dsp/filter/channelizer/ComplexPolyphaseChannelizer.java b/src/dsp/filter/channelizer/ComplexPolyphaseChannelizer.java index c4977b1e..dca0f4ca 100644 --- a/src/dsp/filter/channelizer/ComplexPolyphaseChannelizer.java +++ b/src/dsp/filter/channelizer/ComplexPolyphaseChannelizer.java @@ -25,6 +25,7 @@ import dsp.mixer.Oscillator; import org.jtransforms.fft.FloatFFT_1D; import org.slf4j.Logger; import org.slf4j.LoggerFactory; +import sample.complex.ComplexBuffer; public class ComplexPolyphaseChannelizer extends AbstractComplexPolyphaseChannelizer { @@ -56,6 +57,16 @@ public class ComplexPolyphaseChannelizer extends AbstractComplexPolyphaseChannel } @Override + public void receive(ComplexBuffer complexBuffer) + { + float[] samples = complexBuffer.getSamples(); + + for(int x = 0; x < samples.length; x += 2) + { + filter(samples[x], samples[x + 1]); + } + } + protected void filter(float inphase, float quadrature) { int index = (getChannelCount() - mFilterPointer - 1) * 2; diff --git a/src/dsp/filter/channelizer/ComplexPolyphaseChannelizerM2.java b/src/dsp/filter/channelizer/ComplexPolyphaseChannelizerM2.java index 114b3417..ebd8eb5f 100644 --- a/src/dsp/filter/channelizer/ComplexPolyphaseChannelizerM2.java +++ b/src/dsp/filter/channelizer/ComplexPolyphaseChannelizerM2.java @@ -18,14 +18,13 @@ ******************************************************************************/ package dsp.filter.channelizer; -import buffer.ReverseFloatCircularBuffer; import dsp.filter.FilterFactory; import dsp.filter.Window; import dsp.filter.design.FilterDesignException; -import dsp.mixer.Oscillator; import org.jtransforms.fft.FloatFFT_1D; import org.slf4j.Logger; import org.slf4j.LoggerFactory; +import sample.complex.ComplexBuffer; import java.text.DecimalFormat; @@ -33,15 +32,16 @@ public class ComplexPolyphaseChannelizerM2 extends AbstractComplexPolyphaseChann { private final static Logger mLog = LoggerFactory.getLogger(ComplexPolyphaseChannelizerM2.class); - private static final int DEFAULT_CHANNEL_BANDWIDTH = 12500; + private static final int DEFAULT_MINIMUM_CHANNEL_BANDWIDTH = 12500; - private ReverseFloatCircularBuffer[] mDataBuffers; - private float[][] mFilterCoefficients; - private int mFilterPointer = 0; + private float[][] mSamples; + private float[][] mCoefficients; + private int[][] mSampleIndexMap; + private int mColumnPointer; + private int mRowPointer; + private int mBlockSize; private FloatFFT_1D mFFT; - private DecimalFormat DECIMAL_FORMAT = new DecimalFormat("0.0000"); - /** * Non-Maximally Decimated Polyphase Filter Bank (NMDPFB) channelizer that divides the input frequency band into * equal bandwidth channels that are each oversampled by 2x for output. @@ -51,251 +51,299 @@ public class ComplexPolyphaseChannelizerM2 extends AbstractComplexPolyphaseChann * filter outputs) a new bandwidth signal from the outputs of this filter, then the filter should be designed * as a nyquist filter with -6 dB attenuation at the channel bandwidth cutoff frequency * - * @param channels - number of filters/channels to output. Since this filter bank oversamples each filter - * output, this number must be even (divisible by the oversample rate). - * - * @param channelSampleRate for each decimated, oversampled channel + * @param sampleRate of the incoming sample stream + * @param channelCount - number of filters/channels to output. Since this filter bank oversamples each filter + * output, this number must be even (divisible by the 2x oversample rate). */ - public ComplexPolyphaseChannelizerM2(float[] taps, int channels, int channelSampleRate) + public ComplexPolyphaseChannelizerM2(float[] taps, int sampleRate, int channelCount) { - super(channels, channelSampleRate); + super(sampleRate, channelCount); - if(channels % 2 != 0) + if(channelCount % 2 != 0) { throw new IllegalArgumentException("Channel count must be an even multiple of the oversample rate (2)"); } - initFilters(taps); + init(taps); } /** * Non-Maximally Decimated Polyphase Filter Bank (NMDPFB) channelizer that divides the input frequency band into * equal bandwidth channels that are each oversampled by 2x for output. * - * Note: this constructor uses a default channel bandwidth of 12.5 kHz and produces channels with an oversampled - * channel sample rate of 25.0 kHz. + * Note: this constructor uses a default minimum channel bandwidth of 12.5 kHz. The number of channels is calculate + * from the sample rate and this default minimum channel bandwidth. The resulting per-channel bandwidth will be + * between 12.5 and 25.0 kHz with the output per-channel sample rate of 2x the bandwidth. * * @param sampleRate to be channelized. + * @param tapsPerFilter to use when designing the filter */ - private ComplexPolyphaseChannelizerM2(int sampleRate, int tapsPerFilter) throws FilterDesignException + public ComplexPolyphaseChannelizerM2(int sampleRate, int tapsPerFilter) throws FilterDesignException { - super(sampleRate / DEFAULT_CHANNEL_BANDWIDTH, DEFAULT_CHANNEL_BANDWIDTH); - - mLog.debug("Sample Rate: " + sampleRate); + super(sampleRate, (int)(sampleRate / DEFAULT_MINIMUM_CHANNEL_BANDWIDTH)); float[] filterTaps = FilterFactory.getSincChannelizer(getChannelSampleRate(), getChannelCount(), tapsPerFilter, Window.WindowType.BLACKMAN_HARRIS_7, true); - initFilters(filterTaps); - } - - /** - * Non-Maximally Decimated Polyphase Filter Bank (NMDPFB) channelizer with a default channel bandwidth of 12.5 kHz - * and a channel output sample rate of 25.0 kHz. - * - * @param sampleRate that is an even multiple of 25.0 kHz - * @return channelizer - * @throws IllegalArgumentException if the sample rate argument is not an even multiple of 25.0 kHz - */ - public static ComplexPolyphaseChannelizerM2 create(int sampleRate, int tapsPerFilter) throws IllegalArgumentException, - FilterDesignException - { - if(sampleRate % (2 * DEFAULT_CHANNEL_BANDWIDTH) != 0) - { - throw new IllegalArgumentException("Sample rate must be an even multiple of 25 kHz"); - } - - return new ComplexPolyphaseChannelizerM2(sampleRate, tapsPerFilter); + init(filterTaps); } /** * Updates this channelizer to use the new sample rate. This method creates a new filter suitable for the * sample rate and reinitializes all internal data structures to prepare for processing the new sample rate. - * @param sampleRate that is a multiple of 25.0 kHz - * @throws IllegalArgumentException if the sample rate is not a multiple of 25.0 kHz - * @throws FilterDesignException if a filter cannot be designed for the target sample rate + * @param sampleRate in hertz */ - public void updateSampleRate(int sampleRate) throws IllegalArgumentException, FilterDesignException - { - if(sampleRate % (2 * DEFAULT_CHANNEL_BANDWIDTH) != 0) - { - throw new IllegalArgumentException("Sample rate must be an even multiple of 25 kHz"); - } - - float[] filterTaps = FilterFactory.getSincChannelizer(getChannelSampleRate(), getChannelCount(), - 15, Window.WindowType.BLACKMAN_HARRIS_7, true); - - initFilters(filterTaps); - } - - /** - * Filters the incoming complex sample pair - * - * @param inphase sample - * @param quadrature sample - */ - protected void filter(float inphase, float quadrature) + @Override + public void setSampleRate(int sampleRate) { try { - mDataBuffers[mFilterPointer--].put(quadrature); - mDataBuffers[mFilterPointer].put(inphase); + super.setSampleRate(sampleRate); - if(mFilterPointer == 0) - { - calculate(); - mFilterPointer = getChannelCount() * 2; - } - else if(mFilterPointer == getChannelCount()) - { - calculate(); - } + float[] filterTaps = FilterFactory.getSincChannelizer(getChannelSampleRate(), getChannelCount(), + 15, Window.WindowType.BLACKMAN_HARRIS_7, true); - mFilterPointer--; + init(filterTaps); } - catch(Exception e) + catch(FilterDesignException fde) { - mLog.error("Error!", e); + throw new IllegalArgumentException("Cannot create a channelizer filter for the specified sample rate [" + + sampleRate + "]"); } } - private void calculate() - { - float[] samples = new float[getChannelCount() * 2]; - - if(mFilterPointer == 0) - { - for(int x = 0; x < mFilterCoefficients.length; x++) - { - int index = x * 2; - - //Inphase sample - samples[index] = convolve(mFilterCoefficients[x], mDataBuffers[index]); - - //Quadrature sample - samples[index + 1] = convolve(mFilterCoefficients[x], mDataBuffers[index + 1]); - - } - } - else - { - int half = mFilterCoefficients.length / 2; - - for(int x = 0; x < half; x++) - { - int index = x * 2; - int filterIndex = x + half; - - //Inphase sample - samples[index] = convolve(mFilterCoefficients[filterIndex], mDataBuffers[index]); - - //Quadrature sample - samples[index + 1] = convolve(mFilterCoefficients[filterIndex], mDataBuffers[index + 1]); - } - - for(int x = half; x < mFilterCoefficients.length; x++) - { - int index = x * 2; - int filterIndex = x - half; - - //Inphase sample - samples[index] = convolve(mFilterCoefficients[filterIndex], mDataBuffers[index]); - - //Quadrature sample - samples[index + 1] = convolve(mFilterCoefficients[filterIndex], mDataBuffers[index + 1]); - } - } - - //IFFT is executed in-place with the output overwriting the input - mFFT.complexInverse(samples, true); - - dispatch(samples); - } - /** - * Filters the samples contained in the buffer against the filter coefficients - * - * @param coefficients of the sub filter - * @param buffer containing data samples - * @return + * Receives the complex sample buffer and processes the results through the channelizer. */ - private float convolve(float[] coefficients, ReverseFloatCircularBuffer buffer) + @Override + public void receive(ComplexBuffer complexBuffer) + { + float[] samples = complexBuffer.getSamples(); + + int samplesPointer = 0; + + while(samplesPointer < samples.length) + { + int lengthToCopy = mBlockSize - (mRowPointer % mBlockSize); + + if((samplesPointer + lengthToCopy) > samples.length) + { + lengthToCopy = samples.length - samplesPointer; + } + + System.arraycopy(samples, samplesPointer, mSamples[mColumnPointer], mRowPointer, lengthToCopy); + samplesPointer += lengthToCopy; + mRowPointer += lengthToCopy; + + if(mRowPointer % mBlockSize == 0) + { + process(); + } + } + } + + /** + * Processes the sample buffer for each new block of sample data that is loaded and distributes the results to any + * registered channel listeners. + */ + private void process() + { + int sampleRowCount = getChannelCount() * 2; + + float[] processed = new float[sampleRowCount]; + + + //Top of block processing + if(mRowPointer >= sampleRowCount) + { + int[] sampleIndexMap = mSampleIndexMap[mColumnPointer]; + + for(int channel = 0; channel < getChannelCount(); channel++) + { + int outputIndex = 2 * (getChannelCount() - channel - 1); + int sampleIndex = 2 * channel; + + float[] filter = mCoefficients[channel]; + + processed[outputIndex] = filter((sampleIndex), mCoefficients[channel], sampleIndexMap); + processed[outputIndex + 1] = filter((sampleIndex + 1), mCoefficients[channel], sampleIndexMap); + } + + mRowPointer = 0; + mColumnPointer--; + + if(mColumnPointer < 0) + { + mColumnPointer = mSamples.length - 1; + } + } + //Mid-point processing + else + { + int[] sampleIndexMap = mSampleIndexMap[mColumnPointer]; + + int half = getChannelCount() / 2; + + for(int channel = 0; channel < half; channel++) + { + int outputIndex = 2 * (getChannelCount() - channel - 1); + int sampleIndex = 2 * channel; + int filterIndex = channel + half; + + processed[outputIndex] = filter(sampleIndex, mCoefficients[filterIndex], sampleIndexMap); + processed[outputIndex + 1] = filter(sampleIndex + 1, mCoefficients[filterIndex], sampleIndexMap); + } + + //The second half of the sample buffer was loaded using the previous column pointer - adjust for that now + sampleIndexMap = mSampleIndexMap[(mColumnPointer == (mSamples.length - 1) ? 0 : mColumnPointer + 1)]; + + for(int channel = half; channel < getChannelCount(); channel++) + { + int outputIndex = 2 * (getChannelCount() - channel - 1); + int sampleIndex = 2 * channel; + int filterIndex = channel - half; + + processed[outputIndex] = filter(sampleIndex, mCoefficients[filterIndex], sampleIndexMap); + processed[outputIndex + 1] = filter(sampleIndex + 1, mCoefficients[filterIndex], sampleIndexMap); + } + } + + //Rotate each of the channels to the correct phase using the IFFT + mFFT.complexInverse(processed, true); + + dispatch(processed); + } + + /** + * Calculates the filtered output for polyphase filter arm. + * + * @param sampleRow index pointing to a row in the mSamples array to filter + * @param coefficients to use in filtering the samples + * @param sampleIndexMap to translate the column index offset based on the current sample column pointer + * @return filtered sample for the row + */ + private float filter(int sampleRow, float[] coefficients, int[] sampleIndexMap) { float accumulator = 0.0f; - for(int x = 0; x < coefficients.length; x++) + for(int column = 0; column < mSamples.length; column++) { - accumulator += coefficients[x] * buffer.get(x); + accumulator += mSamples[sampleIndexMap[column]][sampleRow] * coefficients[column]; } return accumulator; } + /** - * Distributes the filter taps to each of the polyphase filters + * Initializes the channelizer structures. Distributes the prototype filter coefficients to each of the polyphase + * filters, sets up the sample buffer array and pointers and initializes the FFT. * - * @param taps + * These structures resemble those described by Fred Harris in Multirate Signal Processing for Communications + * Systems, p230-233. However, instead of swapping the input sample buffers and swapping the output sample array + * on every other processing block, we simply swap the filter coefficient banks and leave the input sample buffer + * and output array indexes aligned. However, the entire structure (sample buffer, coefficients and output array) + * is inverted (row zero index is M-1 and M-1 is zero index) to facilitate easy loading of data into the input + * sample buffer array using Java's efficient System.arrayCopy() utility. + * + * Note: the filter coefficients array is organized (row,column) for ease of accessing each filter arm row during + * convolution. The samples array is organized (column,row) for ease/efficiency in loading each row of data. Also, + * the input sample array implements a circular buffer where the mCoefficientsMap provides a lookup to each sample + * index based on the current value of the mColumnPointer. + * + * @param coefficients of the prototype filter for this channelizer */ - private void initFilters(float[] taps) + private void init(float[] coefficients) { - int tapCount = (int)Math.ceil((double)taps.length / (double)getChannelCount()); + int columnCount = (int)Math.ceil((double)coefficients.length / (double)getChannelCount()); + int filterRowCount = getChannelCount(); + int sampleRowCount = getChannelCount() * 2; - mDataBuffers = new ReverseFloatCircularBuffer[getChannelCount() * 2]; + //Sample and filter coefficient arrays are specifically setup as column/row and row/column + mSamples = new float[columnCount][sampleRowCount]; + mCoefficients = new float[filterRowCount][columnCount]; - for(int x = 0; x < getChannelCount() * 2; x++) + mColumnPointer = columnCount - 1; + mBlockSize = getChannelCount(); + mRowPointer = mBlockSize; + + //Setup the FFT + mFFT = new FloatFFT_1D(getChannelCount()); + + //Setup the polyphase filters + + //Setup the index map for the sample array circular buffer + initIndexMap(columnCount); + + int coefficientIndex = 0; + + //Load filter columns left-2-right (samples are loaded in reverse column order, right-2-left, for easy convolution) + for(int columnIndex = 0; columnIndex < columnCount; columnIndex++) { - mDataBuffers[x] = new ReverseFloatCircularBuffer(tapCount); - } - - mFilterCoefficients = new float[getChannelCount()][tapCount]; - - for(int tap = 0; tap < tapCount; tap++) - { - for(int filter = 0; filter < getChannelCount(); filter++) + //Load filter rows bottom to top, opposite of sample row loading and sample output ordering + for(int rowIndex = filterRowCount - 1; rowIndex >= 0; rowIndex--) { - int index = tap * getChannelCount() + filter; - - if(index < taps.length) - { - mFilterCoefficients[filter][tap] = taps[index]; - } + mCoefficients[rowIndex][columnIndex] = coefficients[coefficientIndex++]; } } + } - mFFT = new FloatFFT_1D(getChannelCount()); - mFilterPointer = getChannelCount() - 1; //Start at M/2 + /** + * Generates a circular buffer index map to support lookup of the translated index based on the current column + * pointer and the desired sample index. + */ + private void initIndexMap(int size) + { + mSampleIndexMap = new int[size][size]; + + for(int row = 0; row < size; row++) + { + for(int column = 0;column < size;column++) + { + int offset = row + column; + + mSampleIndexMap[row][column] = ((offset < size) ? offset : offset - size); + } + } } public static void main(String[] args) { mLog.debug("Starting ..."); - int symbolRate = 4800; - int samplesPerSymbol = 2; - int channels = 800; + int channelCount = 4; int channelBandwidth = 12500; - int sampleRate = channels * channelBandwidth; - - float[] taps = FilterFactory.getLowPass(10000000, 12500, 5600, Window.WindowType.BLACKMAN_HARRIS_7); + int sampleRate = channelCount * channelBandwidth; + int tapsPerFilter = 14; int channelCenter = (int)(12500 * 1.5); - StringBuilder sb = new StringBuilder(); - sb.append("\nPolyphase TunerChannelizer\n"); - sb.append("Sample Rate:" + sampleRate + " Channels:" + channels + " Channel Rate:" + channelBandwidth + "\n"); - sb.append("Tap Count:" + taps.length + "\n"); - sb.append("Channel:" + channelCenter); - - mLog.debug(sb.toString()); - - ComplexPolyphaseChannelizerM2 channelizer = new ComplexPolyphaseChannelizerM2(taps, 8, channelBandwidth * 2); - - Oscillator oscillator = new Oscillator(channelCenter, 2 * sampleRate); - - for(int x = 0; x < 2000; x++) + float[] taps = new float[16]; + for(int x = 0; x < taps.length; x++) { - channelizer.filter(oscillator.getComplex().inphase(), oscillator.getComplex().quadrature()); - oscillator.rotate(); + taps[x] = x; + } + float[] samples = new float[32]; + for(int x = 0; x < samples.length; x++) + { + samples[x] = x; + } + + try + { +// ComplexPolyphaseChannelizerM2 channelizer = new ComplexPolyphaseChannelizerM2(sampleRate, tapsPerFilter); + ComplexPolyphaseChannelizerM2 channelizer = new ComplexPolyphaseChannelizerM2(taps,sampleRate, channelCount); + +// Oscillator oscillator = new Oscillator(channelCenter, 2 * sampleRate); +// +// ComplexBuffer complexBuffer = oscillator.generateComplexBuffer(1000); + ComplexBuffer complexBuffer = new ComplexBuffer(samples); + + channelizer.receive(complexBuffer); + } + catch(Exception e) + { + mLog.error("Error", e); } mLog.debug("Finished!"); diff --git a/src/dsp/filter/channelizer/PolyphaseChannelManager.java b/src/dsp/filter/channelizer/PolyphaseChannelManager.java index 6bd797af..b82b212d 100644 --- a/src/dsp/filter/channelizer/PolyphaseChannelManager.java +++ b/src/dsp/filter/channelizer/PolyphaseChannelManager.java @@ -25,7 +25,6 @@ import dsp.filter.design.FilterDesignException; import org.slf4j.Logger; import org.slf4j.LoggerFactory; import sample.Listener; -import source.ISourceEventProcessor; import source.Source; import source.SourceEvent; import source.SourceException; @@ -45,7 +44,7 @@ public class PolyphaseChannelManager implements Listener private Tuner mTuner; private List mChannelSources = new CopyOnWriteArrayList<>(); - private ChannelIndexCalculator mChannelIndexCalculator; + private ChannelCalculator mChannelCalculator; private ComplexPolyphaseChannelizerM2 mPolyphaseChannelizer; private ChannelSourceEventListener mChannelSourceEventListener = new ChannelSourceEventListener(); private ScheduledBufferProcessor mBufferProcessor; @@ -81,10 +80,9 @@ public class PolyphaseChannelManager implements Listener long frequency = mTuner.getTunerController().getFrequency(); int channelCount = sampleRate / CHANNEL_BANDWIDTH; - mChannelIndexCalculator = new ChannelIndexCalculator(frequency, channelCount, CHANNEL_BANDWIDTH, - CHANNEL_OVERSAMPLING); + mChannelCalculator = new ChannelCalculator(sampleRate, channelCount, frequency, CHANNEL_OVERSAMPLING); - mBufferProcessor = new ScheduledBufferProcessor(500, 100, 100, 50); + mBufferProcessor = new ScheduledBufferProcessor(500, 100, 50, 50); updateChannelizer(sampleRate, null); @@ -104,7 +102,7 @@ public class PolyphaseChannelManager implements Listener try { - List polyphaseIndexes = mChannelIndexCalculator.getPolyphaseChannelIndexes(tunerChannel); + List polyphaseIndexes = mChannelCalculator.getChannelIndexes(tunerChannel); IPolyphaseChannelOutputProcessor outputProcessor = getOutputProcessor(polyphaseIndexes); @@ -119,8 +117,8 @@ public class PolyphaseChannelManager implements Listener catch(IllegalArgumentException iae) { mLog.info("Can't provide DDC for " + tunerChannel.toString() + " due to channelizer frequency [" + - mChannelIndexCalculator.getCenterFrequency() + "] and sample rate [" + - (mChannelIndexCalculator.getChannelCount() * mChannelIndexCalculator.getChannelBandwidth()) + "]"); + mChannelCalculator.getCenterFrequency() + "] and sample rate [" + + (mChannelCalculator.getChannelCount() * mChannelCalculator.getChannelBandwidth()) + "]"); } return channelSource; @@ -198,7 +196,7 @@ public class PolyphaseChannelManager implements Listener { case NOTIFICATION_FREQUENCY_CHANGE: //Update the index calculator and each of the output processors to update each channel frequency - mChannelIndexCalculator.setCenterFrequency(sourceEvent.getValue().longValue()); + mChannelCalculator.setCenterFrequency(sourceEvent.getValue().longValue()); updateOutputProcessors(sourceEvent); break; case NOTIFICATION_SAMPLE_RATE_CHANGE: @@ -218,7 +216,7 @@ public class PolyphaseChannelManager implements Listener */ private void updateChannelizer(int sampleRate, SourceEvent sourceEvent) { - mChannelIndexCalculator.setSampleRate(sampleRate); + mChannelCalculator.setSampleRate(sampleRate); //Lock on the buffer processor to block any channel create/start/stop requests while we configure synchronized(mBufferProcessor) @@ -233,14 +231,14 @@ public class PolyphaseChannelManager implements Listener { if(mPolyphaseChannelizer == null) { - mPolyphaseChannelizer = ComplexPolyphaseChannelizerM2.create(sampleRate, + mPolyphaseChannelizer = new ComplexPolyphaseChannelizerM2(sampleRate, POLYPHASE_FILTER_TAPS_PER_CHANNEL); mBufferProcessor.setListener(mPolyphaseChannelizer); } else { - mPolyphaseChannelizer.updateSampleRate(sampleRate); + mPolyphaseChannelizer.setSampleRate(sampleRate); } } catch(IllegalArgumentException iae) @@ -304,7 +302,7 @@ public class PolyphaseChannelManager implements Listener { try { - List indexes = mChannelIndexCalculator.getPolyphaseChannelIndexes(channelSource.getTunerChannel()); + List indexes = mChannelCalculator.getChannelIndexes(channelSource.getTunerChannel()); //If the indexes size is the same then update the current processor, otherwise create a new one if(channelSource.getPolyphaseChannelOutputProcessor().getInputChannelCount() == indexes.size()) diff --git a/src/dsp/filter/channelizer/ScheduledBufferProcessor.java b/src/dsp/filter/channelizer/ScheduledBufferProcessor.java index 17635504..adc7ac9b 100644 --- a/src/dsp/filter/channelizer/ScheduledBufferProcessor.java +++ b/src/dsp/filter/channelizer/ScheduledBufferProcessor.java @@ -106,10 +106,7 @@ public class ScheduledBufferProcessor implements Listener @Override public void receive(E e) { - if(mRunning.get()) - { - mQueue.offer(e); - } + mQueue.offer(e); } /** diff --git a/src/source/tuner/test/TestTunerController.java b/src/source/tuner/test/TestTunerController.java index 280f853a..b09f630b 100644 --- a/src/source/tuner/test/TestTunerController.java +++ b/src/source/tuner/test/TestTunerController.java @@ -51,7 +51,7 @@ public class TestTunerController extends TunerController int sweepRate = 0; //Hz per interval - mSampleGenerator = new SampleGenerator(DEFAULT_SAMPLE_RATE, 5000, SAMPLE_GENERATION_INTERVAL, sweepRate); + mSampleGenerator = new SampleGenerator(DEFAULT_SAMPLE_RATE, 12500, SAMPLE_GENERATION_INTERVAL, sweepRate); try { From 57fab2b7cade444b278debe30e3067db38f95a3d Mon Sep 17 00:00:00 2001 From: Denny Date: Sun, 3 Dec 2017 11:10:50 -0500 Subject: [PATCH 32/87] #39 WIP - initial efforts to restructure the way tuners and tuner controllers manager sample buffer processing and channel management. General cleanup across all modules to remove the executor from each of the start() methods. Removed the B100 tuner class since it's not yet implemented. --- src/audio/AudioModule.java | 2 +- .../state/AlwaysUnsquelchedDecoderState.java | 12 +- src/channel/state/ChannelState.java | 6 +- src/channel/state/DecoderState.java | 15 +- .../traffic/TrafficChannelManager.java | 4 +- src/controller/channel/Channel.java | 2 +- .../filter/channelizer/ChannelCalculator.java | 2 +- .../filter/channelizer/ChannelizerViewer.java | 31 +- .../channelizer/PolyphaseChannelManager.java | 33 +- .../channelizer/PolyphaseChannelSource.java | 6 +- src/module/Module.java | 4 +- src/module/ProcessingChain.java | 12 +- src/module/decode/Decoder.java | 49 +- .../decode/fleetsync2/Fleetsync2Decoder.java | 15 - .../fleetsync2/Fleetsync2DecoderState.java | 15 - src/module/decode/lj1200/LJ1200Decoder.java | 15 - .../decode/lj1200/LJ1200DecoderState.java | 16 - src/module/decode/ltrnet/LTRNetDecoder.java | 10 - .../decode/ltrnet/LTRNetDecoderState.java | 16 - .../ltrstandard/LTRStandardDecoder.java | 10 - .../ltrstandard/LTRStandardDecoderState.java | 17 - src/module/decode/mdc1200/MDCDecoder.java | 21 - .../decode/mdc1200/MDCDecoderState.java | 16 - src/module/decode/mpt1327/MPT1327Decoder.java | 12 - .../decode/mpt1327/MPT1327DecoderState.java | 16 - src/module/decode/p25/P25DecoderState.java | 15 +- src/module/decode/p25/P25_C4FMDecoder.java | 21 - src/module/decode/p25/P25_LSMDecoder.java | 21 - .../decode/p25/audio/P25AudioModule.java | 2 +- .../decode/passport/PassportDecoder.java | 10 - .../decode/passport/PassportDecoderState.java | 15 - src/module/decode/tait/Tait1200Decoder.java | 21 - .../decode/tait/Tait1200DecoderState.java | 16 - .../demodulate/am/AMDemodulatorModule.java | 164 +-- .../audio/DemodulatedAudioFilterModule.java | 2 +- .../demodulate/fm/FMDemodulatorModule.java | 2 +- src/module/log/EventLogger.java | 2 +- src/record/AudioRecorder.java | 5 +- .../wave/ComplexBufferWaveRecorder.java | 351 +++-- src/record/wave/RealBufferWaveRecorder.java | 5 +- src/sample/OverflowableTransferQueue.java | 1 - .../complex/IComplexBufferProvider.java | 39 + src/source/Source.java | 14 +- src/source/SourceEvent.java | 10 + src/source/config/SourceConfigRecording.java | 2 +- src/source/config/SourceConfigTuner.java | 2 +- .../heartbeat/Heartbeat.java | 2 +- src/source/heartbeat/HeartbeatManager.java | 58 + .../heartbeat/IHeartbeatListener.java | 2 +- .../heartbeat/IHeartbeatProvider.java | 8 +- src/source/mixer/RealMixerSource.java | 3 +- src/source/recording/Recording.java | 4 +- .../recording/RecordingSourceManager.java | 4 +- src/source/tuner/ITunerChannelProvider.java | 4 +- src/source/tuner/MixerTuner.java | 131 -- src/source/tuner/Tuner.java | 151 +- src/source/tuner/TunerChannelProvider.java | 27 - src/source/tuner/TunerController.java | 60 +- src/source/tuner/TunerManager.java | 27 +- src/source/tuner/TunerModel.java | 508 ++++--- src/source/tuner/airspy/AirspyTuner.java | 54 - .../tuner/airspy/AirspyTunerController.java | 53 +- .../DecimatingTunerChannelSource.java} | 116 +- .../tuner/{ => channel}/TunerChannel.java | 2 +- .../tuner/channel/TunerChannelProvider.java | 45 + .../tuner/channel/TunerChannelSource.java | 105 ++ src/source/tuner/ettus/B100Tuner.java | 83 -- .../tuner/ettus/B100TunerEditorPanel.java | 44 - src/source/tuner/fcd/FCDTuner.java | 56 +- src/source/tuner/fcd/FCDTunerController.java | 53 +- .../tuner/fcd/proV1/FCD1TunerController.java | 1268 ++++++++--------- .../fcd/proplusV2/FCD2TunerController.java | 8 +- .../frequency/DCTrackingSourceControl.java | 2 +- .../tuner/frequency/FrequencyController.java | 15 + src/source/tuner/hackrf/HackRFTuner.java | 47 - .../tuner/hackrf/HackRFTunerController.java | 28 +- .../tuner/manager/AbstractSourceManager.java | 111 ++ .../tuner/manager/TunerSourceManager.java | 83 ++ src/source/tuner/rtl/RTL2832Tuner.java | 150 +- .../tuner/rtl/RTL2832TunerController.java | 38 +- src/source/tuner/test/TestTuner.java | 103 +- .../tuner/test/TestTunerController.java | 14 +- .../tuner/usb/USBTransferProcessor.java | 35 +- src/source/tuner/usb/USBTunerController.java | 72 + src/source/wave/ComplexWaveSource.java | 2 +- src/source/wave/RealWaveSource.java | 2 +- src/spectrum/OverlayPanel.java | 2 +- src/spectrum/SpectralDisplayPanel.java | 2 +- 88 files changed, 2112 insertions(+), 2547 deletions(-) create mode 100644 src/sample/complex/IComplexBufferProvider.java rename src/{channel => source}/heartbeat/Heartbeat.java (97%) create mode 100644 src/source/heartbeat/HeartbeatManager.java rename src/{channel => source}/heartbeat/IHeartbeatListener.java (97%) rename src/{channel => source}/heartbeat/IHeartbeatProvider.java (84%) delete mode 100644 src/source/tuner/MixerTuner.java delete mode 100644 src/source/tuner/TunerChannelProvider.java rename src/source/tuner/{TunerChannelSource.java => channel/DecimatingTunerChannelSource.java} (89%) rename src/source/tuner/{ => channel}/TunerChannel.java (98%) create mode 100644 src/source/tuner/channel/TunerChannelProvider.java create mode 100644 src/source/tuner/channel/TunerChannelSource.java delete mode 100644 src/source/tuner/ettus/B100Tuner.java delete mode 100644 src/source/tuner/ettus/B100TunerEditorPanel.java create mode 100644 src/source/tuner/manager/AbstractSourceManager.java create mode 100644 src/source/tuner/manager/TunerSourceManager.java create mode 100644 src/source/tuner/usb/USBTunerController.java diff --git a/src/audio/AudioModule.java b/src/audio/AudioModule.java index 58b0be0f..f1cdf38f 100644 --- a/src/audio/AudioModule.java +++ b/src/audio/AudioModule.java @@ -117,7 +117,7 @@ public class AudioModule extends Module implements IAudioPacketProvider, IFilter } @Override - public void start(ScheduledExecutorService executor) + public void start() { /* No start operations provided */ } diff --git a/src/channel/state/AlwaysUnsquelchedDecoderState.java b/src/channel/state/AlwaysUnsquelchedDecoderState.java index ef6a0db0..b2a67cc9 100644 --- a/src/channel/state/AlwaysUnsquelchedDecoderState.java +++ b/src/channel/state/AlwaysUnsquelchedDecoderState.java @@ -47,11 +47,6 @@ public class AlwaysUnsquelchedDecoderState extends DecoderState mChannelName = channelName; } - @Override - public void init() - { - } - @Override public void reset() { @@ -94,14 +89,9 @@ public class AlwaysUnsquelchedDecoderState extends DecoderState } @Override - public void start(ScheduledExecutorService executor) + public void start() { broadcast(new AttributeChangeRequest(Attribute.PRIMARY_ADDRESS_TO, mChannelName)); broadcast(new DecoderStateEvent(this, Event.ALWAYS_UNSQUELCH, State.IDLE)); } - - @Override - public void stop() - { - } } diff --git a/src/channel/state/ChannelState.java b/src/channel/state/ChannelState.java index 01fc6175..3bb4d00b 100644 --- a/src/channel/state/ChannelState.java +++ b/src/channel/state/ChannelState.java @@ -20,8 +20,8 @@ package channel.state; import audio.squelch.ISquelchStateProvider; import audio.squelch.SquelchState; -import channel.heartbeat.Heartbeat; -import channel.heartbeat.IHeartbeatListener; +import source.heartbeat.Heartbeat; +import source.heartbeat.IHeartbeatListener; import channel.metadata.Attribute; import channel.metadata.AttributeChangeRequest; import channel.metadata.IAttributeChangeRequestListener; @@ -124,7 +124,7 @@ public class ChannelState extends Module implements ICallEventProvider, IDecoder } @Override - public void start(ScheduledExecutorService executor) + public void start() { mMutableMetadata.receive(new AttributeChangeRequest(Attribute.CHANNEL_STATE, mState)); diff --git a/src/channel/state/DecoderState.java b/src/channel/state/DecoderState.java index 4e68bb8f..707f0241 100644 --- a/src/channel/state/DecoderState.java +++ b/src/channel/state/DecoderState.java @@ -71,7 +71,20 @@ public abstract class DecoderState extends Module implements ActivitySummaryProv /** * Allow the decoder to perform any setup actions */ - public abstract void init(); + public void init() + { + + } + + @Override + public void start() + { + } + + @Override + public void stop() + { + } /** * Implements the IDecoderStateEventListener interface to receive state diff --git a/src/channel/traffic/TrafficChannelManager.java b/src/channel/traffic/TrafficChannelManager.java index d7fac5a2..f01bc492 100644 --- a/src/channel/traffic/TrafficChannelManager.java +++ b/src/channel/traffic/TrafficChannelManager.java @@ -36,7 +36,7 @@ import org.slf4j.LoggerFactory; import record.config.RecordConfiguration; import sample.Listener; import source.config.SourceConfigTuner; -import source.tuner.TunerChannel; +import source.tuner.channel.TunerChannel; import java.util.ArrayList; import java.util.List; @@ -330,7 +330,7 @@ public class TrafficChannelManager extends Module implements ICallEventProvider, } @Override - public void start(ScheduledExecutorService executor) + public void start() { } diff --git a/src/controller/channel/Channel.java b/src/controller/channel/Channel.java index b3e49224..c099b517 100644 --- a/src/controller/channel/Channel.java +++ b/src/controller/channel/Channel.java @@ -33,7 +33,7 @@ import source.config.SourceConfigFactory; import source.config.SourceConfigRecording; import source.config.SourceConfigTuner; import source.config.SourceConfiguration; -import source.tuner.TunerChannel; +import source.tuner.channel.TunerChannel; import source.SourceEvent; import source.SourceEvent.Event; diff --git a/src/dsp/filter/channelizer/ChannelCalculator.java b/src/dsp/filter/channelizer/ChannelCalculator.java index bf98e1ea..0f80483b 100644 --- a/src/dsp/filter/channelizer/ChannelCalculator.java +++ b/src/dsp/filter/channelizer/ChannelCalculator.java @@ -20,7 +20,7 @@ package dsp.filter.channelizer; import org.slf4j.Logger; import org.slf4j.LoggerFactory; -import source.tuner.TunerChannel; +import source.tuner.channel.TunerChannel; import java.text.DecimalFormat; import java.util.ArrayList; diff --git a/src/dsp/filter/channelizer/ChannelizerViewer.java b/src/dsp/filter/channelizer/ChannelizerViewer.java index 8afc76b6..91db8580 100644 --- a/src/dsp/filter/channelizer/ChannelizerViewer.java +++ b/src/dsp/filter/channelizer/ChannelizerViewer.java @@ -28,7 +28,8 @@ import settings.SettingsManager; import source.ISourceEventProcessor; import source.SourceEvent; import source.SourceException; -import source.tuner.TunerChannel; +import source.tuner.channel.TunerChannel; +import source.tuner.channel.TunerChannelSource; import source.tuner.configuration.TunerConfigurationModel; import source.tuner.test.TestTuner; import spectrum.DFTProcessor; @@ -70,12 +71,12 @@ public class ChannelizerViewer extends JFrame { mTestTuner = new TestTuner(); - mChannelCount = mTestTuner.getSampleRate() / CHANNEL_BANDWIDTH; + mChannelCount = mTestTuner.getTunerController().getSampleRate() / CHANNEL_BANDWIDTH; mChannelsPerRow = channelsPerRow; mToneFrequency = mTestTuner.getTunerController().getFrequency() + 19200; - mTestTuner.setToneFrequency(mToneFrequency); + mTestTuner.getTunerController().setToneFrequency(mToneFrequency); init(); } @@ -108,10 +109,11 @@ public class ChannelizerViewer extends JFrame { if(mPrimarySpectrumPanel == null) { - mPrimarySpectrumPanel = new PrimarySpectrumPanel(mSettingsManager, mTestTuner.getSampleRate()); + mPrimarySpectrumPanel = new PrimarySpectrumPanel(mSettingsManager, + mTestTuner.getTunerController().getSampleRate()); mPrimarySpectrumPanel.setPreferredSize(new Dimension(1000, 200)); mPrimarySpectrumPanel.setDFTSize(mMainPanelDFTSize); - mTestTuner.addListener(mPrimarySpectrumPanel); + mTestTuner.getSourceManager().addComplexBufferListener(mPrimarySpectrumPanel); } return mPrimarySpectrumPanel; @@ -125,7 +127,7 @@ public class ChannelizerViewer extends JFrame mControlPanel.setLayout(new MigLayout("insets 0 0 0 0", "", "")); mControlPanel.add(new JLabel("Tone:"), "align left"); - int maximumFrequencyOffset = mTestTuner.getSampleRate() / 2; + int maximumFrequencyOffset = mTestTuner.getTunerController().getSampleRate() / 2; long minimumFrequency = mTestTuner.getTunerController().getFrequency() - maximumFrequencyOffset; long maximumFrequency = mTestTuner.getTunerController().getFrequency() + maximumFrequencyOffset; @@ -138,7 +140,7 @@ public class ChannelizerViewer extends JFrame public void stateChanged(ChangeEvent e) { int toneFrequency = model.getNumber().intValue(); - mTestTuner.setToneFrequency(toneFrequency); + mTestTuner.getTunerController().setToneFrequency(toneFrequency); } }); @@ -147,9 +149,9 @@ public class ChannelizerViewer extends JFrame mControlPanel.add(spinner); mControlPanel.add(new JLabel("Hz")); - mControlPanel.add(new JLabel("Sample Rate: " + mTestTuner.getSampleRate() + " Hz"), "push,align right"); + mControlPanel.add(new JLabel("Sample Rate: " + mTestTuner.getTunerController().getSampleRate() + " Hz"), "push,align right"); - mControlPanel.add(new JLabel("Max: " + (mTestTuner.getSampleRate() / 2) + " Hz"), "push,align right"); + mControlPanel.add(new JLabel("Max: " + (mTestTuner.getTunerController().getSampleRate() / 2) + " Hz"), "push,align right"); mControlPanel.add(new JLabel("Channels: " + mChannelCount), "push,align right"); } @@ -183,7 +185,7 @@ public class ChannelizerViewer extends JFrame { setLayout(new MigLayout("insets 0 0 0 0", "fill", "fill")); - long spectralBandwidth = mTestTuner.getSampleRate(); + long spectralBandwidth = mTestTuner.getTunerController().getSampleRate(); long halfSpectralBandwidth = spectralBandwidth / 2; int channelToLog = -1; @@ -248,7 +250,7 @@ public class ChannelizerViewer extends JFrame public class ChannelPanel extends JPanel implements Listener, ISourceEventProcessor { - private PolyphaseChannelSource mSource; + private TunerChannelSource mSource; private DFTProcessor mDFTProcessor = new DFTProcessor(SampleType.COMPLEX); private ComplexDecibelConverter mComplexDecibelConverter = new ComplexDecibelConverter(); private SpectrumPanel mSpectrumPanel; @@ -268,7 +270,7 @@ public class ChannelizerViewer extends JFrame mFrequency = frequency; TunerChannel tunerChannel = new TunerChannel(frequency, bandwidth); - mSource = (PolyphaseChannelSource)mTestTuner.getChannelSource(tunerChannel); + mSource = mTestTuner.getSourceManager().getSource(tunerChannel); if(mSource != null) { @@ -288,7 +290,8 @@ public class ChannelizerViewer extends JFrame { mSource.setListener(mDFTProcessor); } - mSource.start(ThreadPool.SCHEDULED); + + mSource.start(); } else { @@ -296,7 +299,7 @@ public class ChannelizerViewer extends JFrame } } - public PolyphaseChannelSource getSource() + public TunerChannelSource getSource() { return mSource; } diff --git a/src/dsp/filter/channelizer/PolyphaseChannelManager.java b/src/dsp/filter/channelizer/PolyphaseChannelManager.java index b82b212d..aa95a3c3 100644 --- a/src/dsp/filter/channelizer/PolyphaseChannelManager.java +++ b/src/dsp/filter/channelizer/PolyphaseChannelManager.java @@ -25,11 +25,11 @@ import dsp.filter.design.FilterDesignException; import org.slf4j.Logger; import org.slf4j.LoggerFactory; import sample.Listener; +import sample.complex.IComplexBufferProvider; import source.Source; import source.SourceEvent; import source.SourceException; -import source.tuner.Tuner; -import source.tuner.TunerChannel; +import source.tuner.channel.TunerChannel; import java.util.List; import java.util.concurrent.CopyOnWriteArrayList; @@ -42,7 +42,7 @@ public class PolyphaseChannelManager implements Listener private static final int CHANNEL_SAMPLE_RATE = (int)(CHANNEL_BANDWIDTH * CHANNEL_OVERSAMPLING); private static final int POLYPHASE_FILTER_TAPS_PER_CHANNEL = 17; - private Tuner mTuner; + private IComplexBufferProvider mComplexBufferProvider; private List mChannelSources = new CopyOnWriteArrayList<>(); private ChannelCalculator mChannelCalculator; private ComplexPolyphaseChannelizerM2 mPolyphaseChannelizer; @@ -57,27 +57,29 @@ public class PolyphaseChannelManager implements Listener * active DDC polyphase channel sources accordingly. This class also monitors source event requests and * notifications received from active DDC polyphase channel sources to adjust sample streams as required. * - * @param tuner providing complex sample buffers + * Note: add this channel manager as a source event listener to the complex buffer provider to ensure this manager + * adapts to changes in source frequency and sample rate. + * + * @param complexBufferProvider that supports register/deregister of this channel manager + * @param frequency of the provided complex buffer samples + * @param sampleRate of the provided complex buffer samples */ - public PolyphaseChannelManager(Tuner tuner) + public PolyphaseChannelManager(IComplexBufferProvider complexBufferProvider, long frequency, int sampleRate) { - if(tuner == null) + if(complexBufferProvider == null) { - throw new IllegalArgumentException("Tuner argument cannot be null"); + throw new IllegalArgumentException("Complex buffer provider argument cannot be null"); } - mTuner = tuner; + mComplexBufferProvider = complexBufferProvider; - int sampleRate = mTuner.getTunerController().getSampleRate(); - - //Ensure that tuner sample rate is a multiple of channel sample rate since polyphase channelizer is M2 + //Ensure that sample rate is a multiple of channel sample rate since polyphase channelizer is M2 if(sampleRate % (CHANNEL_SAMPLE_RATE) != 0) { throw new IllegalArgumentException("Tuner sample rate [" + sampleRate + "] must be an even multiple of " + CHANNEL_SAMPLE_RATE + " Hz"); } - long frequency = mTuner.getTunerController().getFrequency(); int channelCount = sampleRate / CHANNEL_BANDWIDTH; mChannelCalculator = new ChannelCalculator(sampleRate, channelCount, frequency, CHANNEL_OVERSAMPLING); @@ -85,9 +87,6 @@ public class PolyphaseChannelManager implements Listener mBufferProcessor = new ScheduledBufferProcessor(500, 100, 50, 50); updateChannelizer(sampleRate, null); - - mTuner.addSourceEventListener(this); - } /** @@ -159,7 +158,7 @@ public class PolyphaseChannelManager implements Listener if(mPolyphaseChannelizer.getRegisteredChannelCount() == 1) { - mTuner.addListener(mBufferProcessor); + mComplexBufferProvider.addComplexBufferListener(mBufferProcessor); mBufferProcessor.start(); } } @@ -179,7 +178,7 @@ public class PolyphaseChannelManager implements Listener if(mPolyphaseChannelizer.getRegisteredChannelCount() == 0) { - mTuner.removeListener(mBufferProcessor); + mComplexBufferProvider.removeComplexBufferListener(mBufferProcessor); mBufferProcessor.stop(); } } diff --git a/src/dsp/filter/channelizer/PolyphaseChannelSource.java b/src/dsp/filter/channelizer/PolyphaseChannelSource.java index 89a7e0df..20e789fb 100644 --- a/src/dsp/filter/channelizer/PolyphaseChannelSource.java +++ b/src/dsp/filter/channelizer/PolyphaseChannelSource.java @@ -18,7 +18,7 @@ ******************************************************************************/ package dsp.filter.channelizer; -import channel.heartbeat.Heartbeat; +import source.heartbeat.Heartbeat; import dsp.filter.channelizer.output.IPolyphaseChannelOutputProcessor; import org.slf4j.Logger; import org.slf4j.LoggerFactory; @@ -27,12 +27,10 @@ import sample.complex.ComplexBuffer; import sample.complex.ComplexToComplexBufferAssembler; import source.ComplexSource; import source.ISourceEventProcessor; -import source.Source; import source.SourceEvent; import source.SourceException; -import source.tuner.TunerChannel; +import source.tuner.channel.TunerChannel; -import java.util.Arrays; import java.util.concurrent.ScheduledExecutorService; import java.util.concurrent.ScheduledFuture; import java.util.concurrent.TimeUnit; diff --git a/src/module/Module.java b/src/module/Module.java index a46b3e9c..e7d1fcc4 100644 --- a/src/module/Module.java +++ b/src/module/Module.java @@ -59,9 +59,9 @@ public abstract class Module public abstract void reset(); /** - * Start processing. Provides an executor to use for scheduling tasks + * Start processing. */ - public abstract void start( ScheduledExecutorService executor ); + public abstract void start(); /** * Stop processing diff --git a/src/module/ProcessingChain.java b/src/module/ProcessingChain.java index 6736aece..516d71c8 100644 --- a/src/module/ProcessingChain.java +++ b/src/module/ProcessingChain.java @@ -24,9 +24,9 @@ import audio.IAudioPacketProvider; import audio.squelch.ISquelchStateListener; import audio.squelch.ISquelchStateProvider; import audio.squelch.SquelchState; -import channel.heartbeat.Heartbeat; -import channel.heartbeat.IHeartbeatListener; -import channel.heartbeat.IHeartbeatProvider; +import source.heartbeat.Heartbeat; +import source.heartbeat.IHeartbeatListener; +import source.heartbeat.IHeartbeatProvider; import channel.metadata.AttributeChangeRequest; import channel.metadata.IAttributeChangeRequestListener; import channel.metadata.IAttributeChangeRequestProvider; @@ -454,7 +454,7 @@ public class ProcessingChain implements IChannelEventListener if(module instanceof IHeartbeatProvider) { - ((IHeartbeatProvider)module).setHeartbeatListener(mHeartbeatBroadcaster); + ((IHeartbeatProvider)module).getHeartbeatManager().addHeartbeatListener(mHeartbeatBroadcaster); } if(module instanceof IMessageProvider) @@ -521,7 +521,7 @@ public class ProcessingChain implements IChannelEventListener if(module instanceof IHeartbeatProvider) { - ((IHeartbeatProvider)module).removeHeartbeatListener(); + ((IHeartbeatProvider)module).getHeartbeatManager().removeHeartbeatListener(mHeartbeatBroadcaster); } if(module instanceof IMessageProvider) @@ -595,7 +595,7 @@ public class ProcessingChain implements IChannelEventListener { try { - module.start(ThreadPool.SCHEDULED); + module.start(); } catch(Exception e) { diff --git a/src/module/decode/Decoder.java b/src/module/decode/Decoder.java index cc6880a7..3a029035 100644 --- a/src/module/decode/Decoder.java +++ b/src/module/decode/Decoder.java @@ -1,19 +1,20 @@ /******************************************************************************* - * SDR Trunk - * Copyright (C) 2014-2016 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 + * 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 module.decode; @@ -22,8 +23,7 @@ import message.Message; import module.Module; import sample.Listener; -public abstract class Decoder extends Module - implements IMessageProvider, Listener +public abstract class Decoder extends Module implements IMessageProvider, Listener { /* This has to be a broadcaster in order for references to persist */ protected Listener mMessageListener; @@ -82,4 +82,19 @@ public abstract class Decoder extends Module { broadcast( message ); } + + @Override + public void reset() + { + } + + @Override + public void start() + { + } + + @Override + public void stop() + { + } } diff --git a/src/module/decode/fleetsync2/Fleetsync2Decoder.java b/src/module/decode/fleetsync2/Fleetsync2Decoder.java index 5b18628b..c27d4a97 100644 --- a/src/module/decode/fleetsync2/Fleetsync2Decoder.java +++ b/src/module/decode/fleetsync2/Fleetsync2Decoder.java @@ -174,19 +174,4 @@ public class Fleetsync2Decoder extends Decoder implements IFilteredRealBufferLis { return mDecimationFilter; } - - @Override - public void reset() - { - } - - @Override - public void start( ScheduledExecutorService executor ) - { - } - - @Override - public void stop() - { - } } diff --git a/src/module/decode/fleetsync2/Fleetsync2DecoderState.java b/src/module/decode/fleetsync2/Fleetsync2DecoderState.java index e3aa69ec..d72346e1 100644 --- a/src/module/decode/fleetsync2/Fleetsync2DecoderState.java +++ b/src/module/decode/fleetsync2/Fleetsync2DecoderState.java @@ -63,27 +63,12 @@ public class Fleetsync2DecoderState extends DecoderState return DecoderType.FLEETSYNC2; } - @Override - public void start(ScheduledExecutorService executor) - { - } - - @Override - public void stop() - { - } - @Override public void dispose() { super.dispose(); } - @Override - public void init() - { - } - /** * Resets the overall decoder state and clears any accumulated event details */ diff --git a/src/module/decode/lj1200/LJ1200Decoder.java b/src/module/decode/lj1200/LJ1200Decoder.java index 3e12b544..2e936984 100644 --- a/src/module/decode/lj1200/LJ1200Decoder.java +++ b/src/module/decode/lj1200/LJ1200Decoder.java @@ -192,19 +192,4 @@ public class LJ1200Decoder extends Decoder implements IFilteredRealBufferListene { return mDecimationFilter; } - - @Override - public void reset() - { - } - - @Override - public void start( ScheduledExecutorService executor ) - { - } - - @Override - public void stop() - { - } } diff --git a/src/module/decode/lj1200/LJ1200DecoderState.java b/src/module/decode/lj1200/LJ1200DecoderState.java index 5382f034..a5cac1b8 100644 --- a/src/module/decode/lj1200/LJ1200DecoderState.java +++ b/src/module/decode/lj1200/LJ1200DecoderState.java @@ -63,16 +63,6 @@ public class LJ1200DecoderState extends DecoderState return DecoderType.LJ_1200; } - @Override - public void start(ScheduledExecutorService executor) - { - } - - @Override - public void stop() - { - } - @Override public void receive(Message message) { @@ -125,12 +115,6 @@ public class LJ1200DecoderState extends DecoderState return sb.toString(); } - @Override - public void init() - { - /* No initialization steps */ - } - @Override public void receiveDecoderStateEvent(DecoderStateEvent event) { diff --git a/src/module/decode/ltrnet/LTRNetDecoder.java b/src/module/decode/ltrnet/LTRNetDecoder.java index 5e1eecff..bf1fbcd4 100644 --- a/src/module/decode/ltrnet/LTRNetDecoder.java +++ b/src/module/decode/ltrnet/LTRNetDecoder.java @@ -120,14 +120,4 @@ public class LTRNetDecoder extends Decoder { mLTRMessageFramer.reset(); } - - @Override - public void start( ScheduledExecutorService executor ) - { - } - - @Override - public void stop() - { - } } diff --git a/src/module/decode/ltrnet/LTRNetDecoderState.java b/src/module/decode/ltrnet/LTRNetDecoderState.java index e643b558..544302bc 100644 --- a/src/module/decode/ltrnet/LTRNetDecoderState.java +++ b/src/module/decode/ltrnet/LTRNetDecoderState.java @@ -765,12 +765,6 @@ public class LTRNetDecoderState extends DecoderState } } - @Override - public void init() - { - /* No initialization steps required */ - } - @Override public void receiveDecoderStateEvent(DecoderStateEvent event) { @@ -786,14 +780,4 @@ public class LTRNetDecoderState extends DecoderState break; } } - - @Override - public void start(ScheduledExecutorService executor) - { - } - - @Override - public void stop() - { - } } diff --git a/src/module/decode/ltrstandard/LTRStandardDecoder.java b/src/module/decode/ltrstandard/LTRStandardDecoder.java index 06f0a927..40b355c5 100644 --- a/src/module/decode/ltrstandard/LTRStandardDecoder.java +++ b/src/module/decode/ltrstandard/LTRStandardDecoder.java @@ -84,14 +84,4 @@ public class LTRStandardDecoder extends Decoder implements IUnFilteredRealBuffer { mLTRMessageFramer.reset(); } - - @Override - public void start( ScheduledExecutorService executor ) - { - } - - @Override - public void stop() - { - } } diff --git a/src/module/decode/ltrstandard/LTRStandardDecoderState.java b/src/module/decode/ltrstandard/LTRStandardDecoderState.java index a826ceeb..1693b2b6 100644 --- a/src/module/decode/ltrstandard/LTRStandardDecoderState.java +++ b/src/module/decode/ltrstandard/LTRStandardDecoderState.java @@ -76,18 +76,6 @@ public class LTRStandardDecoderState extends DecoderState return DecoderType.LTR_STANDARD; } - @Override - public void start(ScheduledExecutorService executor) - { - // TODO Auto-generated method stub - - } - - @Override - public void stop() - { - } - @Override public void receive(Message message) { @@ -257,11 +245,6 @@ public class LTRStandardDecoderState extends DecoderState } } - @Override - public void init() - { - } - @Override public void receiveDecoderStateEvent(DecoderStateEvent event) { diff --git a/src/module/decode/mdc1200/MDCDecoder.java b/src/module/decode/mdc1200/MDCDecoder.java index 89f04afc..2bd89fba 100644 --- a/src/module/decode/mdc1200/MDCDecoder.java +++ b/src/module/decode/mdc1200/MDCDecoder.java @@ -202,25 +202,4 @@ public class MDCDecoder extends Decoder implements IFilteredRealBufferListener, { return mDecimationFilter; } - - @Override - public void reset() - { - // TODO Auto-generated method stub - - } - - @Override - public void start( ScheduledExecutorService executor ) - { - // TODO Auto-generated method stub - - } - - @Override - public void stop() - { - // TODO Auto-generated method stub - - } } diff --git a/src/module/decode/mdc1200/MDCDecoderState.java b/src/module/decode/mdc1200/MDCDecoderState.java index eecd0de9..a70c8f0f 100644 --- a/src/module/decode/mdc1200/MDCDecoderState.java +++ b/src/module/decode/mdc1200/MDCDecoderState.java @@ -59,16 +59,6 @@ public class MDCDecoderState extends DecoderState return DecoderType.MDC1200; } - @Override - public void start(ScheduledExecutorService executor) - { - } - - @Override - public void stop() - { - } - @Override public void reset() { @@ -245,12 +235,6 @@ public class MDCDecoderState extends DecoderState return sb.toString(); } - @Override - public void init() - { - /* No initialization required */ - } - /** * Responds to reset events issued by the channel state */ diff --git a/src/module/decode/mpt1327/MPT1327Decoder.java b/src/module/decode/mpt1327/MPT1327Decoder.java index 15163f11..228f8dc9 100644 --- a/src/module/decode/mpt1327/MPT1327Decoder.java +++ b/src/module/decode/mpt1327/MPT1327Decoder.java @@ -297,16 +297,4 @@ public class MPT1327Decoder extends Decoder implements IFilteredRealBufferListen mControlMessageFramer.reset(); mTrafficMessageFramer.reset(); } - - @Override - public void start(ScheduledExecutorService executor) - { - // TODO Auto-generated method stub - } - - @Override - public void stop() - { - // TODO Auto-generated method stub - } } diff --git a/src/module/decode/mpt1327/MPT1327DecoderState.java b/src/module/decode/mpt1327/MPT1327DecoderState.java index a70b96fa..940e1496 100644 --- a/src/module/decode/mpt1327/MPT1327DecoderState.java +++ b/src/module/decode/mpt1327/MPT1327DecoderState.java @@ -87,16 +87,6 @@ public class MPT1327DecoderState extends DecoderState return DecoderType.MPT1327; } - @Override - public void start(ScheduledExecutorService executor) - { - } - - @Override - public void stop() - { - } - public ChannelType getChannelType() { return mChannelType; @@ -501,12 +491,6 @@ public class MPT1327DecoderState extends DecoderState return sb.toString(); } - @Override - public void init() - { - /* No initialization required */ - } - @Override public void receiveDecoderStateEvent(DecoderStateEvent event) { diff --git a/src/module/decode/p25/P25DecoderState.java b/src/module/decode/p25/P25DecoderState.java index 5072bc31..2a84255b 100644 --- a/src/module/decode/p25/P25DecoderState.java +++ b/src/module/decode/p25/P25DecoderState.java @@ -221,11 +221,6 @@ public class P25DecoderState extends DecoderState return DecoderType.P25_PHASE1; } - @Override - public void stop() - { - } - /** * Performs a full reset to prepare this object for reuse on a new channel */ @@ -257,14 +252,6 @@ public class P25DecoderState extends DecoderState mCurrentCallEvent = null; } - /** - * Performs any initialization operations to prepare for use - */ - @Override - public void init() - { - } - /** * Indicates if there a call event exists for the specified channel and * from and to talkgroup identifiers. @@ -3624,7 +3611,7 @@ public class P25DecoderState extends DecoderState } @Override - public void start(ScheduledExecutorService executor) + public void start() { //Change the default (45-second) traffic channel timeout to 1 second if(mChannelType == ChannelType.TRAFFIC) diff --git a/src/module/decode/p25/P25_C4FMDecoder.java b/src/module/decode/p25/P25_C4FMDecoder.java index cbae323d..3dc771fc 100644 --- a/src/module/decode/p25/P25_C4FMDecoder.java +++ b/src/module/decode/p25/P25_C4FMDecoder.java @@ -194,27 +194,6 @@ public class P25_C4FMDecoder extends P25Decoder implements IFilteredRealBufferLi } } - @Override - public void reset() - { - // TODO Auto-generated method stub - - } - - @Override - public void start( ScheduledExecutorService executor ) - { - // TODO Auto-generated method stub - - } - - @Override - public void stop() - { - // TODO Auto-generated method stub - - } - @Override public void setSourceEventListener(Listener listener ) { diff --git a/src/module/decode/p25/P25_LSMDecoder.java b/src/module/decode/p25/P25_LSMDecoder.java index 3e30e9ae..d461cfd0 100644 --- a/src/module/decode/p25/P25_LSMDecoder.java +++ b/src/module/decode/p25/P25_LSMDecoder.java @@ -237,25 +237,4 @@ public class P25_LSMDecoder extends P25Decoder implements IComplexBufferListener tap.getName() ); } } - - @Override - public void reset() - { - // TODO Auto-generated method stub - - } - - @Override - public void start( ScheduledExecutorService executor ) - { - // TODO Auto-generated method stub - - } - - @Override - public void stop() - { - // TODO Auto-generated method stub - - } } diff --git a/src/module/decode/p25/audio/P25AudioModule.java b/src/module/decode/p25/audio/P25AudioModule.java index d6aa4c9f..c49dd2f4 100644 --- a/src/module/decode/p25/audio/P25AudioModule.java +++ b/src/module/decode/p25/audio/P25AudioModule.java @@ -71,7 +71,7 @@ public class P25AudioModule extends Module implements Listener, IAudioP } @Override - public void start(ScheduledExecutorService executor) + public void start() { } diff --git a/src/module/decode/passport/PassportDecoder.java b/src/module/decode/passport/PassportDecoder.java index 859e7c9d..1c298888 100644 --- a/src/module/decode/passport/PassportDecoder.java +++ b/src/module/decode/passport/PassportDecoder.java @@ -109,14 +109,4 @@ public class PassportDecoder extends Decoder { mPassportMessageFramer.reset(); } - - @Override - public void start( ScheduledExecutorService executor ) - { - } - - @Override - public void stop() - { - } } diff --git a/src/module/decode/passport/PassportDecoderState.java b/src/module/decode/passport/PassportDecoderState.java index ee5370e7..c98ef2c6 100644 --- a/src/module/decode/passport/PassportDecoderState.java +++ b/src/module/decode/passport/PassportDecoderState.java @@ -78,21 +78,6 @@ public class PassportDecoderState extends DecoderState return DecoderType.PASSPORT; } - @Override - public void start(ScheduledExecutorService executor) - { - } - - @Override - public void init() - { - } - - @Override - public void stop() - { - } - private void logTalkgroup(String talkgroup) { if(mTalkgroupsFirstHeard.contains(talkgroup)) diff --git a/src/module/decode/tait/Tait1200Decoder.java b/src/module/decode/tait/Tait1200Decoder.java index 97b9b788..917a2192 100644 --- a/src/module/decode/tait/Tait1200Decoder.java +++ b/src/module/decode/tait/Tait1200Decoder.java @@ -190,25 +190,4 @@ public class Tait1200Decoder extends Decoder implements IFilteredRealBufferListe { return DecoderType.TAIT_1200; } - - @Override - public void reset() - { - // TODO Auto-generated method stub - - } - - @Override - public void start( ScheduledExecutorService executor ) - { - // TODO Auto-generated method stub - - } - - @Override - public void stop() - { - // TODO Auto-generated method stub - - } } diff --git a/src/module/decode/tait/Tait1200DecoderState.java b/src/module/decode/tait/Tait1200DecoderState.java index b18ccb24..d8750b52 100644 --- a/src/module/decode/tait/Tait1200DecoderState.java +++ b/src/module/decode/tait/Tait1200DecoderState.java @@ -65,16 +65,6 @@ public class Tait1200DecoderState extends DecoderState return DecoderType.TAIT_1200; } - @Override - public void start(ScheduledExecutorService executor) - { - } - - @Override - public void stop() - { - } - @Override public void reset() { @@ -176,12 +166,6 @@ public class Tait1200DecoderState extends DecoderState return sb.toString(); } - @Override - public void init() - { - /* No initialization required */ - } - /** * Responds to reset events issued by the channel state */ diff --git a/src/module/demodulate/am/AMDemodulatorModule.java b/src/module/demodulate/am/AMDemodulatorModule.java index 374491aa..9c9f1ace 100644 --- a/src/module/demodulate/am/AMDemodulatorModule.java +++ b/src/module/demodulate/am/AMDemodulatorModule.java @@ -1,124 +1,110 @@ /******************************************************************************* * SDR Trunk * Copyright (C) 2014,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 ******************************************************************************/ package module.demodulate.am; -import java.util.concurrent.ScheduledExecutorService; - -import module.Module; -import sample.Listener; -import sample.complex.ComplexBuffer; -import sample.complex.IComplexBufferListener; -import sample.real.IFilteredRealBufferProvider; -import sample.real.RealBuffer; import dsp.am.AMDemodulator_CB; import dsp.filter.FilterFactory; import dsp.filter.Window.WindowType; import dsp.filter.fir.complex.ComplexFIRFilter_CB_CB; import dsp.filter.fir.real.RealFIRFilter_RB_RB; import dsp.gain.AutomaticGainControl_RB; +import module.Module; +import sample.Listener; +import sample.complex.ComplexBuffer; +import sample.complex.IComplexBufferListener; +import sample.real.IFilteredRealBufferProvider; +import sample.real.RealBuffer; -public class AMDemodulatorModule extends Module - implements IComplexBufferListener, IFilteredRealBufferProvider +public class AMDemodulatorModule extends Module + implements IComplexBufferListener, IFilteredRealBufferProvider { - private static final int SAMPLE_RATE = 48000; - - private ComplexFIRFilter_CB_CB mIQFilter; - private AMDemodulator_CB mDemodulator; - private RealFIRFilter_RB_RB mLowPassFilter; + private static final int SAMPLE_RATE = 48000; + + private ComplexFIRFilter_CB_CB mIQFilter; + private AMDemodulator_CB mDemodulator; + private RealFIRFilter_RB_RB mLowPassFilter; private AutomaticGainControl_RB mAGC; - /** - * FM Demodulator with integrated DC removal and automatic frequency - * correction control. - * - * @param pass - pass frequency for IQ filtering prior to demodulation. This - * frequency should be half of the signal bandwidth since the filter will - * be applied against each of the inphase and quadrature signals and the - * combined pass bandwidth will be twice this value. - * - * @param stop - stop frequency for IQ filtering prior to demodulation. - * - * @param maxFrequencyCorrection - defines the maximum frequency +/- that - * the correction controller can adjust from the center tuned frequency. - * Set this value to 0 for no frequency correction control. - */ - public AMDemodulatorModule() - { - mIQFilter = new ComplexFIRFilter_CB_CB( FilterFactory.getLowPass( - SAMPLE_RATE, 5000, 73, WindowType.HAMMING ), 1.0f ); - - mDemodulator = new AMDemodulator_CB( 500.0f ); - mIQFilter.setListener( mDemodulator ); + /** + * AM Demodulator Module + */ + public AMDemodulatorModule() + { + mIQFilter = new ComplexFIRFilter_CB_CB(FilterFactory.getLowPass( + SAMPLE_RATE, 5000, 73, WindowType.HAMMING), 1.0f); - mLowPassFilter = new RealFIRFilter_RB_RB( - FilterFactory.getLowPass( 48000, 3000, 31, WindowType.COSINE ), 1.0f ); - mDemodulator.setListener( mLowPassFilter ); - - mAGC = new AutomaticGainControl_RB(); - mLowPassFilter.setListener( mAGC ); - } + mDemodulator = new AMDemodulator_CB(500.0f); + mIQFilter.setListener(mDemodulator); - @Override - public Listener getComplexBufferListener() - { - return mIQFilter; - } + mLowPassFilter = new RealFIRFilter_RB_RB( + FilterFactory.getLowPass(48000, 3000, 31, WindowType.COSINE), 1.0f); + mDemodulator.setListener(mLowPassFilter); - @Override - public void dispose() - { - mIQFilter.dispose(); - mIQFilter = null; - - mDemodulator.dispose(); - mDemodulator = null; - - mLowPassFilter.dispose(); - mLowPassFilter = null; - - mAGC.dispose(); - mAGC = null; - } + mAGC = new AutomaticGainControl_RB(); + mLowPassFilter.setListener(mAGC); + } - @Override - public void reset() - { - } + @Override + public Listener getComplexBufferListener() + { + return mIQFilter; + } - @Override - public void setFilteredRealBufferListener( Listener listener ) - { - mAGC.setListener( listener ); - } + @Override + public void dispose() + { + mIQFilter.dispose(); + mIQFilter = null; - @Override - public void removeFilteredRealBufferListener() - { - mAGC.removeListener(); - } + mDemodulator.dispose(); + mDemodulator = null; - @Override - public void start( ScheduledExecutorService executor ) - { - } + mLowPassFilter.dispose(); + mLowPassFilter = null; - @Override - public void stop() - { - } + mAGC.dispose(); + mAGC = null; + } + + @Override + public void reset() + { + } + + @Override + public void setFilteredRealBufferListener(Listener listener) + { + mAGC.setListener(listener); + } + + @Override + public void removeFilteredRealBufferListener() + { + mAGC.removeListener(); + } + + @Override + public void start() + { + } + + @Override + public void stop() + { + } } diff --git a/src/module/demodulate/audio/DemodulatedAudioFilterModule.java b/src/module/demodulate/audio/DemodulatedAudioFilterModule.java index a28ff3fd..fa73f27d 100644 --- a/src/module/demodulate/audio/DemodulatedAudioFilterModule.java +++ b/src/module/demodulate/audio/DemodulatedAudioFilterModule.java @@ -97,7 +97,7 @@ public class DemodulatedAudioFilterModule extends Module implements IUnFilteredR } @Override - public void start(ScheduledExecutorService executor) + public void start() { } diff --git a/src/module/demodulate/fm/FMDemodulatorModule.java b/src/module/demodulate/fm/FMDemodulatorModule.java index 8b7646de..d3278e68 100644 --- a/src/module/demodulate/fm/FMDemodulatorModule.java +++ b/src/module/demodulate/fm/FMDemodulatorModule.java @@ -104,7 +104,7 @@ public class FMDemodulatorModule extends Module implements IComplexBufferListene } @Override - public void start(ScheduledExecutorService executor) + public void start() { } diff --git a/src/module/log/EventLogger.java b/src/module/log/EventLogger.java index b6d800f5..dcd90caa 100644 --- a/src/module/log/EventLogger.java +++ b/src/module/log/EventLogger.java @@ -65,7 +65,7 @@ public abstract class EventLogger extends Module public abstract String getHeader(); @Override - public void start(ScheduledExecutorService executor) + public void start() { if(mLogFile == null) { diff --git a/src/record/AudioRecorder.java b/src/record/AudioRecorder.java index 15723eba..3c1b7f64 100644 --- a/src/record/AudioRecorder.java +++ b/src/record/AudioRecorder.java @@ -25,6 +25,7 @@ import module.Module; import org.slf4j.Logger; import org.slf4j.LoggerFactory; import sample.Listener; +import util.ThreadPool; import java.io.FileOutputStream; import java.io.IOException; @@ -196,7 +197,7 @@ public abstract class AudioRecorder extends Module implements Listener ******************************************************************************/ package record.wave; -import java.io.IOException; -import java.nio.file.Path; -import java.nio.file.Paths; -import java.util.concurrent.LinkedBlockingQueue; -import java.util.concurrent.ScheduledExecutorService; -import java.util.concurrent.ScheduledFuture; -import java.util.concurrent.TimeUnit; -import java.util.concurrent.atomic.AtomicBoolean; - -import javax.sound.sampled.AudioFormat; - import module.Module; - import org.slf4j.Logger; import org.slf4j.LoggerFactory; - import sample.Buffer; import sample.ConversionUtils; import sample.Listener; import sample.complex.ComplexBuffer; import sample.complex.IComplexBufferListener; +import util.ThreadPool; import util.TimeStamp; +import javax.sound.sampled.AudioFormat; +import java.io.IOException; +import java.nio.file.Path; +import java.nio.file.Paths; +import java.util.concurrent.LinkedBlockingQueue; +import java.util.concurrent.ScheduledFuture; +import java.util.concurrent.TimeUnit; +import java.util.concurrent.atomic.AtomicBoolean; + /** * WAVE audio recorder module for recording complex (I&Q) samples to a wave file */ -public class ComplexBufferWaveRecorder extends Module - implements IComplexBufferListener, Listener +public class ComplexBufferWaveRecorder extends Module + implements IComplexBufferListener, Listener { - private final static Logger mLog = - LoggerFactory.getLogger( ComplexBufferWaveRecorder.class ); + private final static Logger mLog = + LoggerFactory.getLogger(ComplexBufferWaveRecorder.class); private WaveWriter mWriter; private String mFilePrefix; private Path mFile; - private AudioFormat mAudioFormat; - - private BufferProcessor mBufferProcessor; - private ScheduledFuture mProcessorHandle; - private LinkedBlockingQueue mBuffers = new LinkedBlockingQueue<>( 500 ); - - private AtomicBoolean mRunning = new AtomicBoolean(); - - public ComplexBufferWaveRecorder( int sampleRate, String filePrefix ) - { - mAudioFormat = new AudioFormat( sampleRate, //SampleRate - 16, //Sample Size - 2, //Channels - true, //Signed - false ); //Little Endian + private AudioFormat mAudioFormat; - mFilePrefix = filePrefix; - } + private BufferProcessor mBufferProcessor; + private ScheduledFuture mProcessorHandle; + private LinkedBlockingQueue mBuffers = new LinkedBlockingQueue<>(500); - public Path getFile() - { - return mFile; - } - - public void start( ScheduledExecutorService executor ) - { - if( mRunning.compareAndSet( false, true ) ) - { - if( mBufferProcessor == null ) - { - mBufferProcessor = new BufferProcessor(); - } + private AtomicBoolean mRunning = new AtomicBoolean(); - try - { - StringBuilder sb = new StringBuilder(); - sb.append( mFilePrefix ); - sb.append( "_" ); - sb.append( TimeStamp.getTimeStamp( "_" ) ); - sb.append( ".wav" ); - - mFile = Paths.get( sb.toString() ); + public ComplexBufferWaveRecorder(int sampleRate, String filePrefix) + { + mAudioFormat = new AudioFormat(sampleRate, //SampleRate + 16, //Sample Size + 2, //Channels + true, //Signed + false); //Little Endian - mWriter = new WaveWriter( mAudioFormat, mFile ); + mFilePrefix = filePrefix; + } + + public Path getFile() + { + return mFile; + } + + public void start() + { + if(mRunning.compareAndSet(false, true)) + { + if(mBufferProcessor == null) + { + mBufferProcessor = new BufferProcessor(); + } + + try + { + StringBuilder sb = new StringBuilder(); + sb.append(mFilePrefix); + sb.append("_"); + sb.append(TimeStamp.getTimeStamp("_")); + sb.append(".wav"); + + mFile = Paths.get(sb.toString()); + + mWriter = new WaveWriter(mAudioFormat, mFile); /* Schedule the processor to run every 500 milliseconds */ - mProcessorHandle = executor.scheduleAtFixedRate( - mBufferProcessor, 0, 500, TimeUnit.MILLISECONDS ); - } - catch( IOException io ) - { - mLog.error( "Error starting complex baseband recorder", io ); - } - } - } - - public void stop() - { - if( mRunning.compareAndSet( true, false ) ) - { - receive( new PoisonPill() ); - } - } - - @Override - public void receive( ComplexBuffer buffer ) - { - if( mRunning.get() ) - { - boolean success = mBuffers.offer( buffer ); - - if( !success ) - { - mLog.error( "recorder buffer overflow - purging [" + - mFile.toFile().getAbsolutePath() + "]" ); - - mBuffers.clear(); - } - } - } - - @Override - public Listener getComplexBufferListener() - { - return this; - } - - @Override - public void dispose() - { - stop(); - } - - @Override - public void reset() - { - } - - public class BufferProcessor implements Runnable - { - public void run() - { - try - { - Buffer buffer = mBuffers.poll(); - - while( buffer != null ) - { - if( buffer instanceof PoisonPill ) - { - buffer = null; - - mBuffers.clear(); - - if( mWriter != null ) - { - try - { - mWriter.close(); - mWriter = null; - } - catch( IOException io ) - { - mLog.error( "Error stopping complex wave recorder [" + - getFile() + "]", io ); - } - } - - mFile = null; - - if( mProcessorHandle != null ) - { - mProcessorHandle.cancel( true ); - } - - mProcessorHandle = null; - } - else - { - mWriter.write( ConversionUtils.convertToSigned16BitSamples( buffer ) ); - buffer = mBuffers.poll(); - } - } + mProcessorHandle = ThreadPool.SCHEDULED.scheduleAtFixedRate( + mBufferProcessor, 0, 500, TimeUnit.MILLISECONDS); } - catch ( IOException ioe ) - { - /* Stop this module if/when we get an IO exception */ - mBuffers.clear(); - stop(); - - mLog.error( "IOException while trying to write to the wave " - + "writer", ioe ); - } - } + catch(IOException io) + { + mLog.error("Error starting complex baseband recorder", io); + } + } } - /** - * This is used as a sentinel value to signal the buffer processor to end - */ - public class PoisonPill extends ComplexBuffer - { - public PoisonPill() - { - super( new float[ 1 ] ); - } - } + public void stop() + { + if(mRunning.compareAndSet(true, false)) + { + receive(new PoisonPill()); + } + } + + @Override + public void receive(ComplexBuffer buffer) + { + if(mRunning.get()) + { + boolean success = mBuffers.offer(buffer); + + if(!success) + { + mLog.error("recorder buffer overflow - purging [" + + mFile.toFile().getAbsolutePath() + "]"); + + mBuffers.clear(); + } + } + } + + @Override + public Listener getComplexBufferListener() + { + return this; + } + + @Override + public void dispose() + { + stop(); + } + + @Override + public void reset() + { + } + + public class BufferProcessor implements Runnable + { + public void run() + { + try + { + Buffer buffer = mBuffers.poll(); + + while(buffer != null) + { + if(buffer instanceof PoisonPill) + { + buffer = null; + + mBuffers.clear(); + + if(mWriter != null) + { + try + { + mWriter.close(); + mWriter = null; + } + catch(IOException io) + { + mLog.error("Error stopping complex wave recorder [" + + getFile() + "]", io); + } + } + + mFile = null; + + if(mProcessorHandle != null) + { + mProcessorHandle.cancel(true); + } + + mProcessorHandle = null; + } + else + { + mWriter.write(ConversionUtils.convertToSigned16BitSamples(buffer)); + buffer = mBuffers.poll(); + } + } + } + catch(IOException ioe) + { + /* Stop this module if/when we get an IO exception */ + mBuffers.clear(); + stop(); + + mLog.error("IOException while trying to write to the wave " + + "writer", ioe); + } + } + } + + /** + * This is used as a sentinel value to signal the buffer processor to end + */ + public class PoisonPill extends ComplexBuffer + { + public PoisonPill() + { + super(new float[1]); + } + } } diff --git a/src/record/wave/RealBufferWaveRecorder.java b/src/record/wave/RealBufferWaveRecorder.java index 763c9ded..d006fe4a 100644 --- a/src/record/wave/RealBufferWaveRecorder.java +++ b/src/record/wave/RealBufferWaveRecorder.java @@ -25,6 +25,7 @@ import sample.ConversionUtils; import sample.Listener; import sample.real.IFilteredRealBufferListener; import sample.real.RealBuffer; +import util.ThreadPool; import util.TimeStamp; import javax.sound.sampled.AudioFormat; @@ -89,7 +90,7 @@ public class RealBufferWaveRecorder extends Module return mFile; } - public void start(ScheduledExecutorService executor) + public void start() { if(mRunning.compareAndSet(false, true)) { @@ -111,7 +112,7 @@ public class RealBufferWaveRecorder extends Module mWriter = new WaveWriter(mAudioFormat, mFile); /* Schedule the processor to run every 500 milliseconds */ - mProcessorHandle = executor.scheduleAtFixedRate(mBufferProcessor, 0, 500, TimeUnit.MILLISECONDS); + mProcessorHandle = ThreadPool.SCHEDULED.scheduleAtFixedRate(mBufferProcessor, 0, 500, TimeUnit.MILLISECONDS); } catch(IOException io) { diff --git a/src/sample/OverflowableTransferQueue.java b/src/sample/OverflowableTransferQueue.java index f37314a4..193ef811 100644 --- a/src/sample/OverflowableTransferQueue.java +++ b/src/sample/OverflowableTransferQueue.java @@ -21,7 +21,6 @@ package sample; import org.slf4j.Logger; import org.slf4j.LoggerFactory; import sample.real.IOverflowListener; -import source.tuner.TunerChannelSource; import java.util.Collection; import java.util.concurrent.LinkedTransferQueue; diff --git a/src/sample/complex/IComplexBufferProvider.java b/src/sample/complex/IComplexBufferProvider.java new file mode 100644 index 00000000..480cede0 --- /dev/null +++ b/src/sample/complex/IComplexBufferProvider.java @@ -0,0 +1,39 @@ +/******************************************************************************* + * 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 sample.complex; + +import sample.Listener; + +public interface IComplexBufferProvider +{ + /** + * Adds the listener to receive complex buffer samples + */ + void addComplexBufferListener(Listener listener); + + /** + * Removes the listener from receiving complex buffer samples + */ + void removeComplexBufferListener(Listener listener); + + /** + * Indicates if there are any complex buffer listeners registered + */ + boolean hasComplexBufferListeners(); +} diff --git a/src/source/Source.java b/src/source/Source.java index 1518279d..9304ddbe 100644 --- a/src/source/Source.java +++ b/src/source/Source.java @@ -18,7 +18,8 @@ ******************************************************************************/ package source; -import channel.heartbeat.IHeartbeatProvider; +import source.heartbeat.HeartbeatManager; +import source.heartbeat.IHeartbeatProvider; import module.Module; import sample.SampleType; import sample.real.IOverflowListener; @@ -28,6 +29,17 @@ import sample.real.IOverflowListener; */ public abstract class Source extends Module implements ISourceEventListener, ISourceEventProvider, IHeartbeatProvider { + private HeartbeatManager mHeartbeatManager = new HeartbeatManager(); + + /** + * Heartbeat manager for this source + */ + public HeartbeatManager getHeartbeatManager() + { + return mHeartbeatManager; + } + + protected IOverflowListener mOverflowListener; /** diff --git a/src/source/SourceEvent.java b/src/source/SourceEvent.java index 5a9dd3b7..074eee9a 100644 --- a/src/source/SourceEvent.java +++ b/src/source/SourceEvent.java @@ -31,6 +31,7 @@ public class SourceEvent REQUEST_CHANNEL_FREQUENCY_CORRECTION_CHANGE, REQUEST_FREQUENCY_CHANGE, + REQUEST_SOURCE_DISPOSE, REQUEST_START_SAMPLE_STREAM, REQUEST_STOP_SAMPLE_STREAM; } @@ -189,4 +190,13 @@ public class SourceEvent { return new SourceEvent(Event.REQUEST_STOP_SAMPLE_STREAM, source); } + + /** + * Creates a request to dispose of the source. This is normally used for TunerChannelSource to notify the + * parent source manager that the channel is no longer needed + */ + public static SourceEvent sourceDisposeRequest(Source source) + { + return new SourceEvent(Event.REQUEST_SOURCE_DISPOSE, source); + } } diff --git a/src/source/config/SourceConfigRecording.java b/src/source/config/SourceConfigRecording.java index f1b7c1c3..d92b4fe5 100644 --- a/src/source/config/SourceConfigRecording.java +++ b/src/source/config/SourceConfigRecording.java @@ -19,7 +19,7 @@ package source.config; import source.SourceType; -import source.tuner.TunerChannel; +import source.tuner.channel.TunerChannel; import javax.xml.bind.annotation.XmlAttribute; diff --git a/src/source/config/SourceConfigTuner.java b/src/source/config/SourceConfigTuner.java index 2f31457d..8fe54732 100644 --- a/src/source/config/SourceConfigTuner.java +++ b/src/source/config/SourceConfigTuner.java @@ -19,7 +19,7 @@ package source.config; import source.SourceType; -import source.tuner.TunerChannel; +import source.tuner.channel.TunerChannel; import javax.xml.bind.annotation.XmlAttribute; import java.text.DecimalFormat; diff --git a/src/channel/heartbeat/Heartbeat.java b/src/source/heartbeat/Heartbeat.java similarity index 97% rename from src/channel/heartbeat/Heartbeat.java rename to src/source/heartbeat/Heartbeat.java index 4799754e..993d51ee 100644 --- a/src/channel/heartbeat/Heartbeat.java +++ b/src/source/heartbeat/Heartbeat.java @@ -16,7 +16,7 @@ * along with this program. If not, see * ******************************************************************************/ -package channel.heartbeat; +package source.heartbeat; public class Heartbeat { diff --git a/src/source/heartbeat/HeartbeatManager.java b/src/source/heartbeat/HeartbeatManager.java new file mode 100644 index 00000000..d35188e3 --- /dev/null +++ b/src/source/heartbeat/HeartbeatManager.java @@ -0,0 +1,58 @@ +/******************************************************************************* + * 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 source.heartbeat; + +import sample.Broadcaster; +import sample.Listener; + +public class HeartbeatManager +{ + private static final Heartbeat HEARTBEAT = new Heartbeat(); + private Broadcaster mHeartbeatBroadcaster = new Broadcaster<>(); + + public HeartbeatManager() + { + } + + /** + * Sends a heartbeat to all registered listeners + */ + public void broadcast() + { + mHeartbeatBroadcaster.broadcast(HEARTBEAT); + } + + /** + * Adds the listener to receive heartbeats + */ + public void addHeartbeatListener(Listener listener) + { + mHeartbeatBroadcaster.addListener(listener); + } + + /** + * Removes the listener from receiving heartbeats + * @param listener + */ + public void removeHeartbeatListener(Listener listener) + { + mHeartbeatBroadcaster.removeListener(listener); + } + +} diff --git a/src/channel/heartbeat/IHeartbeatListener.java b/src/source/heartbeat/IHeartbeatListener.java similarity index 97% rename from src/channel/heartbeat/IHeartbeatListener.java rename to src/source/heartbeat/IHeartbeatListener.java index 904d0501..296860a7 100644 --- a/src/channel/heartbeat/IHeartbeatListener.java +++ b/src/source/heartbeat/IHeartbeatListener.java @@ -16,7 +16,7 @@ * along with this program. If not, see * ******************************************************************************/ -package channel.heartbeat; +package source.heartbeat; import sample.Listener; diff --git a/src/channel/heartbeat/IHeartbeatProvider.java b/src/source/heartbeat/IHeartbeatProvider.java similarity index 84% rename from src/channel/heartbeat/IHeartbeatProvider.java rename to src/source/heartbeat/IHeartbeatProvider.java index 3583cd42..9941b716 100644 --- a/src/channel/heartbeat/IHeartbeatProvider.java +++ b/src/source/heartbeat/IHeartbeatProvider.java @@ -16,13 +16,9 @@ * along with this program. If not, see * ******************************************************************************/ -package channel.heartbeat; - -import sample.Listener; +package source.heartbeat; public interface IHeartbeatProvider { - public void setHeartbeatListener(Listener listener); - - public void removeHeartbeatListener(); + public HeartbeatManager getHeartbeatManager(); } diff --git a/src/source/mixer/RealMixerSource.java b/src/source/mixer/RealMixerSource.java index 408f04ea..e8558429 100644 --- a/src/source/mixer/RealMixerSource.java +++ b/src/source/mixer/RealMixerSource.java @@ -17,8 +17,7 @@ ******************************************************************************/ package source.mixer; -import channel.heartbeat.Heartbeat; -import dsp.gain.AutomaticGainControl; +import source.heartbeat.Heartbeat; import org.slf4j.Logger; import org.slf4j.LoggerFactory; import sample.Listener; diff --git a/src/source/recording/Recording.java b/src/source/recording/Recording.java index a89f77eb..938dc6a6 100644 --- a/src/source/recording/Recording.java +++ b/src/source/recording/Recording.java @@ -7,8 +7,8 @@ import java.util.concurrent.RejectedExecutionException; import settings.SettingsManager; import source.SourceException; import source.tuner.ITunerChannelProvider; -import source.tuner.TunerChannel; -import source.tuner.TunerChannelSource; +import source.tuner.channel.TunerChannel; +import source.tuner.channel.TunerChannelSource; import source.SourceEvent; import source.SourceEvent.Event; import source.ISourceEventProcessor; diff --git a/src/source/recording/RecordingSourceManager.java b/src/source/recording/RecordingSourceManager.java index 4a47c660..d5fb4a69 100644 --- a/src/source/recording/RecordingSourceManager.java +++ b/src/source/recording/RecordingSourceManager.java @@ -29,8 +29,8 @@ import source.Source; import source.SourceException; import source.config.SourceConfigRecording; import source.config.SourceConfiguration; -import source.tuner.TunerChannel; -import source.tuner.TunerChannelSource; +import source.tuner.channel.TunerChannel; +import source.tuner.channel.TunerChannelSource; public class RecordingSourceManager { diff --git a/src/source/tuner/ITunerChannelProvider.java b/src/source/tuner/ITunerChannelProvider.java index b3a69a63..17558dbc 100644 --- a/src/source/tuner/ITunerChannelProvider.java +++ b/src/source/tuner/ITunerChannelProvider.java @@ -20,6 +20,8 @@ package source.tuner; import java.util.concurrent.RejectedExecutionException; import source.SourceException; +import source.tuner.channel.TunerChannel; +import source.tuner.channel.TunerChannelSource; public interface ITunerChannelProvider { @@ -30,7 +32,7 @@ public interface ITunerChannelProvider * * @return - source for 48k sample rate */ - public abstract TunerChannelSource getChannel( TunerChannel channel ) + public abstract TunerChannelSource getChannel(TunerChannel channel ) throws RejectedExecutionException, SourceException; /** diff --git a/src/source/tuner/MixerTuner.java b/src/source/tuner/MixerTuner.java deleted file mode 100644 index c873aa8c..00000000 --- a/src/source/tuner/MixerTuner.java +++ /dev/null @@ -1,131 +0,0 @@ -/******************************************************************************* - * SDR Trunk - * Copyright (C) 2014 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 source.tuner; - -import java.util.ArrayList; - -import javax.sound.sampled.TargetDataLine; - -import org.slf4j.Logger; -import org.slf4j.LoggerFactory; - -import sample.Listener; -import sample.adapter.ISampleAdapter; -import sample.complex.ComplexBuffer; -import sample.complex.ComplexSampleListListener; -import source.mixer.ComplexMixer; - -public abstract class MixerTuner extends Tuner implements Listener -{ - private final static Logger mLog = - LoggerFactory.getLogger( MixerTuner.class ); - - protected MixerTunerType mMixerTunerType; - protected ComplexMixer mComplexMixer; - protected ArrayList mListeners = - new ArrayList(); - - /** - * Wrapper class to add basic Tuner functionality to the ComplexMixer. - * Subclasses should couple this functionality with a tuner controller class - * to support tuning. - */ - public MixerTuner( String name, - TunerController tunerController, - MixerTunerType mixerTunerType, - TargetDataLine targetDataLine, - ISampleAdapter sampleAdapter ) - { - super( name, tunerController ); - - mMixerTunerType = mixerTunerType; - - mComplexMixer = new ComplexMixer( targetDataLine, - mMixerTunerType.getAudioFormat(), - name, - sampleAdapter, - (Listener)this ); - } - - public MixerTuner( String name, - TunerController tunerController, - MixerTunerDataLine mixerTuner, - ISampleAdapter sampleAdapter ) - { - this( name, - tunerController, - mixerTuner.getMixerTunerType(), - mixerTuner.getTargetDataLine(), - sampleAdapter ); - } - - public void dispose() - { - mComplexMixer.stop(); - - super.dispose(); - } - - public MixerTunerType getMixerTunerType() - { - return mMixerTunerType; - } - - public TargetDataLine getTargetDataLine() - { - return mComplexMixer.getTargetDataLine(); - } - - /** - * Dispatches the received complex buffers from the mComplexMixer to all - * listeners registered on the parent Tuner class - */ - @Override - public void receive( ComplexBuffer buffer ) - { - super.broadcast( buffer ); - } - - /** - * Overrides the parent Tuner class listener management method to allow - * starting the buffer processing thread - */ - @Override - public void addListener( Listener listener ) - { - super.addListener( listener ); - - /* We can call start multiple times -- it ignores additional requests */ - mComplexMixer.start(); - } - - /** - * Overrides the parent Tuner class listener management method to allow - * stopping the buffer processing thread when there are no more listeners - */ - @Override - public void removeListener( Listener listener ) - { - super.removeListener( listener ); - - if( !hasListeners() ) - { - mComplexMixer.stop(); - } - } -} diff --git a/src/source/tuner/Tuner.java b/src/source/tuner/Tuner.java index aac3297e..13370fb3 100644 --- a/src/source/tuner/Tuner.java +++ b/src/source/tuner/Tuner.java @@ -23,9 +23,12 @@ import org.slf4j.LoggerFactory; import sample.Broadcaster; import sample.Listener; import sample.complex.ComplexBuffer; +import sample.complex.IComplexBufferProvider; import source.ISourceEventProcessor; import source.SourceEvent; import source.SourceException; +import source.tuner.manager.AbstractSourceManager; +import source.tuner.manager.TunerSourceManager; import java.util.ArrayList; import java.util.List; @@ -35,21 +38,27 @@ import java.util.concurrent.RejectedExecutionException; /** * Tuner - provides tuner channel sources, representing a channel frequency */ -public abstract class Tuner implements ITunerChannelProvider +public abstract class Tuner { private final static Logger mLog = LoggerFactory.getLogger(Tuner.class); - private String mName; + protected Broadcaster mTunerEventBroadcaster = new Broadcaster<>(); private TunerController mTunerController; - private Broadcaster mSampleBroadcaster = new Broadcaster<>(); - protected List> mTunerChangeListeners = new ArrayList<>(); - protected Broadcaster mSourceEventBroadcaster = new Broadcaster<>(); + private String mName; + private AbstractSourceManager mTunerSourceManager; + /** + * Abstract tuner class. + * @param name of the tuner + * @param tunerController for the tuner + */ public Tuner(String name, TunerController tunerController) { mName = name; mTunerController = tunerController; + mTunerSourceManager = new TunerSourceManager(mTunerController); + //Rebroadcast frequency and sample rate change events as tuner events mTunerController.addListener(new ISourceEventProcessor() { @@ -59,12 +68,10 @@ public abstract class Tuner implements ITunerChannelProvider switch(event.getEvent()) { case NOTIFICATION_FREQUENCY_CHANGE: - broadcast(new TunerEvent(Tuner.this, - source.tuner.TunerEvent.Event.FREQUENCY)); + broadcast(new TunerEvent(Tuner.this, source.tuner.TunerEvent.Event.FREQUENCY)); break; case NOTIFICATION_SAMPLE_RATE_CHANGE: - broadcast(new TunerEvent(Tuner.this, - source.tuner.TunerEvent.Event.SAMPLE_RATE)); + broadcast(new TunerEvent(Tuner.this, source.tuner.TunerEvent.Event.SAMPLE_RATE)); break; default: break; @@ -73,6 +80,14 @@ public abstract class Tuner implements ITunerChannelProvider }); } + /** + * Source Manager. Provides access to registering for complex buffer samples and source event notifications. + */ + public AbstractSourceManager getSourceManager() + { + return mTunerSourceManager; + } + public TunerController getTunerController() { return mTunerController; @@ -85,7 +100,6 @@ public abstract class Tuner implements ITunerChannelProvider public void dispose() { - mSampleBroadcaster.clear(); } public void setName(String name) @@ -129,93 +143,12 @@ public abstract class Tuner implements ITunerChannelProvider */ public abstract double getSampleSize(); - /** - * Returns a tuner frequency channel source, tuned to the correct frequency - * - * Note: ensure the concrete implementation registers the returned - * tuner channel source as a listener on the tuner, to receive frequency - * change events. - * - * @param channel - desired frequency and bandwidth - * @return - source for 48k sample rate - */ - public abstract TunerChannelSource getChannel(TunerChannel channel) - throws RejectedExecutionException, SourceException; - - /** - * Releases the tuned channel resources - * - * Note: ensure the concrete implementation unregisters the tuner channel - * source from the tuner as a frequency change listener - * - * @param source - previously obtained tuner channel - */ - public abstract void releaseChannel(TunerChannelSource source); - - /** - * Registers the listener to receive complex float sample arrays - */ - public void addListener(Listener listener) - { - mSampleBroadcaster.addListener(listener); - } - - /** - * Removes the registered listener - */ - public void removeListener(Listener listener) - { - mSampleBroadcaster.removeListener(listener); - } - - /** - * Number of registered complex sample buffer listeners - */ - public int getListenerCount() - { - return mSampleBroadcaster.getListenerCount(); - } - - /** - * Provides access to the internal sample broadcaster for sub-class use (ie protected). - */ - protected Broadcaster getSampleBroadcaster() - { - return mSampleBroadcaster; - } - - /** - * Indicates if the listener is currently registered with this tuner - * @param listener of complex buffer samples - * @return true if the listener is registered - */ - public boolean hasListener(Listener listener) - { - return mSampleBroadcaster.hasListener(listener); - } - - /** - * Indicates if this tuner has any complex buffer listeners registered - */ - public boolean hasListeners() - { - return mSampleBroadcaster.hasListeners(); - } - - /** - * Broadcasts the samples to all registered listeners - */ - public void broadcast(ComplexBuffer sampleBuffer) - { - mSampleBroadcaster.broadcast(sampleBuffer); - } - /** * Registers the listener */ public void addTunerChangeListener(Listener listener) { - mTunerChangeListeners.add(listener); + mTunerEventBroadcaster.addListener(listener); } /** @@ -223,7 +156,7 @@ public abstract class Tuner implements ITunerChannelProvider */ public void removeTunerChangeListener(Listener listener) { - mTunerChangeListeners.remove(listener); + mTunerEventBroadcaster.removeListener(listener); } /** @@ -231,36 +164,6 @@ public abstract class Tuner implements ITunerChannelProvider */ public void broadcast(TunerEvent tunerEvent) { - for(Listener listener : mTunerChangeListeners) - { - listener.receive(tunerEvent); - } - - //Re-broadcast frequency and sample rate change events as source events - switch(tunerEvent.getEvent()) - { - case FREQUENCY: - mSourceEventBroadcaster.broadcast(SourceEvent.frequencyChange(getTunerController().getFrequency())); - break; - case SAMPLE_RATE: - mSourceEventBroadcaster.broadcast(SourceEvent.sampleRateChange(getTunerController().getSampleRate())); - break; - } - } - - /** - * Adds the listener to receive source events - */ - public void addSourceEventListener(Listener listener) - { - mSourceEventBroadcaster.addListener(listener); - } - - /** - * Removes the listener from receiving source events - */ - public void removeSourceEventListener(Listener listener) - { - mSourceEventBroadcaster.removeListener(listener); + mTunerEventBroadcaster.broadcast(tunerEvent); } } diff --git a/src/source/tuner/TunerChannelProvider.java b/src/source/tuner/TunerChannelProvider.java deleted file mode 100644 index 8f53f144..00000000 --- a/src/source/tuner/TunerChannelProvider.java +++ /dev/null @@ -1,27 +0,0 @@ -package source.tuner; - -import java.util.concurrent.RejectedExecutionException; - -import source.SourceException; - -public interface TunerChannelProvider -{ - /** - * Returns a tuner frequency channel source, tuned to the correct frequency - * - * @param frequency - desired frequency - * - * @return - source for 48k sample rate - */ - public abstract TunerChannelSource getChannel( TunerChannel channel ) - throws RejectedExecutionException, SourceException; - - /** - * Releases the tuned channel resources - * - * @param channel - previously obtained tuner channel - */ - public abstract void releaseChannel( TunerChannelSource source ); - - -} diff --git a/src/source/tuner/TunerController.java b/src/source/tuner/TunerController.java index 8fd2cc54..50c4a98c 100644 --- a/src/source/tuner/TunerController.java +++ b/src/source/tuner/TunerController.java @@ -19,10 +19,16 @@ package source.tuner; import org.slf4j.Logger; import org.slf4j.LoggerFactory; +import sample.Broadcaster; +import sample.Listener; +import sample.complex.ComplexBuffer; +import sample.complex.IComplexBufferProvider; import source.ISourceEventProcessor; import source.SourceEvent; import source.SourceEvent.Event; import source.SourceException; +import source.tuner.channel.TunerChannel; +import source.tuner.channel.TunerChannelSource; import source.tuner.configuration.TunerConfiguration; import source.tuner.frequency.FrequencyController; import source.tuner.frequency.FrequencyController.Tunable; @@ -31,9 +37,11 @@ import java.util.SortedSet; import java.util.concurrent.ConcurrentSkipListSet; import java.util.concurrent.RejectedExecutionException; -public abstract class TunerController implements Tunable, ISourceEventProcessor +public abstract class TunerController implements Tunable, ISourceEventProcessor, + IComplexBufferProvider, Listener { private final static Logger mLog = LoggerFactory.getLogger(TunerController.class); + protected Broadcaster mSampleBroadcaster = new Broadcaster<>(); /* List of currently tuned channels being served to demod channels */ private SortedSet mTunedChannels = new ConcurrentSkipListSet<>(); @@ -102,6 +110,12 @@ public abstract class TunerController implements Tunable, ISourceEventProcessor return mFrequencyController.getFrequency(); } + @Override + public boolean canTune(long frequency) + { + return mFrequencyController.canTune(frequency); + } + public int getSampleRate() { return mFrequencyController.getSampleRate(); @@ -369,4 +383,48 @@ public abstract class TunerController implements Tunable, ISourceEventProcessor { mFrequencyController.removeFrequencyChangeProcessor(processor); } + + /** + * Adds the listener to receive complex buffer samples + */ + @Override + public void addComplexBufferListener(Listener listener) + { + mSampleBroadcaster.addListener(listener); + } + + /** + * Removes the listener from receiving complex buffer samples + */ + @Override + public void removeComplexBufferListener(Listener listener) + { + mSampleBroadcaster.removeListener(listener); + } + + /** + * Indicates if there are any complex buffer listeners registered on this controller + */ + @Override + public boolean hasComplexBufferListeners() + { + return mSampleBroadcaster.hasListeners(); + } + + /** + * Broadcasts the buffer to any registered listeners + */ + protected void broadcast(ComplexBuffer complexBuffer) + { + mSampleBroadcaster.broadcast(complexBuffer); + } + + /** + * Implements the Listener interface to receive and distribute complex buffers from subclass implementations + */ + @Override + public void receive(ComplexBuffer complexBuffer) + { + broadcast(complexBuffer); + } } diff --git a/src/source/tuner/TunerManager.java b/src/source/tuner/TunerManager.java index b5181c26..12d228aa 100644 --- a/src/source/tuner/TunerManager.java +++ b/src/source/tuner/TunerManager.java @@ -246,25 +246,21 @@ public class TunerManager + "supported"); } - private TunerInitStatus initFuncubeProTuner(Device device, - DeviceDescriptor descriptor) + private TunerInitStatus initFuncubeProTuner(Device device, DeviceDescriptor descriptor) { String reason = "NOT LOADED"; - MixerTunerDataLine dataline = getMixerTunerDataLine( - TunerClass.FUNCUBE_DONGLE_PRO.getTunerType()); + MixerTunerDataLine dataline = getMixerTunerDataLine(TunerClass.FUNCUBE_DONGLE_PRO.getTunerType()); if(dataline != null) { - FCD1TunerController controller = - new FCD1TunerController(device, descriptor); + FCD1TunerController controller = new FCD1TunerController(dataline, device, descriptor); try { controller.init(); - FCDTuner tuner = - new FCDTuner(dataline, controller); + FCDTuner tuner = new FCDTuner(controller); return new TunerInitStatus(tuner, "LOADED"); } @@ -284,25 +280,21 @@ public class TunerManager + "loaded - " + reason); } - private TunerInitStatus initFuncubeProPlusTuner(Device device, - DeviceDescriptor descriptor) + private TunerInitStatus initFuncubeProPlusTuner(Device device, DeviceDescriptor descriptor) { String reason = "NOT LOADED"; - MixerTunerDataLine dataline = getMixerTunerDataLine( - TunerClass.FUNCUBE_DONGLE_PRO_PLUS.getTunerType()); + MixerTunerDataLine dataline = getMixerTunerDataLine(TunerClass.FUNCUBE_DONGLE_PRO_PLUS.getTunerType()); if(dataline != null) { - FCD2TunerController controller = - new FCD2TunerController(device, descriptor); + FCD2TunerController controller = new FCD2TunerController(dataline, device, descriptor); try { controller.init(); - FCDTuner tuner = - new FCDTuner(dataline, controller); + FCDTuner tuner = new FCDTuner(controller); return new TunerInitStatus(tuner, "LOADED"); } @@ -323,8 +315,7 @@ public class TunerManager + "loaded - " + reason); } - private TunerInitStatus initHackRFTuner(Device device, - DeviceDescriptor descriptor) + private TunerInitStatus initHackRFTuner(Device device, DeviceDescriptor descriptor) { try { diff --git a/src/source/tuner/TunerModel.java b/src/source/tuner/TunerModel.java index 09a74804..9680d732 100644 --- a/src/source/tuner/TunerModel.java +++ b/src/source/tuner/TunerModel.java @@ -1,298 +1,292 @@ package source.tuner; -import java.text.DecimalFormat; -import java.util.ArrayList; -import java.util.Iterator; -import java.util.List; -import java.util.concurrent.RejectedExecutionException; - -import javax.swing.table.AbstractTableModel; - import org.slf4j.Logger; import org.slf4j.LoggerFactory; - import sample.Listener; import source.Source; import source.SourceException; import source.config.SourceConfigTuner; import source.tuner.TunerEvent.Event; +import source.tuner.channel.TunerChannel; +import source.tuner.channel.TunerChannelSource; import source.tuner.configuration.TunerConfiguration; import source.tuner.configuration.TunerConfigurationModel; +import javax.swing.table.AbstractTableModel; +import java.text.DecimalFormat; +import java.util.ArrayList; +import java.util.Iterator; +import java.util.List; + public class TunerModel extends AbstractTableModel implements Listener { - private static final long serialVersionUID = 1L; - - private final static Logger mLog = LoggerFactory.getLogger( TunerModel.class ); + private static final long serialVersionUID = 1L; - public static final int TUNER_TYPE = 0; - public static final int TUNER_ID = 1; - public static final int SAMPLE_RATE = 2; - public static final int FREQUENCY = 3; - public static final int CHANNEL_COUNT = 4; - public static final int SPECTRAL_DISPLAY_MAIN = 5; - public static final int SPECTRAL_DISPLAY_NEW = 6; - - private static final String MHZ = " MHz"; - private static final String[] COLUMNS = - { "Tuner", "ID", "Sample Rate", "Frequency", "Channels", "Spectral", "Display" }; - - private List mTuners = new ArrayList<>(); - private List> mTunerEventListeners = new ArrayList<>(); - private DecimalFormat mFrequencyFormat = new DecimalFormat( "0.00000" ); - private DecimalFormat mSampleRateFormat = new DecimalFormat( "0.000" ); - - private TunerConfigurationModel mTunerConfigurationModel; + private final static Logger mLog = LoggerFactory.getLogger(TunerModel.class); - public TunerModel( TunerConfigurationModel tunerConfigurationModel ) - { - mTunerConfigurationModel = tunerConfigurationModel; - } - - public TunerConfigurationModel getTunerConfigurationModel() - { - return mTunerConfigurationModel; - } + public static final int TUNER_TYPE = 0; + public static final int TUNER_ID = 1; + public static final int SAMPLE_RATE = 2; + public static final int FREQUENCY = 3; + public static final int CHANNEL_COUNT = 4; + public static final int SPECTRAL_DISPLAY_MAIN = 5; + public static final int SPECTRAL_DISPLAY_NEW = 6; - /** - * List of Tuners currently in the model - */ - public List getTuners() - { - return mTuners; - } - - public Tuner getTuner( int index ) - { - if( index < mTuners.size() ) - { - return mTuners.get( index ); - } - - return null; - } + private static final String MHZ = " MHz"; + private static final String[] COLUMNS = + {"Tuner", "ID", "Sample Rate", "Frequency", "Channels", "Spectral", "Display"}; - public void addTuners( List Tuners ) - { - for( Tuner tuner: Tuners ) - { - addTuner( tuner ); - } - } + private List mTuners = new ArrayList<>(); + private List> mTunerEventListeners = new ArrayList<>(); + private DecimalFormat mFrequencyFormat = new DecimalFormat("0.00000"); + private DecimalFormat mSampleRateFormat = new DecimalFormat("0.000"); - /** - * Adds the Tuner to this model - */ - public void addTuner( Tuner tuner ) - { - if( !mTuners.contains( tuner ) ) - { - //Get the tuner configuration and apply it to the tuner - this - //call should always produce a tuner configuration - TunerConfiguration config = mTunerConfigurationModel - .getTunerConfiguration( tuner.getTunerType(), tuner.getUniqueID() ); - - try - { - tuner.getTunerController().apply( config ); - - mTuners.add( tuner ); - - int index = mTuners.indexOf( tuner ); - - fireTableRowsInserted( index, index ); - - tuner.addTunerChangeListener( this ); - } - catch( SourceException se ) - { - mLog.error( "Couldn't apply tuner configuration to tuner - [" - + tuner.getTunerType().name() + "] with id [" - + tuner.getUniqueID() + "] - tuner will not be included" ); - } - - } - } + private TunerConfigurationModel mTunerConfigurationModel; - /** - * Removes the Tuner from this model - */ - public void removeTuner( Tuner tuner ) - { - if( mTuners.contains( tuner ) ) - { - tuner.removeTunerChangeListener( this ); - - int index = mTuners.indexOf( tuner ); - - mTuners.remove( tuner ); - - fireTableRowsDeleted( index, index ); - } - } - - public void addListener( Listener listener ) - { - mTunerEventListeners.add( listener ); - } + public TunerModel(TunerConfigurationModel tunerConfigurationModel) + { + mTunerConfigurationModel = tunerConfigurationModel; + } - public void removeListener( Listener listener ) - { - mTunerEventListeners.remove( listener ); - } + public TunerConfigurationModel getTunerConfigurationModel() + { + return mTunerConfigurationModel; + } - public void broadcast( TunerEvent event ) - { - for( Listener listener: mTunerEventListeners ) - { - listener.receive( event ); - } - } + /** + * List of Tuners currently in the model + */ + public List getTuners() + { + return mTuners; + } - /** - * Requests to display the first tuner in this model. Invoke this method - * after all listeners have registered and tuners have been added to this - * model, in order to inform the primary display to use the first tuner. - */ - public void requestFirstTunerDisplay() - { - if( mTuners.size() > 0 ) - { - broadcast( new TunerEvent( mTuners.get( 0 ), - Event.REQUEST_MAIN_SPECTRAL_DISPLAY ) ); - } - } - - @Override - public void receive( TunerEvent event ) - { - if( event.getTuner() != null ) - { - int index = mTuners.indexOf( event.getTuner() ); - - if( index >= 0 ) - { - switch( event.getEvent() ) - { - case CHANNEL_COUNT: - fireTableCellUpdated( index, CHANNEL_COUNT ); - break; - case FREQUENCY: - fireTableCellUpdated( index, FREQUENCY ); - mTunerConfigurationModel.tunerFrequencyChanged( event.getTuner() ); - break; - case SAMPLE_RATE: - fireTableCellUpdated( index, SAMPLE_RATE ); - break; - case REQUEST_MAIN_SPECTRAL_DISPLAY: - fireTableCellUpdated( index, SPECTRAL_DISPLAY_MAIN ); - default: - break; - } - } - } - - broadcast( event ); - } + public Tuner getTuner(int index) + { + if(index < mTuners.size()) + { + return mTuners.get(index); + } - @Override - public int getRowCount() - { - return mTuners.size(); - } + return null; + } - @Override - public int getColumnCount() - { - return COLUMNS.length; - } + public void addTuners(List Tuners) + { + for(Tuner tuner : Tuners) + { + addTuner(tuner); + } + } - @Override - public Object getValueAt( int rowIndex, int columnIndex ) - { - if( rowIndex < mTuners.size() ) - { - Tuner tuner = mTuners.get( rowIndex ); + /** + * Adds the Tuner to this model + */ + public void addTuner(Tuner tuner) + { + if(!mTuners.contains(tuner)) + { + //Get the tuner configuration and apply it to the tuner - this + //call should always produce a tuner configuration + TunerConfiguration config = mTunerConfigurationModel + .getTunerConfiguration(tuner.getTunerType(), tuner.getUniqueID()); - switch( columnIndex ) - { - case TUNER_TYPE: - return tuner.getTunerType().getLabel(); - case TUNER_ID: - return tuner.getUniqueID(); - case SAMPLE_RATE: - int sampleRate = tuner.getTunerController().getSampleRate(); + try + { + tuner.getTunerController().apply(config); - return mSampleRateFormat.format( sampleRate / 1E6D ) + MHZ; - case FREQUENCY: - try - { - long frequency = tuner.getTunerController().getFrequency(); - - return mFrequencyFormat.format( frequency / 1E6D ) + MHZ; - } - catch( Exception e ) - { - return 0; - } - case CHANNEL_COUNT: - return tuner.getTunerController().getChannelCount(); - case SPECTRAL_DISPLAY_MAIN: - return "Main"; - case SPECTRAL_DISPLAY_NEW: - return "New"; - default: - break; - } - } + mTuners.add(tuner); + + int index = mTuners.indexOf(tuner); + + fireTableRowsInserted(index, index); + + tuner.addTunerChangeListener(this); + } + catch(SourceException se) + { + mLog.error("Couldn't apply tuner configuration to tuner - [" + + tuner.getTunerType().name() + "] with id [" + + tuner.getUniqueID() + "] - tuner will not be included"); + } + + } + } + + /** + * Removes the Tuner from this model + */ + public void removeTuner(Tuner tuner) + { + if(mTuners.contains(tuner)) + { + tuner.removeTunerChangeListener(this); + + int index = mTuners.indexOf(tuner); + + mTuners.remove(tuner); + + fireTableRowsDeleted(index, index); + } + } + + public void addListener(Listener listener) + { + mTunerEventListeners.add(listener); + } + + public void removeListener(Listener listener) + { + mTunerEventListeners.remove(listener); + } + + public void broadcast(TunerEvent event) + { + for(Listener listener : mTunerEventListeners) + { + listener.receive(event); + } + } + + /** + * Requests to display the first tuner in this model. Invoke this method + * after all listeners have registered and tuners have been added to this + * model, in order to inform the primary display to use the first tuner. + */ + public void requestFirstTunerDisplay() + { + if(mTuners.size() > 0) + { + broadcast(new TunerEvent(mTuners.get(0), + Event.REQUEST_MAIN_SPECTRAL_DISPLAY)); + } + } + + @Override + public void receive(TunerEvent event) + { + if(event.getTuner() != null) + { + int index = mTuners.indexOf(event.getTuner()); + + if(index >= 0) + { + switch(event.getEvent()) + { + case CHANNEL_COUNT: + fireTableCellUpdated(index, CHANNEL_COUNT); + break; + case FREQUENCY: + fireTableCellUpdated(index, FREQUENCY); + mTunerConfigurationModel.tunerFrequencyChanged(event.getTuner()); + break; + case SAMPLE_RATE: + fireTableCellUpdated(index, SAMPLE_RATE); + break; + case REQUEST_MAIN_SPECTRAL_DISPLAY: + fireTableCellUpdated(index, SPECTRAL_DISPLAY_MAIN); + default: + break; + } + } + } + + broadcast(event); + } + + @Override + public int getRowCount() + { + return mTuners.size(); + } + + @Override + public int getColumnCount() + { + return COLUMNS.length; + } + + @Override + public Object getValueAt(int rowIndex, int columnIndex) + { + if(rowIndex < mTuners.size()) + { + Tuner tuner = mTuners.get(rowIndex); + + switch(columnIndex) + { + case TUNER_TYPE: + return tuner.getTunerType().getLabel(); + case TUNER_ID: + return tuner.getUniqueID(); + case SAMPLE_RATE: + int sampleRate = tuner.getTunerController().getSampleRate(); + + return mSampleRateFormat.format(sampleRate / 1E6D) + MHZ; + case FREQUENCY: + try + { + long frequency = tuner.getTunerController().getFrequency(); + + return mFrequencyFormat.format(frequency / 1E6D) + MHZ; + } + catch(Exception e) + { + return 0; + } + case CHANNEL_COUNT: + return tuner.getTunerController().getChannelCount(); + case SPECTRAL_DISPLAY_MAIN: + return "Main"; + case SPECTRAL_DISPLAY_NEW: + return "New"; + default: + break; + } + } + + return null; + } + + @Override + public String getColumnName(int columnIndex) + { + return COLUMNS[columnIndex]; + } - return null; - } - - @Override - public String getColumnName( int columnIndex ) - { - return COLUMNS[ columnIndex ]; - } - /** * Iterates current tuners to get a tuner channel source for the frequency * specified in the channel config's source config object. - * + * * Returns null if no tuner can source the channel */ - public Source getSource( SourceConfigTuner config, int bandwidth ) + public Source getSource(SourceConfigTuner config, int bandwidth) { - TunerChannelSource retVal = null; - - TunerChannel tunerChannel = config.getTunerChannel(); - - tunerChannel.setBandwidth( bandwidth ); - - Iterator it = mTuners.iterator(); - - Tuner tuner; - - while( it.hasNext() && retVal == null ) - { - tuner = it.next(); - - try + TunerChannelSource retVal = null; + + TunerChannel tunerChannel = config.getTunerChannel(); + + tunerChannel.setBandwidth(bandwidth); + + Iterator it = mTuners.iterator(); + + Tuner tuner; + + while(it.hasNext() && retVal == null) + { + tuner = it.next(); + + try { - retVal = tuner.getChannel( tunerChannel ); + retVal = tuner.getSourceManager().getSource(tunerChannel); } - catch ( RejectedExecutionException ree ) - { - mLog.error( "couldn't provide tuner channel source", ree ); - } - catch ( SourceException e ) + catch(Exception e) { - mLog.error( "error obtaining channel from tuner [" + - tuner.getName() + "]", e ); + mLog.error("Error obtaining channel source from tuner [" + tuner.getName() + "]", e); } - } - - return retVal; + } + + return retVal; } - + } diff --git a/src/source/tuner/airspy/AirspyTuner.java b/src/source/tuner/airspy/AirspyTuner.java index 48c06d3b..649a4eae 100644 --- a/src/source/tuner/airspy/AirspyTuner.java +++ b/src/source/tuner/airspy/AirspyTuner.java @@ -17,19 +17,11 @@ ******************************************************************************/ package source.tuner.airspy; -import java.util.concurrent.RejectedExecutionException; - import org.slf4j.Logger; import org.slf4j.LoggerFactory; -import sample.Listener; -import sample.complex.ComplexBuffer; -import source.SourceException; import source.tuner.Tuner; -import source.tuner.TunerChannel; -import source.tuner.TunerChannelSource; import source.tuner.TunerClass; -import source.tuner.TunerEvent; import source.tuner.TunerType; import source.tuner.airspy.AirspyTunerController.BoardID; @@ -79,50 +71,4 @@ public class AirspyTuner extends Tuner { return 13.0; } - - @Override - public TunerChannelSource getChannel( TunerChannel channel ) - throws RejectedExecutionException, SourceException - { - //TODO: this channel has a decimated sample rate of: - // 10.0 MSps = 10,000,000 / 208 = 48076.923 - // 5.0 MSps = 5,000,000 / 104 = 48076.923 - // 2.5 MSps = 2,500,000 / 52 = 48076.923 - //Consider implementing a fractional resampler to get a correct 48 kHz - //output sample rate - - TunerChannelSource source = getController().getChannel( this, channel ); - - if( source != null ) - { - broadcast( new TunerEvent( this, TunerEvent.Event.CHANNEL_COUNT ) ); - } - - return source; - } - - @Override - public void releaseChannel( TunerChannelSource source ) - { - /* Unregister for receiving samples */ - removeListener( (Listener)source ); - - /* Tell the controller to release the channel and cleanup */ - if( source != null ) - { - getController().releaseChannel( source ); - } - } - - @Override - public void addListener( Listener listener ) - { - getController().addListener( listener ); - } - - @Override - public void removeListener( Listener listener ) - { - getController().removeListener( listener ); - } } diff --git a/src/source/tuner/airspy/AirspyTunerController.java b/src/source/tuner/airspy/AirspyTunerController.java index f8362bf6..490cd231 100644 --- a/src/source/tuner/airspy/AirspyTunerController.java +++ b/src/source/tuner/airspy/AirspyTunerController.java @@ -1,7 +1,5 @@ package source.tuner.airspy; -import dsp.filter.dc.DCRemovalFilter_RB; -import dsp.filter.hilbert.HilbertTransform; import org.apache.commons.io.EndianUtils; import org.slf4j.Logger; import org.slf4j.LoggerFactory; @@ -9,16 +7,11 @@ import org.usb4java.Device; import org.usb4java.DeviceHandle; import org.usb4java.LibUsb; import org.usb4java.LibUsbException; -import org.usb4java.Transfer; -import org.usb4java.TransferCallback; -import sample.Broadcaster; -import sample.Listener; -import sample.complex.ComplexBuffer; import source.SourceException; import source.tuner.TunerController; import source.tuner.configuration.TunerConfiguration; import source.tuner.usb.USBTransferProcessor; -import util.ThreadPool; +import source.tuner.usb.USBTunerController; import javax.usb.UsbException; import java.nio.ByteBuffer; @@ -27,10 +20,6 @@ import java.text.DecimalFormat; import java.util.ArrayList; import java.util.EnumSet; import java.util.List; -import java.util.concurrent.LinkedTransferQueue; -import java.util.concurrent.ScheduledFuture; -import java.util.concurrent.TimeUnit; -import java.util.concurrent.atomic.AtomicBoolean; /** * SDR Trunk @@ -72,7 +61,7 @@ import java.util.concurrent.atomic.AtomicBoolean; * POSSIBILITY OF SUCH DAMAGE. */ -public class AirspyTunerController extends TunerController +public class AirspyTunerController extends USBTunerController { public static final Gain LINEARITY_GAIN_DEFAULT = Gain.LINEARITY_14; public static final Gain SENSITIVITY_GAIN_DEFAULT = Gain.SENSITIVITY_10; @@ -193,6 +182,14 @@ public class AirspyTunerController extends TunerController USB_TRANSFER_BUFFER_SIZE); } + + @Override + protected USBTransferProcessor getUSBTransferProcessor() + { + return mUSBTransferProcessor; + } + + /** * Claims the USB interface. If another application currently has * the interface claimed, the USB_FORCE_CLAIM_INTERFACE setting @@ -1193,34 +1190,4 @@ public class AirspyTunerController extends TunerController return mValue; } } - - /** - * Adds the IQ buffer listener and automatically starts buffer transfer processing, if not already started. - */ - public void addListener(Listener listener) - { - if(mUSBTransferProcessor != null) - { - mUSBTransferProcessor.addListener(listener); - } - else - { - mLog.error("Couldn't add IQ buffer listener to Airspy tuner - processor is null"); - } - } - - /** - * Removes the IQ buffer listener and stops buffer transfer processing if there are no more listeners. - */ - public void removeListener(Listener listener) - { - if(mUSBTransferProcessor != null) - { - mUSBTransferProcessor.removeListener(listener); - } - else - { - mLog.error("Couldn't remove IQ buffer listener from Airspy tuner - processor is null"); - } - } } diff --git a/src/source/tuner/TunerChannelSource.java b/src/source/tuner/channel/DecimatingTunerChannelSource.java similarity index 89% rename from src/source/tuner/TunerChannelSource.java rename to src/source/tuner/channel/DecimatingTunerChannelSource.java index a41b22de..134601b1 100644 --- a/src/source/tuner/TunerChannelSource.java +++ b/src/source/tuner/channel/DecimatingTunerChannelSource.java @@ -16,12 +16,10 @@ * along with this program. If not, see * ******************************************************************************/ -package source.tuner; +package source.tuner.channel; -import channel.heartbeat.Heartbeat; -import channel.heartbeat.IHeartbeatProvider; import dsp.filter.FilterFactory; -import dsp.filter.Window.WindowType; +import dsp.filter.Window; import dsp.filter.cic.ComplexPrimeCICDecimate; import dsp.mixer.Oscillator; import org.slf4j.Logger; @@ -32,12 +30,13 @@ import sample.OverflowableTransferQueue; import sample.complex.Complex; import sample.complex.ComplexBuffer; import sample.real.IOverflowListener; -import source.ComplexSource; -import source.SourceException; import source.ISourceEventListener; -import source.ISourceEventProvider; -import source.SourceEvent; import source.ISourceEventProcessor; +import source.ISourceEventProvider; +import source.Source; +import source.SourceEvent; +import source.SourceException; +import source.tuner.Tuner; import java.util.ArrayList; import java.util.List; @@ -47,9 +46,9 @@ import java.util.concurrent.ScheduledFuture; import java.util.concurrent.TimeUnit; import java.util.concurrent.atomic.AtomicBoolean; -public class TunerChannelSource extends ComplexSource implements ISourceEventProcessor, Listener +public class DecimatingTunerChannelSource extends TunerChannelSource { - private final static Logger mLog = LoggerFactory.getLogger(TunerChannelSource.class); + private final static Logger mLog = LoggerFactory.getLogger(DecimatingTunerChannelSource.class); //Maximum number of filled buffers for the blocking queue private static final int BUFFER_MAX_CAPACITY = 300; @@ -59,17 +58,14 @@ public class TunerChannelSource extends ComplexSource implements ISourceEventPro private static int CHANNEL_RATE = 48000; private static int CHANNEL_PASS_FREQUENCY = 12000; - private static final Heartbeat HEARTBEAT = new Heartbeat(); private OverflowableTransferQueue mBuffer; - private Tuner mTuner; private TunerChannel mTunerChannel; private Oscillator mMixer; private ComplexPrimeCICDecimate mDecimationFilter; private Listener mListener; private ISourceEventProcessor mFrequencyChangeProcessor; - private Listener mHeartbeatListener; private DownstreamProcessor mDownstreamFrequencyEventProcessor = new DownstreamProcessor(); private ScheduledFuture mTaskHandle; @@ -93,28 +89,50 @@ public class TunerChannelSource extends ComplexSource implements ISourceEventPro * and thus the tuner might no longer be able to source this channel once it * has been stopped. * - * @param tuner to obtain wideband IQ samples from * @param tunerChannel specifying the center frequency for the DDC * @throws RejectedExecutionException if the thread pool manager cannot * accept the decimation processing task * @throws SourceException if the tuner has an issue providing IQ samples */ - public TunerChannelSource(Tuner tuner, TunerChannel tunerChannel) throws RejectedExecutionException, SourceException + public DecimatingTunerChannelSource(Listener listener, TunerChannel tunerChannel, int inputSampleRate, + long frequency) { - mTuner = tuner; + super(listener, tunerChannel); + mTunerChannel = tunerChannel; - mTuner.getTunerController().addListener((ISourceEventProcessor) this); - mTunerFrequency = mTuner.getTunerController().getFrequency(); + mTunerFrequency = frequency; mBuffer = new OverflowableTransferQueue<>(BUFFER_MAX_CAPACITY, BUFFER_OVERFLOW_RESET_THRESHOLD); /* Setup the frequency translator to the current source frequency */ long frequencyOffset = mTunerFrequency - mTunerChannel.getFrequency(); - mMixer = new Oscillator(frequencyOffset, mTuner.getTunerController().getSampleRate()); + mMixer = new Oscillator(frequencyOffset, inputSampleRate); /* Fire a sample rate change event to setup the decimation chain */ - process(SourceEvent.sampleRateChange(mTuner.getTunerController().getSampleRate())); + try + { + process(SourceEvent.sampleRateChange(inputSampleRate)); + } + catch(SourceException se) + { + mLog.error("Error", se); + } + + } + + /** + * Changes the frequency correction value and broadcasts the change to the registered downstream listener. + * @param correction current frequency correction value. + */ + private void setFrequencyCorrection(int correction) + { + mChannelFrequencyCorrection = correction; + + updateMixerFrequencyOffset(); + + mDownstreamFrequencyEventProcessor.broadcast( + SourceEvent.channelFrequencyCorrectionChange(mChannelFrequencyCorrection)); } /** @@ -145,7 +163,7 @@ public class TunerChannelSource extends ComplexSource implements ISourceEventPro mTaskHandle = executor.scheduleAtFixedRate(mDecimationProcessor, 0, 9, TimeUnit.MILLISECONDS); /* Finally, register to receive samples from the tuner */ - mTuner.addListener((Listener) this); + mSourceEventListener.receive(SourceEvent.startSampleStream(this)); } else { @@ -163,7 +181,7 @@ public class TunerChannelSource extends ComplexSource implements ISourceEventPro { if(mRunning.compareAndSet(true, false)) { - mTuner.releaseChannel(this); + mSourceEventListener.receive(SourceEvent.stopSampleStream(this)); mDecimationProcessor.shutdown(); if(mTaskHandle != null) @@ -186,30 +204,6 @@ public class TunerChannelSource extends ComplexSource implements ISourceEventPro @Override public void dispose() { - if(!mRunning.get()) - { - /* Tell the tuner to release/unregister our resources */ - mTuner.getTunerController().removeListener(this); - } - } - - /** - * Changes the frequency correction value and broadcasts the change to the registered downstream listener. - * @param correction current frequency correction value. - */ - private void setFrequencyCorrection(int correction) - { - mChannelFrequencyCorrection = correction; - - updateMixerFrequencyOffset(); - - mDownstreamFrequencyEventProcessor.broadcast( - SourceEvent.channelFrequencyCorrectionChange(mChannelFrequencyCorrection)); - } - - public Tuner getTuner() - { - return mTuner; } public TunerChannel getTunerChannel() @@ -234,7 +228,7 @@ public class TunerChannelSource extends ComplexSource implements ISourceEventPro @Override public void setListener(Listener listener) { - /* Save a pointer to the listener so that if we have to change the + /* Save a pointer to the listener so that if we have to change the * decimation filter, we can re-add the listener */ mListener = listener; @@ -290,7 +284,7 @@ public class TunerChannelSource extends ComplexSource implements ISourceEventPro /* Get new decimation filter */ mDecimationFilter = FilterFactory.getDecimationFilter(sampleRate, CHANNEL_RATE, 1, - CHANNEL_PASS_FREQUENCY, 60, WindowType.HAMMING); + CHANNEL_PASS_FREQUENCY, 60, Window.WindowType.HAMMING); /* re-add the original output listener */ mDecimationFilter.setListener(mListener); @@ -336,7 +330,7 @@ public class TunerChannelSource extends ComplexSource implements ISourceEventPro /** * Implements ISourceEventProvider to remove the frequency change listener from receiving down-stream frequency * change events. - */ + */ @Override public void removeSourceEventListener() { @@ -354,25 +348,6 @@ public class TunerChannelSource extends ComplexSource implements ISourceEventPro return mDownstreamFrequencyEventProcessor.getSourceEventListener(); } - /** - * Registers the listener to receive heartbeats from this source. - * @param listener to receive heartbeats - */ - @Override - public void setHeartbeatListener(Listener listener) - { - mHeartbeatListener = listener; - } - - /** - * Removes the currently registered heartbeat listener - */ - @Override - public void removeHeartbeatListener() - { - mHeartbeatListener = null; - } - /** * Managers frequency change requests and notifications from/to any downstream component. Downstream * components are those that receive samples from this tuner channel source. These downstream components will be @@ -500,10 +475,7 @@ public class TunerChannelSource extends ComplexSource implements ISourceEventPro { //Send a heartbeat every time this runs to allow downstream components to perform periodic //state monitoring functions on this thread - if(mHeartbeatListener != null) - { - mHeartbeatListener.receive(HEARTBEAT); - } + getHeartbeatManager().broadcast(); mBuffer.drainTo(mSampleBuffers, 20); diff --git a/src/source/tuner/TunerChannel.java b/src/source/tuner/channel/TunerChannel.java similarity index 98% rename from src/source/tuner/TunerChannel.java rename to src/source/tuner/channel/TunerChannel.java index f2b383b3..0fa851a0 100644 --- a/src/source/tuner/TunerChannel.java +++ b/src/source/tuner/channel/TunerChannel.java @@ -16,7 +16,7 @@ * along with this program. If not, see * ******************************************************************************/ -package source.tuner; +package source.tuner.channel; public class TunerChannel implements Comparable { diff --git a/src/source/tuner/channel/TunerChannelProvider.java b/src/source/tuner/channel/TunerChannelProvider.java new file mode 100644 index 00000000..56b85aa1 --- /dev/null +++ b/src/source/tuner/channel/TunerChannelProvider.java @@ -0,0 +1,45 @@ +/******************************************************************************* + * 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 source.tuner.channel; + +import java.util.concurrent.RejectedExecutionException; + +import source.SourceException; + +public interface TunerChannelProvider +{ + /** + * Returns a tuner frequency channel source, tuned to the correct frequency + * + * @param frequency - desired frequency + * + * @return - source for 48k sample rate + */ + public abstract TunerChannelSource getChannel( TunerChannel channel ) + throws RejectedExecutionException, SourceException; + + /** + * Releases the tuned channel resources + * + * @param channel - previously obtained tuner channel + */ + public abstract void releaseChannel( TunerChannelSource source ); + + +} diff --git a/src/source/tuner/channel/TunerChannelSource.java b/src/source/tuner/channel/TunerChannelSource.java new file mode 100644 index 00000000..54a2bdc3 --- /dev/null +++ b/src/source/tuner/channel/TunerChannelSource.java @@ -0,0 +1,105 @@ +/******************************************************************************* + * 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 source.tuner.channel; + +import dsp.filter.FilterFactory; +import dsp.filter.Window.WindowType; +import dsp.filter.cic.ComplexPrimeCICDecimate; +import org.slf4j.Logger; +import org.slf4j.LoggerFactory; +import sample.Buffer; +import sample.Listener; +import sample.complex.Complex; +import sample.complex.ComplexBuffer; +import sample.real.IOverflowListener; +import source.ComplexSource; +import source.SourceException; +import source.ISourceEventListener; +import source.ISourceEventProvider; +import source.SourceEvent; +import source.ISourceEventProcessor; + +import java.util.ArrayList; +import java.util.List; +import java.util.concurrent.ScheduledExecutorService; +import java.util.concurrent.TimeUnit; + +public abstract class TunerChannelSource extends ComplexSource implements ISourceEventProcessor, Listener +{ + private final static Logger mLog = LoggerFactory.getLogger(TunerChannelSource.class); + private TunerChannel mTunerChannel; + protected Listener mSourceEventListener; + + /** + * Requirements: + * 1. Provide complex buffer samples + * 2. Fetch complex buffer samples from subclass implementations (for distribution) + * 3. Provide heartbeat at each interval + * 4. Provide start/stop running + * 5. Provide support for downstream consumer source events (delegate to the sub class) + * 6. Support buffer overflow listeners + * + * @param sourceEventListener to receive source dispose request + * @param tunerChannel + */ + public TunerChannelSource(Listener sourceEventListener, TunerChannel tunerChannel) + { + mSourceEventListener = sourceEventListener; + } + + /** + * Tuner channel for this tuner channel source + */ + public TunerChannel getTunerChannel() + { + return mTunerChannel; + } + + /** + * Starts this tuner channel source producing sample stream. + */ + public void start() + { + if(mSourceEventListener != null) + { + mSourceEventListener.receive(SourceEvent.startSampleStream(this)); + } + + //TODO: broadcast center frequency and sample rate to downstream listeners. + } + + /** + * Stops this tuner channel source from producing a sample stream. Note: tuner channel sources are one-time usage + * only. Invoking this method will also tell the source manager to dispose of this source. + */ + public void stop() + { + if(mSourceEventListener != null) + { + mSourceEventListener.receive(SourceEvent.stopSampleStream(this)); + mSourceEventListener.receive(SourceEvent.sourceDisposeRequest(this)); + } + } + + @Override + public void reset() + { + //Reset is not valid for a tuner channel source - ignored + } +} diff --git a/src/source/tuner/ettus/B100Tuner.java b/src/source/tuner/ettus/B100Tuner.java deleted file mode 100644 index be5271f8..00000000 --- a/src/source/tuner/ettus/B100Tuner.java +++ /dev/null @@ -1,83 +0,0 @@ -/******************************************************************************* - * SDR Trunk - * Copyright (C) 2014 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 source.tuner.ettus; - -import java.util.concurrent.RejectedExecutionException; - -import source.SourceException; -import source.tuner.Tuner; -import source.tuner.TunerChannel; -import source.tuner.TunerChannelSource; -import source.tuner.TunerClass; -import source.tuner.TunerType; -import source.tuner.usb.USBTunerDevice; - -public class B100Tuner extends Tuner -{ - public B100Tuner( USBTunerDevice device ) - { - //TODO: this should extend a USBTuner class, so that we can push the - //complex sample listener and the frequency change listener up to that class - - super( "Ettus B100 with WBX Tuner", null ); - } - - public void dispose() - { - - } - - @Override - public TunerClass getTunerClass() - { - return TunerClass.ETTUS_USRP_B100; - } - - @Override - public TunerType getTunerType() - { - return TunerType.ETTUS_WBX; - } - - @Override - public double getSampleSize() - { - return 16.0; - } - - @Override - public TunerChannelSource getChannel( TunerChannel channel ) - throws RejectedExecutionException, SourceException - { - //TODO: pass this call to the tuner controller - return null; - } - - @Override - public void releaseChannel( TunerChannelSource source ) - { - //TODO: pass this call to the tuner controller - } - - @Override - public String getUniqueID() - { - // TODO Auto-generated method stub - return null; - } -} diff --git a/src/source/tuner/ettus/B100TunerEditorPanel.java b/src/source/tuner/ettus/B100TunerEditorPanel.java deleted file mode 100644 index ed30ed68..00000000 --- a/src/source/tuner/ettus/B100TunerEditorPanel.java +++ /dev/null @@ -1,44 +0,0 @@ -/******************************************************************************* - * SDR Trunk - * Copyright (C) 2014 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 source.tuner.ettus; - -import javax.swing.BorderFactory; -import javax.swing.JLabel; -import javax.swing.JPanel; - -import net.miginfocom.swing.MigLayout; -import settings.SettingsManager; - -public class B100TunerEditorPanel extends JPanel -{ - private static final long serialVersionUID = 1L; - - public B100TunerEditorPanel( B100Tuner tuner, SettingsManager settingsManager ) - { - init(); - } - - private void init() - { - setLayout( new MigLayout( "", "[grow,fill]", "[grow,fill]" ) ); - - setBorder( BorderFactory.createTitledBorder( "Ettus B100 Tuner" ) ); - - add( new JLabel( "Note: SDRTrunk can only recognize the B100 at this time. It cannot use this tuner") ); - } -} diff --git a/src/source/tuner/fcd/FCDTuner.java b/src/source/tuner/fcd/FCDTuner.java index 9c32abfd..18c89fc6 100644 --- a/src/source/tuner/fcd/FCDTuner.java +++ b/src/source/tuner/fcd/FCDTuner.java @@ -17,35 +17,19 @@ ******************************************************************************/ package source.tuner.fcd; -import java.util.concurrent.RejectedExecutionException; - -import sample.Listener; -import sample.adapter.ShortAdapter; -import sample.complex.ComplexBuffer; -import source.SourceException; -import source.tuner.MixerTuner; -import source.tuner.MixerTunerDataLine; -import source.tuner.TunerChannel; -import source.tuner.TunerChannelSource; +import source.tuner.Tuner; import source.tuner.TunerClass; -import source.tuner.TunerEvent; import source.tuner.TunerType; -public class FCDTuner extends MixerTuner +public class FCDTuner extends Tuner { - public FCDTuner( MixerTunerDataLine mixerTDL, - FCDTunerController controller ) + public FCDTuner(FCDTunerController controller) { - super( controller.getConfiguration().toString(), - controller, - mixerTDL, - new ShortAdapter() ); + super( controller.getConfiguration().toString(), controller); } public void dispose() { - //TODO: release the mixer tuner data line as well - getController().dispose(); } @@ -77,36 +61,4 @@ public class FCDTuner extends MixerTuner { return 16.0; } - - @Override - public TunerChannelSource getChannel( TunerChannel tunerChannel ) - throws RejectedExecutionException, SourceException - { - TunerChannelSource source = getController().getChannel( this, tunerChannel ); - - if( source != null ) - { - broadcast( new TunerEvent( this, TunerEvent.Event.CHANNEL_COUNT ) ); - } - - return source; - } - - /** - * Releases the tuned channel so that the tuner controller can tune to - * other frequencies as needed. - */ - @Override - public void releaseChannel( TunerChannelSource source ) - { - if( source != null ) - { - /* Unregister for receiving samples */ - removeListener( (Listener)source ); - - /* Tell the controller to release the channel and cleanup */ - /* This will release the channel as a frequency change listener */ - getController().releaseChannel( source ); - } - } } diff --git a/src/source/tuner/fcd/FCDTunerController.java b/src/source/tuner/fcd/FCDTunerController.java index f8dae0e8..3b907683 100644 --- a/src/source/tuner/fcd/FCDTunerController.java +++ b/src/source/tuner/fcd/FCDTunerController.java @@ -25,13 +25,19 @@ import org.usb4java.DeviceDescriptor; import org.usb4java.DeviceHandle; import org.usb4java.LibUsb; import org.usb4java.LibUsbException; +import sample.Listener; +import sample.adapter.ShortAdapter; +import sample.complex.ComplexBuffer; import source.SourceException; +import source.mixer.ComplexMixer; +import source.tuner.MixerTunerDataLine; import source.tuner.TunerClass; import source.tuner.TunerController; import source.tuner.TunerType; import source.tuner.configuration.TunerConfiguration; import source.tuner.fcd.proV1.FCD1TunerController.Block; +import javax.sound.sampled.AudioFormat; import javax.usb.UsbClaimException; import javax.usb.UsbException; import java.nio.ByteBuffer; @@ -54,6 +60,8 @@ public abstract class FCDTunerController extends TunerController private DeviceHandle mDeviceHandle; private FCDConfiguration mConfiguration = new FCDConfiguration(); + protected ComplexMixer mComplexMixer; + /** * Generic FCD tuner controller - contains functionality common across both @@ -64,11 +72,8 @@ public abstract class FCDTunerController extends TunerController * @param minTunableFrequency * @param maxTunableFrequency */ - public FCDTunerController(Device device, - DeviceDescriptor descriptor, - int sampleRate, - int minTunableFrequency, - int maxTunableFrequency) + public FCDTunerController(MixerTunerDataLine mixerTDL, Device device, DeviceDescriptor descriptor, String tunerName, + int minTunableFrequency, int maxTunableFrequency, AudioFormat audioFormat) { super(minTunableFrequency, maxTunableFrequency, DC_SPIKE_AVOID_BUFFER, USABLE_BANDWIDTH_PERCENT); mDevice = device; @@ -76,11 +81,45 @@ public abstract class FCDTunerController extends TunerController try { - mFrequencyController.setSampleRate(sampleRate); + mFrequencyController.setSampleRate((int)audioFormat.getSampleRate()); } catch(SourceException se) { - mLog.error("Error setting sample rate to [" + sampleRate + "]", se); + mLog.error("Error setting sample rate to [" + audioFormat.getSampleRate() + "]", se); + } + + mComplexMixer = new ComplexMixer( mixerTDL.getTargetDataLine(), audioFormat, tunerName, new ShortAdapter(), + mSampleBroadcaster); + } + + /** + * Overrides the super class functionality to auto-start the complex mixer and provide samples to listeners + */ + @Override + public void addComplexBufferListener(Listener listener) + { + boolean hasExistingListeners = hasComplexBufferListeners(); + + super.addComplexBufferListener(listener); + + if(!hasExistingListeners) + { + mComplexMixer.start(); + } + } + + /** + * Overrides the super class functionality to auto-stop the complex mixer and stop sample stream when there are no + * more registered listeners + */ + @Override + public void removeComplexBufferListener(Listener listener) + { + super.removeComplexBufferListener(listener); + + if(!hasComplexBufferListeners()) + { + mComplexMixer.stop(); } } diff --git a/src/source/tuner/fcd/proV1/FCD1TunerController.java b/src/source/tuner/fcd/proV1/FCD1TunerController.java index 9df0590e..143f820f 100644 --- a/src/source/tuner/fcd/proV1/FCD1TunerController.java +++ b/src/source/tuner/fcd/proV1/FCD1TunerController.java @@ -1,34 +1,28 @@ /******************************************************************************* * SDR Trunk * Copyright (C) 2014 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 source.tuner.fcd.proV1; -import java.nio.ByteBuffer; -import java.nio.ByteOrder; - -import javax.usb.UsbClaimException; -import javax.usb.UsbException; - import org.slf4j.Logger; import org.slf4j.LoggerFactory; import org.usb4java.Device; import org.usb4java.DeviceDescriptor; - import source.SourceException; +import source.tuner.MixerTunerDataLine; import source.tuner.MixerTunerType; import source.tuner.TunerClass; import source.tuner.TunerType; @@ -36,377 +30,381 @@ import source.tuner.configuration.TunerConfiguration; import source.tuner.fcd.FCDCommand; import source.tuner.fcd.FCDTunerController; +import javax.usb.UsbClaimException; +import javax.usb.UsbException; +import java.nio.ByteBuffer; +import java.nio.ByteOrder; + public class FCD1TunerController extends FCDTunerController { - private final static Logger mLog = - LoggerFactory.getLogger( FCD1TunerController.class ); + private final static Logger mLog = LoggerFactory.getLogger(FCD1TunerController.class); - public static final int sMINIMUM_TUNABLE_FREQUENCY = 64000000; - public static final int sMAXIMUM_TUNABLE_FREQUENCY = 1700000000; - public static final int sSAMPLE_RATE = 96000; - - private double mDCCorrectionInPhase = 0.0; - private double mDCCorrectionQuadrature = 0.0; - private double mPhaseCorrection = 0.0; - private double mGainCorrection = 0.0; - private LNAGain mLNAGain; - private LNAEnhance mLNAEnhance; - private MixerGain mMixerGain; - private FCD1TunerConfiguration mTunerConfiguration; - private FCD1TunerEditor mEditor; + public static final int MINIMUM_TUNABLE_FREQUENCY = 64000000; + public static final int MAXIMUM_TUNABLE_FREQUENCY = 1700000000; + public static final int SAMPLE_RATE = 96000; - public FCD1TunerController( Device device, DeviceDescriptor descriptor ) - { - super( device, - descriptor, - (int)MixerTunerType.FUNCUBE_DONGLE_PRO.getAudioFormat().getSampleRate(), - sMINIMUM_TUNABLE_FREQUENCY, - sMAXIMUM_TUNABLE_FREQUENCY ); - } - - public void init() throws SourceException - { - super.init(); - - mFrequencyController.setSampleRate( sSAMPLE_RATE ); - - try - { - setFCDMode( Mode.APPLICATION ); - getPhaseAndGainCorrection(); - getDCIQCorrection(); - getLNAGainSetting(); - getLNAEnhanceSetting(); - getMixerGainSetting(); - - send( FCDCommand.APP_SET_MIXER_GAIN, 1l ); - } - catch( Exception e ) - { - e.printStackTrace(); - - throw new SourceException( "FCDTunerController error " + - "during construction", e ); - } - } - - public int getCurrentSampleRate() - { - return sSAMPLE_RATE; - } + private double mDCCorrectionInPhase = 0.0; + private double mDCCorrectionQuadrature = 0.0; + private double mPhaseCorrection = 0.0; + private double mGainCorrection = 0.0; + private LNAGain mLNAGain; + private LNAEnhance mLNAEnhance; + private MixerGain mMixerGain; + private FCD1TunerConfiguration mTunerConfiguration; + private FCD1TunerEditor mEditor; - @Override + public FCD1TunerController(MixerTunerDataLine mixerTDL, Device device, DeviceDescriptor descriptor) + { + super(mixerTDL, device, descriptor, MixerTunerType.FUNCUBE_DONGLE_PRO.getDisplayString(), MINIMUM_TUNABLE_FREQUENCY, + MAXIMUM_TUNABLE_FREQUENCY, MixerTunerType.FUNCUBE_DONGLE_PRO.getAudioFormat()); + } + + public void init() throws SourceException + { + super.init(); + + mFrequencyController.setSampleRate(SAMPLE_RATE); + + try + { + setFCDMode(Mode.APPLICATION); + getPhaseAndGainCorrection(); + getDCIQCorrection(); + getLNAGainSetting(); + getLNAEnhanceSetting(); + getMixerGainSetting(); + + send(FCDCommand.APP_SET_MIXER_GAIN, 1l); + } + catch(Exception e) + { + e.printStackTrace(); + + throw new SourceException("FCDTunerController error " + + "during construction", e); + } + } + + public int getCurrentSampleRate() + { + return SAMPLE_RATE; + } + + @Override public TunerClass getTunerClass() { - return TunerClass.FUNCUBE_DONGLE_PRO; + return TunerClass.FUNCUBE_DONGLE_PRO; } - @Override + @Override public TunerType getTunerType() { - return TunerType.FUNCUBE_DONGLE_PRO; + return TunerType.FUNCUBE_DONGLE_PRO; } - /** - * Get the tuner configuration - * - * @return current config or null if one hasn't been applied yet - */ - public FCD1TunerConfiguration getTunerConfiguration() - { - return mTunerConfiguration; - } - - @Override - public void apply( TunerConfiguration tunerConfig ) - throws SourceException + /** + * Get the tuner configuration + * + * @return current config or null if one hasn't been applied yet + */ + public FCD1TunerConfiguration getTunerConfiguration() { - FCD1TunerConfiguration config = (FCD1TunerConfiguration)tunerConfig; - - try - { - setFrequencyCorrection( config.getFrequencyCorrection() ); - setLNAGain( config.getLNAGain() ); - setLNAEnhance( config.getLNAEnhance() ); - setMixerGain( config.getMixerGain() ); - setDCCorrectionInPhase( config.getInphaseDCCorrection() ); - setDCCorrectionQuadrature( config.getQuadratureDCCorrection() ); - setGainCorrection( config.getGainCorrection() ); - setPhaseCorrection( config.getPhaseCorrection() ); - - //If we get to here without exception, store this config - mTunerConfiguration = config; - } - catch( Exception e) - { - //Re-cast any usb exceptions as source exceptions - throw new SourceException( "FCD 1 Controller encountered an " - + "exception while applying tuner config - " + - e.getLocalizedMessage() ); - } - - try - { - setFrequency( config.getFrequency() ); - } - catch( SourceException se ) - { - //Do nothing, we couldn't set the frequency - } - } + return mTunerConfiguration; + } - /** - * Gets the current LNA Gain value from the controller and stores it - */ - private void getLNAGainSetting() throws UsbClaimException, UsbException - { - ByteBuffer buffer = send( FCDCommand.APP_GET_LNA_GAIN ); - - buffer.order( ByteOrder.LITTLE_ENDIAN ); - - int gain = buffer.getInt( 2 ); + @Override + public void apply(TunerConfiguration tunerConfig) + throws SourceException + { + FCD1TunerConfiguration config = (FCD1TunerConfiguration)tunerConfig; - mLNAGain = LNAGain.valueOf( gain ); - } - - /** - * @return - current lna gain setting - */ - public LNAGain getLNAGain() - { - return mLNAGain; - } + try + { + setFrequencyCorrection(config.getFrequencyCorrection()); + setLNAGain(config.getLNAGain()); + setLNAEnhance(config.getLNAEnhance()); + setMixerGain(config.getMixerGain()); + setDCCorrectionInPhase(config.getInphaseDCCorrection()); + setDCCorrectionQuadrature(config.getQuadratureDCCorrection()); + setGainCorrection(config.getGainCorrection()); + setPhaseCorrection(config.getPhaseCorrection()); - /** - * Sets lna gain for the controller - * @param gain - * @throws UsbException - * @throws UsbClaimException - */ - public void setLNAGain( LNAGain gain ) - throws UsbException, UsbClaimException - { - send( FCDCommand.APP_SET_LNA_GAIN, gain.getSetting() ); - } - - /** - * @return - current phase correction setting - */ - public double getPhaseCorrection() - { - return mPhaseCorrection; - } + //If we get to here without exception, store this config + mTunerConfiguration = config; + } + catch(Exception e) + { + //Re-cast any usb exceptions as source exceptions + throw new SourceException("FCD 1 Controller encountered an " + + "exception while applying tuner config - " + + e.getLocalizedMessage()); + } - /** - * Sets the phase correction - * @param value - new phase correction - * @throws IllegalArgumentException for value outside limits (0.0 <> 1.0) - * @throws UsbClaimException - if cannot claim the FCD HID controller - * @throws UsbException - if there was a usb error - */ - public void setPhaseCorrection( double value ) - throws UsbClaimException, UsbException - { - if( value < -1.0 || value > 1.0 ) - { - throw new IllegalArgumentException( "Phase correction value [" + - value + "] outside limits ( -1.0 <> 1.0 )" ); - } - - mPhaseCorrection = value; - setPhaseAndGainCorrection(); - } + try + { + setFrequency(config.getFrequency()); + } + catch(SourceException se) + { + //Do nothing, we couldn't set the frequency + } + } - /** - * @return current gain correction setting - */ - public double getGainCorrection() - { - return mGainCorrection; - } + /** + * Gets the current LNA Gain value from the controller and stores it + */ + private void getLNAGainSetting() throws UsbClaimException, UsbException + { + ByteBuffer buffer = send(FCDCommand.APP_GET_LNA_GAIN); - /** - * - * @param value - gain correction value - * @throws IllegalArgumentException for value outside limits (0.0 <> 1.0) - * @throws UsbClaimException - if cannot claim the FCD HID controller - * @throws UsbException - if there was a usb error - */ - public void setGainCorrection( double value ) - throws UsbClaimException, UsbException - { - if( value < -1.0 || value > 1.0 ) - { - throw new IllegalArgumentException( "Gain correction value [" + - value + "] outside limits ( -1.0 <> 1.0 )" ); - } - - mGainCorrection = value; - setPhaseAndGainCorrection(); - } - - /** - * Sends the phase and gain correction values to FCD. - * - * The interface expects an argument with the SIGNED phase value in the first - * 16 bits and the UNSIGNED gain value in the second 16 bits, both in - * big-endian format. Since we're sending the argument in java little endian - * format, we place the phase in the high-order bits and the gain in the - * low-order bits, so that the send(command) function will reorder the bytes - * into big endian format, and place the arguments correctly. - * - * Gain correction values range -1.0 to 0.0 to 1.0 and applied to the dongle - * as values 0 to 65534 - * - * Phase correction values range -1.0 to 0.0 to 1.0 and applied to the - * dongle as signed values -32768 to 32767 - */ - private void setPhaseAndGainCorrection() throws UsbClaimException, - UsbException - { - //UNSIGNED short gain value - masked into a long to avoid sign-extension - long gain = (long)( ( 1.0 + mGainCorrection ) * Short.MAX_VALUE ); - - //Left shift gain to place value in upper 16 bits - long longGain = Long.rotateLeft( ( 0xFFFF & gain ), 16 ); + buffer.order(ByteOrder.LITTLE_ENDIAN); - //SIGNED short phase value - short phase = (short)( mPhaseCorrection * Short.MAX_VALUE ); + int gain = buffer.getInt(2); - //Merge the results - long correction = longGain | phase; + mLNAGain = LNAGain.valueOf(gain); + } - send( FCDCommand.APP_SET_IQ_CORRECTION, correction ); - } + /** + * @return - current lna gain setting + */ + public LNAGain getLNAGain() + { + return mLNAGain; + } - /** - * Retrieves the stored phase and gain correction values from the FCD. - * - * Note: testing shows that when the dongle is unplugged, any stored phase - * and gain correction values are reset. Reading the dongle after a fresh - * plugin shows the values reset. - * - * @throws UsbClaimException - * @throws UsbException - */ - private void getPhaseAndGainCorrection() throws UsbClaimException, - UsbException - { - ByteBuffer buffer = send( FCDCommand.APP_GET_IQ_CORRECTION ); - - buffer.order( ByteOrder.LITTLE_ENDIAN ); - - int correction = buffer.getInt( 2 ); - - /** - * Gain: mask the upper 16 phase bits and right shift to get the - * unsigned short value and then divide by 32768 to get the double - * value. We place the unsigned short value in an int to avoid sign - * issues. - */ - int gain = (int)( Long.rotateRight( correction & 0xFFFF0000, 16 ) ); + /** + * Sets lna gain for the controller + * + * @param gain + * @throws UsbException + * @throws UsbClaimException + */ + public void setLNAGain(LNAGain gain) + throws UsbException, UsbClaimException + { + send(FCDCommand.APP_SET_LNA_GAIN, gain.getSetting()); + } - mGainCorrection = ( (double)gain / (double)Short.MAX_VALUE ) - 1.0; + /** + * @return - current phase correction setting + */ + public double getPhaseCorrection() + { + return mPhaseCorrection; + } - /** - * Phase: mask the lower 16 bits and divide by 32768 to get the double - * value - */ - short phase = (short)( correction & 0x0000FFFF ); - - mPhaseCorrection = ( (double)phase / (double)Short.MAX_VALUE ); - } - - /** - * @return current DC correction for the I (inphase) component - */ - public double getDCCorrectionInPhase() - { - return mDCCorrectionInPhase; - } + /** + * Sets the phase correction + * + * @param value - new phase correction + * @throws IllegalArgumentException for value outside limits (0.0 <> 1.0) + * @throws UsbClaimException - if cannot claim the FCD HID controller + * @throws UsbException - if there was a usb error + */ + public void setPhaseCorrection(double value) + throws UsbClaimException, UsbException + { + if(value < -1.0 || value > 1.0) + { + throw new IllegalArgumentException("Phase correction value [" + + value + "] outside limits ( -1.0 <> 1.0 )"); + } - /** - * Sets DC correction for the I (inphase) component and sends the new value - * to the FCD controller - * @param value - new DC correction value for the inphase component - * @throws IllegalArgumentException for values outside limits (-1.0 <> 1.0) - * @throws UsbClaimException - if cannot claim the FCD HID controller - * @throws UsbException - if there was a usb error - */ - public void setDCCorrectionInPhase( double value ) - throws UsbClaimException, UsbException - { - if( value < -1.0 || value > 1.0 ) - { - throw new IllegalArgumentException( "DC inphase correction value " - + "[" + value + "] outside limits ( 0.0 <> 1.0 )" ); - } - - mDCCorrectionInPhase = value; - setDCIQCorrection(); - } + mPhaseCorrection = value; + setPhaseAndGainCorrection(); + } - /** - * @return current DC correction for the Q (quadrature) component - */ - public double getDCCorrectionQuadrature() - { - return mDCCorrectionQuadrature; - } + /** + * @return current gain correction setting + */ + public double getGainCorrection() + { + return mGainCorrection; + } - /** - * Sets the DC correction for the Q (quadrature) component and send the - * new value to the FCD controller - * @param value - new DC correction value for the quadrature component - * @throws IllegalArgumentException for values outside limits (-1.0 <> 1.0) - * @throws UsbClaimException - if cannot claim the FCD HID controller - * @throws UsbException - if there was a usb error - */ - public void setDCCorrectionQuadrature( double value ) - throws UsbClaimException, UsbException - { - if( value < -1.0 || value > 1.0 ) - { - throw new IllegalArgumentException( "DC quadrature correction " - + "value [" + value + "] outside limits ( 0.0 <> 1.0 )" ); - } - - mDCCorrectionQuadrature = value; - setDCIQCorrection(); - } + /** + * @param value - gain correction value + * @throws IllegalArgumentException for value outside limits (0.0 <> 1.0) + * @throws UsbClaimException - if cannot claim the FCD HID controller + * @throws UsbException - if there was a usb error + */ + public void setGainCorrection(double value) + throws UsbClaimException, UsbException + { + if(value < -1.0 || value > 1.0) + { + throw new IllegalArgumentException("Gain correction value [" + + value + "] outside limits ( -1.0 <> 1.0 )"); + } - /** - * Sends the I/Q DC offset correction values to FCD. - * - * The interface expects an argument with the signed inphase value in the - * first 16 bits and the signed quadrature value in the second 16 bits, - * both in big-endian format. - * - * Since we're sending the argument in java little endian format, we place - * the quadrature in the high-order bits and the inphase in the low-order - * bits, so that the send(command) function will reorder the bytes into big - * endian format, and place the arguments correctly. - * - * Both Inphase and Quadrature correction values range -1.0 to 0.0 to 1.0 - * and are applied to the dongle as signed short values -32768 to 0 to 32767 - */ - private void setDCIQCorrection() throws UsbClaimException, UsbException - { - //I & Q DC offset correction values are signed short values - short inphase = (short)( mDCCorrectionInPhase * Short.MAX_VALUE ); - short quadrature = (short)( mDCCorrectionQuadrature * Short.MAX_VALUE ); + mGainCorrection = value; + setPhaseAndGainCorrection(); + } + + /** + * Sends the phase and gain correction values to FCD. + * + * The interface expects an argument with the SIGNED phase value in the first + * 16 bits and the UNSIGNED gain value in the second 16 bits, both in + * big-endian format. Since we're sending the argument in java little endian + * format, we place the phase in the high-order bits and the gain in the + * low-order bits, so that the send(command) function will reorder the bytes + * into big endian format, and place the arguments correctly. + * + * Gain correction values range -1.0 to 0.0 to 1.0 and applied to the dongle + * as values 0 to 65534 + * + * Phase correction values range -1.0 to 0.0 to 1.0 and applied to the + * dongle as signed values -32768 to 32767 + */ + private void setPhaseAndGainCorrection() throws UsbClaimException, + UsbException + { + //UNSIGNED short gain value - masked into a long to avoid sign-extension + long gain = (long)((1.0 + mGainCorrection) * Short.MAX_VALUE); + + //Left shift gain to place value in upper 16 bits + long longGain = Long.rotateLeft((0xFFFF & gain), 16); + + //SIGNED short phase value + short phase = (short)(mPhaseCorrection * Short.MAX_VALUE); + + //Merge the results + long correction = longGain | phase; + + send(FCDCommand.APP_SET_IQ_CORRECTION, correction); + } + + /** + * Retrieves the stored phase and gain correction values from the FCD. + * + * Note: testing shows that when the dongle is unplugged, any stored phase + * and gain correction values are reset. Reading the dongle after a fresh + * plugin shows the values reset. + * + * @throws UsbClaimException + * @throws UsbException + */ + private void getPhaseAndGainCorrection() throws UsbClaimException, + UsbException + { + ByteBuffer buffer = send(FCDCommand.APP_GET_IQ_CORRECTION); + + buffer.order(ByteOrder.LITTLE_ENDIAN); + + int correction = buffer.getInt(2); + + /** + * Gain: mask the upper 16 phase bits and right shift to get the + * unsigned short value and then divide by 32768 to get the double + * value. We place the unsigned short value in an int to avoid sign + * issues. + */ + int gain = (int)(Long.rotateRight(correction & 0xFFFF0000, 16)); + + mGainCorrection = ((double)gain / (double)Short.MAX_VALUE) - 1.0; + + /** + * Phase: mask the lower 16 bits and divide by 32768 to get the double + * value + */ + short phase = (short)(correction & 0x0000FFFF); + + mPhaseCorrection = ((double)phase / (double)Short.MAX_VALUE); + } + + /** + * @return current DC correction for the I (inphase) component + */ + public double getDCCorrectionInPhase() + { + return mDCCorrectionInPhase; + } + + /** + * Sets DC correction for the I (inphase) component and sends the new value + * to the FCD controller + * + * @param value - new DC correction value for the inphase component + * @throws IllegalArgumentException for values outside limits (-1.0 <> 1.0) + * @throws UsbClaimException - if cannot claim the FCD HID controller + * @throws UsbException - if there was a usb error + */ + public void setDCCorrectionInPhase(double value) + throws UsbClaimException, UsbException + { + if(value < -1.0 || value > 1.0) + { + throw new IllegalArgumentException("DC inphase correction value " + + "[" + value + "] outside limits ( 0.0 <> 1.0 )"); + } + + mDCCorrectionInPhase = value; + setDCIQCorrection(); + } + + /** + * @return current DC correction for the Q (quadrature) component + */ + public double getDCCorrectionQuadrature() + { + return mDCCorrectionQuadrature; + } + + /** + * Sets the DC correction for the Q (quadrature) component and send the + * new value to the FCD controller + * + * @param value - new DC correction value for the quadrature component + * @throws IllegalArgumentException for values outside limits (-1.0 <> 1.0) + * @throws UsbClaimException - if cannot claim the FCD HID controller + * @throws UsbException - if there was a usb error + */ + public void setDCCorrectionQuadrature(double value) + throws UsbClaimException, UsbException + { + if(value < -1.0 || value > 1.0) + { + throw new IllegalArgumentException("DC quadrature correction " + + "value [" + value + "] outside limits ( 0.0 <> 1.0 )"); + } + + mDCCorrectionQuadrature = value; + setDCIQCorrection(); + } + + /** + * Sends the I/Q DC offset correction values to FCD. + * + * The interface expects an argument with the signed inphase value in the + * first 16 bits and the signed quadrature value in the second 16 bits, + * both in big-endian format. + * + * Since we're sending the argument in java little endian format, we place + * the quadrature in the high-order bits and the inphase in the low-order + * bits, so that the send(command) function will reorder the bytes into big + * endian format, and place the arguments correctly. + * + * Both Inphase and Quadrature correction values range -1.0 to 0.0 to 1.0 + * and are applied to the dongle as signed short values -32768 to 0 to 32767 + */ + private void setDCIQCorrection() throws UsbClaimException, UsbException + { + //I & Q DC offset correction values are signed short values + short inphase = (short)(mDCCorrectionInPhase * Short.MAX_VALUE); + short quadrature = (short)(mDCCorrectionQuadrature * Short.MAX_VALUE); + + //Mask the shorts into longs to preserve the sign bit and prepare + //for merging to a single 32-bit value + long maskedInphase = inphase & 0x0000FFFF; + long maskedQuadrature = quadrature & 0x0000FFFF; + + //Left shift quadrature to place value in upper 16 bits + long shiftedQuadrature = Long.rotateLeft(maskedQuadrature, 16); + + //Merge the results + long correction = shiftedQuadrature | maskedInphase; - //Mask the shorts into longs to preserve the sign bit and prepare - //for merging to a single 32-bit value - long maskedInphase = inphase & 0x0000FFFF; - long maskedQuadrature = quadrature & 0x0000FFFF; - - //Left shift quadrature to place value in upper 16 bits - long shiftedQuadrature = Long.rotateLeft( maskedQuadrature, 16 ); - - //Merge the results - long correction = shiftedQuadrature | maskedInphase; - // Log.info( "FCD1: setting iq correction," + // " inphase:" + inphase + // " masked inphase:" + maskedInphase + @@ -415,305 +413,305 @@ public class FCD1TunerController extends FCDTunerController // " shifted quadrature:" + shiftedQuadrature + // " correction:" + correction ); - send( FCDCommand.APP_SET_DC_CORRECTION, correction ); - } - - private void getDCIQCorrection() throws UsbClaimException, - UsbException - { - ByteBuffer buffer = send( FCDCommand.APP_GET_DC_CORRECTION ); - - buffer.order( ByteOrder.LITTLE_ENDIAN ); - - int correction = buffer.getInt( 2 ); + send(FCDCommand.APP_SET_DC_CORRECTION, correction); + } - /** - * Quadrature: mask the upper 16 bits and divide by 32768 to get the - * stored quadrature value. Cast the 16-bit value to a short to get - * the signed short value - */ - long shiftedQuadrature = correction & 0xFFFF0000; - long quadrature = Long.rotateRight( shiftedQuadrature, 16 ); + private void getDCIQCorrection() throws UsbClaimException, + UsbException + { + ByteBuffer buffer = send(FCDCommand.APP_GET_DC_CORRECTION); - mDCCorrectionQuadrature = ( (short)quadrature / (double)Short.MAX_VALUE ); + buffer.order(ByteOrder.LITTLE_ENDIAN); + + int correction = buffer.getInt(2); + + /** + * Quadrature: mask the upper 16 bits and divide by 32768 to get the + * stored quadrature value. Cast the 16-bit value to a short to get + * the signed short value + */ + long shiftedQuadrature = correction & 0xFFFF0000; + long quadrature = Long.rotateRight(shiftedQuadrature, 16); + + mDCCorrectionQuadrature = ((short)quadrature / (double)Short.MAX_VALUE); + + /** + * InPhase: mask the lower 16 bits to get the short + * value and then divide by 32768 to get the stored inphase value. + * Cast the 16-bit value to a short to get the signed short value + */ + long inphase = correction & 0x0000FFFF; + mDCCorrectionInPhase = ((short)inphase / (double)Short.MAX_VALUE); - /** - * InPhase: mask the lower 16 bits to get the short - * value and then divide by 32768 to get the stored inphase value. - * Cast the 16-bit value to a short to get the signed short value - */ - long inphase = correction & 0x0000FFFF; - mDCCorrectionInPhase = ( (short)inphase / (double)Short.MAX_VALUE ); - // Log.info( "FCD1:" + // " correction:" + correction + // " Q:" + quadrature + // " I:" + inphase + // " QCorr:" + mDCCorrectionQuadrature + // " ICorr:" + mDCCorrectionInPhase ); - } - - private void getLNAEnhanceSetting() throws UsbClaimException, UsbException - { - ByteBuffer buffer = send( FCDCommand.APP_GET_LNA_ENHANCE ); - - buffer.order( ByteOrder.LITTLE_ENDIAN ); - - int enhance = buffer.getInt( 2 ); - - mLNAEnhance = LNAEnhance.valueOf( enhance ); - } - - public LNAEnhance getLNAEnhance() - { - return mLNAEnhance; - } - - public void setLNAEnhance( LNAEnhance enhance ) - throws UsbClaimException, UsbException - { - send( FCDCommand.APP_SET_LNA_ENHANCE, enhance.getSetting() ); - } - - private void getMixerGainSetting() throws UsbClaimException, UsbException - { - ByteBuffer buffer = send( FCDCommand.APP_GET_MIXER_GAIN ); - - buffer.order( ByteOrder.LITTLE_ENDIAN ); - - int gain = buffer.getInt( 2 ); - - mMixerGain = MixerGain.valueOf( gain ); - } - - public MixerGain getMixerGain() - { - return mMixerGain; - } - - public void setMixerGain( MixerGain gain ) throws UsbClaimException, UsbException - { - send( FCDCommand.APP_SET_MIXER_GAIN, gain.getSetting() ); - } - - public enum Block - { - CELLULAR_BAND_BLOCKED( "Blocked" ), - NO_BAND_BLOCK( "Unblocked" ), - UNKNOWN( "Unknown" ); - - private String mLabel; - - private Block( String label ) - { - mLabel = label; - } - - public String getLabel() - { - return mLabel; - } - - public static Block getBlock( String block ) - { - Block retVal = UNKNOWN; - - if( block.equalsIgnoreCase( "No blk" ) ) - { - retVal = NO_BAND_BLOCK; - } - else if( block.equalsIgnoreCase( "Cell blk" ) ) - { - retVal = CELLULAR_BAND_BLOCKED; - } - - return retVal; - } - } - - /** - * LNA Gain values suppported by the FCD Pro 1.0 - */ - public enum LNAGain - { - LNA_GAIN_MINUS_5_0( 0, "-5.0 dB" ), - LNA_GAIN_MINUS_2_5( 1, "-2.5 dB" ), - LNA_GAIN_PLUS_0_0( 4, "0.0 dB" ), - LNA_GAIN_PLUS_2_5( 5, "2.5 dB" ), - LNA_GAIN_PLUS_5_0( 6, "5.0 dB" ), - LNA_GAIN_PLUS_7_5( 7, "7.5 dB" ), - LNA_GAIN_PLUS_10_0( 8, "10.0 dB" ), - LNA_GAIN_PLUS_12_5( 9, "12.5 dB" ), - LNA_GAIN_PLUS_15_0( 10, "15.0 dB" ), - LNA_GAIN_PLUS_17_5( 11, "17.5 dB" ), - LNA_GAIN_PLUS_20_0( 12, "20.0 dB" ), - LNA_GAIN_PLUS_25_0( 13, "25.0 dB" ), - LNA_GAIN_PLUS_30_0( 14, "30.0 dB" ); - - private int mSetting; - private String mLabel; - - private LNAGain( int setting, String label ) - { - mSetting = setting; - mLabel = label; - } - - public static LNAGain valueOf( int setting ) - { - switch( setting ) - { - case 0: - return LNA_GAIN_MINUS_5_0; - case 1: - return LNA_GAIN_MINUS_2_5; - case 4: - return LNA_GAIN_PLUS_0_0; - case 5: - return LNA_GAIN_PLUS_2_5; - case 6: - return LNA_GAIN_PLUS_5_0; - case 7: - return LNA_GAIN_PLUS_7_5; - case 8: - return LNA_GAIN_PLUS_10_0; - case 9: - return LNA_GAIN_PLUS_12_5; - case 10: - return LNA_GAIN_PLUS_15_0; - case 11: - return LNA_GAIN_PLUS_17_5; - case 12: - return LNA_GAIN_PLUS_20_0; - case 13: - return LNA_GAIN_PLUS_25_0; - case 14: - return LNA_GAIN_PLUS_30_0; - default: - throw new IllegalArgumentException( "FCD 1.0 Tuner " - + "Controller - unrecognized LNA gain setting " - + "value [" + setting + "]" ); - } - } - - public int getSetting() - { - return mSetting; - } - - public String getLabel() - { - return mLabel; - } - - public String toString() - { - return getLabel(); - } } - /** - * LNA Enhance values suppported by the FCD Pro 1.0 - */ - public enum LNAEnhance + private void getLNAEnhanceSetting() throws UsbClaimException, UsbException { - LNA_ENHANCE_OFF( 0, "Off" ), - LNA_ENHANCE_0( 1, "0" ), - LNA_ENHANCE_1( 3, "1" ), - LNA_ENHANCE_2( 5, "2" ), - LNA_ENHANCE_3( 7, "3" ); - - private int mSetting; - private String mLabel; - - private LNAEnhance( int setting, String label ) - { - mSetting = setting; - mLabel = label; - } - - public static LNAEnhance valueOf( int setting ) - { - switch( setting ) - { - case 0: - return LNA_ENHANCE_OFF; - case 1: - return LNA_ENHANCE_0; - case 3: - return LNA_ENHANCE_1; - case 5: - return LNA_ENHANCE_2; - case 7: - return LNA_ENHANCE_3; - default: - throw new IllegalArgumentException( "FCD 1.0 Tuner " - + "Controller - unrecognized LNA enhance setting " - + "value [" + setting + "]" ); - } - } - - public int getSetting() - { - return mSetting; - } - - public String getLabel() - { - return mLabel; - } - - public String toString() - { - return getLabel(); - } + ByteBuffer buffer = send(FCDCommand.APP_GET_LNA_ENHANCE); + + buffer.order(ByteOrder.LITTLE_ENDIAN); + + int enhance = buffer.getInt(2); + + mLNAEnhance = LNAEnhance.valueOf(enhance); } - /** - * Mixer Gain values suppported by the FCD Pro 1.0 - */ - public enum MixerGain + public LNAEnhance getLNAEnhance() { - MIXER_GAIN_PLUS_4_0( 0, "4.0 dB" ), - MIXER_GAIN_PLUS_12_0( 1, "12.0 dB" ); - - private int mSetting; - private String mLabel; - - private MixerGain( int setting, String label ) - { - mSetting = setting; - mLabel = label; - } - - public static MixerGain valueOf( int setting ) - { - switch( setting ) - { - case 0: - return MIXER_GAIN_PLUS_4_0; - case 1: - return MIXER_GAIN_PLUS_12_0; - default: - throw new IllegalArgumentException( "FCD 1.0 Tuner " - + "Controller - unrecognized mixer gain setting " - + "value [" + setting + "]" ); - } - } - - public int getSetting() - { - return mSetting; - } - - public String getLabel() - { - return mLabel; - } - - public String toString() - { - return getLabel(); - } + return mLNAEnhance; + } + + public void setLNAEnhance(LNAEnhance enhance) + throws UsbClaimException, UsbException + { + send(FCDCommand.APP_SET_LNA_ENHANCE, enhance.getSetting()); + } + + private void getMixerGainSetting() throws UsbClaimException, UsbException + { + ByteBuffer buffer = send(FCDCommand.APP_GET_MIXER_GAIN); + + buffer.order(ByteOrder.LITTLE_ENDIAN); + + int gain = buffer.getInt(2); + + mMixerGain = MixerGain.valueOf(gain); + } + + public MixerGain getMixerGain() + { + return mMixerGain; + } + + public void setMixerGain(MixerGain gain) throws UsbClaimException, UsbException + { + send(FCDCommand.APP_SET_MIXER_GAIN, gain.getSetting()); + } + + public enum Block + { + CELLULAR_BAND_BLOCKED("Blocked"), + NO_BAND_BLOCK("Unblocked"), + UNKNOWN("Unknown"); + + private String mLabel; + + private Block(String label) + { + mLabel = label; + } + + public String getLabel() + { + return mLabel; + } + + public static Block getBlock(String block) + { + Block retVal = UNKNOWN; + + if(block.equalsIgnoreCase("No blk")) + { + retVal = NO_BAND_BLOCK; + } + else if(block.equalsIgnoreCase("Cell blk")) + { + retVal = CELLULAR_BAND_BLOCKED; + } + + return retVal; + } + } + + /** + * LNA Gain values suppported by the FCD Pro 1.0 + */ + public enum LNAGain + { + LNA_GAIN_MINUS_5_0(0, "-5.0 dB"), + LNA_GAIN_MINUS_2_5(1, "-2.5 dB"), + LNA_GAIN_PLUS_0_0(4, "0.0 dB"), + LNA_GAIN_PLUS_2_5(5, "2.5 dB"), + LNA_GAIN_PLUS_5_0(6, "5.0 dB"), + LNA_GAIN_PLUS_7_5(7, "7.5 dB"), + LNA_GAIN_PLUS_10_0(8, "10.0 dB"), + LNA_GAIN_PLUS_12_5(9, "12.5 dB"), + LNA_GAIN_PLUS_15_0(10, "15.0 dB"), + LNA_GAIN_PLUS_17_5(11, "17.5 dB"), + LNA_GAIN_PLUS_20_0(12, "20.0 dB"), + LNA_GAIN_PLUS_25_0(13, "25.0 dB"), + LNA_GAIN_PLUS_30_0(14, "30.0 dB"); + + private int mSetting; + private String mLabel; + + private LNAGain(int setting, String label) + { + mSetting = setting; + mLabel = label; + } + + public static LNAGain valueOf(int setting) + { + switch(setting) + { + case 0: + return LNA_GAIN_MINUS_5_0; + case 1: + return LNA_GAIN_MINUS_2_5; + case 4: + return LNA_GAIN_PLUS_0_0; + case 5: + return LNA_GAIN_PLUS_2_5; + case 6: + return LNA_GAIN_PLUS_5_0; + case 7: + return LNA_GAIN_PLUS_7_5; + case 8: + return LNA_GAIN_PLUS_10_0; + case 9: + return LNA_GAIN_PLUS_12_5; + case 10: + return LNA_GAIN_PLUS_15_0; + case 11: + return LNA_GAIN_PLUS_17_5; + case 12: + return LNA_GAIN_PLUS_20_0; + case 13: + return LNA_GAIN_PLUS_25_0; + case 14: + return LNA_GAIN_PLUS_30_0; + default: + throw new IllegalArgumentException("FCD 1.0 Tuner " + + "Controller - unrecognized LNA gain setting " + + "value [" + setting + "]"); + } + } + + public int getSetting() + { + return mSetting; + } + + public String getLabel() + { + return mLabel; + } + + public String toString() + { + return getLabel(); + } + } + + /** + * LNA Enhance values suppported by the FCD Pro 1.0 + */ + public enum LNAEnhance + { + LNA_ENHANCE_OFF(0, "Off"), + LNA_ENHANCE_0(1, "0"), + LNA_ENHANCE_1(3, "1"), + LNA_ENHANCE_2(5, "2"), + LNA_ENHANCE_3(7, "3"); + + private int mSetting; + private String mLabel; + + private LNAEnhance(int setting, String label) + { + mSetting = setting; + mLabel = label; + } + + public static LNAEnhance valueOf(int setting) + { + switch(setting) + { + case 0: + return LNA_ENHANCE_OFF; + case 1: + return LNA_ENHANCE_0; + case 3: + return LNA_ENHANCE_1; + case 5: + return LNA_ENHANCE_2; + case 7: + return LNA_ENHANCE_3; + default: + throw new IllegalArgumentException("FCD 1.0 Tuner " + + "Controller - unrecognized LNA enhance setting " + + "value [" + setting + "]"); + } + } + + public int getSetting() + { + return mSetting; + } + + public String getLabel() + { + return mLabel; + } + + public String toString() + { + return getLabel(); + } + } + + /** + * Mixer Gain values suppported by the FCD Pro 1.0 + */ + public enum MixerGain + { + MIXER_GAIN_PLUS_4_0(0, "4.0 dB"), + MIXER_GAIN_PLUS_12_0(1, "12.0 dB"); + + private int mSetting; + private String mLabel; + + private MixerGain(int setting, String label) + { + mSetting = setting; + mLabel = label; + } + + public static MixerGain valueOf(int setting) + { + switch(setting) + { + case 0: + return MIXER_GAIN_PLUS_4_0; + case 1: + return MIXER_GAIN_PLUS_12_0; + default: + throw new IllegalArgumentException("FCD 1.0 Tuner " + + "Controller - unrecognized mixer gain setting " + + "value [" + setting + "]"); + } + } + + public int getSetting() + { + return mSetting; + } + + public String getLabel() + { + return mLabel; + } + + public String toString() + { + return getLabel(); + } } } diff --git a/src/source/tuner/fcd/proplusV2/FCD2TunerController.java b/src/source/tuner/fcd/proplusV2/FCD2TunerController.java index f2173ceb..f6c39834 100644 --- a/src/source/tuner/fcd/proplusV2/FCD2TunerController.java +++ b/src/source/tuner/fcd/proplusV2/FCD2TunerController.java @@ -23,6 +23,7 @@ import org.slf4j.LoggerFactory; import org.usb4java.Device; import org.usb4java.DeviceDescriptor; import source.SourceException; +import source.tuner.MixerTunerDataLine; import source.tuner.MixerTunerType; import source.tuner.TunerClass; import source.tuner.TunerType; @@ -41,11 +42,10 @@ public class FCD2TunerController extends FCDTunerController public static final int MAXIMUM_TUNABLE_FREQUENCY = 2050000000; public static final int SAMPLE_RATE = 192000; - public FCD2TunerController(Device device, DeviceDescriptor descriptor) + public FCD2TunerController(MixerTunerDataLine mixerTDL, Device device, DeviceDescriptor descriptor) { - super(device, descriptor, - (int)MixerTunerType.FUNCUBE_DONGLE_PRO_PLUS.getAudioFormat().getSampleRate(), - MINIMUM_TUNABLE_FREQUENCY, MAXIMUM_TUNABLE_FREQUENCY); + super(mixerTDL, device, descriptor, MixerTunerType.FUNCUBE_DONGLE_PRO_PLUS.getDisplayString(), + MINIMUM_TUNABLE_FREQUENCY, MAXIMUM_TUNABLE_FREQUENCY, MixerTunerType.FUNCUBE_DONGLE_PRO.getAudioFormat()); } public void init() throws SourceException diff --git a/src/source/tuner/frequency/DCTrackingSourceControl.java b/src/source/tuner/frequency/DCTrackingSourceControl.java index 09b48831..de042699 100644 --- a/src/source/tuner/frequency/DCTrackingSourceControl.java +++ b/src/source/tuner/frequency/DCTrackingSourceControl.java @@ -113,7 +113,7 @@ public class DCTrackingSourceControl extends Module } @Override - public void start(ScheduledExecutorService executor) + public void start() { } diff --git a/src/source/tuner/frequency/FrequencyController.java b/src/source/tuner/frequency/FrequencyController.java index 7403ad64..86723b10 100644 --- a/src/source/tuner/frequency/FrequencyController.java +++ b/src/source/tuner/frequency/FrequencyController.java @@ -95,6 +95,16 @@ public class FrequencyController setFrequency(frequency, true); } + /** + * Indicates if the specified frequency can be tuned (ie is within min/max frequency) by this controller. + * @param frequency to evaluate + * @return true if the frequency falls within the tuning range of this controller + */ + public boolean canTune(long frequency) + { + return getMinimumFrequency() <= frequency && frequency <= getMaximumFrequency(); + } + /** * Set frequency with optional broadcast of frequency change event. This * method supports changing the frequency correction value without @@ -257,5 +267,10 @@ public class FrequencyController * Gets the current bandwidth setting of the device */ public int getCurrentSampleRate() throws SourceException; + + /** + * Indicates if this tunable can tune the frequency + */ + public boolean canTune(long frequency); } } diff --git a/src/source/tuner/hackrf/HackRFTuner.java b/src/source/tuner/hackrf/HackRFTuner.java index 2194a7de..fa0ff210 100644 --- a/src/source/tuner/hackrf/HackRFTuner.java +++ b/src/source/tuner/hackrf/HackRFTuner.java @@ -17,19 +17,12 @@ ******************************************************************************/ package source.tuner.hackrf; -import java.util.concurrent.RejectedExecutionException; - import org.slf4j.Logger; import org.slf4j.LoggerFactory; -import sample.Listener; -import sample.complex.ComplexBuffer; import source.SourceException; import source.tuner.Tuner; -import source.tuner.TunerChannel; -import source.tuner.TunerChannelSource; import source.tuner.TunerClass; -import source.tuner.TunerEvent; import source.tuner.TunerType; import source.tuner.hackrf.HackRFTunerController.BoardID; @@ -49,7 +42,6 @@ public class HackRFTuner extends Tuner public void dispose() { - //TODO: dispose of something here } @Override @@ -70,33 +62,6 @@ public class HackRFTuner extends Tuner return 11.0; } - @Override - public TunerChannelSource getChannel( TunerChannel channel ) - throws RejectedExecutionException, SourceException - { - TunerChannelSource source = getController().getChannel( this, channel ); - - if( source != null ) - { - broadcast( new TunerEvent( this, TunerEvent.Event.CHANNEL_COUNT ) ); - } - - return source; - } - - @Override - public void releaseChannel( TunerChannelSource source ) - { - /* Unregister for receiving samples */ - removeListener( (Listener)source ); - - /* Tell the controller to release the channel and cleanup */ - if( source != null ) - { - getController().releaseChannel( source ); - } - } - @Override public String getUniqueID() { @@ -111,16 +76,4 @@ public class HackRFTuner extends Tuner return BoardID.HACKRF_ONE.getLabel(); } - - @Override - public void addListener( Listener listener ) - { - getController().addListener( listener ); - } - - @Override - public void removeListener( Listener listener ) - { - getController().removeListener( listener ); - } } diff --git a/src/source/tuner/hackrf/HackRFTunerController.java b/src/source/tuner/hackrf/HackRFTunerController.java index 1741bfaf..812f37f5 100644 --- a/src/source/tuner/hackrf/HackRFTunerController.java +++ b/src/source/tuner/hackrf/HackRFTunerController.java @@ -69,6 +69,7 @@ import source.SourceException; import source.tuner.TunerController; import source.tuner.configuration.TunerConfiguration; import source.tuner.usb.USBTransferProcessor; +import source.tuner.usb.USBTunerController; import javax.usb.UsbException; import java.nio.ByteBuffer; @@ -76,7 +77,7 @@ import java.nio.ByteOrder; import java.util.Arrays; import java.util.concurrent.LinkedTransferQueue; -public class HackRFTunerController extends TunerController +public class HackRFTunerController extends USBTunerController { private final static Logger mLog = LoggerFactory.getLogger(HackRFTunerController.class); @@ -160,6 +161,12 @@ public class HackRFTunerController extends TunerController mUSBTransferProcessor = new USBTransferProcessor(name, mDeviceHandle, mSampleAdapter, USB_TRANSFER_BUFFER_SIZE); } + @Override + protected USBTransferProcessor getUSBTransferProcessor() + { + return mUSBTransferProcessor; + } + /** * Claims the USB interface. If another application currently has * the interface claimed, the USB_FORCE_CLAIM_INTERFACE setting @@ -874,23 +881,4 @@ public class HackRFTunerController extends TunerController return sb.toString(); } } - - /** - * Adds a sample listener. If the buffer processing thread is - * not currently running, starts it running in a new thread. - */ - public void addListener(Listener listener) - { - mUSBTransferProcessor.addListener(listener); - } - - /** - * Removes the sample listener. If this is the last registered listener, - * shuts down the buffer processing thread. - */ - public void removeListener(Listener listener) - { - mUSBTransferProcessor.removeListener(listener); - } - } diff --git a/src/source/tuner/manager/AbstractSourceManager.java b/src/source/tuner/manager/AbstractSourceManager.java new file mode 100644 index 00000000..9c373d2d --- /dev/null +++ b/src/source/tuner/manager/AbstractSourceManager.java @@ -0,0 +1,111 @@ +/******************************************************************************* + * 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 source.tuner.manager; + +import sample.Broadcaster; +import sample.Listener; +import sample.complex.ComplexBuffer; +import sample.complex.IComplexBufferProvider; +import source.SourceEvent; +import source.tuner.channel.TunerChannel; +import source.tuner.channel.TunerChannelSource; + +import java.util.List; + +/** + * Interface to define the functionality of a source manager for handling tuner channel management, complex buffer + * listeners, and source event listeners. + */ +public abstract class AbstractSourceManager implements IComplexBufferProvider, Listener +{ + protected Broadcaster mComplexBufferBroadcaster = new Broadcaster<>(); + + /** + * Indicates if this source manager can tune (ie provide a source for) the specified tuner channel + */ + public abstract boolean canTune(TunerChannel tunerChannel); + + /** + * List of tuner channels being sourced by this source manager + */ + public abstract List getChannels(); + + /** + * Obtains a source for the tuner channel or returns null if the channel cannot be sourced by this tuner. + * Note: use the canTune() method to check if a tuner channel can be sourced/tuned prior to accessing this method. + * + * @param tunerChannel for requested source + * @return tuner channel source or null + */ + public abstract TunerChannelSource getSource(TunerChannel tunerChannel); + + + /** + * Releases the source and all related resources + */ + public abstract void releaseSource(TunerChannelSource source); + + /** + * Adds the listener to receive source events from this source manager + * + * @param sourceEventListener to add + */ + public abstract void addSourceEventListener(Listener sourceEventListener); + + /** + * Removes the listener from receiving source events from this source manager + * @param sourceEventListener to remove + */ + public abstract void removeSourceEventListener(Listener sourceEventListener); + + /** + * Adds the listener to receive complex buffer samples + */ + @Override + public void addComplexBufferListener(Listener listener) + { + mComplexBufferBroadcaster.addListener(listener); + } + + /** + * Removes the listener from receiving complex buffer samples + */ + @Override + public void removeComplexBufferListener(Listener listener) + { + mComplexBufferBroadcaster.removeListener(listener); + } + + /** + * Indicates if there are any complex buffer listeners registered with this source manager + */ + @Override + public boolean hasComplexBufferListeners() + { + return mComplexBufferBroadcaster.hasListeners(); + } + + /** + * Broadcasts the buffer to any registered listeners + */ + protected void broadcast(ComplexBuffer complexBuffer) + { + mComplexBufferBroadcaster.broadcast(complexBuffer); + } +} diff --git a/src/source/tuner/manager/TunerSourceManager.java b/src/source/tuner/manager/TunerSourceManager.java new file mode 100644 index 00000000..fb6856bd --- /dev/null +++ b/src/source/tuner/manager/TunerSourceManager.java @@ -0,0 +1,83 @@ +/******************************************************************************* + * 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 source.tuner.manager; + +import dsp.filter.channelizer.PolyphaseChannelManager; +import sample.Listener; +import source.SourceEvent; +import source.tuner.channel.TunerChannel; +import source.tuner.channel.TunerChannelSource; +import source.tuner.TunerController; + +import java.util.List; + +public class TunerSourceManager extends AbstractSourceManager +{ + private PolyphaseChannelManager mPolyphaseChannelManager; + private TunerController mTunerController; + + public TunerSourceManager(TunerController tunerController) + { + mTunerController = tunerController; + } + + //TODO: broadcast a tuner event each time we add a new channel to the polyphase manager + + @Override + public boolean canTune(TunerChannel tunerChannel) + { + return false; + } + + @Override + public List getChannels() + { + return null; + } + + @Override + public TunerChannelSource getSource(TunerChannel tunerChannel) + { + return null; + } + + @Override + public void releaseSource(TunerChannelSource source) + { + + } + + @Override + public void addSourceEventListener(Listener sourceEventListener) + { + + } + + @Override + public void removeSourceEventListener(Listener sourceEventListener) + { + + } + + @Override + public void receive(SourceEvent sourceEvent) + { + + } +} diff --git a/src/source/tuner/rtl/RTL2832Tuner.java b/src/source/tuner/rtl/RTL2832Tuner.java index 20b746ea..dceece1c 100644 --- a/src/source/tuner/rtl/RTL2832Tuner.java +++ b/src/source/tuner/rtl/RTL2832Tuner.java @@ -1,161 +1,75 @@ /******************************************************************************* * SDR Trunk * Copyright (C) 2014-2016 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 source.tuner.rtl; -import java.util.concurrent.RejectedExecutionException; - import org.slf4j.Logger; import org.slf4j.LoggerFactory; -import org.usb4java.LibUsbException; - -import sample.Listener; -import sample.complex.ComplexBuffer; import source.SourceException; import source.tuner.Tuner; -import source.tuner.TunerChannel; -import source.tuner.TunerChannelSource; import source.tuner.TunerClass; -import source.tuner.TunerEvent; import source.tuner.TunerType; -import source.tuner.rtl.RTL2832TunerController.SampleRate; public class RTL2832Tuner extends Tuner { - private final static Logger mLog = - LoggerFactory.getLogger( RTL2832Tuner.class ); + private final static Logger mLog = LoggerFactory.getLogger(RTL2832Tuner.class); - private TunerClass mTunerClass; + private TunerClass mTunerClass; - public RTL2832Tuner( TunerClass tunerClass, - RTL2832TunerController controller ) - throws SourceException - { - super( tunerClass.getVendorDeviceLabel() + "/" + - controller.getTunerType().getLabel() + " " + - controller.getUniqueID(), controller ); - - mTunerClass = tunerClass; - } - - public void dispose() - { - //TODO: dispose of something here - } - - public RTL2832TunerController getController() - { - return (RTL2832TunerController)getTunerController(); - } + public RTL2832Tuner(TunerClass tunerClass, RTL2832TunerController controller) throws SourceException + { + super(tunerClass.getVendorDeviceLabel() + "/" + controller.getTunerType().getLabel() + " " + + controller.getUniqueID(), controller); - @Override + mTunerClass = tunerClass; + } + + public void dispose() + { + } + + public RTL2832TunerController getController() + { + return (RTL2832TunerController)getTunerController(); + } + + @Override public String getUniqueID() { - return getController().getUniqueID(); + return getController().getUniqueID(); } - @Override + @Override public TunerClass getTunerClass() { - return mTunerClass; + return mTunerClass; } - @Override + @Override public TunerType getTunerType() { - return getController().getTunerType(); + return getController().getTunerType(); } - - public void setSampleRate( SampleRate sampleRate ) throws SourceException - { - try - { - getController().setSampleRate( sampleRate ); - } - catch( LibUsbException e ) - { - throw new SourceException( "RTL2832 Tuner - error setting " - + "sample rate", e ); - } - } - - @Override - public double getSampleSize() - { - //Note: although sample size is 8, we set it to 11 to align with the - //actual noise floor. - return 11.0; - } - public void setFrequency( int frequency ) throws SourceException - { - getController().setFrequency( frequency ); - } - - @Override - public TunerChannelSource getChannel( TunerChannel channel ) - throws RejectedExecutionException, - SourceException - { - TunerChannelSource source = null; - - try - { - source = getController().getChannel( this, channel ); - - if( source != null ) - { - broadcast( new TunerEvent( this, TunerEvent.Event.CHANNEL_COUNT ) ); - } - } - catch( Exception e ) - { - mLog.error( "couldn't provide source channel", e ); - } - - return source; - } - - /** - * Releases the tuned channel so that the tuner controller can tune to - * other frequencies as needed. - */ - @Override - public void releaseChannel( TunerChannelSource source ) + @Override + public double getSampleSize() { - /* Unregister for receiving samples */ - removeListener( (Listener)source ); - - /* Tell the controller to release the channel and cleanup */ - if( source != null ) - { - getController().releaseChannel( source ); - } + //Note: although sample size is 8, we set it to 11 to align with the + //actual noise floor. + return 11.0; } - - @Override - public void addListener( Listener listener ) - { - getController().addListener( listener ); - } - - @Override - public void removeListener( Listener listener ) - { - getController().removeListener( listener ); - } } diff --git a/src/source/tuner/rtl/RTL2832TunerController.java b/src/source/tuner/rtl/RTL2832TunerController.java index 7666890b..6102f951 100644 --- a/src/source/tuner/rtl/RTL2832TunerController.java +++ b/src/source/tuner/rtl/RTL2832TunerController.java @@ -38,6 +38,7 @@ import source.tuner.TunerController; import source.tuner.TunerManager; import source.tuner.TunerType; import source.tuner.usb.USBTransferProcessor; +import source.tuner.usb.USBTunerController; import javax.usb.UsbDisconnectedException; import javax.usb.UsbException; @@ -47,7 +48,7 @@ import java.nio.charset.Charset; import java.util.ArrayList; import java.util.Arrays; -public abstract class RTL2832TunerController extends TunerController +public abstract class RTL2832TunerController extends USBTunerController { private final static Logger mLog = LoggerFactory.getLogger(RTL2832TunerController.class); @@ -163,6 +164,12 @@ public abstract class RTL2832TunerController extends TunerController USB_TRANSFER_BUFFER_SIZE); } + @Override + protected USBTransferProcessor getUSBTransferProcessor() + { + return mUSBTransferProcessor; + } + /** * Claims the USB interface. Attempts to detach the active kernel driver * if one is currently attached. @@ -449,7 +456,7 @@ public abstract class RTL2832TunerController extends TunerController { if(mUSBTransferProcessor != null) { - mUSBTransferProcessor.removeAllListeners(); + mUSBTransferProcessor.removeListener(); TunerManager.LIBUSB_TRANSFER_PROCESSOR.unregisterTransferProcessor(mUSBTransferProcessor); } @@ -1490,20 +1497,33 @@ public abstract class RTL2832TunerController extends TunerController } /** - * Adds a sample listener. If the USB transfer processor is not currently running, it will auto-start + * Adds the IQ buffer listener and automatically starts buffer transfer processing, if not already started. */ - public void addListener(Listener listener) + @Override + public void addComplexBufferListener(Listener listener) { - mUSBTransferProcessor.addListener(listener); + boolean hasExistingListeners = hasComplexBufferListeners(); + + super.addComplexBufferListener(listener); + + if(!hasExistingListeners) + { + mUSBTransferProcessor.setListener(this); + } } /** - * Removes the sample listener. If this is the last registered listener, the USB transfer processor will - * halt buffer processing + * Removes the IQ buffer listener and stops buffer transfer processing if there are no more listeners. */ - public void removeListener(Listener listener) + @Override + public void removeComplexBufferListener(Listener listener) { - mUSBTransferProcessor.removeListener(listener); + super.removeComplexBufferListener(listener); + + if(!hasComplexBufferListeners()) + { + mUSBTransferProcessor.removeListener(); + } } /** diff --git a/src/source/tuner/test/TestTuner.java b/src/source/tuner/test/TestTuner.java index 476c6174..f43e646b 100644 --- a/src/source/tuner/test/TestTuner.java +++ b/src/source/tuner/test/TestTuner.java @@ -18,21 +18,12 @@ ******************************************************************************/ package source.tuner.test; -import dsp.filter.channelizer.PolyphaseChannelManager; import org.slf4j.Logger; import org.slf4j.LoggerFactory; -import sample.Listener; -import sample.complex.ComplexBuffer; -import source.Source; -import source.SourceException; import source.tuner.Tuner; -import source.tuner.TunerChannel; -import source.tuner.TunerChannelSource; import source.tuner.TunerClass; import source.tuner.TunerType; -import java.util.concurrent.RejectedExecutionException; - /** * Testing tuner that implements an internal oscillator to output a unity gain tone at a specified frequency offset * from a configurable center tune frequency and sample rate @@ -43,13 +34,9 @@ public class TestTuner extends Tuner private static int mInstanceCounter = 1; private final int mInstanceID = mInstanceCounter++; - private PolyphaseChannelManager mPolyphaseChannelManager; - public TestTuner() { super("Test Tuner", new TestTunerController()); - - mPolyphaseChannelManager = new PolyphaseChannelManager(this); } /** @@ -58,41 +45,11 @@ public class TestTuner extends Tuner * Note: this is a temporary method until the tuner controller interface can be updated with the start(listener) * and stop() methods. */ - private TestTunerController getTestTunerController() + public TestTunerController getTunerController() { return (TestTunerController)getTunerController(); } - /** - * Sets the center tuned frequency for this tuner - * @param frequency - * @throws SourceException - */ - public void setFrequency(long frequency) throws SourceException - { - getTunerController().setFrequency(frequency); - } - - public void setToneFrequency(long frequency) - { - getTestTunerController().setToneFrequency(frequency); - } - - public long getToneFrequency() - { - return getTestTunerController().getToneFrequency(); - } - - public void setSampleRate(int sampleRate) throws SourceException - { - getTestTunerController().setSampleRate(sampleRate); - } - - public int getSampleRate() - { - return getTestTunerController().getSampleRate(); - } - @Override public String getUniqueID() { @@ -116,62 +73,4 @@ public class TestTuner extends Tuner { return 16.0; } - - /** - * Creates a polyphase channel source. The provided channel is a one-use channel that can be commanded to start - * and stop processing via the source event interface. Once stopped, the channel is deregistered from this tuner - * and is no longer usable -- a new channel source must be requested from the tuner. - * - * @param tunerChannel identifying the channel center frequency and bandwidth - * @return channel source or null if the channel source cannot be provided by this tuner - */ - public Source getChannelSource(TunerChannel tunerChannel) - { - return mPolyphaseChannelManager.getChannel(tunerChannel); - } - - @Override - public TunerChannelSource getChannel(TunerChannel channel) throws RejectedExecutionException, SourceException - { - mLog.error("This method is deprecated in this test tuner implementation"); - throw new IllegalStateException("Method deprecated - this test tuner cannot provide a TunerChannelSource"); - } - - @Override - public void releaseChannel(TunerChannelSource source) - { - mLog.error("This method is deprecated in this test tuner implementation"); - } - - /** - * Adds the listener to receive complex sample buffers and auto-starts the sample stream if this is the first - * registered listener. - */ - @Override - public void addListener(Listener listener) - { - boolean isRunning = hasListeners(); - - super.addListener(listener); - - if(!isRunning) - { - getTestTunerController().start(getSampleBroadcaster()); - } - } - - /** - * Removes the listener from receiving complex sample buffers and auto-stops the sample stream when there are - * no more listeners. - */ - @Override - public void removeListener(Listener listener) - { - super.removeListener(listener); - - if(!hasListeners()) - { - getTestTunerController().stop(); - } - } } diff --git a/src/source/tuner/test/TestTunerController.java b/src/source/tuner/test/TestTunerController.java index b09f630b..5664d9d0 100644 --- a/src/source/tuner/test/TestTunerController.java +++ b/src/source/tuner/test/TestTunerController.java @@ -70,18 +70,28 @@ public class TestTunerController extends TunerController * Starts the tuner controller producing samples that will be delivered to the specified listener */ public void start(Listener listener) + { + } + + @Override + public void addComplexBufferListener(Listener listener) { mSampleGenerator.setListener(listener); mSampleGenerator.start(); } + @Override + public void removeComplexBufferListener(Listener listener) + { + mSampleGenerator.stop(); + mSampleGenerator.setListener(null); + } + /** * Stops the tuner controller sample generator and deregisters the listener */ public void stop() { - mSampleGenerator.stop(); - mSampleGenerator.setListener(null); } @Override diff --git a/src/source/tuner/usb/USBTransferProcessor.java b/src/source/tuner/usb/USBTransferProcessor.java index 7052db0c..60d6642a 100644 --- a/src/source/tuner/usb/USBTransferProcessor.java +++ b/src/source/tuner/usb/USBTransferProcessor.java @@ -70,10 +70,10 @@ public class USBTransferProcessor implements TransferCallback //Byte array transfer buffers size in bytes private int mBufferSize; - private Broadcaster mComplexBufferBroadcaster = new Broadcaster<>(); private OverflowableTransferQueue mFilledBuffers; private LinkedTransferQueue mAvailableTransfers = new LinkedTransferQueue<>(); private LinkedTransferQueue mTransfersInProgress = new LinkedTransferQueue<>(); + private Listener mComplexBufferListener; private AtomicBoolean mRunning = new AtomicBoolean(); @@ -234,36 +234,26 @@ public class USBTransferProcessor implements TransferCallback } /** - * Indicates if there are complex buffer listeners registered with this processor + * Sets the listener and auto-starts the buffer processor */ - public boolean hasListeners() + public void setListener(Listener listener) { - return mComplexBufferBroadcaster.hasListeners(); - } - - public void addListener(Listener listener) - { - mComplexBufferBroadcaster.addListener(listener); - + mComplexBufferListener = listener; start(); } - public void removeListener(Listener listener) + /** + * Auto-stops the buffer processor and removes the listener + */ + public void removeListener() { - mComplexBufferBroadcaster.removeListener(listener); - - if(!hasListeners()) + if(mComplexBufferListener != null) { stop(); + mComplexBufferListener = null; } } - public void removeAllListeners() - { - mComplexBufferBroadcaster.clear(); - stop(); - } - /** * Prepares (allocates) a set of transfer buffers for use in transferring data from the USB device via the bulk * interface. Since we're using direct allocation (native), buffers are retained and reused across multiple @@ -425,7 +415,10 @@ public class USBTransferProcessor implements TransferCallback { float[] complexSamples = mSampleAdapter.convert(buffer); - mComplexBufferBroadcaster.broadcast(new ComplexBuffer(complexSamples)); + if(mComplexBufferListener != null) + { + mComplexBufferListener.receive(new ComplexBuffer(complexSamples)); + } } } catch(Exception e) diff --git a/src/source/tuner/usb/USBTunerController.java b/src/source/tuner/usb/USBTunerController.java new file mode 100644 index 00000000..f731783b --- /dev/null +++ b/src/source/tuner/usb/USBTunerController.java @@ -0,0 +1,72 @@ +/******************************************************************************* + * 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 source.tuner.usb; + +import sample.Listener; +import sample.complex.ComplexBuffer; +import source.tuner.TunerController; + +public abstract class USBTunerController extends TunerController +{ + /** + * USB tuner controller class. Provides auto-start and auto-stop function when complex buffer listeners are added + * or removed from this tuner controller. + * + * @param minimumFrequency minimum uncorrected tunable frequency + * @param maximumFrequency maximum uncorrected tunable frequency + * @param middleUnusable is the +/- value center DC spike to avoid for channels + * @param usableBandwidth percentage of usable bandwidth relative to space at the extreme ends of the spectrum + */ + public USBTunerController(long minimumFrequency, long maximumFrequency, int middleUnusable, double usableBandwidth) + { + super(minimumFrequency, maximumFrequency, middleUnusable, usableBandwidth); + } + + protected abstract USBTransferProcessor getUSBTransferProcessor(); + + /** + * Adds the IQ buffer listener and automatically starts buffer transfer processing, if not already started. + */ + @Override + public void addComplexBufferListener(Listener listener) + { + boolean hasExistingListeners = hasComplexBufferListeners(); + + super.addComplexBufferListener(listener); + + if(!hasExistingListeners) + { + getUSBTransferProcessor().setListener(this); + } + } + + /** + * Removes the IQ buffer listener and stops buffer transfer processing if there are no more listeners. + */ + @Override + public void removeComplexBufferListener(Listener listener) + { + super.removeComplexBufferListener(listener); + + if(!hasComplexBufferListeners()) + { + getUSBTransferProcessor().removeListener(); + } + } +} diff --git a/src/source/wave/ComplexWaveSource.java b/src/source/wave/ComplexWaveSource.java index 9ec77f2a..f7d0c601 100644 --- a/src/source/wave/ComplexWaveSource.java +++ b/src/source/wave/ComplexWaveSource.java @@ -18,7 +18,7 @@ ******************************************************************************/ package source.wave; -import channel.heartbeat.Heartbeat; +import source.heartbeat.Heartbeat; import org.slf4j.Logger; import org.slf4j.LoggerFactory; import sample.ConversionUtils; diff --git a/src/source/wave/RealWaveSource.java b/src/source/wave/RealWaveSource.java index 3545efc2..f64eef05 100644 --- a/src/source/wave/RealWaveSource.java +++ b/src/source/wave/RealWaveSource.java @@ -17,7 +17,7 @@ ******************************************************************************/ package source.wave; -import channel.heartbeat.Heartbeat; +import source.heartbeat.Heartbeat; import org.slf4j.Logger; import org.slf4j.LoggerFactory; import sample.ConversionUtils; diff --git a/src/spectrum/OverlayPanel.java b/src/spectrum/OverlayPanel.java index d78101e2..dc211bae 100644 --- a/src/spectrum/OverlayPanel.java +++ b/src/spectrum/OverlayPanel.java @@ -30,7 +30,7 @@ import settings.ColorSetting.ColorSettingName; import settings.Setting; import settings.SettingChangeListener; import settings.SettingsManager; -import source.tuner.TunerChannel; +import source.tuner.channel.TunerChannel; import source.SourceEvent; import source.ISourceEventProcessor; diff --git a/src/spectrum/SpectralDisplayPanel.java b/src/spectrum/SpectralDisplayPanel.java index 9c871aea..c050e27c 100644 --- a/src/spectrum/SpectralDisplayPanel.java +++ b/src/spectrum/SpectralDisplayPanel.java @@ -424,7 +424,7 @@ public class SpectralDisplayPanel extends JPanel implements Listener)mDFTProcessor); + mTuner.getSourceManager().addComplexBufferListener(mDFTProcessor); mSpectrumPanel.setSampleSize(mTuner.getSampleSize()); From 441b595325741860ab5f633d913d556c69195e7d Mon Sep 17 00:00:00 2001 From: Denny Date: Fri, 8 Dec 2017 04:02:49 -0500 Subject: [PATCH 33/87] #39 WIP - initial efforts to restructure the way tuners and tuner controllers manager sample buffer processing and channel management. General cleanup across all modules to remove the executor from each of the start() methods. Removed the B100 tuner class since it's not yet implemented. --- src/source/tuner/channel/DecimatingTunerChannelSource.java | 2 -- 1 file changed, 2 deletions(-) diff --git a/src/source/tuner/channel/DecimatingTunerChannelSource.java b/src/source/tuner/channel/DecimatingTunerChannelSource.java index 134601b1..b8530116 100644 --- a/src/source/tuner/channel/DecimatingTunerChannelSource.java +++ b/src/source/tuner/channel/DecimatingTunerChannelSource.java @@ -33,10 +33,8 @@ import sample.real.IOverflowListener; import source.ISourceEventListener; import source.ISourceEventProcessor; import source.ISourceEventProvider; -import source.Source; import source.SourceEvent; import source.SourceException; -import source.tuner.Tuner; import java.util.ArrayList; import java.util.List; From d2085052f6f461468cdf0e367cd8909e0e8feb59 Mon Sep 17 00:00:00 2001 From: Denny Date: Sat, 9 Dec 2017 15:23:40 -0500 Subject: [PATCH 34/87] #39 WIP - updates tunerchannelsource as the parent class for the CIC and polyphase sub class implementations. Converts all source implementations and tuners to treat sample rate as a double. --- src/audio/broadcast/StreamManager.java | 2 +- .../filter/channelizer/ChannelizerViewer.java | 16 +- .../channelizer/PolyphaseChannelSource.java | 333 ++--------- .../output/ChannelOutputProcessor.java | 23 +- .../IPolyphaseChannelOutputProcessor.java | 19 + .../output/OneChannelOutputProcessor.java | 2 +- src/dsp/mixer/Oscillator.java | 8 +- src/module/decode/am/AMDecoder.java | 73 ++- src/module/decode/nbfm/NBFMDecoder.java | 35 +- src/record/RecorderManager.java | 2 +- src/sample/OverflowableTransferQueue.java | 17 +- src/source/Source.java | 6 +- src/source/SourceEvent.java | 4 +- ...SourceEventListenerToProcessorAdapter.java | 61 ++ src/source/heartbeat/HeartbeatManager.java | 4 +- src/source/mixer/RealMixerSource.java | 28 +- src/source/tuner/ComplexChannelSource.java | 169 ------ src/source/tuner/TunerController.java | 48 +- src/source/tuner/TunerModel.java | 2 +- .../tuner/airspy/AirspyTunerController.java | 2 +- .../tuner/channel/CICTunerChannelSource.java | 225 ++++++++ .../channel/DecimatingTunerChannelSource.java | 541 ------------------ .../tuner/channel/TunerChannelSource.java | 265 +++++++-- src/source/tuner/fcd/FCDTunerController.java | 2 +- .../tuner/fcd/proV1/FCD1TunerController.java | 2 +- .../fcd/proplusV2/FCD2TunerController.java | 2 +- .../tuner/frequency/FrequencyController.java | 10 +- .../tuner/hackrf/HackRFTunerController.java | 4 +- .../tuner/rtl/RTL2832TunerController.java | 2 +- .../tuner/test/TestTunerController.java | 2 +- src/source/wave/ComplexWaveSource.java | 24 +- src/source/wave/RealWaveSource.java | 25 +- src/spectrum/ChannelSpectrumPanel.java | 2 +- src/spectrum/SpectralDisplayPanel.java | 2 +- 34 files changed, 770 insertions(+), 1192 deletions(-) create mode 100644 src/source/SourceEventListenerToProcessorAdapter.java delete mode 100644 src/source/tuner/ComplexChannelSource.java create mode 100644 src/source/tuner/channel/CICTunerChannelSource.java delete mode 100644 src/source/tuner/channel/DecimatingTunerChannelSource.java diff --git a/src/audio/broadcast/StreamManager.java b/src/audio/broadcast/StreamManager.java index 538ad5be..8e84ccdd 100644 --- a/src/audio/broadcast/StreamManager.java +++ b/src/audio/broadcast/StreamManager.java @@ -140,7 +140,7 @@ public class StreamManager implements Listener { AudioRecorder recorder = BroadcastFactory.getAudioRecorder(getTemporaryRecordingPath(), mBroadcastFormat); - recorder.start(ThreadPool.SCHEDULED); + recorder.start(); recorder.receive(audioPacket); mStreamRecorders.put(channelMetadataID, recorder); } diff --git a/src/dsp/filter/channelizer/ChannelizerViewer.java b/src/dsp/filter/channelizer/ChannelizerViewer.java index 91db8580..c072a9d0 100644 --- a/src/dsp/filter/channelizer/ChannelizerViewer.java +++ b/src/dsp/filter/channelizer/ChannelizerViewer.java @@ -71,7 +71,7 @@ public class ChannelizerViewer extends JFrame { mTestTuner = new TestTuner(); - mChannelCount = mTestTuner.getTunerController().getSampleRate() / CHANNEL_BANDWIDTH; + mChannelCount = (int)(mTestTuner.getTunerController().getSampleRate() / CHANNEL_BANDWIDTH); mChannelsPerRow = channelsPerRow; mToneFrequency = mTestTuner.getTunerController().getFrequency() + 19200; @@ -127,7 +127,7 @@ public class ChannelizerViewer extends JFrame mControlPanel.setLayout(new MigLayout("insets 0 0 0 0", "", "")); mControlPanel.add(new JLabel("Tone:"), "align left"); - int maximumFrequencyOffset = mTestTuner.getTunerController().getSampleRate() / 2; + int maximumFrequencyOffset = (int)(mTestTuner.getTunerController().getSampleRate() / 2); long minimumFrequency = mTestTuner.getTunerController().getFrequency() - maximumFrequencyOffset; long maximumFrequency = mTestTuner.getTunerController().getFrequency() + maximumFrequencyOffset; @@ -185,16 +185,16 @@ public class ChannelizerViewer extends JFrame { setLayout(new MigLayout("insets 0 0 0 0", "fill", "fill")); - long spectralBandwidth = mTestTuner.getTunerController().getSampleRate(); - long halfSpectralBandwidth = spectralBandwidth / 2; + double spectralBandwidth = mTestTuner.getTunerController().getSampleRate(); + double halfSpectralBandwidth = spectralBandwidth / 2.0; int channelToLog = -1; for(int x = 0; x < mChannelCount; x++) { //place the channels left and right of 10.0 MHz - long frequency = mTestTuner.getTunerController().getFrequency() + - ((x * CHANNEL_BANDWIDTH) - halfSpectralBandwidth) + (CHANNEL_BANDWIDTH / 2); + long frequency = (long)(mTestTuner.getTunerController().getFrequency() + + ((x * CHANNEL_BANDWIDTH) - halfSpectralBandwidth) + (CHANNEL_BANDWIDTH / 2)); mLog.debug("Channel " + x + " Frequency: " + frequency); @@ -219,7 +219,7 @@ public class ChannelizerViewer extends JFrame private ComplexDecibelConverter mComplexDecibelConverter = new ComplexDecibelConverter(); private SpectrumPanel mSpectrumPanel; - public PrimarySpectrumPanel(SettingsManager settingsManager, int sampleRate) + public PrimarySpectrumPanel(SettingsManager settingsManager, double sampleRate) { setLayout(new MigLayout("insets 0 0 0 0", "[grow,fill]", "[grow,fill]")); mSpectrumPanel = new SpectrumPanel(settingsManager); @@ -256,7 +256,7 @@ public class ChannelizerViewer extends JFrame private SpectrumPanel mSpectrumPanel; private long mFrequency; - public ChannelPanel(SettingsManager settingsManager, int sampleRate, long frequency, int bandwidth, boolean enableLogging) + public ChannelPanel(SettingsManager settingsManager, double sampleRate, long frequency, int bandwidth, boolean enableLogging) { setLayout(new MigLayout("insets 0 0 0 0", "[grow,fill]", "[grow,fill]")); mSpectrumPanel = new SpectrumPanel(settingsManager); diff --git a/src/dsp/filter/channelizer/PolyphaseChannelSource.java b/src/dsp/filter/channelizer/PolyphaseChannelSource.java index 20e789fb..fea1ce78 100644 --- a/src/dsp/filter/channelizer/PolyphaseChannelSource.java +++ b/src/dsp/filter/channelizer/PolyphaseChannelSource.java @@ -18,55 +18,41 @@ ******************************************************************************/ package dsp.filter.channelizer; -import source.heartbeat.Heartbeat; import dsp.filter.channelizer.output.IPolyphaseChannelOutputProcessor; import org.slf4j.Logger; import org.slf4j.LoggerFactory; import sample.Listener; import sample.complex.ComplexBuffer; import sample.complex.ComplexToComplexBufferAssembler; -import source.ComplexSource; -import source.ISourceEventProcessor; +import sample.real.IOverflowListener; import source.SourceEvent; -import source.SourceException; import source.tuner.channel.TunerChannel; +import source.tuner.channel.TunerChannelSource; -import java.util.concurrent.ScheduledExecutorService; -import java.util.concurrent.ScheduledFuture; -import java.util.concurrent.TimeUnit; - -public class PolyphaseChannelSource extends ComplexSource implements ISourceEventProcessor +public class PolyphaseChannelSource extends TunerChannelSource { private final static Logger mLog = LoggerFactory.getLogger(PolyphaseChannelSource.class); - private TunerChannel mTunerChannel; - private int mChannelSampleRate; - - private DownstreamSourceEventHandler mDownstreamSourceEventHandler = new DownstreamSourceEventHandler(); - private UpstreamSourceEventHandler mUpstreamSourceEventHandler; - private ComplexToComplexBufferAssembler mComplexBufferAssembler = new ComplexToComplexBufferAssembler(2500); private IPolyphaseChannelOutputProcessor mPolyphaseChannelOutputProcessor; - private ChannelResultsProcessor mChannelResultsProcessor; - private ScheduledFuture mChannelResultsProcessorFuture; + private double mChannelSampleRate; /** - * Polyphase channelizer channel implementation. Adapts the channel array output samples from the polyphase - * channelizer into a single channel, or a channel synthesized from two adjacent channels that is frequency - * translated and decimated to a single channel. + * Polyphase channelizer tuner channel source implementation. Adapts the channel array output samples from the + * polyphase channelizer into a single channel, or a channel synthesized from two adjacent channels that is + * frequency translated and decimated to a single channel. * - * @param tunerChannel - requested output tuner channel frequency and bandwidth. + * @param tunerChannel describing the desired channel frequency and bandwidth/minimum sample rate * @param outputProcessor - to process polyphase channelizer channel results into a channel stream - * @param externalEventListener to process sample stream start/stop requests + * @param producerSourceEventListener to receive source event requests (e.g. start/stop sample stream) + * @param channelSampleRate for the downstream sample output */ public PolyphaseChannelSource(TunerChannel tunerChannel, IPolyphaseChannelOutputProcessor outputProcessor, - Listener externalEventListener, int channelSampleRate) + Listener producerSourceEventListener, double channelSampleRate) { - mTunerChannel = tunerChannel; + super(producerSourceEventListener, tunerChannel); mPolyphaseChannelOutputProcessor = outputProcessor; - mUpstreamSourceEventHandler = new UpstreamSourceEventHandler(externalEventListener);; mChannelSampleRate = channelSampleRate; - } /** @@ -75,21 +61,8 @@ public class PolyphaseChannelSource extends ComplexSource implements ISourceEven @Override public void setListener(final Listener listener) { -// mComplexBufferAssembler.setListener(listener); - - mComplexBufferAssembler.setListener(new Listener() - { - @Override - public void receive(ComplexBuffer complexBuffer) - { - if(mTunerChannel.getFrequency() == 99981250) - { -// mLog.debug(Arrays.toString(complexBuffer.getSamples())); - } - listener.receive(complexBuffer); - } - }); - } + mComplexBufferAssembler.setListener(listener); + } /** * Removes the listener from receiving complex sample buffers from this channel source @@ -100,97 +73,6 @@ public class PolyphaseChannelSource extends ComplexSource implements ISourceEven mComplexBufferAssembler.setListener(null); } - /** - * Starts scheduled thread pool processing of the inbound polyphase channel results buffer to produce channelized - * complex sample buffer output for broadcast to a registered complex sample buffer listener. - * - * @param executor to use when scheduling a buffer processor - */ - @Override - public void start(ScheduledExecutorService executor) - { - if(mUpstreamSourceEventHandler != null) - { - mUpstreamSourceEventHandler.broadcast(SourceEvent.startSampleStream(PolyphaseChannelSource.this)); - } - - mChannelResultsProcessor = new ChannelResultsProcessor(); - - if(mChannelResultsProcessorFuture != null) - { - mChannelResultsProcessorFuture.cancel(true); - mLog.error("An existing channel results processor scheduled future was cancelled"); - } - - //Schedule the results processor to run every 100 milliseconds - mChannelResultsProcessorFuture = - executor.scheduleAtFixedRate(mChannelResultsProcessor, 0, 100, TimeUnit.MILLISECONDS); - } - - /** - * Stops scheduled thread pool processing of the inbound polyphase channel results buffer to produce channelized - * complex sample buffer output for broadcast to a registered complex sample buffer listener. - */ - @Override - public void stop() - { - if(mUpstreamSourceEventHandler != null) - { - mUpstreamSourceEventHandler.broadcast(SourceEvent.stopSampleStream(PolyphaseChannelSource.this)); - } - - if(mChannelResultsProcessorFuture != null) - { - mChannelResultsProcessorFuture.cancel(true); - mChannelResultsProcessorFuture = null; - } - - mChannelResultsProcessor = null; - } - - - /** - * Module-level ISourceEventListener interface implementation for processing downstream events - */ - @Override - public Listener getSourceEventListener() - { - return mDownstreamSourceEventHandler; - } - - /** - * Module-level ISourceEventProvider interface implementation, sets the listener to receive downstream module - * level source events from this source - */ - @Override - public void setSourceEventListener(Listener listener) - { - mDownstreamSourceEventHandler.setDownstreamEventListener(listener); - } - - /** - * Module-level ISourceEventProvider interface implementation, removes the listener from receiving downstream module - * level source events from this source - */ - @Override - public void removeSourceEventListener() - { - mDownstreamSourceEventHandler.removeDownstreamEventListener(); - } - - /** - * ISourceEventProcessor implementation for processing source events from the external polyphase channel manager - * and/or tuner providing samples to the polphase channelizer implementation. - * - * @param sourceEvent containing a request or notification - * @throws SourceException if there are any errors while processing the source event - */ - @Override - public void process(SourceEvent sourceEvent) throws SourceException - { - mUpstreamSourceEventHandler.receive(sourceEvent); - } - /** * Channel output processor used by this channel source to convert polyphase channel results into a specific * channel complex buffer output stream. @@ -214,6 +96,13 @@ public class PolyphaseChannelSource extends ComplexSource implements ISourceEven //Swap out the processor so that incoming samples can accumulate in the new channel output processor mPolyphaseChannelOutputProcessor = outputProcessor; + //Remove the buffer overflow listener and clear any previous buffer overflow state + existingProcessor.setSourceOverflowListener(null); + broadcastOverflowState(false); + + //Register to receive buffer overflow notifications so we can push them up through the source chain + mPolyphaseChannelOutputProcessor.setSourceOverflowListener(this); + //Fully process the residual channel results buffer of the previous channel output processor if(existingProcessor != null) { @@ -222,14 +111,6 @@ public class PolyphaseChannelSource extends ComplexSource implements ISourceEven } } - /** - * Tuner channel that is being sourced by this polyphase channel source. - */ - public TunerChannel getTunerChannel() - { - return mTunerChannel; - } - /** * Primary method for receiving channel results output from a polyphase channelizer. The results array will be * queued for processing to extract the target channel samples, process them for frequency correction and/or @@ -243,186 +124,80 @@ public class PolyphaseChannelSource extends ComplexSource implements ISourceEven } /** - * Downstream sample rate + * Downstream channel sample rate * * @return sample rate in Hertz - * @throws SourceException never */ @Override - public int getSampleRate() throws SourceException + public double getSampleRate() { return mChannelSampleRate; } /** - * Center tuned frequency for this channel. - * - * @return frequency in hertz. - * @throws SourceException never + * Sets the downstream channel sample rate + * @param sampleRate in hertz */ @Override - public long getFrequency() throws SourceException + protected void setSampleRate(double sampleRate) { - return mTunerChannel.getFrequency(); + mChannelSampleRate = sampleRate; } - @Override - public void setHeartbeatListener(Listener listener) - { - mLog.info("Polyphase channel source received request to set heartbeat listener"); - //TODO: implement this - } - - @Override - public void removeHeartbeatListener() - { - mLog.info("Polyphase channel source received request to remove heartbeat listener"); - //TODO: implement this - } - - @Override - public void reset() - { - mLog.info("Polyphase channel source received request to reset"); - } - - @Override public void dispose() { mLog.info("Polyphase channel source received request to dispose"); } + @Override + public long getFrequencyCorrection() + { + IPolyphaseChannelOutputProcessor processor = mPolyphaseChannelOutputProcessor; + + if(processor != null) + { + return processor.getFrequencyCorrection(); + } + + return 0; + } + /** * Adjusts the frequency correction value that is being applied to the channelized output stream by the * polyphase channel output processor. + * * @param value to apply for frequency correction in hertz */ - private void setFrequencyCorrection(long value) + protected void setFrequencyCorrection(long value) { if(mPolyphaseChannelOutputProcessor != null) { mPolyphaseChannelOutputProcessor.setFrequencyCorrection(value); - mDownstreamSourceEventHandler.broadcast(SourceEvent.frequencyCorrectionChange(value)); + broadcastConsumerSourceEvent(SourceEvent.frequencyCorrectionChange(value)); } } /** - * Runnable for processing the incoming polyphase channelizer results queue and distributing the channel - * results to the registered complex sample listener + * Sets the center frequency for the incoming sample stream channel results + * @param frequency in hertz */ - public class ChannelResultsProcessor implements Runnable + @Override + protected void setFrequency(long frequency) { - @Override - public void run() + if(mPolyphaseChannelOutputProcessor != null) { - try - { - //Lock on the output processor so that it can't be changed out in the middle of processing - synchronized(mPolyphaseChannelOutputProcessor) - { - mPolyphaseChannelOutputProcessor.processChannelResults(mComplexBufferAssembler); - } - } - catch(Throwable throwable) - { - mLog.error("Error while processing polyphase channel samples", throwable); - } + mPolyphaseChannelOutputProcessor.setFrequency(frequency); } } - public class UpstreamSourceEventHandler implements Listener + @Override + protected void processSamples() { - private Listener mExternalSourceEventListener; - - /** - * Upstream source event processor for handling any source events generated by the polyphase channelizer that - * provides this polyphase channel source. - * - * @param externalSourceEventListener to receive requests for start/stop sample streams - */ - public UpstreamSourceEventHandler(Listener externalSourceEventListener) + //Lock on the output processor so that it can't be changed out in the middle of processing + synchronized(mPolyphaseChannelOutputProcessor) { - mExternalSourceEventListener = externalSourceEventListener; - } - - /** - * Broadcasts the request source event to the external source event listener. This listener is normally the - * polyphase channel manager. - * - * @param sourceEvent to broadcast - */ - public void broadcast(SourceEvent sourceEvent) - { - if(mExternalSourceEventListener != null) - { - mExternalSourceEventListener.receive(sourceEvent); - } - } - - /** - * Process source events received from the upstream (ie tuner or channelizer) sample provider. - * @param sourceEvent to process - */ - @Override - public void receive(SourceEvent sourceEvent) - { - switch(sourceEvent.getEvent()) - { - //Reset frequency correction anytime the upstream frequency or sample rate changes so that the - //downstream modules can reset and recalculate any needed frequency correction - case NOTIFICATION_FREQUENCY_CHANGE: - case NOTIFICATION_SAMPLE_RATE_CHANGE: - setFrequencyCorrection(0); - break; - } - } - } - - /** - * Downstream source event handler implements the ISourceEventProvider and ISourceEventProcessor interfaces on - * for this module to handle broadcasting source events to other downstream modules and receiving requests from - * any downstream modules. - */ - public class DownstreamSourceEventHandler implements Listener - { - private Listener mDownstreamEventListener; - - public void broadcast(SourceEvent sourceEvent) - { - if(mDownstreamEventListener != null) - { - mDownstreamEventListener.receive(sourceEvent); - } - } - - /** - * Sets the downstream event listener to receive source events from this channel source module - * @param listener to receive events - */ - public void setDownstreamEventListener(Listener listener) - { - mDownstreamEventListener = listener; - } - - public void removeDownstreamEventListener() - { - mDownstreamEventListener = null; - } - - /** - * Process source events received from the downstream channel modules. - * @param sourceEvent to process - */ - @Override - public void receive(SourceEvent sourceEvent) - { - switch(sourceEvent.getEvent()) - { - case REQUEST_CHANNEL_FREQUENCY_CORRECTION_CHANGE: - setFrequencyCorrection(sourceEvent.getValue().intValue()); - break; - } + mPolyphaseChannelOutputProcessor.processChannelResults(mComplexBufferAssembler); } } } diff --git a/src/dsp/filter/channelizer/output/ChannelOutputProcessor.java b/src/dsp/filter/channelizer/output/ChannelOutputProcessor.java index 653be07a..d4f9a095 100644 --- a/src/dsp/filter/channelizer/output/ChannelOutputProcessor.java +++ b/src/dsp/filter/channelizer/output/ChannelOutputProcessor.java @@ -25,6 +25,7 @@ import sample.OverflowableTransferQueue; import sample.complex.Complex; import sample.complex.ComplexSampleListener; import sample.real.IOverflowListener; +import source.Source; import java.util.ArrayList; import java.util.List; @@ -50,13 +51,25 @@ public abstract class ChannelOutputProcessor implements IPolyphaseChannelOutputP * @param sampleRate of the output channel. This is used to match the oscillator's sample rate to the output * channel sample rate for frequency translation/correction. */ - public ChannelOutputProcessor(int inputChannelCount, int sampleRate) + public ChannelOutputProcessor(int inputChannelCount, double sampleRate) { mInputChannelCount = inputChannelCount; mFrequencyCorrectionMixer = new Oscillator(0, sampleRate); - mMaxResultsToProcess = sampleRate / 10 * 2; //process at 100 millis interval, twice the expected inflow rate + mMaxResultsToProcess = (int)(sampleRate / 10) * 2; //process at 100 millis interval, twice the expected inflow rate - mChannelResultsQueue = new OverflowableTransferQueue<>(sampleRate * 3, (int)(sampleRate * 0.5)); + mChannelResultsQueue = new OverflowableTransferQueue<>((int)(sampleRate * 3), (int)(sampleRate * 0.5)); + } + + @Override + public void setFrequency(long frequency) + { + + } + + @Override + public long getFrequencyCorrection() + { + return 0; } @Override @@ -100,9 +113,9 @@ public abstract class ChannelOutputProcessor implements IPolyphaseChannelOutputP /** * Sets the overflow listener to monitor the internal channelizer channel results queue overflow state */ - public void setOverflowListener(IOverflowListener listener) + public void setSourceOverflowListener(Source source) { - mChannelResultsQueue.setOverflowListener(listener); + mChannelResultsQueue.setSourceOverflowListener(source); } /** diff --git a/src/dsp/filter/channelizer/output/IPolyphaseChannelOutputProcessor.java b/src/dsp/filter/channelizer/output/IPolyphaseChannelOutputProcessor.java index 453e7bfd..2dd26684 100644 --- a/src/dsp/filter/channelizer/output/IPolyphaseChannelOutputProcessor.java +++ b/src/dsp/filter/channelizer/output/IPolyphaseChannelOutputProcessor.java @@ -19,6 +19,8 @@ package dsp.filter.channelizer.output; import sample.complex.ComplexSampleListener; +import sample.real.IOverflowListener; +import source.Source; import java.util.List; @@ -37,6 +39,12 @@ public interface IPolyphaseChannelOutputProcessor */ void processChannelResults(ComplexSampleListener listener); + /** + * Sets the center frequency of the incoming channel results samples + * @param frequency in hertz + */ + void setFrequency(long frequency); + /** * Specifies the frequency correction (+/-) that should be applied to samples extracted from the polyphase * channelizer output results. @@ -45,6 +53,11 @@ public interface IPolyphaseChannelOutputProcessor */ void setFrequencyCorrection(long frequencyCorrection); + /** + * Current frequency correction value. + */ + long getFrequencyCorrection(); + /** * Indicates the number of input channels processed by this output processor * @return @@ -56,4 +69,10 @@ public interface IPolyphaseChannelOutputProcessor * @param indexes */ void setPolyphaseChannelIndices(List indexes); + + /** + * Sets the listener to receive buffer overflow notifications + * @param source to receive overflow notifications + */ + void setSourceOverflowListener(Source source); } diff --git a/src/dsp/filter/channelizer/output/OneChannelOutputProcessor.java b/src/dsp/filter/channelizer/output/OneChannelOutputProcessor.java index 44f5f396..aa3e7503 100644 --- a/src/dsp/filter/channelizer/output/OneChannelOutputProcessor.java +++ b/src/dsp/filter/channelizer/output/OneChannelOutputProcessor.java @@ -38,7 +38,7 @@ public class OneChannelOutputProcessor extends ChannelOutputProcessor * @param sampleRate of the output sample stream. * @param channelIndexes containing a single channel index. */ - public OneChannelOutputProcessor(int sampleRate, List channelIndexes) + public OneChannelOutputProcessor(double sampleRate, List channelIndexes) { super(1, sampleRate); setPolyphaseChannelIndices(channelIndexes); diff --git a/src/dsp/mixer/Oscillator.java b/src/dsp/mixer/Oscillator.java index 865f4bcf..b711c4ab 100644 --- a/src/dsp/mixer/Oscillator.java +++ b/src/dsp/mixer/Oscillator.java @@ -41,11 +41,11 @@ public class Oscillator * @param frequency - positive or negative frequency in hertz * @param sampleRate - in hertz */ - public Oscillator(long frequency, int sampleRate) + public Oscillator(long frequency, double sampleRate) { mEnabled = frequency != 0; - mSampleRate = (double)sampleRate; + mSampleRate = sampleRate; mFrequency = (double)frequency; update(); @@ -87,9 +87,9 @@ public class Oscillator /** * Sets or changes the sample rate of this oscillator */ - public void setSampleRate(int sampleRate) + public void setSampleRate(double sampleRate) { - mSampleRate = (double)sampleRate; + mSampleRate = sampleRate; update(); } diff --git a/src/module/decode/am/AMDecoder.java b/src/module/decode/am/AMDecoder.java index c5b371e3..29c5b52d 100644 --- a/src/module/decode/am/AMDecoder.java +++ b/src/module/decode/am/AMDecoder.java @@ -1,54 +1,53 @@ /******************************************************************************* - * SDR Trunk - * Copyright (C) 2014,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 + * 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 module.decode.am; -import java.util.concurrent.ScheduledExecutorService; - import module.decode.DecoderType; import module.decode.PrimaryDecoder; import module.decode.config.DecodeConfiguration; public class AMDecoder extends PrimaryDecoder { - public AMDecoder( DecodeConfiguration config ) - { - super( config ); - } - - @Override - public DecoderType getDecoderType() + public AMDecoder(DecodeConfiguration config) { - return DecoderType.AM; + super(config); } - @Override - public void reset() - { - } + @Override + public DecoderType getDecoderType() + { + return DecoderType.AM; + } - @Override - public void start( ScheduledExecutorService executor ) - { - } + @Override + public void reset() + { + } - @Override - public void stop() - { - } + @Override + public void start() + { + } + + @Override + public void stop() + { + } } diff --git a/src/module/decode/nbfm/NBFMDecoder.java b/src/module/decode/nbfm/NBFMDecoder.java index 1b055a91..dfe83545 100644 --- a/src/module/decode/nbfm/NBFMDecoder.java +++ b/src/module/decode/nbfm/NBFMDecoder.java @@ -1,24 +1,23 @@ /******************************************************************************* - * SDR Trunk - * Copyright (C) 2014,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 + * 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 module.decode.nbfm; -import java.util.concurrent.ScheduledExecutorService; - import module.decode.DecoderType; import module.decode.PrimaryDecoder; import module.decode.config.DecodeConfiguration; @@ -42,7 +41,7 @@ public class NBFMDecoder extends PrimaryDecoder } @Override - public void start( ScheduledExecutorService executor ) + public void start() { } diff --git a/src/record/RecorderManager.java b/src/record/RecorderManager.java index 4d9789e0..4e054b40 100644 --- a/src/record/RecorderManager.java +++ b/src/record/RecorderManager.java @@ -145,7 +145,7 @@ public class RecorderManager implements Listener { recorder = new RealBufferWaveRecorder(AUDIO_SAMPLE_RATE, filePrefix); - recorder.start(ThreadPool.SCHEDULED); + recorder.start(); recorder.receive(audioPacket.getAudioBuffer()); mRecorders.put(identifier, recorder); diff --git a/src/sample/OverflowableTransferQueue.java b/src/sample/OverflowableTransferQueue.java index 193ef811..e532bc61 100644 --- a/src/sample/OverflowableTransferQueue.java +++ b/src/sample/OverflowableTransferQueue.java @@ -21,6 +21,7 @@ package sample; import org.slf4j.Logger; import org.slf4j.LoggerFactory; import sample.real.IOverflowListener; +import source.Source; import java.util.Collection; import java.util.concurrent.LinkedTransferQueue; @@ -33,6 +34,7 @@ public class OverflowableTransferQueue public enum State {NORMAL, OVERFLOW}; private IOverflowListener mOverflowListener; + private Source mSourceOverflowListener; private LinkedTransferQueue mQueue = new LinkedTransferQueue(); private AtomicInteger mCounter = new AtomicInteger(); @@ -99,13 +101,21 @@ public class OverflowableTransferQueue } /** - * Sets a listener to receive overflow state change events + * Sets a listener to receive overflow state change events. */ public void setOverflowListener(IOverflowListener listener) { mOverflowListener = listener; } + /** + * Sets the source to receive overflow state change events (in addition to an IOverflow listener) + */ + public void setSourceOverflowListener(Source source) + { + mSourceOverflowListener = source; + } + /** * Toggles the overflow state and broadcast state change to listener @@ -118,6 +128,11 @@ public class OverflowableTransferQueue { mOverflowListener.sourceOverflow(overflow); } + + if(mSourceOverflowListener != null) + { + mSourceOverflowListener.broadcastOverflowState(overflow); + } } } diff --git a/src/source/Source.java b/src/source/Source.java index 9304ddbe..16a8caeb 100644 --- a/src/source/Source.java +++ b/src/source/Source.java @@ -52,14 +52,14 @@ public abstract class Source extends Module implements ISourceEventListener, ISo * * @throws SourceException if there is an issue determining the sample rate for this source */ - public abstract int getSampleRate() throws SourceException; + public abstract double getSampleRate(); /** * Center frequency for this source in Hertz * * @throws SourceException if there is an issue in determining the center frequency for this source */ - public abstract long getFrequency() throws SourceException; + public abstract long getFrequency(); /** * Process any cleanup actions to prepare for garbage collection of this source @@ -79,7 +79,7 @@ public abstract class Source extends Module implements ISourceEventListener, ISo * * @param overflow true if overflow, false if normal */ - protected void broadcastOverflowState(boolean overflow) + public void broadcastOverflowState(boolean overflow) { if(mOverflowListener != null) { diff --git a/src/source/SourceEvent.java b/src/source/SourceEvent.java index 074eee9a..bade0e58 100644 --- a/src/source/SourceEvent.java +++ b/src/source/SourceEvent.java @@ -130,7 +130,7 @@ public class SourceEvent * * @param sampleRate in hertz */ - public static SourceEvent sampleRateChange(int sampleRate) + public static SourceEvent sampleRateChange(double sampleRate) { return new SourceEvent(Event.NOTIFICATION_SAMPLE_RATE_CHANGE, sampleRate); } @@ -140,7 +140,7 @@ public class SourceEvent * * @param sampleRate in hertz */ - public static SourceEvent channelSampleRateChange(int sampleRate) + public static SourceEvent channelSampleRateChange(double sampleRate) { return new SourceEvent(Event.NOTIFICATION_CHANNEL_SAMPLE_RATE_CHANGE, sampleRate); } diff --git a/src/source/SourceEventListenerToProcessorAdapter.java b/src/source/SourceEventListenerToProcessorAdapter.java new file mode 100644 index 00000000..85f50138 --- /dev/null +++ b/src/source/SourceEventListenerToProcessorAdapter.java @@ -0,0 +1,61 @@ +/******************************************************************************* + * 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 source; + +import org.slf4j.Logger; +import org.slf4j.LoggerFactory; +import sample.Listener; +import source.tuner.channel.TunerChannelSource; + +public class SourceEventListenerToProcessorAdapter implements Listener +{ + private final static Logger mLog = LoggerFactory.getLogger(TunerChannelSource.class); + + private ISourceEventProcessor mSourceEventProcessor; + + /** + * Adapter to implement ISourceEventListener/Listener and adapt to a source event processor + */ + public SourceEventListenerToProcessorAdapter(ISourceEventProcessor sourceEventProcessor) + { + mSourceEventProcessor = sourceEventProcessor; + } + + public void dispose() + { + mSourceEventProcessor = null; + } + + @Override + public void receive(SourceEvent sourceEvent) + { + if(mSourceEventProcessor != null) + { + try + { + mSourceEventProcessor.process(sourceEvent); + } + catch(SourceException e) + { + mLog.error("Error while transferring source event to source event processor [" + + mSourceEventProcessor.getClass() + "]", e); + } + } + } +} diff --git a/src/source/heartbeat/HeartbeatManager.java b/src/source/heartbeat/HeartbeatManager.java index d35188e3..f570171b 100644 --- a/src/source/heartbeat/HeartbeatManager.java +++ b/src/source/heartbeat/HeartbeatManager.java @@ -26,6 +26,9 @@ public class HeartbeatManager private static final Heartbeat HEARTBEAT = new Heartbeat(); private Broadcaster mHeartbeatBroadcaster = new Broadcaster<>(); + /** + * Broadcasts heartbeat as commanded and handles listener registration details. + */ public HeartbeatManager() { } @@ -54,5 +57,4 @@ public class HeartbeatManager { mHeartbeatBroadcaster.removeListener(listener); } - } diff --git a/src/source/mixer/RealMixerSource.java b/src/source/mixer/RealMixerSource.java index e8558429..83d72d3d 100644 --- a/src/source/mixer/RealMixerSource.java +++ b/src/source/mixer/RealMixerSource.java @@ -93,33 +93,13 @@ public class RealMixerSource extends RealSource return null; } - /** - * Registers the listener to receive heartbeats from this source. - * - * @param listener to receive heartbeats - */ - @Override - public void setHeartbeatListener(Listener listener) - { - mHeartbeatListener = listener; - } - - /** - * Removes the currently registered heartbeat listener - */ - @Override - public void removeHeartbeatListener() - { - mHeartbeatListener = null; - } - @Override public void reset() { } @Override - public void start(ScheduledExecutorService executor) + public void start() { } @@ -174,11 +154,11 @@ public class RealMixerSource extends RealSource } } - public int getSampleRate() + public double getSampleRate() { if(mTargetDataLine != null) { - return (int)mTargetDataLine.getFormat().getSampleRate(); + return mTargetDataLine.getFormat().getSampleRate(); } else { @@ -189,7 +169,7 @@ public class RealMixerSource extends RealSource /** * Returns the frequency of this source. Default is 0. */ - public long getFrequency() throws SourceException + public long getFrequency() { return mFrequency; } diff --git a/src/source/tuner/ComplexChannelSource.java b/src/source/tuner/ComplexChannelSource.java deleted file mode 100644 index f505980f..00000000 --- a/src/source/tuner/ComplexChannelSource.java +++ /dev/null @@ -1,169 +0,0 @@ -/******************************************************************************* - * 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 source.tuner; - -import org.slf4j.Logger; -import org.slf4j.LoggerFactory; -import sample.Listener; -import sample.complex.ComplexBuffer; -import source.ComplexSource; -import source.ISourceEventProcessor; -import source.SourceEvent; -import source.SourceException; - -import java.util.concurrent.ScheduledExecutorService; - -/** - * Complex source that provides a complex sample stream at the channel's required sample rate - */ -public abstract class ComplexChannelSource extends ComplexSource implements ISourceEventProcessor -{ - private final static Logger mLog = LoggerFactory.getLogger(ComplexChannelSource.class); - private SourceEventProcessor mSourceEventProcessor = new SourceEventProcessor(); - private Listener mSourceEventListener; - private Listener mComplexBufferListener; - private ISourceEventProcessor mUpstreamSourceEventProcessor; - - public ComplexChannelSource(ISourceEventProcessor upstreamSourceEventProcessor) - { - mUpstreamSourceEventProcessor = upstreamSourceEventProcessor; - } - - /** - * Sets the source event listener to receive source events from this source - */ - @Override - public void setSourceEventListener(Listener listener) - { - mSourceEventListener = listener; - } - - /** - * Removes the source event listener from receiving source events from this source - */ - @Override - public void removeSourceEventListener() - { - mSourceEventListener = null; - } - - /** - * Broadcasts the source event to the registered source event listener - */ - protected void broadcast(SourceEvent sourceEvent) - { - if(mSourceEventListener != null) - { - mSourceEventListener.receive(sourceEvent); - } - } - - /** - * Registers the listener to receive complex sample buffers from this channel source - */ - @Override - public void setListener(Listener listener) - { - mComplexBufferListener = listener; - } - - @Override - public void removeListener(Listener listener) - { - mComplexBufferListener = null; - } - - /** - * Dispatches the processed complex buffer to the downstream consumer. - * @param complexBuffer containing complex samples - */ - protected void broadcast(ComplexBuffer complexBuffer) - { - if(mComplexBufferListener != null) - { - mComplexBufferListener.receive(complexBuffer); - } - } - - @Override - public void start(ScheduledExecutorService executor) - { - //TODO: start a runnable to process downstream buffer dispatching - - if(mUpstreamSourceEventProcessor != null) - { - try - { - mUpstreamSourceEventProcessor.process(SourceEvent.startSampleStream(ComplexChannelSource.this)); - } - catch(SourceException se) - { - mLog.error("Error starting complex channel source"); - } - } - } - - @Override - public void stop() - { - if(mUpstreamSourceEventProcessor != null) - { - try - { - mUpstreamSourceEventProcessor.process(SourceEvent.stopSampleStream(ComplexChannelSource.this)); - } - catch(SourceException se) - { - mLog.error("Error starting complex channel source"); - } - } - - //TODO: cancel a runnable to process downstream buffer dispatching - } - - - /** - * Source Event processor interface for this source to receive source events - */ - @Override - public Listener getSourceEventListener() - { - return mSourceEventProcessor; - } - - /** - * Adapter for implementing ISourceEventProcessor and dispatching source events to subclasses via the - * ISourceEventProcessor.process() method. - */ - public class SourceEventProcessor implements Listener - { - @Override - public void receive(SourceEvent sourceEvent) - { - try - { - process(sourceEvent); - } - catch(SourceException se) - { - mLog.error("Error processing source event", se); - } - } - } -} diff --git a/src/source/tuner/TunerController.java b/src/source/tuner/TunerController.java index 50c4a98c..70dee706 100644 --- a/src/source/tuner/TunerController.java +++ b/src/source/tuner/TunerController.java @@ -21,12 +21,16 @@ import org.slf4j.Logger; import org.slf4j.LoggerFactory; import sample.Broadcaster; import sample.Listener; +import sample.complex.Complex; import sample.complex.ComplexBuffer; import sample.complex.IComplexBufferProvider; +import source.ISourceEventListener; import source.ISourceEventProcessor; import source.SourceEvent; import source.SourceEvent.Event; +import source.SourceEventListenerToProcessorAdapter; import source.SourceException; +import source.tuner.channel.CICTunerChannelSource; import source.tuner.channel.TunerChannel; import source.tuner.channel.TunerChannelSource; import source.tuner.configuration.TunerConfiguration; @@ -37,7 +41,7 @@ import java.util.SortedSet; import java.util.concurrent.ConcurrentSkipListSet; import java.util.concurrent.RejectedExecutionException; -public abstract class TunerController implements Tunable, ISourceEventProcessor, +public abstract class TunerController implements Tunable, ISourceEventProcessor, ISourceEventListener, IComplexBufferProvider, Listener { private final static Logger mLog = LoggerFactory.getLogger(TunerController.class); @@ -48,6 +52,7 @@ public abstract class TunerController implements Tunable, ISourceEventProcessor, protected FrequencyController mFrequencyController; private int mMiddleUnusable; private double mUsableBandwidthPercentage; + private Listener mSourceEventListener; /** * Abstract tuner controller class. The tuner controller manages frequency bandwidth and currently tuned channels @@ -65,6 +70,16 @@ public abstract class TunerController implements Tunable, ISourceEventProcessor, mFrequencyController = new FrequencyController(this, minimumFrequency, maximumFrequency, 0.0d); mMiddleUnusable = middleUnusable; mUsableBandwidthPercentage = usableBandwidth; + mSourceEventListener = new SourceEventListenerToProcessorAdapter(this); + } + + /** + * Implements the ISourceEventListener interface to receive requests from sample consumers + */ + @Override + public Listener getSourceEventListener() + { + return mSourceEventListener; } /** @@ -76,12 +91,29 @@ public abstract class TunerController implements Tunable, ISourceEventProcessor, * Responds to requests to set the frequency */ @Override - public void process(SourceEvent event ) throws SourceException + public void process(SourceEvent sourceEvent ) throws SourceException { - if( event.getEvent() == Event.REQUEST_FREQUENCY_CHANGE ) - { - setFrequency( event.getValue().longValue() ); - } + switch(sourceEvent.getEvent()) + { + case REQUEST_FREQUENCY_CHANGE: + setFrequency( sourceEvent.getValue().longValue() ); + break; + case REQUEST_START_SAMPLE_STREAM: + if(sourceEvent.getSource() instanceof Listener) + { + addComplexBufferListener((Listener)sourceEvent.getSource()); + } + break; + case REQUEST_STOP_SAMPLE_STREAM: + if(sourceEvent.getSource() instanceof Listener) + { + removeComplexBufferListener((Listener)sourceEvent.getSource()); + } + break; + default: + mLog.error("Ignoring unrecognized source event: " + sourceEvent.getEvent().name() + " from [" + + sourceEvent.getSource().getClass() + "]" ); + } } public int getBandwidth() @@ -116,7 +148,7 @@ public abstract class TunerController implements Tunable, ISourceEventProcessor, return mFrequencyController.canTune(frequency); } - public int getSampleRate() + public double getSampleRate() { return mFrequencyController.getSampleRate(); } @@ -224,7 +256,7 @@ public abstract class TunerController implements Tunable, ISourceEventProcessor, updateLOFrequency(); } - source = new TunerChannelSource(tuner, channel); + source = new CICTunerChannelSource(tuner, channel); } catch(SourceException se) { diff --git a/src/source/tuner/TunerModel.java b/src/source/tuner/TunerModel.java index 9680d732..5f4bea95 100644 --- a/src/source/tuner/TunerModel.java +++ b/src/source/tuner/TunerModel.java @@ -220,7 +220,7 @@ public class TunerModel extends AbstractTableModel implements Listener + * + ******************************************************************************/ +package source.tuner.channel; + +import dsp.filter.FilterFactory; +import dsp.filter.Window; +import dsp.filter.cic.ComplexPrimeCICDecimate; +import dsp.mixer.Oscillator; +import org.slf4j.Logger; +import org.slf4j.LoggerFactory; +import sample.Buffer; +import sample.Listener; +import sample.OverflowableTransferQueue; +import sample.complex.Complex; +import sample.complex.ComplexBuffer; +import source.SourceEvent; +import source.tuner.Tuner; + +import java.util.ArrayList; +import java.util.List; + +public class CICTunerChannelSource extends TunerChannelSource +{ + private final static Logger mLog = LoggerFactory.getLogger(CICTunerChannelSource.class); + + //Maximum number of filled buffers for the blocking queue + private static final int BUFFER_MAX_CAPACITY = 300; + + //Threshold for resetting buffer overflow condition + private static final int BUFFER_OVERFLOW_RESET_THRESHOLD = 100; + + private static double CHANNEL_RATE = 48000.0; + private static int CHANNEL_PASS_FREQUENCY = 12000; + + private OverflowableTransferQueue mBuffer; + + private Oscillator mFrequencyCorrectionMixer; + private ComplexPrimeCICDecimate mDecimationFilter; + private Listener mListener; + private List mSampleBuffers = new ArrayList(); + private long mTunerFrequency = 0; + private double mTunerSampleRate; + private long mChannelFrequencyCorrection = 0; + + /** + * Implements a heterodyne/decimate Digital Drop Channel (DDC) to decimate the IQ output from a tuner down to a + * fixed 48 kHz IQ channel rate. + * + * Note: this class can only be used once (started and stopped) and a new tuner channel source must be requested + * from the tuner once this object has been stopped. This is because channels are managed dynamically and center + * tuned frequency may have changed since this source was obtained and thus the tuner might no longer be able to + * source this channel once it has been stopped. + * + * @param tunerChannel specifying the center frequency for the DDC + */ + public CICTunerChannelSource(Tuner tuner, TunerChannel tunerChannel) + { + super(tuner.getTunerController().getSourceEventListener(), tunerChannel); + + mBuffer = new OverflowableTransferQueue<>(BUFFER_MAX_CAPACITY, BUFFER_OVERFLOW_RESET_THRESHOLD); + mBuffer.setSourceOverflowListener(this); + + //Setup the frequency translator to the current source frequency + mTunerFrequency = tuner.getTunerController().getFrequency(); + long frequencyOffset = mTunerFrequency - getTunerChannel().getFrequency(); + mFrequencyCorrectionMixer = new Oscillator(frequencyOffset, tuner.getTunerController().getSampleRate()); + + //Setup the decimation filter chain + setSampleRate(tuner.getTunerController().getSampleRate()); + } + + /** + * Sets the center frequency for the incoming sample buffers + * @param frequency in hertz + */ + @Override + protected void setFrequency(long frequency) + { + mTunerFrequency = frequency; + + //Reset frequency correction so that consumer components can adjust + setFrequencyCorrection(0); + + updateMixerFrequencyOffset(); + } + + /** + * Current frequency correction being applied to the channel sample stream + * @return correction in hertz + */ + @Override + public long getFrequencyCorrection() + { + return mChannelFrequencyCorrection; + } + + /** + * Changes the frequency correction value and broadcasts the change to the registered downstream listener. + * @param correction current frequency correction value. + */ + protected void setFrequencyCorrection(long correction) + { + mChannelFrequencyCorrection = correction; + + updateMixerFrequencyOffset(); + + broadcastConsumerSourceEvent(SourceEvent.frequencyCorrectionChange(mChannelFrequencyCorrection)); + } + + /** + * Primary input method for receiving complex sample buffers from the wideband source (ie tuner) + * @param buffer to receive and eventually process + */ + public void receive(ComplexBuffer buffer) + { + mBuffer.offer(buffer); + } + + @Override + public void setListener(Listener listener) + { + /* Save a pointer to the listener so that if we have to change the + * decimation filter, we can re-add the listener */ + mListener = listener; + + mDecimationFilter.setListener(listener); + } + + @Override + public void removeListener(Listener listener) + { + mDecimationFilter.removeListener(); + } + + /** + * Updates the sample rate to the requested value and notifies any downstream components of the change + * @param sampleRate to set + */ + protected void setSampleRate(double sampleRate) + { + if(mTunerSampleRate != sampleRate) + { + mFrequencyCorrectionMixer.setSampleRate(sampleRate); + + /* Get new decimation filter */ + mDecimationFilter = FilterFactory.getDecimationFilter((int)sampleRate, (int)CHANNEL_RATE, 1, + CHANNEL_PASS_FREQUENCY, 60, Window.WindowType.HAMMING); + + /* re-add the original output listener */ + mDecimationFilter.setListener(mListener); + + mTunerSampleRate = sampleRate; + + broadcastConsumerSourceEvent(SourceEvent.channelSampleRateChange(getSampleRate())); + } + } + + /** + * Calculates the local mixer frequency offset from the tuned frequency, + * channel's requested frequency, and channel frequency correction. + */ + private void updateMixerFrequencyOffset() + { + long offset = mTunerFrequency - getTunerChannel().getFrequency() - mChannelFrequencyCorrection; + mFrequencyCorrectionMixer.setFrequency(offset); + } + + public double getSampleRate() + { + return CHANNEL_RATE; + } + + @Override + protected void processSamples() + { + mBuffer.drainTo(mSampleBuffers, 20); + + for(Buffer buffer : mSampleBuffers) + { + float[] samples = buffer.getSamples(); + + /* We make a copy of the buffer so that we don't affect + * anyone else that is using the same buffer, like other + * channels or the spectral display */ + float[] translated = new float[samples.length]; + + /* Perform frequency translation */ + for(int x = 0; x < samples.length; x += 2) + { + mFrequencyCorrectionMixer.rotate(); + + translated[x] = Complex.multiplyInphase( + samples[x], samples[x + 1], mFrequencyCorrectionMixer.inphase(), mFrequencyCorrectionMixer.quadrature()); + + translated[x + 1] = Complex.multiplyQuadrature( + samples[x], samples[x + 1], mFrequencyCorrectionMixer.inphase(), mFrequencyCorrectionMixer.quadrature()); + } + + final ComplexPrimeCICDecimate filter = mDecimationFilter; + + if(filter != null) + { + filter.receive(new ComplexBuffer(translated)); + } + } + + mSampleBuffers.clear(); + } +} diff --git a/src/source/tuner/channel/DecimatingTunerChannelSource.java b/src/source/tuner/channel/DecimatingTunerChannelSource.java deleted file mode 100644 index b8530116..00000000 --- a/src/source/tuner/channel/DecimatingTunerChannelSource.java +++ /dev/null @@ -1,541 +0,0 @@ -/******************************************************************************* - * 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 source.tuner.channel; - -import dsp.filter.FilterFactory; -import dsp.filter.Window; -import dsp.filter.cic.ComplexPrimeCICDecimate; -import dsp.mixer.Oscillator; -import org.slf4j.Logger; -import org.slf4j.LoggerFactory; -import sample.Buffer; -import sample.Listener; -import sample.OverflowableTransferQueue; -import sample.complex.Complex; -import sample.complex.ComplexBuffer; -import sample.real.IOverflowListener; -import source.ISourceEventListener; -import source.ISourceEventProcessor; -import source.ISourceEventProvider; -import source.SourceEvent; -import source.SourceException; - -import java.util.ArrayList; -import java.util.List; -import java.util.concurrent.RejectedExecutionException; -import java.util.concurrent.ScheduledExecutorService; -import java.util.concurrent.ScheduledFuture; -import java.util.concurrent.TimeUnit; -import java.util.concurrent.atomic.AtomicBoolean; - -public class DecimatingTunerChannelSource extends TunerChannelSource -{ - private final static Logger mLog = LoggerFactory.getLogger(DecimatingTunerChannelSource.class); - - //Maximum number of filled buffers for the blocking queue - private static final int BUFFER_MAX_CAPACITY = 300; - - //Threshold for resetting buffer overflow condition - private static final int BUFFER_OVERFLOW_RESET_THRESHOLD = 100; - - private static int CHANNEL_RATE = 48000; - private static int CHANNEL_PASS_FREQUENCY = 12000; - - private OverflowableTransferQueue mBuffer; - - private TunerChannel mTunerChannel; - private Oscillator mMixer; - private ComplexPrimeCICDecimate mDecimationFilter; - private Listener mListener; - private ISourceEventProcessor mFrequencyChangeProcessor; - private DownstreamProcessor mDownstreamFrequencyEventProcessor = new DownstreamProcessor(); - private ScheduledFuture mTaskHandle; - - private long mTunerFrequency = 0; - private int mTunerSampleRate; - private int mChannelFrequencyCorrection = 0; - - private DecimationProcessor mDecimationProcessor = new DecimationProcessor(); - - private AtomicBoolean mRunning = new AtomicBoolean(); - private boolean mExpended = false; - - /** - * Provides a Digital Drop Channel (DDC) to decimate the IQ output from a - * tuner down to a 48 kHz IQ channel rate. - * - * Note: this class can only be used once (started and stopped) and a new - * tuner channel source must be requested from the tuner once this object - * has been stopped. This is because channels are managed dynamically and - * center tuned frequency may have changed since this source was obtained - * and thus the tuner might no longer be able to source this channel once it - * has been stopped. - * - * @param tunerChannel specifying the center frequency for the DDC - * @throws RejectedExecutionException if the thread pool manager cannot - * accept the decimation processing task - * @throws SourceException if the tuner has an issue providing IQ samples - */ - public DecimatingTunerChannelSource(Listener listener, TunerChannel tunerChannel, int inputSampleRate, - long frequency) - { - super(listener, tunerChannel); - - mTunerChannel = tunerChannel; - mTunerFrequency = frequency; - - mBuffer = new OverflowableTransferQueue<>(BUFFER_MAX_CAPACITY, BUFFER_OVERFLOW_RESET_THRESHOLD); - - /* Setup the frequency translator to the current source frequency */ - long frequencyOffset = mTunerFrequency - mTunerChannel.getFrequency(); - - mMixer = new Oscillator(frequencyOffset, inputSampleRate); - - /* Fire a sample rate change event to setup the decimation chain */ - try - { - process(SourceEvent.sampleRateChange(inputSampleRate)); - } - catch(SourceException se) - { - mLog.error("Error", se); - } - - } - - /** - * Changes the frequency correction value and broadcasts the change to the registered downstream listener. - * @param correction current frequency correction value. - */ - private void setFrequencyCorrection(int correction) - { - mChannelFrequencyCorrection = correction; - - updateMixerFrequencyOffset(); - - mDownstreamFrequencyEventProcessor.broadcast( - SourceEvent.channelFrequencyCorrectionChange(mChannelFrequencyCorrection)); - } - - /** - * Overrides the default source overflow listener management to delegate responsibility to the overflow buffer - */ - @Override - public void setOverflowListener(IOverflowListener listener) - { - mBuffer.setOverflowListener(listener); - } - - public void start(ScheduledExecutorService executor) - { - if(mExpended) - { - throw new IllegalStateException("Attempt to re-start an expended tuner channel source. TunerChannelSource" + - " objects can only be used once. "); - } - - if(mRunning.compareAndSet(false, true)) - { - //Broadcast current frequency and sample rate settings so all downstream components are aware - mDownstreamFrequencyEventProcessor.broadcastCurrentFrequency(); - mDownstreamFrequencyEventProcessor.broadcastCurrentSampleRate(); - - //Schedule the decimation task to run every 9 ms (111 iterations/second), an odd periodicity relative - //to the inbound periodicity of 20 ms, to attempt to avoid thread queue contention - mTaskHandle = executor.scheduleAtFixedRate(mDecimationProcessor, 0, 9, TimeUnit.MILLISECONDS); - - /* Finally, register to receive samples from the tuner */ - mSourceEventListener.receive(SourceEvent.startSampleStream(this)); - } - else - { - mLog.warn("Attempt to start() an already running tuner channel source was ignored"); - } - } - - @Override - public void reset() - { - } - - @Override - public void stop() - { - if(mRunning.compareAndSet(true, false)) - { - mSourceEventListener.receive(SourceEvent.stopSampleStream(this)); - mDecimationProcessor.shutdown(); - - if(mTaskHandle != null) - { - mTaskHandle.cancel(true); - mTaskHandle = null; - } - - mBuffer.clear(); - - mExpended = true; - } - else - { - mLog.warn("Attempt to stop() an already stopped tuner channel source was ignored"); - } - } - - - @Override - public void dispose() - { - } - - public TunerChannel getTunerChannel() - { - return mTunerChannel; - } - - @Override - public void receive(ComplexBuffer buffer) - { - if(mRunning.get()) - { - mBuffer.offer(buffer); - } - } - - public void setFrequencyChangeListener(ISourceEventProcessor processor) - { - mFrequencyChangeProcessor = processor; - } - - @Override - public void setListener(Listener listener) - { - /* Save a pointer to the listener so that if we have to change the - * decimation filter, we can re-add the listener */ - mListener = listener; - - mDecimationFilter.setListener(listener); - } - - @Override - public void removeListener(Listener listener) - { - mDecimationFilter.removeListener(); - } - - /** - * Handler for frequency change events received from the tuner and channel - * frequency correction events received from the channel consumer/listener - */ - @Override - public void process(SourceEvent event) throws SourceException - { - // Echo the event to the registered event listener - if(mFrequencyChangeProcessor != null) - { - mFrequencyChangeProcessor.process(event); - } - - switch(event.getEvent()) - { - case NOTIFICATION_FREQUENCY_CHANGE: - mTunerFrequency = event.getValue().longValue(); - updateMixerFrequencyOffset(); - - //Reset frequency correction so that downstream components can recalculate the value - setFrequencyCorrection(0); - break; - case NOTIFICATION_SAMPLE_RATE_CHANGE: - int sampleRate = event.getValue().intValue(); - setSampleRate(sampleRate); - break; - default: - break; - } - } - - /** - * Updates the sample rate to the requested value and notifies any downstream components of the change - * @param sampleRate to set - */ - private void setSampleRate(int sampleRate) - { - if(mTunerSampleRate != sampleRate) - { - mMixer.setSampleRate(sampleRate); - - /* Get new decimation filter */ - mDecimationFilter = FilterFactory.getDecimationFilter(sampleRate, CHANNEL_RATE, 1, - CHANNEL_PASS_FREQUENCY, 60, Window.WindowType.HAMMING); - - /* re-add the original output listener */ - mDecimationFilter.setListener(mListener); - - mTunerSampleRate = sampleRate; - - mDownstreamFrequencyEventProcessor.broadcastCurrentSampleRate(); - } - } - - /** - * Calculates the local mixer frequency offset from the tuned frequency, - * channel's requested frequency, and channel frequency correction. - */ - private void updateMixerFrequencyOffset() - { - long offset = mTunerFrequency - mTunerChannel.getFrequency() - mChannelFrequencyCorrection; - mMixer.setFrequency(offset); - } - - public int getSampleRate() throws SourceException - { - return CHANNEL_RATE; - } - - public long getFrequency() throws SourceException - { - return mTunerChannel.getFrequency(); - } - - /** - * Implements ISourceEventProvider to enable this source to broadcast frequency change events to downstream - * listeners. - * - * @param listener to receive downstream events - */ - @Override - public void setSourceEventListener(Listener listener) - { - mDownstreamFrequencyEventProcessor.setSourceEventListener(listener); - } - - /** - * Implements ISourceEventProvider to remove the frequency change listener from receiving down-stream frequency - * change events. - */ - @Override - public void removeSourceEventListener() - { - mDownstreamFrequencyEventProcessor.removeSourceEventListener(); - } - - /** - * Implements ISourceEventListener to receive frequency change events containing requests from downstream - * listeners to change frequency values. - * @return listener - */ - @Override - public Listener getSourceEventListener() - { - return mDownstreamFrequencyEventProcessor.getSourceEventListener(); - } - - /** - * Managers frequency change requests and notifications from/to any downstream component. Downstream - * components are those that receive samples from this tuner channel source. These downstream components will be - * notified of any frequency or sample rate change events and will also be able to request frequency correction - * updates. - */ - public class DownstreamProcessor implements ISourceEventListener, ISourceEventProvider, - Listener - { - //Listener to receive downstream events - private Listener mListener; - - /** - * Broadcasts the frequency change event to the downstream frequency change listener - * @param event to broadcast - */ - public void broadcast(SourceEvent event) - { - if(mListener != null) - { - mListener.receive(event); - } - } - - /** - * Broadcasts the current frequency of this tuner channel source to the downstream listener - */ - public void broadcastCurrentFrequency() - { - try - { - long frequency = getFrequency(); - broadcast(SourceEvent.frequencyChange(frequency)); - } - catch(SourceException se) - { - mLog.error("Error obtaining frequency from tuner to broadcast downstream"); - } - } - - /** - * Broadcasts the current decimated sample rate of this tuner channel source - */ - public void broadcastCurrentSampleRate() - { - try - { - //Note: downstream sample rate is currently a fixed value -- it will change in the future - broadcast(SourceEvent.sampleRateChange(getSampleRate())); - } - catch(SourceException se) - { - mLog.error("Error obtaining sample rate from tuner to broadcast downstream"); - } - } - - /** - * Sets the downstream listener to receive frequency change events from this tuner channel source - * @param listener to receive events - */ - @Override - public void setSourceEventListener(Listener listener) - { - mListener = listener; - } - - /** - * Removes the downstream listener from receiving frequency change events. - */ - @Override - public void removeSourceEventListener() - { - mListener = null; - } - - /** - * Listener for receiving frequency change events from downstream components - */ - @Override - public Listener getSourceEventListener() - { - return this; - } - - /** - * Processes frequency change events from downstream components. - * @param event to process - */ - @Override - public void receive(SourceEvent event) - { - switch(event.getEvent()) - { - //Frequency correction requests are the only change requests supported from downstream components - case REQUEST_CHANNEL_FREQUENCY_CORRECTION_CHANGE: - setFrequencyCorrection(event.getValue().intValue()); - break; - } - } - } - - /** - * Decimates an inbound buffer of I/Q samples from the source down to the - * standard 48000 channel sample rate - */ - public class DecimationProcessor implements Runnable - { - private boolean mProcessing = true; - private List mSampleBuffers = new ArrayList(); - - public void shutdown() - { - mProcessing = false; - } - - @Override - public void run() - { - /* General exception handler so that any errors won't kill the - * decimation thread and cause the input buffers to fill up and - * run the program out of memory */ - try - { - if(mProcessing) - { - //Send a heartbeat every time this runs to allow downstream components to perform periodic - //state monitoring functions on this thread - getHeartbeatManager().broadcast(); - - mBuffer.drainTo(mSampleBuffers, 20); - - for(Buffer buffer : mSampleBuffers) - { - /* Check to see if we've been shutdown */ - if(!mProcessing) - { - mBuffer.clear(); - return; - } - else - { - float[] samples = buffer.getSamples(); - - /* We make a copy of the buffer so that we don't affect - * anyone else that is using the same buffer, like other - * channels or the spectral display */ - float[] translated = new float[samples.length]; - - /* Perform frequency translation */ - for(int x = 0; x < samples.length; x += 2) - { - mMixer.rotate(); - - translated[x] = Complex.multiplyInphase( - samples[x], samples[x + 1], mMixer.inphase(), mMixer.quadrature()); - - translated[x + 1] = Complex.multiplyQuadrature( - samples[x], samples[x + 1], mMixer.inphase(), mMixer.quadrature()); - } - - if(mProcessing) - { - final ComplexPrimeCICDecimate filter = mDecimationFilter; - filter.receive(new ComplexBuffer(translated)); - } - } - } - - mSampleBuffers.clear(); - } - } - catch(Exception e) - { - /* Only log the stack trace if we're still processing */ - if(mProcessing) - { - mLog.error("Error encountered during decimation process", e); - } - } - catch(Throwable throwable) - { - mLog.error("Code error encountered during decimation process - channel thread will probably die", throwable); - } - - /* Check to see if we've been shutdown */ - if(!mProcessing) - { - mBuffer.clear(); - mSampleBuffers.clear(); - } - } - } -} diff --git a/src/source/tuner/channel/TunerChannelSource.java b/src/source/tuner/channel/TunerChannelSource.java index 54a2bdc3..c88c7b4d 100644 --- a/src/source/tuner/channel/TunerChannelSource.java +++ b/src/source/tuner/channel/TunerChannelSource.java @@ -18,51 +18,85 @@ ******************************************************************************/ package source.tuner.channel; -import dsp.filter.FilterFactory; -import dsp.filter.Window.WindowType; -import dsp.filter.cic.ComplexPrimeCICDecimate; import org.slf4j.Logger; import org.slf4j.LoggerFactory; -import sample.Buffer; import sample.Listener; -import sample.complex.Complex; import sample.complex.ComplexBuffer; -import sample.real.IOverflowListener; import source.ComplexSource; -import source.SourceException; -import source.ISourceEventListener; -import source.ISourceEventProvider; -import source.SourceEvent; import source.ISourceEventProcessor; +import source.SourceEvent; +import source.SourceEventListenerToProcessorAdapter; +import source.SourceException; +import util.ThreadPool; -import java.util.ArrayList; -import java.util.List; -import java.util.concurrent.ScheduledExecutorService; +import java.util.concurrent.ScheduledFuture; import java.util.concurrent.TimeUnit; -public abstract class TunerChannelSource extends ComplexSource implements ISourceEventProcessor, Listener +public abstract class TunerChannelSource extends ComplexSource implements ISourceEventProcessor { private final static Logger mLog = LoggerFactory.getLogger(TunerChannelSource.class); + private static final long BUFFER_PROCESSOR_RUN_INTERVAL_MILLISECONDS = 50; + private SourceEventListenerToProcessorAdapter mConsumerSourceEventListenerAdapter; private TunerChannel mTunerChannel; - protected Listener mSourceEventListener; + private Listener mProducerSourceEventListener; + private Listener mConsumerSourceEventListener; + private BufferProcessor mBufferProcessor = new BufferProcessor(); /** - * Requirements: - * 1. Provide complex buffer samples - * 2. Fetch complex buffer samples from subclass implementations (for distribution) - * 3. Provide heartbeat at each interval - * 4. Provide start/stop running - * 5. Provide support for downstream consumer source events (delegate to the sub class) - * 6. Support buffer overflow listeners + * Tuner Channel Source is a Digital Drop Channel (DDC) abstract class that defines the minimum functionality + * requirements for a channel sample source. * - * @param sourceEventListener to receive source dispose request - * @param tunerChannel + * @param producerSourceEventListener to receive source event requests (e.g. start/stop sample stream) + * @param tunerChannel describing the desired channel frequency and bandwidth/minimum sample rate */ - public TunerChannelSource(Listener sourceEventListener, TunerChannel tunerChannel) + public TunerChannelSource(Listener producerSourceEventListener, TunerChannel tunerChannel) { - mSourceEventListener = sourceEventListener; + mProducerSourceEventListener = producerSourceEventListener; + mTunerChannel = tunerChannel; + mConsumerSourceEventListenerAdapter = new SourceEventListenerToProcessorAdapter(this); } + @Override + public long getFrequency() + { + return mTunerChannel.getFrequency(); + } + + /** + * Sets the center frequency for the sample streaming being sent from the producer. + * @param frequency in hertz + */ + protected abstract void setFrequency(long frequency); + + /** + * Sets the sample rate of the incoming sample stream from the producer + * @param sampleRate in hertz + */ + protected abstract void setSampleRate(double sampleRate); + + /** + * Sets the frequency correction for the outbound sample stream to the consumer + * @param correction in hertz + */ + protected abstract void setFrequencyCorrection(long correction); + + /** + * Frequency correction value currently being applied to the outbound sample stream + */ + public abstract long getFrequencyCorrection(); + + /** + * Sets the listener to receive the complex buffer sample output from this channel + * @param complexBufferListener to receive complex buffers + */ + public abstract void setListener(Listener complexBufferListener); + + /** + * Commands sub-class to process queued samples and distribute them to the consumer. This method will be invoked + * by an interval timer. + */ + protected abstract void processSamples(); + /** * Tuner channel for this tuner channel source */ @@ -71,17 +105,27 @@ public abstract class TunerChannelSource extends ComplexSource implements ISourc return mTunerChannel; } + @Override + public void dispose() + { + stop(); + } + /** * Starts this tuner channel source producing sample stream. */ public void start() { - if(mSourceEventListener != null) + //Broadcast current frequency and sample rate so consumer can configure correctly + broadcastConsumerSourceEvent(SourceEvent.frequencyChange(getFrequency())); + broadcastConsumerSourceEvent(SourceEvent.sampleRateChange(getSampleRate())); + + if(mProducerSourceEventListener != null) { - mSourceEventListener.receive(SourceEvent.startSampleStream(this)); + mProducerSourceEventListener.receive(SourceEvent.startSampleStream(this)); } - //TODO: broadcast center frequency and sample rate to downstream listeners. + mBufferProcessor.start(); } /** @@ -90,11 +134,13 @@ public abstract class TunerChannelSource extends ComplexSource implements ISourc */ public void stop() { - if(mSourceEventListener != null) + if(mProducerSourceEventListener != null) { - mSourceEventListener.receive(SourceEvent.stopSampleStream(this)); - mSourceEventListener.receive(SourceEvent.sourceDisposeRequest(this)); + mProducerSourceEventListener.receive(SourceEvent.stopSampleStream(this)); + mProducerSourceEventListener.receive(SourceEvent.sourceDisposeRequest(this)); } + + mBufferProcessor.stop(); } @Override @@ -102,4 +148,159 @@ public abstract class TunerChannelSource extends ComplexSource implements ISourc { //Reset is not valid for a tuner channel source - ignored } + + /** + * This method is invoked after the buffer processor is completely shutdown so that this instance can perform any + * cleanup operations needed to dispose of this instance. + */ + protected void performDisposal() + { + mProducerSourceEventListener = null; + mConsumerSourceEventListener = null; + mConsumerSourceEventListenerAdapter.dispose(); + } + + /** + * Processes source event notifications received from the producer/parent and source event requests received from + * the consumer. + * + * @param sourceEvent to process + * @throws SourceException if there is an error while processing the source event + */ + @Override + public void process(SourceEvent sourceEvent) throws SourceException + { + switch(sourceEvent.getEvent()) + { + //Notification events from the producer + case NOTIFICATION_FREQUENCY_CHANGE: + setFrequency(sourceEvent.getValue().longValue()); + break; + case NOTIFICATION_SAMPLE_RATE_CHANGE: + setSampleRate(sourceEvent.getValue().doubleValue()); + break; + + //Request events from the consumer + case REQUEST_CHANNEL_FREQUENCY_CORRECTION_CHANGE: + setFrequencyCorrection(sourceEvent.getValue().longValue()); + break; + + default: + mLog.error("Ignoring unrecognized source event: " + sourceEvent.getEvent() + " from [" + + sourceEvent.getSource().getClass() + "]"); + break; + } + } + + /** + * Broadcasts the source event to the incoming sample stream producer. + * @param sourceEvent to broadcast + */ + protected void broadcastProducerSourceEvent(SourceEvent sourceEvent) + { + if(mProducerSourceEventListener != null) + { + mProducerSourceEventListener.receive(sourceEvent); + } + } + + /** + * Broadcasts the source event to the outbound sample stream consumer. + * @param sourceEvent to broadcast + */ + protected void broadcastConsumerSourceEvent(SourceEvent sourceEvent) + { + if(mConsumerSourceEventListener != null) + { + mConsumerSourceEventListener.receive(sourceEvent); + } + } + + /** + * Implements the ISourceEventListener interface for receiving source events (ie requests) from a consumer. + * + * Note: source events are simply transferred to the process() method in this class. + */ + @Override + public Listener getSourceEventListener() + { + return mConsumerSourceEventListenerAdapter; + } + + /** + * Implements the ISourceEventProvider interface to set the consumer listener for source events. + * @param listener to receive consumer source events + */ + @Override + public void setSourceEventListener(Listener listener) + { + mConsumerSourceEventListener = listener; + } + + /** + * Implements the ISourceEventProvider interface to remove the consumer listener for source events. + */ + @Override + public void removeSourceEventListener() + { + mConsumerSourceEventListener = null; + } + + /** + * Processor to periodically invoke buffer sample processing on an interval timer. At each interval this processor + * sends a heartbeat to the registered consumer and then commands the sub-class implementation to process any + * queued buffers and distribute complex buffer sample(s) to the registered consumer. + */ + public class BufferProcessor implements Runnable + { + private ScheduledFuture mScheduledFuture; + private boolean mStopped = false; + + /** + * Commands this processor to do a shutdown at the end of this or the next iteration. Once successfully + * shutdown, it will invoke the performDisposal() method to cleanup this instance. + */ + public void stop() + { + mStopped = true; + } + + /** + * Starts buffer processing on a fixed interval using a scheduled thread pool + */ + public void start() + { + if(mScheduledFuture == null) + { + mScheduledFuture = ThreadPool.SCHEDULED.scheduleAtFixedRate(this, 0, + BUFFER_PROCESSOR_RUN_INTERVAL_MILLISECONDS, TimeUnit.MILLISECONDS); + } + } + + /** + * Implementation of the Runnable interface to periodically send a heartbeat and then process buffer samples. + */ + @Override + public void run() + { + if(!mStopped) + { + getHeartbeatManager().broadcast(); + processSamples(); + } + + if(mStopped) + { + if(mScheduledFuture != null) + { + //Set may-interrupt to false so that we can complete this iteration + mScheduledFuture.cancel(false); + } + + mScheduledFuture = null; + + performDisposal(); + } + } + } } diff --git a/src/source/tuner/fcd/FCDTunerController.java b/src/source/tuner/fcd/FCDTunerController.java index 3b907683..b85721d0 100644 --- a/src/source/tuner/fcd/FCDTunerController.java +++ b/src/source/tuner/fcd/FCDTunerController.java @@ -174,7 +174,7 @@ public abstract class FCDTunerController extends TunerController /** * Sample rate of the tuner */ - public abstract int getCurrentSampleRate() throws SourceException; + public abstract double getCurrentSampleRate(); /** * Tuner class diff --git a/src/source/tuner/fcd/proV1/FCD1TunerController.java b/src/source/tuner/fcd/proV1/FCD1TunerController.java index 143f820f..587905b7 100644 --- a/src/source/tuner/fcd/proV1/FCD1TunerController.java +++ b/src/source/tuner/fcd/proV1/FCD1TunerController.java @@ -85,7 +85,7 @@ public class FCD1TunerController extends FCDTunerController } } - public int getCurrentSampleRate() + public double getCurrentSampleRate() { return SAMPLE_RATE; } diff --git a/src/source/tuner/fcd/proplusV2/FCD2TunerController.java b/src/source/tuner/fcd/proplusV2/FCD2TunerController.java index f6c39834..d1a99ff2 100644 --- a/src/source/tuner/fcd/proplusV2/FCD2TunerController.java +++ b/src/source/tuner/fcd/proplusV2/FCD2TunerController.java @@ -64,7 +64,7 @@ public class FCD2TunerController extends FCDTunerController } } - public int getCurrentSampleRate() + public double getCurrentSampleRate() { return SAMPLE_RATE; } diff --git a/src/source/tuner/frequency/FrequencyController.java b/src/source/tuner/frequency/FrequencyController.java index 86723b10..ce64fc36 100644 --- a/src/source/tuner/frequency/FrequencyController.java +++ b/src/source/tuner/frequency/FrequencyController.java @@ -36,8 +36,8 @@ public class FrequencyController private long mTunedFrequency = 101100000; private long mMinimumFrequency; private long mMaximumFrequency; - private double mFrequencyCorrection = 0d; - private int mSampleRate = 0; + private double mFrequencyCorrection = 0.0d; + private double mSampleRate = 0.0d; private ArrayList mProcessors = new ArrayList<>(); @@ -54,7 +54,7 @@ public class FrequencyController */ public int getBandwidth() { - return mSampleRate; + return (int)mSampleRate; } /** @@ -70,7 +70,7 @@ public class FrequencyController /** * Get sample rate in hertz */ - public int getSampleRate() + public double getSampleRate() { return mSampleRate; } @@ -266,7 +266,7 @@ public class FrequencyController /** * Gets the current bandwidth setting of the device */ - public int getCurrentSampleRate() throws SourceException; + public double getCurrentSampleRate() throws SourceException; /** * Indicates if this tunable can tune the frequency diff --git a/src/source/tuner/hackrf/HackRFTunerController.java b/src/source/tuner/hackrf/HackRFTunerController.java index 812f37f5..41b1b729 100644 --- a/src/source/tuner/hackrf/HackRFTunerController.java +++ b/src/source/tuner/hackrf/HackRFTunerController.java @@ -337,7 +337,7 @@ public class HackRFTunerController extends USBTunerController } @Override - public int getCurrentSampleRate() throws SourceException + public double getCurrentSampleRate() { return mSampleRate.getRate(); } @@ -531,7 +531,7 @@ public class HackRFTunerController extends USBTunerController write(Request.SET_SAMPLE_RATE, 0, 0, buffer); } - public int getSampleRate() + public double getSampleRate() { return mSampleRate.getRate(); } diff --git a/src/source/tuner/rtl/RTL2832TunerController.java b/src/source/tuner/rtl/RTL2832TunerController.java index 6102f951..372784e4 100644 --- a/src/source/tuner/rtl/RTL2832TunerController.java +++ b/src/source/tuner/rtl/RTL2832TunerController.java @@ -932,7 +932,7 @@ public abstract class RTL2832TunerController extends USBTunerController return false; } - public int getCurrentSampleRate() throws SourceException + public double getCurrentSampleRate() { return mSampleRate.getRate(); } diff --git a/src/source/tuner/test/TestTunerController.java b/src/source/tuner/test/TestTunerController.java index 5664d9d0..98ff7387 100644 --- a/src/source/tuner/test/TestTunerController.java +++ b/src/source/tuner/test/TestTunerController.java @@ -144,7 +144,7 @@ public class TestTunerController extends TunerController * Current sample rate for this tuner controller */ @Override - public int getCurrentSampleRate() throws SourceException + public double getCurrentSampleRate() { return mFrequencyController.getSampleRate(); } diff --git a/src/source/wave/ComplexWaveSource.java b/src/source/wave/ComplexWaveSource.java index f7d0c601..5c76c847 100644 --- a/src/source/wave/ComplexWaveSource.java +++ b/src/source/wave/ComplexWaveSource.java @@ -75,31 +75,15 @@ public class ComplexWaveSource extends ComplexSource implements IControllableFil return null; } - /** - * Not implemented - */ - @Override - public void setHeartbeatListener(Listener listener) - { - } - - /** - * Not implemented - */ - @Override - public void removeHeartbeatListener() - { - } - @Override public void reset() { stop(); - start(null); + start(); } @Override - public void start(ScheduledExecutorService executor) + public void start() { try { @@ -132,11 +116,11 @@ public class ComplexWaveSource extends ComplexSource implements IControllableFil } @Override - public int getSampleRate() + public double getSampleRate() { if(mInputStream != null) { - return (int)mInputStream.getFormat().getSampleRate(); + return mInputStream.getFormat().getSampleRate(); } return 0; diff --git a/src/source/wave/RealWaveSource.java b/src/source/wave/RealWaveSource.java index f64eef05..c0698e6a 100644 --- a/src/source/wave/RealWaveSource.java +++ b/src/source/wave/RealWaveSource.java @@ -73,33 +73,16 @@ public class RealWaveSource extends RealSource implements IControllableFileSourc return null; } - /** - * Not implemented - */ - @Override - public void setHeartbeatListener(Listener listener) - { - } - - /** - * Not implemented - */ - @Override - public void removeHeartbeatListener() - { - } - - @Override public void reset() { stop(); - start(null); + start(); } @Override - public void start(ScheduledExecutorService executor) + public void start() { try { @@ -133,11 +116,11 @@ public class RealWaveSource extends RealSource implements IControllableFileSourc } @Override - public int getSampleRate() + public double getSampleRate() { if(mInputStream != null) { - return (int)mInputStream.getFormat().getSampleRate(); + return mInputStream.getFormat().getSampleRate(); } return 0; diff --git a/src/spectrum/ChannelSpectrumPanel.java b/src/spectrum/ChannelSpectrumPanel.java index 87fabc1d..3e280c60 100644 --- a/src/spectrum/ChannelSpectrumPanel.java +++ b/src/spectrum/ChannelSpectrumPanel.java @@ -90,7 +90,7 @@ public class ChannelSpectrumPanel extends JPanel mDFTConverter.addListener( mSpectrumPanel ); /* Set the DFTProcessor to the decimated 24kHz sample rate */ - mDFTProcessor.process(SourceEvent.sampleRateChange(24000)); + mDFTProcessor.process(SourceEvent.sampleRateChange(24000.0)); initGui(); } diff --git a/src/spectrum/SpectralDisplayPanel.java b/src/spectrum/SpectralDisplayPanel.java index c050e27c..496708b9 100644 --- a/src/spectrum/SpectralDisplayPanel.java +++ b/src/spectrum/SpectralDisplayPanel.java @@ -447,7 +447,7 @@ public class SpectralDisplayPanel extends JPanel implements Listener)mDFTProcessor); + mTuner.getTunerController().removeComplexBufferListener(mDFTProcessor); mTuner = null; } } From f38184fd83d50263320861f7675752f6e6625d19 Mon Sep 17 00:00:00 2001 From: Denny Date: Sun, 10 Dec 2017 06:26:44 -0500 Subject: [PATCH 35/87] #39 WIP - finishes polyphase channel source processing of frequency offset calculation and updating for channel frequency correction. --- .../channelizer/PolyphaseChannelManager.java | 5 +- .../channelizer/PolyphaseChannelSource.java | 60 +++++++++++++------ .../output/ChannelOutputProcessor.java | 29 +++------ .../IPolyphaseChannelOutputProcessor.java | 19 ++---- 4 files changed, 56 insertions(+), 57 deletions(-) diff --git a/src/dsp/filter/channelizer/PolyphaseChannelManager.java b/src/dsp/filter/channelizer/PolyphaseChannelManager.java index aa95a3c3..985174a7 100644 --- a/src/dsp/filter/channelizer/PolyphaseChannelManager.java +++ b/src/dsp/filter/channelizer/PolyphaseChannelManager.java @@ -303,14 +303,17 @@ public class PolyphaseChannelManager implements Listener { List indexes = mChannelCalculator.getChannelIndexes(channelSource.getTunerChannel()); + long centerFrequency = mChannelCalculator.getCenterFrequencyForIndexes(indexes); + //If the indexes size is the same then update the current processor, otherwise create a new one if(channelSource.getPolyphaseChannelOutputProcessor().getInputChannelCount() == indexes.size()) { channelSource.getPolyphaseChannelOutputProcessor().setPolyphaseChannelIndices(indexes); + channelSource.setFrequency(centerFrequency); } else { - channelSource.setPolyphaseChannelOutputProcessor(getOutputProcessor(indexes)); + channelSource.setPolyphaseChannelOutputProcessor(getOutputProcessor(indexes), centerFrequency); } } catch(IllegalArgumentException iae) diff --git a/src/dsp/filter/channelizer/PolyphaseChannelSource.java b/src/dsp/filter/channelizer/PolyphaseChannelSource.java index fea1ce78..6fa882f7 100644 --- a/src/dsp/filter/channelizer/PolyphaseChannelSource.java +++ b/src/dsp/filter/channelizer/PolyphaseChannelSource.java @@ -36,6 +36,8 @@ public class PolyphaseChannelSource extends TunerChannelSource private ComplexToComplexBufferAssembler mComplexBufferAssembler = new ComplexToComplexBufferAssembler(2500); private IPolyphaseChannelOutputProcessor mPolyphaseChannelOutputProcessor; private double mChannelSampleRate; + private long mCenterFrequency; + private long mFrequencyCorrection; /** * Polyphase channelizer tuner channel source implementation. Adapts the channel array output samples from the @@ -84,8 +86,11 @@ public class PolyphaseChannelSource extends TunerChannelSource /** * Sets/updates the output processor for this channel source, replacing the existing output processor. + * + * @param outputProcessor to use + * @param frequency center for the channels processed by the output processor */ - public void setPolyphaseChannelOutputProcessor(IPolyphaseChannelOutputProcessor outputProcessor) + public void setPolyphaseChannelOutputProcessor(IPolyphaseChannelOutputProcessor outputProcessor, long frequency) { //Lock on the channel output processor to block the channel results processor thread from servicing the //channel output processor's buffer while we change out the processors. @@ -108,6 +113,13 @@ public class PolyphaseChannelSource extends TunerChannelSource { existingProcessor.processChannelResults(mComplexBufferAssembler); } + + //Finally, setup the frequency offset for the output processor and reset the frequency correction value + //to allow consumers to adjust and calculate a new frequency correction value, if needed. + mCenterFrequency = frequency; + mFrequencyCorrection = 0; + mPolyphaseChannelOutputProcessor.setFrequencyOffset(getFrequencyOffset()); + broadcastConsumerSourceEvent(SourceEvent.frequencyCorrectionChange(mFrequencyCorrection)); } } @@ -153,14 +165,7 @@ public class PolyphaseChannelSource extends TunerChannelSource @Override public long getFrequencyCorrection() { - IPolyphaseChannelOutputProcessor processor = mPolyphaseChannelOutputProcessor; - - if(processor != null) - { - return processor.getFrequencyCorrection(); - } - - return 0; + return mFrequencyCorrection; } /** @@ -171,24 +176,20 @@ public class PolyphaseChannelSource extends TunerChannelSource */ protected void setFrequencyCorrection(long value) { - if(mPolyphaseChannelOutputProcessor != null) - { - mPolyphaseChannelOutputProcessor.setFrequencyCorrection(value); - broadcastConsumerSourceEvent(SourceEvent.frequencyCorrectionChange(value)); - } + mFrequencyCorrection = value; + updateFrequencyOffset(); + broadcastConsumerSourceEvent(SourceEvent.frequencyCorrectionChange(mFrequencyCorrection)); } /** - * Sets the center frequency for the incoming sample stream channel results + * Sets the center frequency for the incoming sample stream channel results and resets frequency correction to zero. * @param frequency in hertz */ @Override protected void setFrequency(long frequency) { - if(mPolyphaseChannelOutputProcessor != null) - { - mPolyphaseChannelOutputProcessor.setFrequency(frequency); - } + mCenterFrequency = frequency; + setFrequencyCorrection(0); } @Override @@ -200,4 +201,25 @@ public class PolyphaseChannelSource extends TunerChannelSource mPolyphaseChannelOutputProcessor.processChannelResults(mComplexBufferAssembler); } } + + /** + * Calculates the frequency offset required to mix the incoming signal to the desired output baseband + */ + private long getFrequencyOffset() + { + return getTunerChannel().getFrequency() - mCenterFrequency + mFrequencyCorrection; + } + + /** + * Updates the frequency offset being applied by the output processor. Calculates the offset using the center + * frequency of the incoming polyphase channel results and the desired output channel frequency adjusted by any + * requested frequency correction value. + */ + private void updateFrequencyOffset() + { + synchronized(mPolyphaseChannelOutputProcessor) + { + mPolyphaseChannelOutputProcessor.setFrequencyOffset(getFrequencyOffset()); + } + } } diff --git a/src/dsp/filter/channelizer/output/ChannelOutputProcessor.java b/src/dsp/filter/channelizer/output/ChannelOutputProcessor.java index d4f9a095..c1504f7f 100644 --- a/src/dsp/filter/channelizer/output/ChannelOutputProcessor.java +++ b/src/dsp/filter/channelizer/output/ChannelOutputProcessor.java @@ -60,16 +60,15 @@ public abstract class ChannelOutputProcessor implements IPolyphaseChannelOutputP mChannelResultsQueue = new OverflowableTransferQueue<>((int)(sampleRate * 3), (int)(sampleRate * 0.5)); } + /** + * Sets the frequency offset to apply to the incoming samples to mix the desired signal to baseband. + * @param frequencyOffset in hertz + */ @Override - public void setFrequency(long frequency) + public void setFrequencyOffset(long frequencyOffset) { - - } - - @Override - public long getFrequencyCorrection() - { - return 0; + mFrequencyCorrectionMixer.setFrequency(frequencyOffset); + mFrequencyCorrectionEnabled = (frequencyOffset != 0); } @Override @@ -160,20 +159,6 @@ public abstract class ChannelOutputProcessor implements IPolyphaseChannelOutputP mFrequencyCorrectionMixer.quadrature()); } - /** - * Specifies a frequency correction value to be applied to channel samples output from this polyphase channelizer - * output processor. - * - * @param frequencyCorrection offset value - */ - @Override - public void setFrequencyCorrection(long frequencyCorrection) - { - mFrequencyCorrectionMixer.setFrequency(frequencyCorrection); - - mFrequencyCorrectionEnabled = (frequencyCorrection != 0); - } - /** * Indicates if this channel has a frequency correction offset */ diff --git a/src/dsp/filter/channelizer/output/IPolyphaseChannelOutputProcessor.java b/src/dsp/filter/channelizer/output/IPolyphaseChannelOutputProcessor.java index 2dd26684..e285fd70 100644 --- a/src/dsp/filter/channelizer/output/IPolyphaseChannelOutputProcessor.java +++ b/src/dsp/filter/channelizer/output/IPolyphaseChannelOutputProcessor.java @@ -40,23 +40,12 @@ public interface IPolyphaseChannelOutputProcessor void processChannelResults(ComplexSampleListener listener); /** - * Sets the center frequency of the incoming channel results samples + * Sets the desired frequency offset from center. The samples will be mixed with an oscillator set to this offset + * frequency to produce an output where the desired signal is centered in the passband. + * * @param frequency in hertz */ - void setFrequency(long frequency); - - /** - * Specifies the frequency correction (+/-) that should be applied to samples extracted from the polyphase - * channelizer output results. - * - * @param frequencyCorrection correction value - */ - void setFrequencyCorrection(long frequencyCorrection); - - /** - * Current frequency correction value. - */ - long getFrequencyCorrection(); + void setFrequencyOffset(long frequency); /** * Indicates the number of input channels processed by this output processor From 318e6f3586f41ac22032e6b53111ec1d71ec9c1b Mon Sep 17 00:00:00 2001 From: Denny Date: Mon, 11 Dec 2017 20:27:01 -0500 Subject: [PATCH 36/87] #39 WIP - changes tuner controllers to expose the frequency controller and the channel manager in preparation for polyphase. --- .../AbstractComplexPolyphaseChannelizer.java | 12 +- .../filter/channelizer/ChannelCalculator.java | 588 ------------------ .../channelizer/ChannelDistributor.java | 107 ---- .../filter/channelizer/ChannelizerViewer.java | 342 ---------- .../ComplexPolyphaseChannelizerM2.java | 4 +- .../channelizer/PolyphaseChannelManager.java | 366 ----------- .../channelizer/PolyphaseChannelSource.java | 225 ------- .../channelizer/ScheduledBufferProcessor.java | 171 ----- .../output/ChannelOutputProcessor.java | 170 ----- .../IPolyphaseChannelOutputProcessor.java | 67 -- .../output/OneChannelOutputProcessor.java | 97 --- .../output/TwoChannelOutputProcessor.java | 91 --- src/module/ProcessingChain.java | 8 +- src/source/tuner/Tuner.java | 21 - src/source/tuner/TunerController.java | 330 ++-------- src/source/tuner/TunerEditor.java | 2 +- src/source/tuner/TunerModel.java | 4 +- .../tuner/airspy/AirspyTunerController.java | 50 +- .../tuner/airspy/AirspyTunerEditor.java | 2 +- src/source/tuner/channel/ChannelManager.java | 337 ++++++++++ .../FrequencyController.java | 88 ++- ...> FrequencyControllerLockedException.java} | 30 +- .../tuner/channel/TunerChannelSource.java | 15 +- .../{ => cic}/CICTunerChannelSource.java | 13 +- .../TunerConfigurationModel.java | 2 +- .../tuner/fcd/proV1/FCD1TunerController.java | 6 +- .../tuner/fcd/proV1/FCD1TunerEditor.java | 2 +- .../fcd/proplusV2/FCD2TunerController.java | 8 +- .../tuner/fcd/proplusV2/FCD2TunerEditor.java | 2 +- .../frequency/FrequencyLimitException.java | 36 -- .../tuner/hackrf/HackRFTunerController.java | 12 +- .../tuner/hackrf/HackRFTunerEditor.java | 2 +- .../tuner/manager/AbstractSourceManager.java | 111 ---- .../tuner/manager/TunerSourceManager.java | 83 --- .../tuner/rtl/e4k/E4KTunerController.java | 6 +- src/source/tuner/rtl/e4k/E4KTunerEditor.java | 2 +- .../tuner/rtl/r820t/R820TTunerController.java | 4 +- .../tuner/rtl/r820t/R820TTunerEditor.java | 2 +- src/spectrum/SpectralDisplayPanel.java | 4 +- 39 files changed, 547 insertions(+), 2875 deletions(-) delete mode 100644 src/dsp/filter/channelizer/ChannelCalculator.java delete mode 100644 src/dsp/filter/channelizer/ChannelDistributor.java delete mode 100644 src/dsp/filter/channelizer/ChannelizerViewer.java delete mode 100644 src/dsp/filter/channelizer/PolyphaseChannelManager.java delete mode 100644 src/dsp/filter/channelizer/PolyphaseChannelSource.java delete mode 100644 src/dsp/filter/channelizer/ScheduledBufferProcessor.java delete mode 100644 src/dsp/filter/channelizer/output/ChannelOutputProcessor.java delete mode 100644 src/dsp/filter/channelizer/output/IPolyphaseChannelOutputProcessor.java delete mode 100644 src/dsp/filter/channelizer/output/OneChannelOutputProcessor.java delete mode 100644 src/dsp/filter/channelizer/output/TwoChannelOutputProcessor.java create mode 100644 src/source/tuner/channel/ChannelManager.java rename src/source/tuner/{frequency => channel}/FrequencyController.java (73%) rename src/source/tuner/channel/{TunerChannelProvider.java => FrequencyControllerLockedException.java} (61%) rename src/source/tuner/channel/{ => cic}/CICTunerChannelSource.java (94%) delete mode 100644 src/source/tuner/frequency/FrequencyLimitException.java delete mode 100644 src/source/tuner/manager/AbstractSourceManager.java delete mode 100644 src/source/tuner/manager/TunerSourceManager.java diff --git a/src/dsp/filter/channelizer/AbstractComplexPolyphaseChannelizer.java b/src/dsp/filter/channelizer/AbstractComplexPolyphaseChannelizer.java index 5aee3dcf..0505c1b0 100644 --- a/src/dsp/filter/channelizer/AbstractComplexPolyphaseChannelizer.java +++ b/src/dsp/filter/channelizer/AbstractComplexPolyphaseChannelizer.java @@ -25,8 +25,8 @@ import sample.Listener; import sample.complex.ComplexBuffer; import source.ISourceEventListener; import source.SourceEvent; +import source.tuner.channel.polyphase.PolyphaseChannelSource; -import java.util.Arrays; import java.util.List; import java.util.concurrent.CopyOnWriteArrayList; @@ -36,7 +36,7 @@ public abstract class AbstractComplexPolyphaseChannelizer implements Listener mSourceChangeBroadcaster = new Broadcaster(); private List mChannels = new CopyOnWriteArrayList<>(); - private int mSampleRate; + private double mSampleRate; private int mChannelCount; private double mChannelSampleRate; @@ -45,7 +45,7 @@ public abstract class AbstractComplexPolyphaseChannelizer implements Listener - * - ******************************************************************************/ -package dsp.filter.channelizer; - -import org.slf4j.Logger; -import org.slf4j.LoggerFactory; -import source.tuner.channel.TunerChannel; - -import java.text.DecimalFormat; -import java.util.ArrayList; -import java.util.List; - -public class ChannelCalculator -{ - private final static Logger mLog = LoggerFactory.getLogger(ChannelCalculator.class); - - /** - * Policy to determine desired corrective action for methods that calculate an index for a frequency or a frequency - * from an index where the frequency may exist either at the overlapping boundary of two indexes, or in the middle - * of the wrap-around (N/2) index. - */ - private enum IndexBoundaryPolicy - { - ADJUST_POSITIVE, - ADJUST_NEGATIVE - } - - private int mSampleRate; - private int mChannelCount; - private double mCenterFrequency; - private double mChannelBandwidth; - private double mOversampling = 1.0; - - /** - * Calculates channel index(es) from a polyphase channelizer output to source a DDC polyphase channel source - * with a specified channel center frequency and bandwidth. - * - * Polyphase channel indexes are not linear/consecutive owing to the use of the FFT to rotate channels to their - * center frequencies following filtering in the polyphase filter bank. As such, this calculator provides methods - * to determine the min/center/max frequency of each channel that is adjusted for the current center frequency - * value and to determine the channel index(es) that need to be targeted to produce a requested output channel - * with a specific frequency and bandwdith. - * - * @param sampleRate in hertz for the input sample rate from the sample source (ie tuner) - * @param channelCount number of channels in the polyphase filter bank - * @param centerFrequency center frequency for the input sample stream - * @param oversampling set to 1.0 for no oversample, or greater than 1.0 for oversampling - */ - public ChannelCalculator(int sampleRate, int channelCount, double centerFrequency, double oversampling) - { - mSampleRate = sampleRate; - mChannelCount = channelCount; - mCenterFrequency = centerFrequency; - mOversampling = oversampling; - - updateChannelBandwidth();; - } - - /** - * Calculates the channel bandwidth from the sample rate and channel count. - */ - private void updateChannelBandwidth() - { - mChannelBandwidth = (double)mSampleRate / (double)mChannelCount; - } - - /** - * Center frequency for the channelizer - */ - public double getCenterFrequency() - { - return mCenterFrequency; - } - - /** - * Updates the center frequency for the channelizer - */ - public void setCenterFrequency(double frequency) - { - mCenterFrequency = frequency; - } - - /** - * Number of channels being channelized by the polyphase channelizer - */ - public int getChannelCount() - { - return mChannelCount; - } - - /** - * Updates the number of channels being channelized by the polyphase channelizer - */ - public void setChannelCount(int channelCount) - { - mChannelCount = channelCount; - updateChannelBandwidth(); - } - - /** - * Identifies the index of the wrap-around channel meaning that the lower half of the channel exists at the upper - * end of the spectrum and the upper half of the channel exists at the lower end of the spectrum - * - * @return wrap-around channel index - */ - public int getWrapAroundIndex() - { - return mChannelCount / 2; - } - - /** - * PolyphaseChannelManager input sample rate - * @return input sample rate in hertz - */ - public int getSampleRate() - { - return mSampleRate; - } - - /** - * Updates the channel count using the sample rate argument to calculate the number of channels from the - * already established channel bandwidth. - * - * @param sampleRate to use for channel count - */ - public void setSampleRate(int sampleRate) - { - setChannelCount((int)((double)sampleRate / getChannelBandwidth())); - } - - /** - * Channel bandwidth. Note: this value may be smaller than the channel sample rate when oversampling - * is being used. - */ - public double getChannelBandwidth() - { - return mChannelBandwidth; - } - - /** - * Half of the channel bandwidth. - */ - public double getHalfChannelBandwidth() - { - return mChannelBandwidth / 2.0; - } - - /** - * Oversampling rate produced by the channelizer. - * - * @return 1.0 for no oversampling, or a value greater than 1.0 when oversampling is applied. - */ - public double getOversampling() - { - return mOversampling; - } - - /** - * Sets the oversampling rate being applied by the polyphase channelizer - * @param oversampling rate where 1.0 indicates no oversampling, and a value greater than 1.0 indicates - * that the polyphase channelizer is oversampling each channel. - */ - public void setOversampling(double oversampling) - { - mOversampling = oversampling; - } - - /** - * Channel sample rate. This value is the product of the filtered channel bandwidth times the - * oversampling rate. - */ - public double getChannelSampleRate() - { - return (double)getChannelBandwidth() * getOversampling(); - } - - /** - * PolyphaseChannelManager input minimum frequency - * @return minimum frequency in hertz - */ - public long getMinimumFrequency() - { - return (long)(getCenterFrequency() - (getSampleRate() / 2.0)); - } - - /** - * PolyphaseChannelManager input maximum frequency - * @return maximum frequency in hertz - */ - public long getMaximumFrequency() - { - return (long)(getCenterFrequency() + (getSampleRate() / 2.0)); - } - - /** - * Identifies the channelizer channel index(es) required to produce a tuner channel with the specified center - * frequency and channel bandwidth. - * - * @param tunerChannel specifying the desired channel frequency and bandwidth - * - * @return an array of one or more polyphase channelizer index(es) to target in order to produce an output channel - * for the desired frequency and bandwidth. - * - * @throws IllegalArgumentException if the tuner channel's minimum or maximum frequency is not contained - * within the channelizer's minimum or maximum frequency range - */ - public List getChannelIndexes(TunerChannel tunerChannel) throws IllegalArgumentException - { - if(tunerChannel.getMinFrequency() < getMinimumFrequency() || - tunerChannel.getMaxFrequency() > getMaximumFrequency()) - { - throw new IllegalArgumentException("Requested channel cannot be provided by this " + - "channelizer. Requested channel [" + tunerChannel.getMinFrequency() + " - " + - tunerChannel.getMaxFrequency() + "] exceeds current channelizer frequency range [" + - getMinimumFrequency() + " - " + getMaximumFrequency() + "]"); - } - - int minIndex = getIndexForFrequency(tunerChannel.getMinFrequency(), IndexBoundaryPolicy.ADJUST_POSITIVE); - int maxIndex = getIndexForFrequency(tunerChannel.getMaxFrequency(), IndexBoundaryPolicy.ADJUST_NEGATIVE); - - //Detect the unsourceable situation where the wrap around index would be included in both the minimum index - //and the maximum index, meaning that the tuner channel bandwidth is nearly the same as the input sample rate. - if(minIndex == getWrapAroundIndex() && maxIndex == getWrapAroundIndex()) - { - throw new IllegalArgumentException("Requested tuner channel cannot be provided. Requested bandwidth is " + - "within two channel bandwidths of the sample rate."); - } - - List indexes = new ArrayList<>(); - - indexes.add(minIndex); - - if(minIndex != maxIndex) - { - //Ensure our min/max indexes are within the correct bounds 0 <> N-1 to safeguard the while loop below - if(minIndex < 0 || minIndex >= getChannelCount() || maxIndex < 0 || maxIndex >= getChannelCount()) - { - throw new IllegalArgumentException("Something went wrong while calculating the min and max polyphase " + - "channel indexes for tuner channel: " + tunerChannel); - } - - int pointer = minIndex + 1; - - if(pointer >= getChannelCount()) - { - pointer -= getChannelCount(); - } - - while(pointer != maxIndex) - { - indexes.add(pointer); - - pointer++; - - if(pointer >= getChannelCount()) - { - pointer -= getChannelCount(); - } - } - - indexes.add(maxIndex); - } - - return indexes; - } - - /** - * Calculates the index for the specified frequency. Certain frequencies can exist as both the maximum frequency - * of one channel and the minimum frequency of an adjacent channel. The index boundary policy identifies your - * choice to select the higher frequency channel index (ADJUST_POSITIVE) or the lower frequency channel - * index (ADJUST_NEGATIVE). - * - * @param frequency to determine channel index for - * @param indexBoundaryPolicy to determine the behavior when the frequency overlaps two channel boundaries - * @return index for the frequency - */ - public int getIndexForFrequency(long frequency, IndexBoundaryPolicy indexBoundaryPolicy) - { - double offset = frequency - getCenterFrequency(); - - if(Math.abs(offset) < getHalfChannelBandwidth()) - { - return 0; //Center index - } - - if(offset > 0) - { - offset += getHalfChannelBandwidth(); - } - else - { - offset -= getHalfChannelBandwidth(); - } - - int indexOffset = (int)(offset / getChannelBandwidth()); - - if(indexOffset < 0) - { - indexOffset += getChannelCount(); - } - - if(indexBoundaryPolicy == IndexBoundaryPolicy.ADJUST_POSITIVE && - isOverlapFrequency(frequency, indexOffset, indexOffset + 1)) - { - indexOffset = normalize(indexOffset + 1); - } - else if(indexBoundaryPolicy == IndexBoundaryPolicy.ADJUST_NEGATIVE && - isOverlapFrequency(frequency, indexOffset - 1, indexOffset)) - { - indexOffset = normalize(indexOffset - 1); - } - - return indexOffset; - } - - /** - * Indicates if the frequency is an overlapping frequency between channel index 1 and channel index 2 where the - * maximum frequency of index 1 is the same as the minimum frequency of index 2 and both of these frequencies are - * the same as the frequency argument. - * - * Note: both index 1 and index 2 will be normalized to the range of 0 <> N-1 prior to determining overlap - * - * @param frequency to test for overlap between index 1 and 2 - * @param index1 which is the lower channel index located next to index 2. - * @param index2 which is the higher channel index located next to index 1. - * @return true if frequency = index1 maximum frequency = index2 minimum frequency - */ - public boolean isOverlapFrequency(long frequency, int index1, int index2) - { - index1 = normalize(index1); - index2 = normalize(index2); - - //Check that these two indexes are side by side - int delta = normalize(index2 - index1); - - if(delta != 1) - { - return false; - } - - long index1MaximumFrequency = (long)getIndexMaximumFrequency(index1, IndexBoundaryPolicy.ADJUST_POSITIVE); - long index2MinimumFrequency = (long)getIndexMinimumFrequency(index2, IndexBoundaryPolicy.ADJUST_NEGATIVE); - - if(index1 == getWrapAroundIndex()) - { - index1MaximumFrequency = (long)getIndexMaximumFrequency(index1, IndexBoundaryPolicy.ADJUST_NEGATIVE); - } - - if(index2 == getWrapAroundIndex()) - { - index2MinimumFrequency = (long)getIndexMinimumFrequency(index2, IndexBoundaryPolicy.ADJUST_POSITIVE); - } - - return frequency == index1MaximumFrequency && frequency == index2MinimumFrequency; - } - - /** - * Normalizes the index to the range of 0 <> N-1 channel indexes - */ - private int normalize(int index) - { - while(index < 0) - { - index += getChannelCount(); - } - - while(index >= getChannelCount()) - { - index -= getChannelCount(); - } - - return index; - } - - /** - * Calculates the center frequency for the channel identified by the index in the channel results array. - * - * @param index to determine the channel center frequency - * @return center frequency for the indexed channel - */ - public double getIndexCenterFrequency(int index, IndexBoundaryPolicy indexBoundaryPolicy) - { - if(index < 0 || index >= getChannelCount()) - { - throw new IllegalArgumentException("Illegal channel index"); - } - - int wrapAroundIndex = getWrapAroundIndex(); - - if(index == wrapAroundIndex) - { - if(indexBoundaryPolicy == IndexBoundaryPolicy.ADJUST_POSITIVE) - { - return getCenterFrequency() + (index * getChannelBandwidth()); - } - else - { - return getCenterFrequency() - (index * getChannelBandwidth()); - } - } - else if(index < wrapAroundIndex) - { - return getCenterFrequency() + (index * getChannelBandwidth()); - } - else - { - return getCenterFrequency() - ((getChannelCount() - index) * getChannelBandwidth()); - } - } - - /** - * Calculates the minimum frequency band edge for the channel identified by the index in the channel results - * array. - * @param index to calculate the minimum band edge frequency for the channel. - * @param indexBoundaryPolicy for the wrap around index to indicate if this method should return the minimum - * frequency of the upper spectrum half of the wrap around channel (ADJUST_POSITIVE) or the minimum frequency of the - * lower spectrum half of the wrap around channel (ADJUST_NEGATIVE) - * @return frequency of the minimum band edge - */ - public double getIndexMinimumFrequency(int index, IndexBoundaryPolicy indexBoundaryPolicy) - { - if(index < 0 || index >= getChannelCount()) - { - throw new IllegalArgumentException("Can't determine minimum frequency - illegal channel index [" + - index + "]"); - } - - int wrapAroundIndex = getWrapAroundIndex(); - - if(index == wrapAroundIndex) - { - if(indexBoundaryPolicy == IndexBoundaryPolicy.ADJUST_POSITIVE) - { - return getCenterFrequency() + ((double)index * getChannelBandwidth()) - getHalfChannelBandwidth(); - } - else - { - return getIndexCenterFrequency(index, indexBoundaryPolicy); - } - } - else if(index <= wrapAroundIndex) - { - return getCenterFrequency() + ((double)index * getChannelBandwidth()) - getHalfChannelBandwidth(); - } - else - { - return getCenterFrequency() - ((double)(getChannelCount() - index) * getChannelBandwidth()) - - getHalfChannelBandwidth(); - } - } - - /** - * Calculates the maximum frequency band edge for the channel identified by the index in the channel results - * array. - * @param index to calculate the maximum band edge frequency for the channel. - * @param indexBoundaryPolicy for the wrap around index to indicate if this method should return the maximum - * frequency of the upper spectrum half of the wrap around channel (ADJUST_POSITIVE) or the maximum frequency of the - * lower spectrum half of the wrap around channel (ADJUST_NEGATIVE) - * @return frequency of the maximum band edge - */ - public double getIndexMaximumFrequency(int index, IndexBoundaryPolicy indexBoundaryPolicy) - { - if(index < 0 || index >= getChannelCount()) - { - throw new IllegalArgumentException("Can't determine maximum frequency - illegal channel index [" + - index + "]"); - } - - int wrapAroundIndex = getWrapAroundIndex(); - - if(index == wrapAroundIndex) - { - if(indexBoundaryPolicy == IndexBoundaryPolicy.ADJUST_POSITIVE) - { - return getIndexCenterFrequency(index, indexBoundaryPolicy); - } - else - { - return getCenterFrequency() - ((double)index * getChannelBandwidth() - getHalfChannelBandwidth()); - } - } - else if(index <= wrapAroundIndex) - { - return getCenterFrequency() + ((double)index * getChannelBandwidth()) + getHalfChannelBandwidth(); - } - else - { - return getCenterFrequency() - ((double)(getChannelCount() - index) * getChannelBandwidth()) + - getHalfChannelBandwidth(); - } - } - - /** - * Calculates the center frequency for a channel that will be synthesized from the channel indexes. - * @param indexes that are contiguous in the spectrum - * @return center frequency in hertz - */ - public long getCenterFrequencyForIndexes(List indexes) - { - if(indexes.isEmpty()) - { - throw new IllegalArgumentException("Indexes cannot be empty"); - } - - int middleIndex = indexes.get(indexes.size() / 2); - - if(indexes.size() % 2 == 0) - { - //Even number of indexes - return (long)getIndexMinimumFrequency(middleIndex, IndexBoundaryPolicy.ADJUST_POSITIVE); - } - else - { - //Odd number of indexes - return (long)getIndexCenterFrequency(middleIndex, IndexBoundaryPolicy.ADJUST_POSITIVE); - } - } - - /** - * String representation of the channel calculator settings - */ - public String toString() - { - StringBuilder sb = new StringBuilder(); - sb.append("Channels:").append(getChannelCount()); - sb.append(" Sample Rate:").append(getSampleRate()); - sb.append(" Channel Bandwidth:").append(getChannelBandwidth()); - sb.append(" Channel Rate:").append(getChannelSampleRate()); - sb.append(" Min:").append(getMinimumFrequency()); - sb.append(" Max:").append(getMaximumFrequency()); - return sb.toString(); - } - - - public static void main(String[] args) - { - mLog.debug("Starting ...."); - - int channelBandwidth = 12500; - int sampleRate = 100000; //Airspy - int channelCount = sampleRate / channelBandwidth; - double centerFrequency = 455000000; - - ChannelCalculator calculator = new ChannelCalculator(sampleRate, channelCount, centerFrequency, 2.0); - - mLog.debug(calculator.toString()); - - DecimalFormat decimalFormat = new DecimalFormat("000,000,000.0"); - - int wrapAroundIndex = calculator.getWrapAroundIndex(); - - IndexBoundaryPolicy indexBoundaryPolicy = IndexBoundaryPolicy.ADJUST_NEGATIVE; - - for(int index = wrapAroundIndex; index < calculator.getChannelCount(); index++) - { - mLog.debug("Index:" + index + " Min:" + decimalFormat.format(calculator.getIndexMinimumFrequency(index, indexBoundaryPolicy)) + - " Center:" + decimalFormat.format(calculator.getIndexCenterFrequency(index, indexBoundaryPolicy)) + - " Max:" + decimalFormat.format(calculator.getIndexMaximumFrequency(index, indexBoundaryPolicy))); - } - - indexBoundaryPolicy = IndexBoundaryPolicy.ADJUST_POSITIVE; - - for(int index = 0; index <= wrapAroundIndex; index++) - { - mLog.debug("Index:" + index + " Min:" + decimalFormat.format(calculator.getIndexMinimumFrequency(index, indexBoundaryPolicy)) + - " Center:" + decimalFormat.format(calculator.getIndexCenterFrequency(index, indexBoundaryPolicy)) + - " Max:" + decimalFormat.format(calculator.getIndexMaximumFrequency(index, indexBoundaryPolicy))); - } - - mLog.debug("Finished"); - } -} diff --git a/src/dsp/filter/channelizer/ChannelDistributor.java b/src/dsp/filter/channelizer/ChannelDistributor.java deleted file mode 100644 index a3cca8ac..00000000 --- a/src/dsp/filter/channelizer/ChannelDistributor.java +++ /dev/null @@ -1,107 +0,0 @@ -/******************************************************************************* - * 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; -import java.util.Queue; - -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++] = quadrature; - - 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 deleted file mode 100644 index c072a9d0..00000000 --- a/src/dsp/filter/channelizer/ChannelizerViewer.java +++ /dev/null @@ -1,342 +0,0 @@ -/******************************************************************************* - * 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 net.miginfocom.swing.MigLayout; -import org.slf4j.Logger; -import org.slf4j.LoggerFactory; -import sample.Listener; -import sample.SampleType; -import sample.complex.ComplexBuffer; -import settings.SettingsManager; -import source.ISourceEventProcessor; -import source.SourceEvent; -import source.SourceException; -import source.tuner.channel.TunerChannel; -import source.tuner.channel.TunerChannelSource; -import source.tuner.configuration.TunerConfigurationModel; -import source.tuner.test.TestTuner; -import spectrum.DFTProcessor; -import spectrum.DFTSize; -import spectrum.SpectrumPanel; -import spectrum.converter.ComplexDecibelConverter; -import util.ThreadPool; - -import javax.swing.*; -import javax.swing.event.ChangeEvent; -import javax.swing.event.ChangeListener; -import java.awt.*; - -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 JPanel mControlPanel; - private PrimarySpectrumPanel mPrimarySpectrumPanel; - private ChannelArrayPanel mChannelPanel; - private int mChannelCount; - private int mChannelsPerRow; - private long mToneFrequency; - private DFTSize mMainPanelDFTSize = DFTSize.FFT32768; - private DFTSize mChannelPanelDFTSize = DFTSize.FFT01024; - private TestTuner mTestTuner; - - /** - * GUI Test utility for researching polyphase channelizers. - * - * @param channelsPerRow - */ - public ChannelizerViewer(int channelsPerRow) - { - mTestTuner = new TestTuner(); - - mChannelCount = (int)(mTestTuner.getTunerController().getSampleRate() / CHANNEL_BANDWIDTH); - mChannelsPerRow = channelsPerRow; - - mToneFrequency = mTestTuner.getTunerController().getFrequency() + 19200; - - mTestTuner.getTunerController().setToneFrequency(mToneFrequency); - - init(); - } - - private void init() - { - setTitle("TunerChannelizer Viewer"); - setSize(1000, 800); - 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]")); - mPrimaryPanel.add(getSpectrumPanel(), "wrap"); - mPrimaryPanel.add(getControlPanel(), "wrap"); - mPrimaryPanel.add(getChannelArrayPanel()); - } - - return mPrimaryPanel; - } - - private PrimarySpectrumPanel getSpectrumPanel() - { - if(mPrimarySpectrumPanel == null) - { - mPrimarySpectrumPanel = new PrimarySpectrumPanel(mSettingsManager, - mTestTuner.getTunerController().getSampleRate()); - mPrimarySpectrumPanel.setPreferredSize(new Dimension(1000, 200)); - mPrimarySpectrumPanel.setDFTSize(mMainPanelDFTSize); - mTestTuner.getSourceManager().addComplexBufferListener(mPrimarySpectrumPanel); - } - - return mPrimarySpectrumPanel; - } - - private JPanel getControlPanel() - { - if(mControlPanel == null) - { - mControlPanel = new JPanel(); - mControlPanel.setLayout(new MigLayout("insets 0 0 0 0", "", "")); - - mControlPanel.add(new JLabel("Tone:"), "align left"); - int maximumFrequencyOffset = (int)(mTestTuner.getTunerController().getSampleRate() / 2); - long minimumFrequency = mTestTuner.getTunerController().getFrequency() - maximumFrequencyOffset; - long maximumFrequency = mTestTuner.getTunerController().getFrequency() + maximumFrequencyOffset; - - SpinnerNumberModel model = new SpinnerNumberModel(mToneFrequency, minimumFrequency, maximumFrequency, - 100 ); - - model.addChangeListener(new ChangeListener() - { - @Override - public void stateChanged(ChangeEvent e) - { - int toneFrequency = model.getNumber().intValue(); - mTestTuner.getTunerController().setToneFrequency(toneFrequency); - } - }); - - JSpinner spinner = new JSpinner(model); - - mControlPanel.add(spinner); - mControlPanel.add(new JLabel("Hz")); - - mControlPanel.add(new JLabel("Sample Rate: " + mTestTuner.getTunerController().getSampleRate() + " Hz"), "push,align right"); - - mControlPanel.add(new JLabel("Max: " + (mTestTuner.getTunerController().getSampleRate() / 2) + " Hz"), "push,align right"); - mControlPanel.add(new JLabel("Channels: " + mChannelCount), "push,align right"); - } - - return mControlPanel; - } - - private ChannelArrayPanel getChannelArrayPanel() - { - if(mChannelPanel == null) - { - mChannelPanel = new ChannelArrayPanel(); - } - - return mChannelPanel; - } - - public class ChannelArrayPanel extends JPanel - { - public ChannelArrayPanel() - { - int bufferSize = CHANNEL_BANDWIDTH / CHANNEL_FFT_FRAME_RATE; - if(bufferSize % 2 == 1) - { - bufferSize++; - } - - init(); - } - - private void init() - { - setLayout(new MigLayout("insets 0 0 0 0", "fill", "fill")); - - double spectralBandwidth = mTestTuner.getTunerController().getSampleRate(); - double halfSpectralBandwidth = spectralBandwidth / 2.0; - - int channelToLog = -1; - - for(int x = 0; x < mChannelCount; x++) - { - //place the channels left and right of 10.0 MHz - long frequency = (long)(mTestTuner.getTunerController().getFrequency() + - ((x * CHANNEL_BANDWIDTH) - halfSpectralBandwidth) + (CHANNEL_BANDWIDTH / 2)); - - mLog.debug("Channel " + x + " Frequency: " + frequency); - - ChannelPanel channelPanel = new ChannelPanel(mSettingsManager, CHANNEL_BANDWIDTH * 2, frequency, CHANNEL_BANDWIDTH, (x == channelToLog)); - channelPanel.setDFTSize(mChannelPanelDFTSize); - - if(x % mChannelsPerRow == mChannelsPerRow - 1) - { - add(channelPanel, "grow,push,wrap 2px"); - } - else - { - add(channelPanel, "grow,push"); - } - } - } - } - - public class PrimarySpectrumPanel extends JPanel implements Listener, ISourceEventProcessor - { - private DFTProcessor mDFTProcessor = new DFTProcessor(SampleType.COMPLEX); - private ComplexDecibelConverter mComplexDecibelConverter = new ComplexDecibelConverter(); - private SpectrumPanel mSpectrumPanel; - - public PrimarySpectrumPanel(SettingsManager settingsManager, double sampleRate) - { - setLayout(new MigLayout("insets 0 0 0 0", "[grow,fill]", "[grow,fill]")); - mSpectrumPanel = new SpectrumPanel(settingsManager); - add(mSpectrumPanel); - - mDFTProcessor.addConverter(mComplexDecibelConverter); - mDFTProcessor.process(SourceEvent.sampleRateChange(sampleRate)); - mComplexDecibelConverter.addListener(mSpectrumPanel); - } - - public void setDFTSize(DFTSize dftSize) - { - mDFTProcessor.setDFTSize(dftSize); - } - - @Override - public void receive(ComplexBuffer complexBuffer) - { - mDFTProcessor.receive(complexBuffer); - } - - @Override - public void process(SourceEvent event) throws SourceException - { - mLog.debug("Source Event! Add handler support for this to channelizer viewer"); - } - } - - public class ChannelPanel extends JPanel implements Listener, ISourceEventProcessor - { - private TunerChannelSource mSource; - private DFTProcessor mDFTProcessor = new DFTProcessor(SampleType.COMPLEX); - private ComplexDecibelConverter mComplexDecibelConverter = new ComplexDecibelConverter(); - private SpectrumPanel mSpectrumPanel; - private long mFrequency; - - public ChannelPanel(SettingsManager settingsManager, double sampleRate, long frequency, int bandwidth, boolean enableLogging) - { - setLayout(new MigLayout("insets 0 0 0 0", "[grow,fill]", "[grow,fill]")); - mSpectrumPanel = new SpectrumPanel(settingsManager); - add(mSpectrumPanel); - - mDFTProcessor.addConverter(mComplexDecibelConverter); - mDFTProcessor.process(SourceEvent.sampleRateChange(sampleRate)); - - mComplexDecibelConverter.addListener(mSpectrumPanel); - - mFrequency = frequency; - - TunerChannel tunerChannel = new TunerChannel(frequency, bandwidth); - mSource = mTestTuner.getSourceManager().getSource(tunerChannel); - - if(mSource != null) - { - if(enableLogging) - { - mSource.setListener(new Listener() - { - @Override - public void receive(ComplexBuffer complexBuffer) - { - mLog.debug("Sending samples to DFT processor: " + complexBuffer.getSamples().length); - mDFTProcessor.receive(complexBuffer); - } - }); - } - else - { - mSource.setListener(mDFTProcessor); - } - - mSource.start(); - } - else - { - mLog.error("Couldn't get a source from the tuner for frequency: " + frequency); - } - } - - public TunerChannelSource getSource() - { - return mSource; - } - - public void setDFTSize(DFTSize dftSize) - { - mDFTProcessor.setDFTSize(dftSize); - } - - @Override - public void receive(ComplexBuffer complexBuffer) - { - mLog.debug("Got a buffer for the channel dft processor"); - mDFTProcessor.receive(complexBuffer); - } - - @Override - public void process(SourceEvent event) throws SourceException - { - mLog.debug("Source Event! Add handler support for this to channelizer viewer"); - } - } - - public static void main(String[] args) - { - int channelsPerRow = 4; - - final ChannelizerViewer frame = new ChannelizerViewer(channelsPerRow); - - EventQueue.invokeLater(new Runnable() - { - @Override - public void run() - { - frame.setVisible(true); - } - }); - } - -} diff --git a/src/dsp/filter/channelizer/ComplexPolyphaseChannelizerM2.java b/src/dsp/filter/channelizer/ComplexPolyphaseChannelizerM2.java index ebd8eb5f..d3bdded6 100644 --- a/src/dsp/filter/channelizer/ComplexPolyphaseChannelizerM2.java +++ b/src/dsp/filter/channelizer/ComplexPolyphaseChannelizerM2.java @@ -78,7 +78,7 @@ public class ComplexPolyphaseChannelizerM2 extends AbstractComplexPolyphaseChann * @param sampleRate to be channelized. * @param tapsPerFilter to use when designing the filter */ - public ComplexPolyphaseChannelizerM2(int sampleRate, int tapsPerFilter) throws FilterDesignException + public ComplexPolyphaseChannelizerM2(double sampleRate, int tapsPerFilter) throws FilterDesignException { super(sampleRate, (int)(sampleRate / DEFAULT_MINIMUM_CHANNEL_BANDWIDTH)); @@ -94,7 +94,7 @@ public class ComplexPolyphaseChannelizerM2 extends AbstractComplexPolyphaseChann * @param sampleRate in hertz */ @Override - public void setSampleRate(int sampleRate) + public void setSampleRate(double sampleRate) { try { diff --git a/src/dsp/filter/channelizer/PolyphaseChannelManager.java b/src/dsp/filter/channelizer/PolyphaseChannelManager.java deleted file mode 100644 index 985174a7..00000000 --- a/src/dsp/filter/channelizer/PolyphaseChannelManager.java +++ /dev/null @@ -1,366 +0,0 @@ -/******************************************************************************* - * 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 dsp.filter.channelizer.output.IPolyphaseChannelOutputProcessor; -import dsp.filter.channelizer.output.OneChannelOutputProcessor; -import dsp.filter.channelizer.output.TwoChannelOutputProcessor; -import dsp.filter.design.FilterDesignException; -import org.slf4j.Logger; -import org.slf4j.LoggerFactory; -import sample.Listener; -import sample.complex.IComplexBufferProvider; -import source.Source; -import source.SourceEvent; -import source.SourceException; -import source.tuner.channel.TunerChannel; - -import java.util.List; -import java.util.concurrent.CopyOnWriteArrayList; - -public class PolyphaseChannelManager implements Listener -{ - private final static Logger mLog = LoggerFactory.getLogger(PolyphaseChannelManager.class); - private static final int CHANNEL_BANDWIDTH = 12500; - private static final double CHANNEL_OVERSAMPLING = 2.0; - private static final int CHANNEL_SAMPLE_RATE = (int)(CHANNEL_BANDWIDTH * CHANNEL_OVERSAMPLING); - private static final int POLYPHASE_FILTER_TAPS_PER_CHANNEL = 17; - - private IComplexBufferProvider mComplexBufferProvider; - private List mChannelSources = new CopyOnWriteArrayList<>(); - private ChannelCalculator mChannelCalculator; - private ComplexPolyphaseChannelizerM2 mPolyphaseChannelizer; - private ChannelSourceEventListener mChannelSourceEventListener = new ChannelSourceEventListener(); - private ScheduledBufferProcessor mBufferProcessor; - - /** - * Polyphase Channel Manager is a DDC channel manager and complex buffer queue/processor for a tuner. This class - * provides DDC polyphase channel sources and wraps a polyphase channelizer processing sample buffers produced by - * the tuner and distributing channelized sample buffers to each allocated DDC polyphase channel source. This - * class is responsible for monitoring the tuner for changes in center frequency and/or sample rate and updating - * active DDC polyphase channel sources accordingly. This class also monitors source event requests and - * notifications received from active DDC polyphase channel sources to adjust sample streams as required. - * - * Note: add this channel manager as a source event listener to the complex buffer provider to ensure this manager - * adapts to changes in source frequency and sample rate. - * - * @param complexBufferProvider that supports register/deregister of this channel manager - * @param frequency of the provided complex buffer samples - * @param sampleRate of the provided complex buffer samples - */ - public PolyphaseChannelManager(IComplexBufferProvider complexBufferProvider, long frequency, int sampleRate) - { - if(complexBufferProvider == null) - { - throw new IllegalArgumentException("Complex buffer provider argument cannot be null"); - } - - mComplexBufferProvider = complexBufferProvider; - - //Ensure that sample rate is a multiple of channel sample rate since polyphase channelizer is M2 - if(sampleRate % (CHANNEL_SAMPLE_RATE) != 0) - { - throw new IllegalArgumentException("Tuner sample rate [" + sampleRate + "] must be an even multiple of " + - CHANNEL_SAMPLE_RATE + " Hz"); - } - - int channelCount = sampleRate / CHANNEL_BANDWIDTH; - - mChannelCalculator = new ChannelCalculator(sampleRate, channelCount, frequency, CHANNEL_OVERSAMPLING); - - mBufferProcessor = new ScheduledBufferProcessor(500, 100, 50, 50); - - updateChannelizer(sampleRate, null); - } - - /** - * Provides a Digital Drop Channel (DDC) for the specified tuner channel or returns null if the channel can't be - * sourced due to the current center frequency and/or sample rate. - * @param tunerChannel specifying center frequency and bandwidth. - * @return source or null. - */ - public Source getChannel(TunerChannel tunerChannel) - { - PolyphaseChannelSource channelSource = null; - - try - { - List polyphaseIndexes = mChannelCalculator.getChannelIndexes(tunerChannel); - - IPolyphaseChannelOutputProcessor outputProcessor = getOutputProcessor(polyphaseIndexes); - - if(outputProcessor != null) - { - channelSource = new PolyphaseChannelSource(tunerChannel, outputProcessor, mChannelSourceEventListener, - CHANNEL_SAMPLE_RATE); - - mChannelSources.add(channelSource); - } - } - catch(IllegalArgumentException iae) - { - mLog.info("Can't provide DDC for " + tunerChannel.toString() + " due to channelizer frequency [" + - mChannelCalculator.getCenterFrequency() + "] and sample rate [" + - (mChannelCalculator.getChannelCount() * mChannelCalculator.getChannelBandwidth()) + "]"); - } - - return channelSource; - } - - /** - * Creates a processor to process the channelizer channel indexes into a composite output stream providing - * channelized complex sample buffers to a registered source listener. - * @param indexes to target by the output processor - * @return output processor compatible with the number of indexes to monitor - */ - private IPolyphaseChannelOutputProcessor getOutputProcessor(List indexes) - { - switch(indexes.size()) - { - case 1: - return new OneChannelOutputProcessor(CHANNEL_SAMPLE_RATE, indexes); - case 2: - return new TwoChannelOutputProcessor(CHANNEL_SAMPLE_RATE, indexes); - 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()); - return null; - } - } - - /** - * Starts/adds the channel source to receive channelized sample buffers, registering with the tuner to receive - * sample buffers when this is the first channel. - * - * @param channelSource to start - */ - private void startChannelSource(PolyphaseChannelSource channelSource) - { - synchronized(mBufferProcessor) - { - mPolyphaseChannelizer.addChannel(channelSource); - - if(mPolyphaseChannelizer.getRegisteredChannelCount() == 1) - { - mComplexBufferProvider.addComplexBufferListener(mBufferProcessor); - mBufferProcessor.start(); - } - } - } - - /** - * Stops/removes the channel source from receiving channelized sample buffers and deregisters from the tuner - * when this is the last channel being sourced. - * - * @param channelSource to stop - */ - private void stopChannelSource(PolyphaseChannelSource channelSource) - { - synchronized(mBufferProcessor) - { - mChannelSources.remove(channelSource); - - if(mPolyphaseChannelizer.getRegisteredChannelCount() == 0) - { - mComplexBufferProvider.removeComplexBufferListener(mBufferProcessor); - mBufferProcessor.stop(); - } - } - } - - /** - * Implements the Listener interface for receiving notifications of frequency and sample rate - * changes from the tuner. - */ - @Override - public void receive(SourceEvent sourceEvent) - { - switch(sourceEvent.getEvent()) - { - case NOTIFICATION_FREQUENCY_CHANGE: - //Update the index calculator and each of the output processors to update each channel frequency - mChannelCalculator.setCenterFrequency(sourceEvent.getValue().longValue()); - updateOutputProcessors(sourceEvent); - break; - case NOTIFICATION_SAMPLE_RATE_CHANGE: - //Update the channelizer (and the output processors) for the new sample rate - updateChannelizer(sourceEvent.getValue().intValue(), sourceEvent); - break; - } - } - - /** - * Creates or updates the channelizer to process the incoming sample rate and updates any channel processors. - * - * @param sampleRate to use for the channelizer - * @param sourceEvent containing a notification of sample rate change (optional - can be null). This source - * event will be passed to all output processors to ensure the change is fully promulgated before restarting the - * buffer processor if it was already running. - */ - private void updateChannelizer(int sampleRate, SourceEvent sourceEvent) - { - mChannelCalculator.setSampleRate(sampleRate); - - //Lock on the buffer processor to block any channel create/start/stop requests while we configure - synchronized(mBufferProcessor) - { - //Get current processor state so we can restart it after we update the channelizer - boolean bufferProcessorWasRunning = mBufferProcessor.isRunning(); - - //Stop will be ignored if it is not currently running - mBufferProcessor.stop(); - - try - { - if(mPolyphaseChannelizer == null) - { - mPolyphaseChannelizer = new ComplexPolyphaseChannelizerM2(sampleRate, - POLYPHASE_FILTER_TAPS_PER_CHANNEL); - - mBufferProcessor.setListener(mPolyphaseChannelizer); - } - else - { - mPolyphaseChannelizer.setSampleRate(sampleRate); - } - } - catch(IllegalArgumentException iae) - { - mLog.error("Could not create polyphase channelizer for sample rate [" + sampleRate + "]", iae); - } - catch(FilterDesignException fde) - { - mLog.error("Could not create filter for polyphase channelizer for sample rate [" + sampleRate + "]", fde); - } - - if(sourceEvent != null && sourceEvent.getEvent() == SourceEvent.Event.NOTIFICATION_SAMPLE_RATE_CHANGE) - { - updateOutputProcessors(sourceEvent); - } - - if(bufferProcessorWasRunning) - { - mBufferProcessor.start(); - } - } - } - - /** - * Updates each of the output processors for any changes in the tuner's center frequency or sample rate, which - * would cause the output processors to change the polyphase channelizer results channel(s) that the processor is - * consuming - * - * @param sourceEvent that requires an update to the output processors - */ - private void updateOutputProcessors(SourceEvent sourceEvent) - { - for(PolyphaseChannelSource channelSource: mChannelSources) - { - updateOutputProcessor(channelSource); - - //Notify the channel source that the frequency or sample rate has changed so that it can reset - //any frequency correction value. - if(sourceEvent != null) - { - try - { - channelSource.process(sourceEvent); - } - catch(SourceException se) - { - mLog.error("Error while notifying polyphase channel source of a source event", se); - } - } - } - } - - /** - * Updates the polyphase channel source's output processor due to a change in the center frequency or sample - * rate for the source providing sample buffers to the polyphase channelizer, or whenever the DDC channel's - * center tuned frequency changes. - * - * @param channelSource that requires an update to its output processor - */ - private void updateOutputProcessor(PolyphaseChannelSource channelSource) - { - try - { - List indexes = mChannelCalculator.getChannelIndexes(channelSource.getTunerChannel()); - - long centerFrequency = mChannelCalculator.getCenterFrequencyForIndexes(indexes); - - //If the indexes size is the same then update the current processor, otherwise create a new one - if(channelSource.getPolyphaseChannelOutputProcessor().getInputChannelCount() == indexes.size()) - { - channelSource.getPolyphaseChannelOutputProcessor().setPolyphaseChannelIndices(indexes); - channelSource.setFrequency(centerFrequency); - } - else - { - channelSource.setPolyphaseChannelOutputProcessor(getOutputProcessor(indexes), centerFrequency); - } - } - catch(IllegalArgumentException iae) - { - mLog.error("Error updating polyphase channel source - can't determine output channel indexes for " + - "updated tuner center frequency and sample rate. Stopping channel source", iae); - - stopChannelSource(channelSource); - } - } - - /** - * Internal class for handling requests for start/stop sample stream from polyphase channel sources - */ - private class ChannelSourceEventListener implements Listener - { - @Override - public void receive(SourceEvent sourceEvent) - { - switch(sourceEvent.getEvent()) - { - case REQUEST_START_SAMPLE_STREAM: - if(sourceEvent.hasSource() && sourceEvent.getSource() instanceof PolyphaseChannelSource) - { - startChannelSource((PolyphaseChannelSource)sourceEvent.getSource()); - } - else - { - mLog.error("Request to start sample stream for unrecognized source: " + - (sourceEvent.hasSource() ? sourceEvent.getSource().getClass() : "null source")); - } - break; - case REQUEST_STOP_SAMPLE_STREAM: - if(sourceEvent.hasSource() && sourceEvent.getSource() instanceof PolyphaseChannelSource) - { - stopChannelSource((PolyphaseChannelSource)sourceEvent.getSource()); - } - else - { - mLog.error("Request to stop sample stream for unrecognized source: " + - (sourceEvent.hasSource() ? sourceEvent.getSource().getClass() : "null source")); - } - break; - default: - mLog.error("Received unrecognized source event from polyphase channel source [" + - sourceEvent.getEvent() + "]"); - } - } - } -} diff --git a/src/dsp/filter/channelizer/PolyphaseChannelSource.java b/src/dsp/filter/channelizer/PolyphaseChannelSource.java deleted file mode 100644 index 6fa882f7..00000000 --- a/src/dsp/filter/channelizer/PolyphaseChannelSource.java +++ /dev/null @@ -1,225 +0,0 @@ -/******************************************************************************* - * 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 dsp.filter.channelizer.output.IPolyphaseChannelOutputProcessor; -import org.slf4j.Logger; -import org.slf4j.LoggerFactory; -import sample.Listener; -import sample.complex.ComplexBuffer; -import sample.complex.ComplexToComplexBufferAssembler; -import sample.real.IOverflowListener; -import source.SourceEvent; -import source.tuner.channel.TunerChannel; -import source.tuner.channel.TunerChannelSource; - -public class PolyphaseChannelSource extends TunerChannelSource -{ - private final static Logger mLog = LoggerFactory.getLogger(PolyphaseChannelSource.class); - - private ComplexToComplexBufferAssembler mComplexBufferAssembler = new ComplexToComplexBufferAssembler(2500); - private IPolyphaseChannelOutputProcessor mPolyphaseChannelOutputProcessor; - private double mChannelSampleRate; - private long mCenterFrequency; - private long mFrequencyCorrection; - - /** - * Polyphase channelizer tuner channel source implementation. Adapts the channel array output samples from the - * polyphase channelizer into a single channel, or a channel synthesized from two adjacent channels that is - * frequency translated and decimated to a single channel. - * - * @param tunerChannel describing the desired channel frequency and bandwidth/minimum sample rate - * @param outputProcessor - to process polyphase channelizer channel results into a channel stream - * @param producerSourceEventListener to receive source event requests (e.g. start/stop sample stream) - * @param channelSampleRate for the downstream sample output - */ - public PolyphaseChannelSource(TunerChannel tunerChannel, IPolyphaseChannelOutputProcessor outputProcessor, - Listener producerSourceEventListener, double channelSampleRate) - { - super(producerSourceEventListener, tunerChannel); - mPolyphaseChannelOutputProcessor = outputProcessor; - mChannelSampleRate = channelSampleRate; - } - - /** - * Registers the listener to receive complex sample buffers from this channel source - */ - @Override - public void setListener(final Listener listener) - { - mComplexBufferAssembler.setListener(listener); - } - - /** - * Removes the listener from receiving complex sample buffers from this channel source - */ - @Override - public void removeListener(Listener listener) - { - mComplexBufferAssembler.setListener(null); - } - - /** - * Channel output processor used by this channel source to convert polyphase channel results into a specific - * channel complex buffer output stream. - */ - public IPolyphaseChannelOutputProcessor getPolyphaseChannelOutputProcessor() - { - return mPolyphaseChannelOutputProcessor; - } - - /** - * Sets/updates the output processor for this channel source, replacing the existing output processor. - * - * @param outputProcessor to use - * @param frequency center for the channels processed by the output processor - */ - public void setPolyphaseChannelOutputProcessor(IPolyphaseChannelOutputProcessor outputProcessor, long frequency) - { - //Lock on the channel output processor to block the channel results processor thread from servicing the - //channel output processor's buffer while we change out the processors. - synchronized(mPolyphaseChannelOutputProcessor) - { - IPolyphaseChannelOutputProcessor existingProcessor = mPolyphaseChannelOutputProcessor; - - //Swap out the processor so that incoming samples can accumulate in the new channel output processor - mPolyphaseChannelOutputProcessor = outputProcessor; - - //Remove the buffer overflow listener and clear any previous buffer overflow state - existingProcessor.setSourceOverflowListener(null); - broadcastOverflowState(false); - - //Register to receive buffer overflow notifications so we can push them up through the source chain - mPolyphaseChannelOutputProcessor.setSourceOverflowListener(this); - - //Fully process the residual channel results buffer of the previous channel output processor - if(existingProcessor != null) - { - existingProcessor.processChannelResults(mComplexBufferAssembler); - } - - //Finally, setup the frequency offset for the output processor and reset the frequency correction value - //to allow consumers to adjust and calculate a new frequency correction value, if needed. - mCenterFrequency = frequency; - mFrequencyCorrection = 0; - mPolyphaseChannelOutputProcessor.setFrequencyOffset(getFrequencyOffset()); - broadcastConsumerSourceEvent(SourceEvent.frequencyCorrectionChange(mFrequencyCorrection)); - } - } - - /** - * Primary method for receiving channel results output from a polyphase channelizer. The results array will be - * queued for processing to extract the target channel samples, process them for frequency correction and/or - * channel aggregation, and dispatch the results to the downstream sample listener/consumer. - * - * @param channelResultsBuffer containing the polyphase channelizer output for a single complex sample period. - */ - public void receiveChannelResults(float[] channelResultsBuffer) - { - mPolyphaseChannelOutputProcessor.receiveChannelResults(channelResultsBuffer); - } - - /** - * Downstream channel sample rate - * - * @return sample rate in Hertz - */ - @Override - public double getSampleRate() - { - return mChannelSampleRate; - } - - /** - * Sets the downstream channel sample rate - * @param sampleRate in hertz - */ - @Override - protected void setSampleRate(double sampleRate) - { - mChannelSampleRate = sampleRate; - } - - @Override - public void dispose() - { - mLog.info("Polyphase channel source received request to dispose"); - } - - @Override - public long getFrequencyCorrection() - { - return mFrequencyCorrection; - } - - /** - * Adjusts the frequency correction value that is being applied to the channelized output stream by the - * polyphase channel output processor. - * - * @param value to apply for frequency correction in hertz - */ - protected void setFrequencyCorrection(long value) - { - mFrequencyCorrection = value; - updateFrequencyOffset(); - broadcastConsumerSourceEvent(SourceEvent.frequencyCorrectionChange(mFrequencyCorrection)); - } - - /** - * Sets the center frequency for the incoming sample stream channel results and resets frequency correction to zero. - * @param frequency in hertz - */ - @Override - protected void setFrequency(long frequency) - { - mCenterFrequency = frequency; - setFrequencyCorrection(0); - } - - @Override - protected void processSamples() - { - //Lock on the output processor so that it can't be changed out in the middle of processing - synchronized(mPolyphaseChannelOutputProcessor) - { - mPolyphaseChannelOutputProcessor.processChannelResults(mComplexBufferAssembler); - } - } - - /** - * Calculates the frequency offset required to mix the incoming signal to the desired output baseband - */ - private long getFrequencyOffset() - { - return getTunerChannel().getFrequency() - mCenterFrequency + mFrequencyCorrection; - } - - /** - * Updates the frequency offset being applied by the output processor. Calculates the offset using the center - * frequency of the incoming polyphase channel results and the desired output channel frequency adjusted by any - * requested frequency correction value. - */ - private void updateFrequencyOffset() - { - synchronized(mPolyphaseChannelOutputProcessor) - { - mPolyphaseChannelOutputProcessor.setFrequencyOffset(getFrequencyOffset()); - } - } -} diff --git a/src/dsp/filter/channelizer/ScheduledBufferProcessor.java b/src/dsp/filter/channelizer/ScheduledBufferProcessor.java deleted file mode 100644 index adc7ac9b..00000000 --- a/src/dsp/filter/channelizer/ScheduledBufferProcessor.java +++ /dev/null @@ -1,171 +0,0 @@ -/******************************************************************************* - * 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.Listener; -import sample.OverflowableTransferQueue; -import sample.real.IOverflowListener; -import util.ThreadPool; - -import java.util.ArrayList; -import java.util.List; -import java.util.concurrent.ScheduledFuture; -import java.util.concurrent.TimeUnit; -import java.util.concurrent.atomic.AtomicBoolean; - -public class ScheduledBufferProcessor implements Listener -{ - private final static Logger mLog = LoggerFactory.getLogger(ScheduledBufferProcessor.class); - - private OverflowableTransferQueue mQueue; - private Listener mListener; - private ScheduledFuture mScheduledFuture; - private long mDistributionInterval; - private int mMaxBuffersPerInterval; - private AtomicBoolean mRunning = new AtomicBoolean(); - - /** - * Scheduled Buffer Processor combines an internal overflowable buffer with a scheduled runnable processing task - * for periodically distributing internally queued elements to the registered listener. This processor provides - * a convenient way to create a thread-safe buffer for receiving elements from one thread/runnable and then - * distributing those elements to a registered listener where distribution occurs on a separate scheduled thread - * pool runnable thread. This allows the calling input thread to quickly return without incurring any subsequent - * processing workload. - * - * The internal queue is an overflowable queue implementation that allows a listener to be registered to receive - * notifications of overflow and reset state. Queue sizing parameters are specified in the constructor. - * - * @param maximumSize of the internal queue (overflow happens when this is exceeded) - * @param resetThreshold of the internal queue (overflow reset happens once queue size falls below this threshold - * @param distributionInterval in milliseconds specifying how often the internal scheduled thread pool buffer - * distributor should run - * @param maxBuffersPerInterval maximum number of buffers to pull from the internal queue during each distribution - * interval - */ - public ScheduledBufferProcessor(int maximumSize, int resetThreshold, long distributionInterval, int maxBuffersPerInterval) - { - mQueue = new OverflowableTransferQueue<>(maximumSize, resetThreshold); - - mQueue.setOverflowListener(new IOverflowListener() - { - @Override - public void sourceOverflow(boolean overflow) - { - mLog.error("Overflow state changed - overflow:" + overflow); - } - }); - mDistributionInterval = distributionInterval; - mMaxBuffersPerInterval = maxBuffersPerInterval; - } - - /** - * Sets the listener to receive notifications of buffer overflow and/or reset. Note: this method can also be - * invoked with a null argument to clear the previously registered overflow listener. - * - * @param listener to receive overflow/reset notifications. - */ - public void setOverflowListener(IOverflowListener listener) - { - mQueue.setOverflowListener(listener); - } - - /** - * Sets or changes the listener to receive buffers from this processor. - * @param listener to receive buffers - */ - public void setListener(Listener listener) - { - mListener = listener; - } - - /** - * Primary input method for adding buffers to this processor. Note: incoming buffers will be ignored if this - * processor is in a stopped state. You must invoke start() to allow incoming buffers and initiate buffer - * processing. - * - * @param e to enqueue for distribution to a registered listener - */ - @Override - public void receive(E e) - { - mQueue.offer(e); - } - - /** - * Starts this buffer processor and allows queuing of incoming buffers. - */ - public void start() - { - if(mRunning.compareAndSet(false, true)) - { - mScheduledFuture = ThreadPool.SCHEDULED.scheduleAtFixedRate(new Processor(), 0, mDistributionInterval, TimeUnit.MILLISECONDS); - } - } - - /** - * Stops this buffer processor, stops queuing of incoming buffers and clears the internal buffer queue. - */ - public void stop() - { - if(mRunning.compareAndSet(true, false)) - { - if(mScheduledFuture != null) - { - mScheduledFuture.cancel(true); - mScheduledFuture = null; - } - - mQueue.clear(); - } - } - - /** - * Indicates if this processor is currently running - */ - public boolean isRunning() - { - return mRunning.get(); - } - - /** - * Processor to service the buffer queue and distribute the buffers to the registered listener - */ - class Processor implements Runnable - { - private List mBuffers = new ArrayList<>(); - - @Override - public void run() - { - mQueue.drainTo(mBuffers, mMaxBuffersPerInterval); - - if(mListener != null) - { - for(E buffer: mBuffers) - { - mListener.receive(buffer); - } - } - - mBuffers.clear(); - } - } -} diff --git a/src/dsp/filter/channelizer/output/ChannelOutputProcessor.java b/src/dsp/filter/channelizer/output/ChannelOutputProcessor.java deleted file mode 100644 index c1504f7f..00000000 --- a/src/dsp/filter/channelizer/output/ChannelOutputProcessor.java +++ /dev/null @@ -1,170 +0,0 @@ -/******************************************************************************* - * 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.output; - -import dsp.mixer.Oscillator; -import org.slf4j.Logger; -import org.slf4j.LoggerFactory; -import sample.OverflowableTransferQueue; -import sample.complex.Complex; -import sample.complex.ComplexSampleListener; -import sample.real.IOverflowListener; -import source.Source; - -import java.util.ArrayList; -import java.util.List; - -public abstract class ChannelOutputProcessor implements IPolyphaseChannelOutputProcessor -{ - private final static Logger mLog = LoggerFactory.getLogger(ChannelOutputProcessor.class); - - private OverflowableTransferQueue mChannelResultsQueue; - private List mChannelResultsToProcess = new ArrayList<>(); - private int mMaxResultsToProcess; - - private int mInputChannelCount; -//TODO: swap this out and use the LowPhaseNoiseOscillator - private Oscillator mFrequencyCorrectionMixer; - private boolean mFrequencyCorrectionEnabled; - - /** - * Base class for polyphase channelizer output channel processing. Provides built-in frequency translation - * oscillator support to apply frequency correction to the channel sample stream as requested by sample consumer. - * - * @param inputChannelCount is the number of input channels for this output processor - * @param sampleRate of the output channel. This is used to match the oscillator's sample rate to the output - * channel sample rate for frequency translation/correction. - */ - public ChannelOutputProcessor(int inputChannelCount, double sampleRate) - { - mInputChannelCount = inputChannelCount; - mFrequencyCorrectionMixer = new Oscillator(0, sampleRate); - mMaxResultsToProcess = (int)(sampleRate / 10) * 2; //process at 100 millis interval, twice the expected inflow rate - - mChannelResultsQueue = new OverflowableTransferQueue<>((int)(sampleRate * 3), (int)(sampleRate * 0.5)); - } - - /** - * 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) - { - mFrequencyCorrectionMixer.setFrequency(frequencyOffset); - mFrequencyCorrectionEnabled = (frequencyOffset != 0); - } - - @Override - public void receiveChannelResults(float[] channelResults) - { - mChannelResultsQueue.offer(channelResults); - } - - /** - * Processes all enqueued polyphase channelizer results until the internal queue is empty - * @param listener to receive the processed channel results - */ - @Override - public void processChannelResults(ComplexSampleListener listener) - { - try - { - int count = mChannelResultsQueue.drainTo(mChannelResultsToProcess, mMaxResultsToProcess); - - if(count > 0) - { - process(mChannelResultsToProcess, listener); - } - } - catch(Throwable throwable) - { - mLog.error("Error while processing polyphase channel samples", throwable); - } - - mChannelResultsToProcess.clear(); - } - - /** - * Sub-class implementation to process one polyphase channelizer result array. - * @param channelResults to process - * @param listener to receive the channel complex sample - */ - public abstract void process(List channelResults, ComplexSampleListener listener); - - - /** - * Sets the overflow listener to monitor the internal channelizer channel results queue overflow state - */ - public void setSourceOverflowListener(Source source) - { - mChannelResultsQueue.setSourceOverflowListener(source); - } - - /** - * Removes the overflow listener from monitoring the internal channelizer channel results queue overflow state - */ - public void removeOverflowListener(IOverflowListener listener) - { - mChannelResultsQueue.setOverflowListener(null); - } - - @Override - public int getInputChannelCount() - { - return mInputChannelCount; - } - - /** - * Provides frequency correction of the inphase component or simply returns the inphase argument value when no - * frequency correction value has been specified. - * - * @param inphase value of the sample to translate/correct - * @param quadrature value of the sample to translate/correct - * @return corrected inphase value - */ - protected float getFrequencyCorrectedInphase(float inphase, float quadrature) - { - return Complex.multiplyInphase(inphase, quadrature, mFrequencyCorrectionMixer.inphase(), - mFrequencyCorrectionMixer.quadrature()); - } - - /** - * Provides frequency correction of the quadrature component or simply returns the quadrature argument value when no - * frequency correction value has been applied, - * - * @param inphase value of the sample to translate/correct - * @param quadrature value of the sample to translate/correct - * @return translated inphase value - */ - protected float getFrequencyCorrectedQuadrature(float inphase, float quadrature) - { - return Complex.multiplyQuadrature(inphase, quadrature, mFrequencyCorrectionMixer.inphase(), - mFrequencyCorrectionMixer.quadrature()); - } - - /** - * Indicates if this channel has a frequency correction offset - */ - public boolean hasFrequencyCorrection() - { - return mFrequencyCorrectionEnabled; - } - -} diff --git a/src/dsp/filter/channelizer/output/IPolyphaseChannelOutputProcessor.java b/src/dsp/filter/channelizer/output/IPolyphaseChannelOutputProcessor.java deleted file mode 100644 index e285fd70..00000000 --- a/src/dsp/filter/channelizer/output/IPolyphaseChannelOutputProcessor.java +++ /dev/null @@ -1,67 +0,0 @@ -/******************************************************************************* - * 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.output; - -import sample.complex.ComplexSampleListener; -import sample.real.IOverflowListener; -import source.Source; - -import java.util.List; - -public interface IPolyphaseChannelOutputProcessor -{ - /** - * Receive and enqueue output results from the polyphase analysis channelizer - * @param channelResults to enqueue - */ - void receiveChannelResults(float[] channelResults); - - /** - * Process the channel output channel results queue and deliver the output to the listener - * - * @param listener to receive the processed channel results - */ - void processChannelResults(ComplexSampleListener listener); - - /** - * Sets the desired frequency offset from center. The samples will be mixed with an oscillator set to this offset - * frequency to produce an output where the desired signal is centered in the passband. - * - * @param frequency in hertz - */ - void setFrequencyOffset(long frequency); - - /** - * Indicates the number of input channels processed by this output processor - * @return - */ - int getInputChannelCount(); - - /** - * Updates the input polyphase channel index(es) used by this output processor - * @param indexes - */ - void setPolyphaseChannelIndices(List indexes); - - /** - * Sets the listener to receive buffer overflow notifications - * @param source to receive overflow notifications - */ - void setSourceOverflowListener(Source source); -} diff --git a/src/dsp/filter/channelizer/output/OneChannelOutputProcessor.java b/src/dsp/filter/channelizer/output/OneChannelOutputProcessor.java deleted file mode 100644 index aa3e7503..00000000 --- a/src/dsp/filter/channelizer/output/OneChannelOutputProcessor.java +++ /dev/null @@ -1,97 +0,0 @@ -/******************************************************************************* - * 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.output; - -import org.slf4j.Logger; -import org.slf4j.LoggerFactory; -import sample.complex.ComplexSampleListener; - -import java.util.Arrays; -import java.util.List; - -public class OneChannelOutputProcessor extends ChannelOutputProcessor -{ - private final static Logger mLog = LoggerFactory.getLogger(OneChannelOutputProcessor.class); - - private int mChannelOffset; - - /** - * Processor to extract a single channel from a polyphase channelizer and produce an output I/Q complex sample - * from each polyphase channelizer output results array. - * - * @param sampleRate of the output sample stream. - * @param channelIndexes containing a single channel index. - */ - public OneChannelOutputProcessor(double sampleRate, List channelIndexes) - { - super(1, sampleRate); - setPolyphaseChannelIndices(channelIndexes); - mLog.debug("Sample Rate:" + sampleRate + " PolyPhase Channel Indexes:" + channelIndexes); - } - - /** - * Updates this processor to extract a single, specified channel index. - * @param indexes containing a single channel index value. - * @throws IllegalArgumentException if the list of indexes does not contain a single channel index. - */ - public void setPolyphaseChannelIndices(List indexes) - { - if(indexes.size() != 1) - { - throw new IllegalArgumentException("Single channel output processor requires a single index to " + - "process - provided indexes " + indexes.toString()); - } - - //Set the channelized output results offset to twice the channel index to account for each channel having - //an I/Q pair - mChannelOffset = indexes.get(0) * 2; - } - - /** - * Extract the channel from the channel results array, apply frequency translation, and deliver the - * extracted frequency-corrected channel I/Q sample set to the complex sample listener. - * - * @param channelResults to process containing a list of channel array of I/Q sample pairs (I0,Q0,I1,Q1...In,Qn) - * @param listener to receive the extracted, frequency-translated channel results - */ - @Override - public void process(List channelResults, ComplexSampleListener listener) - { - for(float[] channels: channelResults) - { - if(channels.length < mChannelOffset + 1) - { - throw new IllegalArgumentException("Polyphase channelizer output channels array is not large enough to " + - "cover this single channel output processor with channel offset [" + mChannelOffset + "]"); - } - - float i = channels[mChannelOffset]; - float q = channels[mChannelOffset + 1]; - - if(hasFrequencyCorrection()) - { - listener.receive(getFrequencyCorrectedInphase(i, q), getFrequencyCorrectedQuadrature(i, q)); - } - else - { - listener.receive(i, q); - } - } - } -} diff --git a/src/dsp/filter/channelizer/output/TwoChannelOutputProcessor.java b/src/dsp/filter/channelizer/output/TwoChannelOutputProcessor.java deleted file mode 100644 index ba0c3e15..00000000 --- a/src/dsp/filter/channelizer/output/TwoChannelOutputProcessor.java +++ /dev/null @@ -1,91 +0,0 @@ -/******************************************************************************* - * 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.output; - -import org.slf4j.Logger; -import org.slf4j.LoggerFactory; -import sample.complex.ComplexSampleListener; - -import java.util.List; - -public class TwoChannelOutputProcessor extends ChannelOutputProcessor -{ - private final static Logger mLog = LoggerFactory.getLogger(TwoChannelOutputProcessor.class); - - private int mChannelOffset1; - private int mChannelOffset2; - - /** - * Processor to extract two channels from a polyphase channelizer, synthesize/recombine the channels, apply - * frequency translation and frequency correction, down-sample by a factor of two and output an I/Q complex sample. - * - * @param sampleRate of the output sample stream. - * @param channelIndexes containing two channel indices. - */ - public TwoChannelOutputProcessor(int sampleRate, List channelIndexes) - { - //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); - } - - /** - * Updates this processor to extract the two specified channel indexes. - * - * @param indexes containing a single channel index value. - * @throws IllegalArgumentException if the list of indexes does not contain a single channel index. - */ - public void setPolyphaseChannelIndices(List indexes) - { - if(indexes.size() != 2) - { - throw new IllegalArgumentException("Double channel output processor requires two indexes to " + - "process - provided indexes " + indexes.toString()); - } - - //Set the channelized output results offsets to twice the channel index to account for each channel having - //an I/Q pair - mChannelOffset1 = indexes.get(0) * 2; - mChannelOffset2 = indexes.get(1) * 2; - } - - /** - * Extract the channel from the channel results array, apply frequency translation, and deliver the - * extracted frequency-corrected channel I/Q sample set to the complex sample listener. - * - * @param channelResults to process containing an array of channel I/Q sample pairs (I0,Q0,I1,Q1...In,Qn) - * @param listener to receive the extracted, frequency-translated channel results - */ - @Override - public void process(List channelResults, ComplexSampleListener listener) - { - mLog.debug("I got channel results - doing nothing !!"); -// if(channels.length < mChannelOffset + 1) -// { -// throw new IllegalArgumentException("Polyphase channelizer output channels array is not large enough to " + -// "cover this single channel output processor with channel offset [" + mChannelOffset + "]"); -// } -// -// float i = channels[mChannelOffset]; -// float q = channels[mChannelOffset + 1]; - -// listener.receive(getFrequencyCorrectedInphase(i, q), getFrequencyCorrectedQuadrature(i, q)); - } -} diff --git a/src/module/ProcessingChain.java b/src/module/ProcessingChain.java index 516d71c8..fb4a8722 100644 --- a/src/module/ProcessingChain.java +++ b/src/module/ProcessingChain.java @@ -611,8 +611,7 @@ public class ProcessingChain implements IChannelEventListener } /** - * Stops processing if the chain is currently processing. Invocations on - * an already stopped chain have no effect. + * Stops processing if the chain is currently processing. Invocations on an already stopped chain have no effect. */ public void stop() { @@ -639,15 +638,14 @@ public class ProcessingChain implements IChannelEventListener ((RealSource) mSource).removeListener(mFilteredRealBufferBroadcaster); break; default: - throw new IllegalArgumentException("Unrecognized source " - + "sample type - cannot start processing chain"); + throw new IllegalArgumentException("Unrecognized source sample type - cannot start processing " + + "chain"); } /* Release the source */ mSource.dispose(); mSource = null; } - } } diff --git a/src/source/tuner/Tuner.java b/src/source/tuner/Tuner.java index 13370fb3..ee475f4e 100644 --- a/src/source/tuner/Tuner.java +++ b/src/source/tuner/Tuner.java @@ -22,18 +22,8 @@ import org.slf4j.Logger; import org.slf4j.LoggerFactory; import sample.Broadcaster; import sample.Listener; -import sample.complex.ComplexBuffer; -import sample.complex.IComplexBufferProvider; import source.ISourceEventProcessor; import source.SourceEvent; -import source.SourceException; -import source.tuner.manager.AbstractSourceManager; -import source.tuner.manager.TunerSourceManager; - -import java.util.ArrayList; -import java.util.List; -import java.util.concurrent.CopyOnWriteArrayList; -import java.util.concurrent.RejectedExecutionException; /** * Tuner - provides tuner channel sources, representing a channel frequency @@ -45,7 +35,6 @@ public abstract class Tuner protected Broadcaster mTunerEventBroadcaster = new Broadcaster<>(); private TunerController mTunerController; private String mName; - private AbstractSourceManager mTunerSourceManager; /** * Abstract tuner class. @@ -57,8 +46,6 @@ public abstract class Tuner mName = name; mTunerController = tunerController; - mTunerSourceManager = new TunerSourceManager(mTunerController); - //Rebroadcast frequency and sample rate change events as tuner events mTunerController.addListener(new ISourceEventProcessor() { @@ -80,14 +67,6 @@ public abstract class Tuner }); } - /** - * Source Manager. Provides access to registering for complex buffer samples and source event notifications. - */ - public AbstractSourceManager getSourceManager() - { - return mTunerSourceManager; - } - public TunerController getTunerController() { return mTunerController; diff --git a/src/source/tuner/TunerController.java b/src/source/tuner/TunerController.java index 70dee706..24043844 100644 --- a/src/source/tuner/TunerController.java +++ b/src/source/tuner/TunerController.java @@ -21,25 +21,23 @@ import org.slf4j.Logger; import org.slf4j.LoggerFactory; import sample.Broadcaster; import sample.Listener; -import sample.complex.Complex; import sample.complex.ComplexBuffer; import sample.complex.IComplexBufferProvider; import source.ISourceEventListener; import source.ISourceEventProcessor; import source.SourceEvent; -import source.SourceEvent.Event; import source.SourceEventListenerToProcessorAdapter; import source.SourceException; -import source.tuner.channel.CICTunerChannelSource; +import source.tuner.channel.ChannelManager; +import source.tuner.channel.cic.CICTunerChannelSource; import source.tuner.channel.TunerChannel; -import source.tuner.channel.TunerChannelSource; +import source.tuner.channel.polyphase.PolyphaseChannelManager; import source.tuner.configuration.TunerConfiguration; -import source.tuner.frequency.FrequencyController; -import source.tuner.frequency.FrequencyController.Tunable; +import source.tuner.channel.FrequencyController; +import source.tuner.channel.FrequencyController.Tunable; import java.util.SortedSet; import java.util.concurrent.ConcurrentSkipListSet; -import java.util.concurrent.RejectedExecutionException; public abstract class TunerController implements Tunable, ISourceEventProcessor, ISourceEventListener, IComplexBufferProvider, Listener @@ -49,9 +47,8 @@ public abstract class TunerController implements Tunable, ISourceEventProcessor, /* List of currently tuned channels being served to demod channels */ private SortedSet mTunedChannels = new ConcurrentSkipListSet<>(); - protected FrequencyController mFrequencyController; - private int mMiddleUnusable; - private double mUsableBandwidthPercentage; + private FrequencyController mFrequencyController; + private ChannelManager mChannelManager; private Listener mSourceEventListener; /** @@ -65,14 +62,32 @@ public abstract class TunerController implements Tunable, ISourceEventProcessor, * @throws SourceException - for any issues related to constructing the * class, tuning a frequency, or setting the bandwidth */ - public TunerController(long minimumFrequency, long maximumFrequency, int middleUnusable, double usableBandwidth) + public TunerController(long minimumFrequency, long maximumFrequency, long middleUnusable, double usableBandwidth) { - mFrequencyController = new FrequencyController(this, minimumFrequency, maximumFrequency, 0.0d); - mMiddleUnusable = middleUnusable; - mUsableBandwidthPercentage = usableBandwidth; + mFrequencyController = new FrequencyController(this, minimumFrequency, maximumFrequency, + 0.0d, middleUnusable, usableBandwidth); + + mChannelManager = new PolyphaseChannelManager(mFrequencyController, this); + mSourceEventListener = new SourceEventListenerToProcessorAdapter(this); } + /** + * Frequency controller for managing frequency and sample rate. + */ + public FrequencyController getFrequencyController() + { + return mFrequencyController; + } + + /** + * Channel manager for managing Digital Drop Chanel (DDC) tuner channel sources + */ + public ChannelManager getChannelManager() + { + return mChannelManager; + } + /** * Implements the ISourceEventListener interface to receive requests from sample consumers */ @@ -96,18 +111,18 @@ public abstract class TunerController implements Tunable, ISourceEventProcessor, switch(sourceEvent.getEvent()) { case REQUEST_FREQUENCY_CHANGE: - setFrequency( sourceEvent.getValue().longValue() ); + getFrequencyController().setFrequency( sourceEvent.getValue().longValue() ); break; case REQUEST_START_SAMPLE_STREAM: - if(sourceEvent.getSource() instanceof Listener) + if(sourceEvent.getSource() instanceof CICTunerChannelSource) { - addComplexBufferListener((Listener)sourceEvent.getSource()); + addComplexBufferListener((CICTunerChannelSource)sourceEvent.getSource()); } break; case REQUEST_STOP_SAMPLE_STREAM: - if(sourceEvent.getSource() instanceof Listener) + if(sourceEvent.getSource() instanceof CICTunerChannelSource) { - removeComplexBufferListener((Listener)sourceEvent.getSource()); + removeComplexBufferListener((CICTunerChannelSource)sourceEvent.getSource()); } break; default: @@ -115,285 +130,10 @@ public abstract class TunerController implements Tunable, ISourceEventProcessor, sourceEvent.getSource().getClass() + "]" ); } } - - public int getBandwidth() - { - return mFrequencyController.getBandwidth(); - } - - /** - * Sets the center frequency of the local oscillator. - * - * @param frequency in hertz - * @throws SourceException - if the tuner has any issues - */ - public void setFrequency(long frequency) throws SourceException - { - mFrequencyController.setFrequency(frequency); - } - - /** - * Gets the center frequency of the local oscillator - * - * @return frequency in hertz - */ - public long getFrequency() - { - return mFrequencyController.getFrequency(); - } - @Override public boolean canTune(long frequency) { - return mFrequencyController.canTune(frequency); - } - - public double getSampleRate() - { - return mFrequencyController.getSampleRate(); - } - - public double getFrequencyCorrection() - { - return mFrequencyController.getFrequencyCorrection(); - } - - public void setFrequencyCorrection(double correction) throws SourceException - { - mFrequencyController.setFrequencyCorrection(correction); - } - - public long getMinFrequency() - { - return mFrequencyController.getMinimumFrequency(); - } - - public long getMaxFrequency() - { - return mFrequencyController.getMaximumFrequency(); - } - - private long getMinTunedFrequency() throws SourceException - { - return mFrequencyController.getFrequency() - (getUsableBandwidth() / 2); - } - - private long getMaxTunedFrequency() throws SourceException - { - return mFrequencyController.getFrequency() + (getUsableBandwidth() / 2); - } - - /** - * Indicates if channel along with all of the other currently sourced - * channels can fit within the tunable bandwidth. - */ - private boolean canTune(TunerChannel channel) - { - //Make sure we're within the tunable frequency range of this tuner - if(getMinFrequency() < channel.getMinFrequency() && getMaxFrequency() > channel.getMaxFrequency()) - { - //If this is the first lock, then we're good - if(mTunedChannels.isEmpty()) - { - return true; - } - else - { - int usableBandwidth = getUsableBandwidth(); - long minLockedFrequency = mTunedChannels.first().getMinFrequency(); - long maxLockedFrequency = mTunedChannels.last().getMaxFrequency(); - - //Requested channel is within current locked channel frequency range - if(minLockedFrequency <= channel.getMinFrequency() && channel.getMaxFrequency() <= maxLockedFrequency) - { - return true; - } - - //Requested channel is higher than min locked frequency - if(channel.getMaxFrequency() > minLockedFrequency && - channel.getMaxFrequency() - minLockedFrequency <= usableBandwidth) - { - return true; - } - - //Requested channel is lower than the max locked frequency - if(channel.getMinFrequency() <= maxLockedFrequency && - maxLockedFrequency - channel.getMinFrequency() <= usableBandwidth) - { - return true; - } - } - } - - return false; - } - - /** - * Constructs a digital drop channel (DDC) as a tuner channel from from the tuner, or returns null if the channel - * cannot be sourced from the tuner. - * - * This controller can't provide a channel if the channel, along with the current set of sourced channels, can't be - * accomodated within the current tuner bandwidth, or if a center frequency cannot be calculated that will ensure - * all of the channels can be accomodated and any defined central DC spike avoided. - * - * @param tuner to source the channel from - * @param channel with defined center frequency and bandwidth - * @return fully constructed tuner channel or null - * @throws RejectedExecutionException if the decimation processor has an error - */ - public TunerChannelSource getChannel(Tuner tuner, TunerChannel channel) throws RejectedExecutionException - { - TunerChannelSource source = null; - - if(canTune(channel)) - { - try - { - mTunedChannels.add(channel); - - if(requiresLOUpdate(channel)) - { - updateLOFrequency(); - } - - source = new CICTunerChannelSource(tuner, channel); - } - catch(SourceException se) - { - mTunedChannels.remove(channel); - source = null; - } - } - - return source; - } - - public int getChannelCount() - { - return mTunedChannels.size(); - } - - /** - * Indicates if the tuner's LO frequency must be updated in order to accommodate the tuner channel - */ - private boolean requiresLOUpdate(TunerChannel channel) throws SourceException - { - boolean isBelow = channel.getMinFrequency() < getMinTunedFrequency(); - boolean isAbove = channel.getMaxFrequency() > getMaxTunedFrequency(); - boolean overlapsCenter = mMiddleUnusable > 0 && - channel.overlaps(getFrequency() - mMiddleUnusable, getFrequency() + mMiddleUnusable); - - return isAbove || isBelow || overlapsCenter; - } - - /** - * Releases the currently sourced tuner channel from this tuner and shuts down the tuner if no other sources exist. - */ - public void releaseChannel(TunerChannelSource tunerChannelSource) - { - if(tunerChannelSource != null) - { - mTunedChannels.remove(tunerChannelSource.getTunerChannel()); - } - } - - /** - * Sets the Local Oscillator frequency to accomodate the current set of tuned channels. - * - * If there is only a single tuned channel, it is placed immediately to the right of the usable bandwidth right - * of the central DC spike. - * - * Otherwise, it places the highest channel frequency at the upper end of the tuner bandwidth, and then iteratively - * moves the center frequency higher until all channels fit within the bandwidth and none of the channels overlap - * any defined central DC spike unusable region. If a center tune frequency cannot be calculated, throw an - * exception so that the most recently added channel can be removed. - * - * Note: the tuned frequency is not changed until a legitimate new frequency can be calculated. If an exception - * is thrown, the current frequency is retained, so that the recently added channel can be removed and all other - * channels can continue as previously arranged. - * - * @throws SourceException if the set of tuner channels, including a recently added channel, cannot be tuned - * within the current tuner bandwidth and any central DC spike unusable region. - */ - private void updateLOFrequency() throws SourceException - { - long candidateFrequency; - - boolean isValidCandidateFrequency = true; - - //If there is only 1 channel set the center frequency so that the channel is positioned to the right of center - if(mTunedChannels.size() == 1) - { - candidateFrequency = mTunedChannels.first().getMinFrequency() - mMiddleUnusable + 1; - } - else - { - long minLockedFrequency = mTunedChannels.first().getMinFrequency(); - long maxLockedFrequency = mTunedChannels.last().getMaxFrequency(); - - //Start by placing the highest frequency channel at the high end of the spectrum - candidateFrequency = maxLockedFrequency - (getUsableBandwidth() / 2); - - //Iterate the channels and make sure that none of them overlap the center DC spike buffer, if one exists - if(mMiddleUnusable > 0) - { - boolean processingRequired = true; - - while(isValidCandidateFrequency && processingRequired) - { - processingRequired = false; - - long minAvoid = candidateFrequency - mMiddleUnusable; - long maxAvoid = candidateFrequency + mMiddleUnusable; - - //If any of the center channel(s) overlap the central DC spike avoid area, we'll iteratively - //increase the tuned frequency causing the set of channels to move left in the tuned bandwidth until - //we either find a good center tune frequency, or we walk the lowest frequency channel out of the - //minimum tuned range, in which case we'll throw an exception indicating we don't have a solution. - for(TunerChannel channel : mTunedChannels) - { - if(channel.overlaps(minAvoid, maxAvoid)) - { - //Calculate a tuned frequency adjustment that places this overlapping channel just to the - //left of the central DC spike avoid zone - long adjustment = channel.getMaxFrequency() - minAvoid + 1; - - //If the candidate frequency doesn't push the lowest channel out of bounds, make adjustment - if(candidateFrequency + adjustment - (getUsableBandwidth() / 2) <= minLockedFrequency) - { - candidateFrequency += adjustment; - processingRequired = true; - } - //Otherwise, punt and indicate that we can't find a center frequency or add the channel - else - { - isValidCandidateFrequency = false; - } - - //break out of the for/each loop, so that we can start over again with all of the channels - break; - } - } - } - } - } - - if(isValidCandidateFrequency) - { - mFrequencyController.setFrequency(candidateFrequency); - } - else - { - throw new SourceException("Couldn't calculate viable center frequency from set of tuner channels"); - } - } - - /** - * Usable bandwidth - total bandwidth minus the unusable space at either end of the spectrum. - */ - private int getUsableBandwidth() - { - return (int)(getBandwidth() * mUsableBandwidthPercentage); + return getFrequencyController().canTune(frequency); } /** diff --git a/src/source/tuner/TunerEditor.java b/src/source/tuner/TunerEditor.java index f1a0d972..f1ffa2d9 100644 --- a/src/source/tuner/TunerEditor.java +++ b/src/source/tuner/TunerEditor.java @@ -264,7 +264,7 @@ public class TunerEditor extends Editor //Set the displayed frequency without adjusting the tuner's frequency mFrequencyControl.setFrequency( - tuner.getTunerController().getFrequency(), false ); + tuner.getTunerController().getFrequencyController().getFrequency(), false ); } else { diff --git a/src/source/tuner/TunerModel.java b/src/source/tuner/TunerModel.java index 5f4bea95..426ad719 100644 --- a/src/source/tuner/TunerModel.java +++ b/src/source/tuner/TunerModel.java @@ -220,13 +220,13 @@ public class TunerModel extends AbstractTableModel implements Listener + * + ******************************************************************************/ +package source.tuner.channel; + +import org.slf4j.Logger; +import org.slf4j.LoggerFactory; +import sample.Listener; +import source.Source; +import source.SourceEvent; +import source.SourceException; +import source.tuner.Tuner; +import source.tuner.channel.cic.CICTunerChannelSource; + +import java.util.SortedSet; +import java.util.concurrent.ConcurrentSkipListSet; +import java.util.concurrent.RejectedExecutionException; + +public abstract class ChannelManager implements Listener +{ + private final static Logger mLog = LoggerFactory.getLogger(ChannelManager.class); + + private FrequencyController mFrequencyController; + private SortedSet mTunedChannels = new ConcurrentSkipListSet<>(); + + /** + * Channel manager provides access to Digital Drop Channel (DDC) tuner channel sources and automatic updating of + * a frequency controller to ensure that the center tuned frequency is correct for the provided DDC channels. + * + * @param frequencyController with a center tuned frequency that can be adjusted + */ + public ChannelManager(FrequencyController frequencyController) + { + mFrequencyController = frequencyController; + } + + protected FrequencyController getFrequencyController() + { + return mFrequencyController; + } + + /** + * Creates a tuner channel source for the tuner channel if possible. + * + * @param tunerChannel to source + * @return tuner channel source or null if the tuner channel cannot be sourced by this channel manager + */ + public TunerChannelSource getSource(TunerChannel tunerChannel) + { + TunerChannelSource tunerChannelSource = null; + + if(canTune(tunerChannel)) + { + //Add the channel so that we can calculate the new center frequency + mTunedChannels.add(tunerChannel); + + long centerFrequency = calculateCenterFrequency(); + + if(centerFrequency != getFrequencyController().getFrequency()) + { + try + { + getFrequencyController().setLockedFrequency(centerFrequency); + tunerChannelSource = getTunerChannelSource(tunerChannel); + } + catch(SourceException se) + { + mLog.error("Error setting tuner frequency while creating new tuner channel source for channel: " + + tunerChannel, se); + } + } + } + + //If we're not successful, remove the tuner channel from the sorted set + if(tunerChannelSource == null && mTunedChannels.contains(tunerChannel)) + { + mTunedChannels.remove(tunerChannel); + } + + return tunerChannelSource; + } + + /** + * Releases the source and performs any cleanup. + * @param source to release that was previously provided by this channel manager + */ + public void releaseSource(TunerChannelSource source) + { + mTunedChannels.remove(source.getTunerChannel()); + } + + /** + * Determine the center frequency to use for the current set of tuner channels. + */ + protected abstract long calculateCenterFrequency(); + + /** + * Constructs the actual tuner channel source for the tuner channel. This method assumes that the tuner channel + * can be tuned and that the tuner is correctly center tuned to source the channel. + * @param tunerChannel to source + * @return tuner channel source or null if there are any errors in creating the tuner channel source + */ + protected abstract TunerChannelSource getTunerChannelSource(TunerChannel tunerChannel); + + /** + * Indicates if channel along with all of the other currently sourced channels can fit within the tunable bandwidth. + */ + private boolean canTune(TunerChannel channel) + { + //Make sure we're within the tunable frequency range of this tuner + if(getFrequencyController().getMinimumFrequency() < channel.getMinFrequency() && + getFrequencyController().getMaximumFrequency() > channel.getMaxFrequency()) + { + //If this is the first lock, then we're good + if(mTunedChannels.isEmpty()) + { + return true; + } + else + { + int usableBandwidth = getFrequencyController().getUsableBandwidth(); + long minLockedFrequency = mTunedChannels.first().getMinFrequency(); + long maxLockedFrequency = mTunedChannels.last().getMaxFrequency(); + + //Requested channel is within current locked channel frequency range + if(minLockedFrequency <= channel.getMinFrequency() && channel.getMaxFrequency() <= maxLockedFrequency) + { + return true; + } + + //Requested channel is higher than min locked frequency + if(channel.getMaxFrequency() > minLockedFrequency && + channel.getMaxFrequency() - minLockedFrequency <= usableBandwidth) + { + return true; + } + + //Requested channel is lower than the max locked frequency + if(channel.getMinFrequency() <= maxLockedFrequency && + maxLockedFrequency - channel.getMinFrequency() <= usableBandwidth) + { + return true; + } + } + } + + return false; + } + + /** + * Constructs a digital drop channel (DDC) as a tuner channel from from the tuner, or returns null if the channel + * cannot be sourced from the tuner. + * + * This controller can't provide a channel if the channel, along with the current set of sourced channels, can't be + * accomodated within the current tuner bandwidth, or if a center frequency cannot be calculated that will ensure + * all of the channels can be accomodated and any defined central DC spike avoided. + * + * @param tuner to source the channel from + * @param channel with defined center frequency and bandwidth + * @return fully constructed tuner channel or null + * @throws RejectedExecutionException if the decimation processor has an error + */ + public TunerChannelSource getChannel(Tuner tuner, TunerChannel channel) throws RejectedExecutionException + { + TunerChannelSource source = null; + + if(canTune(channel)) + { + try + { + mTunedChannels.add(channel); + + if(requiresLOUpdate(channel)) + { + updateLOFrequency(); + } + + source = new CICTunerChannelSource(tuner, channel); + } + catch(SourceException se) + { + mTunedChannels.remove(channel); + source = null; + } + } + + return source; + } + + public int getChannelCount() + { + return mTunedChannels.size(); + } + + /** + * Indicates if the tuner's LO frequency must be updated in order to accommodate the tuner channel + */ + private boolean requiresLOUpdate(TunerChannel channel) throws SourceException + { + int usableBandwidth = getFrequencyController().getUsableBandwidth(); + + long minimumTunedFrequency = getFrequencyController().getFrequency() - (usableBandwidth / 2); + boolean isBelow = channel.getMinFrequency() < minimumTunedFrequency; + + long maximumTunedFrequency = getFrequencyController().getFrequency() + (usableBandwidth / 2); + boolean isAbove = channel.getMaxFrequency() > maximumTunedFrequency; + + long centerBlackoutBandwidth = getFrequencyController().getCenterBlackoutBandwidth(); + boolean overlapsCenter = centerBlackoutBandwidth > 0 && + channel.overlaps(getFrequencyController().getFrequency() - centerBlackoutBandwidth, + getFrequencyController().getFrequency() + centerBlackoutBandwidth); + + return isAbove || isBelow || overlapsCenter; + } + + /** + * Releases the currently sourced tuner channel from this tuner and shuts down the tuner if no other sources exist. + */ + public void releaseChannel(TunerChannelSource tunerChannelSource) + { + if(tunerChannelSource != null) + { + mTunedChannels.remove(tunerChannelSource.getTunerChannel()); + } + } + + /** + * Sets the Local Oscillator frequency to accomodate the current set of tuned channels. + * + * If there is only a single tuned channel, it is placed immediately to the right of the usable bandwidth right + * of the central DC spike. + * + * Otherwise, it places the highest channel frequency at the upper end of the tuner bandwidth, and then iteratively + * moves the center frequency higher until all channels fit within the bandwidth and none of the channels overlap + * any defined central DC spike unusable region. If a center tune frequency cannot be calculated, throw an + * exception so that the most recently added channel can be removed. + * + * Note: the tuned frequency is not changed until a legitimate new frequency can be calculated. If an exception + * is thrown, the current frequency is retained, so that the recently added channel can be removed and all other + * channels can continue as previously arranged. + * + * @throws SourceException if the set of tuner channels, including a recently added channel, cannot be tuned + * within the current tuner bandwidth and any central DC spike unusable region. + */ + private void updateLOFrequency() throws SourceException + { + long candidateFrequency; + + boolean isValidCandidateFrequency = true; + + //If there is only 1 channel set the center frequency so that the channel is positioned to the right of center + if(mTunedChannels.size() == 1) + { + candidateFrequency = mTunedChannels.first().getMinFrequency() - + getFrequencyController().getCenterBlackoutBandwidth() + 1; + } + else + { + long minLockedFrequency = mTunedChannels.first().getMinFrequency(); + long maxLockedFrequency = mTunedChannels.last().getMaxFrequency(); + + //Start by placing the highest frequency channel at the high end of the spectrum + candidateFrequency = maxLockedFrequency - (getFrequencyController().getUsableBandwidth() / 2); + + //Iterate the channels and make sure that none of them overlap the center DC spike buffer, if one exists + if(getFrequencyController().getCenterBlackoutBandwidth() > 0) + { + boolean processingRequired = true; + + while(isValidCandidateFrequency && processingRequired) + { + processingRequired = false; + + long centerBlackout = getFrequencyController().getCenterBlackoutBandwidth(); + + long minAvoid = candidateFrequency - centerBlackout; + long maxAvoid = candidateFrequency + centerBlackout; + + //If any of the center channel(s) overlap the central DC spike avoid area, we'll iteratively + //increase the tuned frequency causing the set of channels to move left in the tuned bandwidth until + //we either find a good center tune frequency, or we walk the lowest frequency channel out of the + //minimum tuned range, in which case we'll throw an exception indicating we don't have a solution. + for(TunerChannel channel : mTunedChannels) + { + if(channel.overlaps(minAvoid, maxAvoid)) + { + //Calculate a tuned frequency adjustment that places this overlapping channel just to the + //left of the central DC spike avoid zone + long adjustment = channel.getMaxFrequency() - minAvoid + 1; + + //If the candidate frequency doesn't push the lowest channel out of bounds, make adjustment + if(candidateFrequency + adjustment - + (getFrequencyController().getUsableBandwidth() / 2) <= minLockedFrequency) + { + candidateFrequency += adjustment; + processingRequired = true; + } + //Otherwise, punt and indicate that we can't find a center frequency or add the channel + else + { + isValidCandidateFrequency = false; + } + + //break out of the for/each loop, so that we can start over again with all of the channels + break; + } + } + } + } + } + + if(isValidCandidateFrequency) + { + mFrequencyController.setLockedFrequency(candidateFrequency); + } + else + { + throw new SourceException("Couldn't calculate viable center frequency from set of tuner channels"); + } + } + +} diff --git a/src/source/tuner/frequency/FrequencyController.java b/src/source/tuner/channel/FrequencyController.java similarity index 73% rename from src/source/tuner/frequency/FrequencyController.java rename to src/source/tuner/channel/FrequencyController.java index ce64fc36..8ec85198 100644 --- a/src/source/tuner/frequency/FrequencyController.java +++ b/src/source/tuner/channel/FrequencyController.java @@ -16,7 +16,7 @@ * along with this program. If not, see * ******************************************************************************/ -package source.tuner.frequency; +package source.tuner.channel; import org.slf4j.Logger; import org.slf4j.LoggerFactory; @@ -26,6 +26,7 @@ import source.SourceEvent; import source.SourceException; import java.util.ArrayList; +import java.util.List; public class FrequencyController { @@ -38,37 +39,59 @@ public class FrequencyController private long mMaximumFrequency; private double mFrequencyCorrection = 0.0d; private double mSampleRate = 0.0d; + private long mCenterBlackoutBandwidth; + private double mUsableBandwidthPercentage; + private boolean mLocked; - private ArrayList mProcessors = new ArrayList<>(); + private List mProcessors = new ArrayList<>(); - public FrequencyController(Tunable tunable, long minFrequency, long maxFrequency, double frequencyCorrection) + public FrequencyController(Tunable tunable, long minFrequency, long maxFrequency, double frequencyCorrection, + long centerBlackoutBandwidth, double usableBandwidthPercentage) { mTunable = tunable; mMinimumFrequency = minFrequency; mMaximumFrequency = maxFrequency; mFrequencyCorrection = frequencyCorrection; + mCenterBlackoutBandwidth = centerBlackoutBandwidth; + mUsableBandwidthPercentage = usableBandwidthPercentage; } - /** - * Get bandwidth in hertz - */ - public int getBandwidth() + public boolean isLocked() { - return (int)mSampleRate; + return mLocked; } /** - * Set sample rate in hertz + * Sets the locked status. When this control is locked, the sample rate cannot be changed and the center + * tuned frequency can only be changed via the setLockedFrequency() method. + * + * Note: this method is package-private and is intended to be used (only) by the ChannelManager. + * + * @param locked + */ + void setLocked(boolean locked) + { + mLocked = locked; + } + + + /** + * Set sample rate/bandwidth in hertz */ public void setSampleRate(int sampleRate) throws SourceException { + if(isLocked()) + { + throw new FrequencyControllerLockedException(); + } + mSampleRate = sampleRate; broadcastSampleRateChange(); } /** - * Get sample rate in hertz + * Get sample rate/bandwidth in hertz */ public double getSampleRate() { @@ -91,6 +114,23 @@ public class FrequencyController * version of the requested frequency */ public void setFrequency(long frequency) throws SourceException + { + if(isLocked()) + { + throw new FrequencyControllerLockedException(); + } + + setFrequency(frequency, true); + } + + /** + * Sets the frequency when the frequency controller is locked. Note: this method is package-private and is + * only intended to be used by the ChannelManager. + * + * @param frequency to set as the center tuned frequency + * @throws SourceException if there is an error while setting the frequency + */ + void setLockedFrequency(long frequency) throws SourceException { setFrequency(frequency, true); } @@ -250,6 +290,34 @@ public class FrequencyController } } + /** + * Identifies the bandwidth of a blackout frequency range centered about the center frequency. This is + * used to block the frequency range where a DC spike may exist. + * @return center blackout bandwidth in hertz + */ + public long getCenterBlackoutBandwidth() + { + return mCenterBlackoutBandwidth; + } + + /** + * Percentage of the available bandwidth that is usable. Sources with filter roll-off at the ends of the + * spectral range can limit the usable bandwidth to avoid providing samples at these spectral ends. + * + * @return bandwidth percentage (0.0 <> 1.0) + */ + public double getUsableBandwidthPercentage() + { + return mUsableBandwidthPercentage; + } + + /** + * Usable bandwidth in hertz calculated as the sample rate constrained by the usable bandwidth percentage. + */ + public int getUsableBandwidth() + { + return (int)(getSampleRate() * getUsableBandwidthPercentage()); + } public interface Tunable { diff --git a/src/source/tuner/channel/TunerChannelProvider.java b/src/source/tuner/channel/FrequencyControllerLockedException.java similarity index 61% rename from src/source/tuner/channel/TunerChannelProvider.java rename to src/source/tuner/channel/FrequencyControllerLockedException.java index 56b85aa1..9ad9f09a 100644 --- a/src/source/tuner/channel/TunerChannelProvider.java +++ b/src/source/tuner/channel/FrequencyControllerLockedException.java @@ -18,28 +18,18 @@ ******************************************************************************/ package source.tuner.channel; -import java.util.concurrent.RejectedExecutionException; - import source.SourceException; -public interface TunerChannelProvider +public class FrequencyControllerLockedException extends SourceException { - /** - * Returns a tuner frequency channel source, tuned to the correct frequency - * - * @param frequency - desired frequency - * - * @return - source for 48k sample rate - */ - public abstract TunerChannelSource getChannel( TunerChannel channel ) - throws RejectedExecutionException, SourceException; + private static final long serialVersionUID = 1L; - /** - * Releases the tuned channel resources - * - * @param channel - previously obtained tuner channel - */ - public abstract void releaseChannel( TunerChannelSource source ); - - + /** + * Indicates an attempt to adjust frequency or sample rate on a locked frequency controller. + */ + public FrequencyControllerLockedException() + { + super( "Frequency controller is locked -- frequency and sample rate cannot be changed until the controller" + + " is unlocked by the channel manager" ); + } } diff --git a/src/source/tuner/channel/TunerChannelSource.java b/src/source/tuner/channel/TunerChannelSource.java index c88c7b4d..fef2a0eb 100644 --- a/src/source/tuner/channel/TunerChannelSource.java +++ b/src/source/tuner/channel/TunerChannelSource.java @@ -41,6 +41,7 @@ public abstract class TunerChannelSource extends ComplexSource implements ISourc private Listener mProducerSourceEventListener; private Listener mConsumerSourceEventListener; private BufferProcessor mBufferProcessor = new BufferProcessor(); + private boolean mExpended = false; /** * Tuner Channel Source is a Digital Drop Channel (DDC) abstract class that defines the minimum functionality @@ -116,6 +117,12 @@ public abstract class TunerChannelSource extends ComplexSource implements ISourc */ public void start() { + if(mExpended) + { + throw new IllegalStateException("Cannot start an expended tuner channel source. You must request a new" + + " tuner channel source each time. Tuner channel sources are one-use only"); + } + //Broadcast current frequency and sample rate so consumer can configure correctly broadcastConsumerSourceEvent(SourceEvent.frequencyChange(getFrequency())); broadcastConsumerSourceEvent(SourceEvent.sampleRateChange(getSampleRate())); @@ -134,6 +141,8 @@ public abstract class TunerChannelSource extends ComplexSource implements ISourc */ public void stop() { + mExpended = true; + if(mProducerSourceEventListener != null) { mProducerSourceEventListener.receive(SourceEvent.stopSampleStream(this)); @@ -146,7 +155,11 @@ public abstract class TunerChannelSource extends ComplexSource implements ISourc @Override public void reset() { - //Reset is not valid for a tuner channel source - ignored + if(mExpended) + { + throw new IllegalStateException("Cannot reset an expended tuner channel source. You must request a new" + + " tuner channel source each time. Tuner channel sources are one-use only"); + } } /** diff --git a/src/source/tuner/channel/CICTunerChannelSource.java b/src/source/tuner/channel/cic/CICTunerChannelSource.java similarity index 94% rename from src/source/tuner/channel/CICTunerChannelSource.java rename to src/source/tuner/channel/cic/CICTunerChannelSource.java index 50515ce7..a5086ae2 100644 --- a/src/source/tuner/channel/CICTunerChannelSource.java +++ b/src/source/tuner/channel/cic/CICTunerChannelSource.java @@ -16,7 +16,7 @@ * along with this program. If not, see * ******************************************************************************/ -package source.tuner.channel; +package source.tuner.channel.cic; import dsp.filter.FilterFactory; import dsp.filter.Window; @@ -31,11 +31,13 @@ import sample.complex.Complex; import sample.complex.ComplexBuffer; import source.SourceEvent; import source.tuner.Tuner; +import source.tuner.channel.TunerChannel; +import source.tuner.channel.TunerChannelSource; import java.util.ArrayList; import java.util.List; -public class CICTunerChannelSource extends TunerChannelSource +public class CICTunerChannelSource extends TunerChannelSource implements Listener { private final static Logger mLog = LoggerFactory.getLogger(CICTunerChannelSource.class); @@ -77,12 +79,13 @@ public class CICTunerChannelSource extends TunerChannelSource mBuffer.setSourceOverflowListener(this); //Setup the frequency translator to the current source frequency - mTunerFrequency = tuner.getTunerController().getFrequency(); + mTunerFrequency = tuner.getTunerController().getFrequencyController().getFrequency(); long frequencyOffset = mTunerFrequency - getTunerChannel().getFrequency(); - mFrequencyCorrectionMixer = new Oscillator(frequencyOffset, tuner.getTunerController().getSampleRate()); + mFrequencyCorrectionMixer = new Oscillator(frequencyOffset, + tuner.getTunerController().getFrequencyController().getSampleRate()); //Setup the decimation filter chain - setSampleRate(tuner.getTunerController().getSampleRate()); + setSampleRate(tuner.getTunerController().getFrequencyController().getSampleRate()); } /** diff --git a/src/source/tuner/configuration/TunerConfigurationModel.java b/src/source/tuner/configuration/TunerConfigurationModel.java index 40b69708..67fbaf04 100644 --- a/src/source/tuner/configuration/TunerConfigurationModel.java +++ b/src/source/tuner/configuration/TunerConfigurationModel.java @@ -377,7 +377,7 @@ public class TunerConfigurationModel extends AbstractTableModel { TunerConfiguration assigned = getTunerConfiguration( tuner.getTunerType(), tuner.getUniqueID() ); - assigned.setFrequency( tuner.getTunerController().getFrequency() ); + assigned.setFrequency( tuner.getTunerController().getFrequencyController().getFrequency() ); broadcast( new TunerConfigurationEvent( assigned, Event.CHANGE ) ); } diff --git a/src/source/tuner/fcd/proV1/FCD1TunerController.java b/src/source/tuner/fcd/proV1/FCD1TunerController.java index 587905b7..6b576835 100644 --- a/src/source/tuner/fcd/proV1/FCD1TunerController.java +++ b/src/source/tuner/fcd/proV1/FCD1TunerController.java @@ -63,7 +63,7 @@ public class FCD1TunerController extends FCDTunerController { super.init(); - mFrequencyController.setSampleRate(SAMPLE_RATE); + getFrequencyController().setSampleRate(SAMPLE_RATE); try { @@ -120,7 +120,7 @@ public class FCD1TunerController extends FCDTunerController try { - setFrequencyCorrection(config.getFrequencyCorrection()); + getFrequencyController().setFrequencyCorrection(config.getFrequencyCorrection()); setLNAGain(config.getLNAGain()); setLNAEnhance(config.getLNAEnhance()); setMixerGain(config.getMixerGain()); @@ -142,7 +142,7 @@ public class FCD1TunerController extends FCDTunerController try { - setFrequency(config.getFrequency()); + getFrequencyController().setFrequency(config.getFrequency()); } catch(SourceException se) { diff --git a/src/source/tuner/fcd/proV1/FCD1TunerEditor.java b/src/source/tuner/fcd/proV1/FCD1TunerEditor.java index 5c8f834b..40a330b0 100644 --- a/src/source/tuner/fcd/proV1/FCD1TunerEditor.java +++ b/src/source/tuner/fcd/proV1/FCD1TunerEditor.java @@ -149,7 +149,7 @@ public class FCD1TunerEditor extends TunerConfigurationEditor try { - mController.setFrequencyCorrection( value ); + mController.getFrequencyController().setFrequencyCorrection( value ); save(); } catch ( SourceException e1 ) diff --git a/src/source/tuner/fcd/proplusV2/FCD2TunerController.java b/src/source/tuner/fcd/proplusV2/FCD2TunerController.java index d1a99ff2..5e2412c4 100644 --- a/src/source/tuner/fcd/proplusV2/FCD2TunerController.java +++ b/src/source/tuner/fcd/proplusV2/FCD2TunerController.java @@ -52,7 +52,7 @@ public class FCD2TunerController extends FCDTunerController { super.init(); - mFrequencyController.setSampleRate(SAMPLE_RATE); + getFrequencyController().setSampleRate(SAMPLE_RATE); try { @@ -112,7 +112,7 @@ public class FCD2TunerController extends FCDTunerController { FCD2TunerConfiguration fcd2 = (FCD2TunerConfiguration)config; - setFrequencyCorrection(fcd2.getFrequencyCorrection()); + getFrequencyController().setFrequencyCorrection(fcd2.getFrequencyCorrection()); long frequency = MINIMUM_TUNABLE_FREQUENCY; @@ -122,13 +122,13 @@ public class FCD2TunerController extends FCDTunerController frequency = fcd2.getFrequency(); } - setFrequency(frequency); + getFrequencyController().setFrequency(frequency); setLNAGain(fcd2.getGainLNA()); setMixerGain(fcd2.getGainMixer()); try { - setFrequency(fcd2.getFrequency()); + getFrequencyController().setFrequency(fcd2.getFrequency()); } catch(SourceException se) { diff --git a/src/source/tuner/fcd/proplusV2/FCD2TunerEditor.java b/src/source/tuner/fcd/proplusV2/FCD2TunerEditor.java index 1e0ab570..e5c5c8be 100644 --- a/src/source/tuner/fcd/proplusV2/FCD2TunerEditor.java +++ b/src/source/tuner/fcd/proplusV2/FCD2TunerEditor.java @@ -141,7 +141,7 @@ public class FCD2TunerEditor extends TunerConfigurationEditor try { - mController.setFrequencyCorrection( value ); + mController.getFrequencyController().setFrequencyCorrection( value ); save(); } catch ( SourceException e1 ) diff --git a/src/source/tuner/frequency/FrequencyLimitException.java b/src/source/tuner/frequency/FrequencyLimitException.java deleted file mode 100644 index be72a74e..00000000 --- a/src/source/tuner/frequency/FrequencyLimitException.java +++ /dev/null @@ -1,36 +0,0 @@ -package source.tuner.frequency; - -public class FrequencyLimitException extends Exception -{ - private static final long serialVersionUID = 1L; - - private long mFrequencyLimit; - private long mRequestedFrequency; - - /** - * Indicates that the requested frequency is outside of the tunable bounds - * of the tuner. - * @param message - * @param frequency - */ - public FrequencyLimitException( long requestedFrequency, - long frequencyLimit ) - { - super( "Requested frequency [" + requestedFrequency + - "] exceeds the limit [" + frequencyLimit + "]. Tuned " - + "frequency was set to the limit value" ); - - mRequestedFrequency = requestedFrequency; - mFrequencyLimit = frequencyLimit; - } - - public long getRequestedFrequency() - { - return mRequestedFrequency; - } - - public long getFrequencyLimit() - { - return mFrequencyLimit; - } -} diff --git a/src/source/tuner/hackrf/HackRFTunerController.java b/src/source/tuner/hackrf/HackRFTunerController.java index 41b1b729..2af8ed00 100644 --- a/src/source/tuner/hackrf/HackRFTunerController.java +++ b/src/source/tuner/hackrf/HackRFTunerController.java @@ -138,7 +138,7 @@ public class HackRFTunerController extends USBTunerController setMode(Mode.RECEIVE); - setFrequency(DEFAULT_FREQUENCY); + getFrequencyController().setFrequency(DEFAULT_FREQUENCY); } catch(Exception e) { @@ -306,7 +306,7 @@ public class HackRFTunerController extends USBTunerController */ public long getTunedFrequency() throws SourceException { - return mFrequencyController.getTunedFrequency(); + return getFrequencyController().getTunedFrequency(); } @Override @@ -353,11 +353,11 @@ public class HackRFTunerController extends USBTunerController try { setSampleRate(hackRFConfig.getSampleRate()); - setFrequencyCorrection(hackRFConfig.getFrequencyCorrection()); + getFrequencyController().setFrequencyCorrection(hackRFConfig.getFrequencyCorrection()); setAmplifierEnabled(hackRFConfig.getAmplifierEnabled()); setLNAGain(hackRFConfig.getLNAGain()); setVGAGain(hackRFConfig.getVGAGain()); - setFrequency(getFrequency()); + getFrequencyController().setFrequency(getFrequencyController().getFrequency()); } catch(UsbException e) { @@ -367,7 +367,7 @@ public class HackRFTunerController extends USBTunerController try { - setFrequency(hackRFConfig.getFrequency()); + getFrequencyController().setFrequency(hackRFConfig.getFrequency()); } catch(SourceException se) { @@ -511,7 +511,7 @@ public class HackRFTunerController extends USBTunerController { setSampleRateManual(rate.getRate(), 1); - mFrequencyController.setSampleRate(rate.getRate()); + getFrequencyController().setSampleRate(rate.getRate()); setBasebandFilter(rate.getFilter()); diff --git a/src/source/tuner/hackrf/HackRFTunerEditor.java b/src/source/tuner/hackrf/HackRFTunerEditor.java index 8962be28..2691a345 100644 --- a/src/source/tuner/hackrf/HackRFTunerEditor.java +++ b/src/source/tuner/hackrf/HackRFTunerEditor.java @@ -179,7 +179,7 @@ public class HackRFTunerEditor extends TunerConfigurationEditor try { - mController.setFrequencyCorrection( value ); + mController.getFrequencyController().setFrequencyCorrection( value ); save(); } catch ( SourceException e1 ) diff --git a/src/source/tuner/manager/AbstractSourceManager.java b/src/source/tuner/manager/AbstractSourceManager.java deleted file mode 100644 index 9c373d2d..00000000 --- a/src/source/tuner/manager/AbstractSourceManager.java +++ /dev/null @@ -1,111 +0,0 @@ -/******************************************************************************* - * 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 source.tuner.manager; - -import sample.Broadcaster; -import sample.Listener; -import sample.complex.ComplexBuffer; -import sample.complex.IComplexBufferProvider; -import source.SourceEvent; -import source.tuner.channel.TunerChannel; -import source.tuner.channel.TunerChannelSource; - -import java.util.List; - -/** - * Interface to define the functionality of a source manager for handling tuner channel management, complex buffer - * listeners, and source event listeners. - */ -public abstract class AbstractSourceManager implements IComplexBufferProvider, Listener -{ - protected Broadcaster mComplexBufferBroadcaster = new Broadcaster<>(); - - /** - * Indicates if this source manager can tune (ie provide a source for) the specified tuner channel - */ - public abstract boolean canTune(TunerChannel tunerChannel); - - /** - * List of tuner channels being sourced by this source manager - */ - public abstract List getChannels(); - - /** - * Obtains a source for the tuner channel or returns null if the channel cannot be sourced by this tuner. - * Note: use the canTune() method to check if a tuner channel can be sourced/tuned prior to accessing this method. - * - * @param tunerChannel for requested source - * @return tuner channel source or null - */ - public abstract TunerChannelSource getSource(TunerChannel tunerChannel); - - - /** - * Releases the source and all related resources - */ - public abstract void releaseSource(TunerChannelSource source); - - /** - * Adds the listener to receive source events from this source manager - * - * @param sourceEventListener to add - */ - public abstract void addSourceEventListener(Listener sourceEventListener); - - /** - * Removes the listener from receiving source events from this source manager - * @param sourceEventListener to remove - */ - public abstract void removeSourceEventListener(Listener sourceEventListener); - - /** - * Adds the listener to receive complex buffer samples - */ - @Override - public void addComplexBufferListener(Listener listener) - { - mComplexBufferBroadcaster.addListener(listener); - } - - /** - * Removes the listener from receiving complex buffer samples - */ - @Override - public void removeComplexBufferListener(Listener listener) - { - mComplexBufferBroadcaster.removeListener(listener); - } - - /** - * Indicates if there are any complex buffer listeners registered with this source manager - */ - @Override - public boolean hasComplexBufferListeners() - { - return mComplexBufferBroadcaster.hasListeners(); - } - - /** - * Broadcasts the buffer to any registered listeners - */ - protected void broadcast(ComplexBuffer complexBuffer) - { - mComplexBufferBroadcaster.broadcast(complexBuffer); - } -} diff --git a/src/source/tuner/manager/TunerSourceManager.java b/src/source/tuner/manager/TunerSourceManager.java deleted file mode 100644 index fb6856bd..00000000 --- a/src/source/tuner/manager/TunerSourceManager.java +++ /dev/null @@ -1,83 +0,0 @@ -/******************************************************************************* - * 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 source.tuner.manager; - -import dsp.filter.channelizer.PolyphaseChannelManager; -import sample.Listener; -import source.SourceEvent; -import source.tuner.channel.TunerChannel; -import source.tuner.channel.TunerChannelSource; -import source.tuner.TunerController; - -import java.util.List; - -public class TunerSourceManager extends AbstractSourceManager -{ - private PolyphaseChannelManager mPolyphaseChannelManager; - private TunerController mTunerController; - - public TunerSourceManager(TunerController tunerController) - { - mTunerController = tunerController; - } - - //TODO: broadcast a tuner event each time we add a new channel to the polyphase manager - - @Override - public boolean canTune(TunerChannel tunerChannel) - { - return false; - } - - @Override - public List getChannels() - { - return null; - } - - @Override - public TunerChannelSource getSource(TunerChannel tunerChannel) - { - return null; - } - - @Override - public void releaseSource(TunerChannelSource source) - { - - } - - @Override - public void addSourceEventListener(Listener sourceEventListener) - { - - } - - @Override - public void removeSourceEventListener(Listener sourceEventListener) - { - - } - - @Override - public void receive(SourceEvent sourceEvent) - { - - } -} diff --git a/src/source/tuner/rtl/e4k/E4KTunerController.java b/src/source/tuner/rtl/e4k/E4KTunerController.java index 5c53621f..437247c8 100644 --- a/src/source/tuner/rtl/e4k/E4KTunerController.java +++ b/src/source/tuner/rtl/e4k/E4KTunerController.java @@ -129,7 +129,7 @@ public class E4KTunerController extends RTL2832TunerController setSampleRate(sampleRate); double correction = e4kConfig.getFrequencyCorrection(); - setFrequencyCorrection(correction); + getFrequencyController().setFrequencyCorrection(correction); E4KGain masterGain = e4kConfig.getMasterGain(); setGain(masterGain, true); @@ -148,7 +148,7 @@ public class E4KTunerController extends RTL2832TunerController try { - setFrequency(e4kConfig.getFrequency()); + getFrequencyController().setFrequency(e4kConfig.getFrequency()); } catch(SourceException se) { @@ -354,7 +354,7 @@ public class E4KTunerController extends RTL2832TunerController * to rounding errors, 52 mhz becomes 51.999993, so we need to adjust * x to get 52.000003 mhz ... otherwise the PLL won't lock on the freq */ - if(actualFrequency < getMinFrequency()) + if(actualFrequency < getFrequencyController().getMinimumFrequency()) { x++; actualFrequency = calculateActualFrequency(pll, z, x); diff --git a/src/source/tuner/rtl/e4k/E4KTunerEditor.java b/src/source/tuner/rtl/e4k/E4KTunerEditor.java index 5989fdf2..064da0ad 100644 --- a/src/source/tuner/rtl/e4k/E4KTunerEditor.java +++ b/src/source/tuner/rtl/e4k/E4KTunerEditor.java @@ -184,7 +184,7 @@ public class E4KTunerEditor extends TunerConfigurationEditor try { - mController.setFrequencyCorrection( value ); + mController.getFrequencyController().setFrequencyCorrection( value ); save(); } catch ( SourceException e1 ) diff --git a/src/source/tuner/rtl/r820t/R820TTunerController.java b/src/source/tuner/rtl/r820t/R820TTunerController.java index 762cb080..4fc9f782 100644 --- a/src/source/tuner/rtl/r820t/R820TTunerController.java +++ b/src/source/tuner/rtl/r820t/R820TTunerController.java @@ -104,7 +104,7 @@ public class R820TTunerController extends RTL2832TunerController setSampleRate(sampleRate); double correction = config.getFrequencyCorrection(); - setFrequencyCorrection(correction); + getFrequencyController().setFrequencyCorrection(correction); R820TGain masterGain = config.getMasterGain(); setGain(masterGain, true); @@ -123,7 +123,7 @@ public class R820TTunerController extends RTL2832TunerController try { - setFrequency(config.getFrequency()); + getFrequencyController().setFrequency(config.getFrequency()); } catch(SourceException se) { diff --git a/src/source/tuner/rtl/r820t/R820TTunerEditor.java b/src/source/tuner/rtl/r820t/R820TTunerEditor.java index c9b75a2e..bf3cbf16 100644 --- a/src/source/tuner/rtl/r820t/R820TTunerEditor.java +++ b/src/source/tuner/rtl/r820t/R820TTunerEditor.java @@ -191,7 +191,7 @@ public class R820TTunerEditor extends TunerConfigurationEditor try { - mController.setFrequencyCorrection( value ); + mController.getFrequencyController().setFrequencyCorrection( value ); } catch ( SourceException e1 ) { diff --git a/src/spectrum/SpectralDisplayPanel.java b/src/spectrum/SpectralDisplayPanel.java index 496708b9..46d79731 100644 --- a/src/spectrum/SpectralDisplayPanel.java +++ b/src/spectrum/SpectralDisplayPanel.java @@ -430,9 +430,9 @@ public class SpectralDisplayPanel extends JPanel implements Listener Date: Mon, 11 Dec 2017 20:30:19 -0500 Subject: [PATCH 37/87] #39 WIP - --- .../channel/polyphase/ChannelCalculator.java | 588 ++++++++++++++++++ .../channel/polyphase/ChannelizerViewer.java | 343 ++++++++++ .../polyphase/PolyphaseChannelManager.java | 384 ++++++++++++ .../polyphase/PolyphaseChannelSource.java | 224 +++++++ .../polyphase/ScheduledBufferProcessor.java | 171 +++++ .../output/ChannelOutputProcessor.java | 170 +++++ .../IPolyphaseChannelOutputProcessor.java | 66 ++ .../output/OneChannelOutputProcessor.java | 96 +++ .../output/TwoChannelOutputProcessor.java | 91 +++ 9 files changed, 2133 insertions(+) create mode 100644 src/source/tuner/channel/polyphase/ChannelCalculator.java create mode 100644 src/source/tuner/channel/polyphase/ChannelizerViewer.java create mode 100644 src/source/tuner/channel/polyphase/PolyphaseChannelManager.java create mode 100644 src/source/tuner/channel/polyphase/PolyphaseChannelSource.java create mode 100644 src/source/tuner/channel/polyphase/ScheduledBufferProcessor.java create mode 100644 src/source/tuner/channel/polyphase/output/ChannelOutputProcessor.java create mode 100644 src/source/tuner/channel/polyphase/output/IPolyphaseChannelOutputProcessor.java create mode 100644 src/source/tuner/channel/polyphase/output/OneChannelOutputProcessor.java create mode 100644 src/source/tuner/channel/polyphase/output/TwoChannelOutputProcessor.java diff --git a/src/source/tuner/channel/polyphase/ChannelCalculator.java b/src/source/tuner/channel/polyphase/ChannelCalculator.java new file mode 100644 index 00000000..a1b0940c --- /dev/null +++ b/src/source/tuner/channel/polyphase/ChannelCalculator.java @@ -0,0 +1,588 @@ +/******************************************************************************* + * 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 source.tuner.channel.polyphase; + +import org.slf4j.Logger; +import org.slf4j.LoggerFactory; +import source.tuner.channel.TunerChannel; + +import java.text.DecimalFormat; +import java.util.ArrayList; +import java.util.List; + +public class ChannelCalculator +{ + private final static Logger mLog = LoggerFactory.getLogger(ChannelCalculator.class); + + /** + * Policy to determine desired corrective action for methods that calculate an index for a frequency or a frequency + * from an index where the frequency may exist either at the overlapping boundary of two indexes, or in the middle + * of the wrap-around (N/2) index. + */ + private enum IndexBoundaryPolicy + { + ADJUST_POSITIVE, + ADJUST_NEGATIVE + } + + private double mSampleRate; + private int mChannelCount; + private double mCenterFrequency; + private double mChannelBandwidth; + private double mOversampling = 1.0; + + /** + * Calculates channel index(es) from a polyphase channelizer output to source a DDC polyphase channel source + * with a specified channel center frequency and bandwidth. + * + * Polyphase channel indexes are not linear/consecutive owing to the use of the FFT to rotate channels to their + * center frequencies following filtering in the polyphase filter bank. As such, this calculator provides methods + * to determine the min/center/max frequency of each channel that is adjusted for the current center frequency + * value and to determine the channel index(es) that need to be targeted to produce a requested output channel + * with a specific frequency and bandwdith. + * + * @param sampleRate in hertz for the input sample rate from the sample source (ie tuner) + * @param channelCount number of channels in the polyphase filter bank + * @param centerFrequency center frequency for the input sample stream + * @param oversampling set to 1.0 for no oversample, or greater than 1.0 for oversampling + */ + public ChannelCalculator(double sampleRate, int channelCount, double centerFrequency, double oversampling) + { + mSampleRate = sampleRate; + mChannelCount = channelCount; + mCenterFrequency = centerFrequency; + mOversampling = oversampling; + + updateChannelBandwidth();; + } + + /** + * Calculates the channel bandwidth from the sample rate and channel count. + */ + private void updateChannelBandwidth() + { + mChannelBandwidth = (double)mSampleRate / (double)mChannelCount; + } + + /** + * Center frequency for the channelizer + */ + public double getCenterFrequency() + { + return mCenterFrequency; + } + + /** + * Updates the center frequency for the channelizer + */ + public void setCenterFrequency(double frequency) + { + mCenterFrequency = frequency; + } + + /** + * Number of channels being channelized by the polyphase channelizer + */ + public int getChannelCount() + { + return mChannelCount; + } + + /** + * Updates the number of channels being channelized by the polyphase channelizer + */ + public void setChannelCount(int channelCount) + { + mChannelCount = channelCount; + updateChannelBandwidth(); + } + + /** + * Identifies the index of the wrap-around channel meaning that the lower half of the channel exists at the upper + * end of the spectrum and the upper half of the channel exists at the lower end of the spectrum + * + * @return wrap-around channel index + */ + public int getWrapAroundIndex() + { + return mChannelCount / 2; + } + + /** + * PolyphaseChannelManager input sample rate + * @return input sample rate in hertz + */ + public double getSampleRate() + { + return mSampleRate; + } + + /** + * Updates the channel count using the sample rate argument to calculate the number of channels from the + * already established channel bandwidth. + * + * @param sampleRate to use for channel count + */ + public void setSampleRate(double sampleRate) + { + setChannelCount((int)(sampleRate / getChannelBandwidth())); + } + + /** + * Channel bandwidth. Note: this value may be smaller than the channel sample rate when oversampling + * is being used. + */ + public double getChannelBandwidth() + { + return mChannelBandwidth; + } + + /** + * Half of the channel bandwidth. + */ + public double getHalfChannelBandwidth() + { + return mChannelBandwidth / 2.0; + } + + /** + * Oversampling rate produced by the channelizer. + * + * @return 1.0 for no oversampling, or a value greater than 1.0 when oversampling is applied. + */ + public double getOversampling() + { + return mOversampling; + } + + /** + * Sets the oversampling rate being applied by the polyphase channelizer + * @param oversampling rate where 1.0 indicates no oversampling, and a value greater than 1.0 indicates + * that the polyphase channelizer is oversampling each channel. + */ + public void setOversampling(double oversampling) + { + mOversampling = oversampling; + } + + /** + * Channel sample rate. This value is the product of the filtered channel bandwidth times the + * oversampling rate. + */ + public double getChannelSampleRate() + { + return (double)getChannelBandwidth() * getOversampling(); + } + + /** + * PolyphaseChannelManager input minimum frequency + * @return minimum frequency in hertz + */ + public long getMinimumFrequency() + { + return (long)(getCenterFrequency() - (getSampleRate() / 2.0)); + } + + /** + * PolyphaseChannelManager input maximum frequency + * @return maximum frequency in hertz + */ + public long getMaximumFrequency() + { + return (long)(getCenterFrequency() + (getSampleRate() / 2.0)); + } + + /** + * Identifies the channelizer channel index(es) required to produce a tuner channel with the specified center + * frequency and channel bandwidth. + * + * @param tunerChannel specifying the desired channel frequency and bandwidth + * + * @return an array of one or more polyphase channelizer index(es) to target in order to produce an output channel + * for the desired frequency and bandwidth. + * + * @throws IllegalArgumentException if the tuner channel's minimum or maximum frequency is not contained + * within the channelizer's minimum or maximum frequency range + */ + public List getChannelIndexes(TunerChannel tunerChannel) throws IllegalArgumentException + { + if(tunerChannel.getMinFrequency() < getMinimumFrequency() || + tunerChannel.getMaxFrequency() > getMaximumFrequency()) + { + throw new IllegalArgumentException("Requested channel cannot be provided by this " + + "channelizer. Requested channel [" + tunerChannel.getMinFrequency() + " - " + + tunerChannel.getMaxFrequency() + "] exceeds current channelizer frequency range [" + + getMinimumFrequency() + " - " + getMaximumFrequency() + "]"); + } + + int minIndex = getIndexForFrequency(tunerChannel.getMinFrequency(), IndexBoundaryPolicy.ADJUST_POSITIVE); + int maxIndex = getIndexForFrequency(tunerChannel.getMaxFrequency(), IndexBoundaryPolicy.ADJUST_NEGATIVE); + + //Detect the unsourceable situation where the wrap around index would be included in both the minimum index + //and the maximum index, meaning that the tuner channel bandwidth is nearly the same as the input sample rate. + if(minIndex == getWrapAroundIndex() && maxIndex == getWrapAroundIndex()) + { + throw new IllegalArgumentException("Requested tuner channel cannot be provided. Requested bandwidth is " + + "within two channel bandwidths of the sample rate."); + } + + List indexes = new ArrayList<>(); + + indexes.add(minIndex); + + if(minIndex != maxIndex) + { + //Ensure our min/max indexes are within the correct bounds 0 <> N-1 to safeguard the while loop below + if(minIndex < 0 || minIndex >= getChannelCount() || maxIndex < 0 || maxIndex >= getChannelCount()) + { + throw new IllegalArgumentException("Something went wrong while calculating the min and max polyphase " + + "channel indexes for tuner channel: " + tunerChannel); + } + + int pointer = minIndex + 1; + + if(pointer >= getChannelCount()) + { + pointer -= getChannelCount(); + } + + while(pointer != maxIndex) + { + indexes.add(pointer); + + pointer++; + + if(pointer >= getChannelCount()) + { + pointer -= getChannelCount(); + } + } + + indexes.add(maxIndex); + } + + return indexes; + } + + /** + * Calculates the index for the specified frequency. Certain frequencies can exist as both the maximum frequency + * of one channel and the minimum frequency of an adjacent channel. The index boundary policy identifies your + * choice to select the higher frequency channel index (ADJUST_POSITIVE) or the lower frequency channel + * index (ADJUST_NEGATIVE). + * + * @param frequency to determine channel index for + * @param indexBoundaryPolicy to determine the behavior when the frequency overlaps two channel boundaries + * @return index for the frequency + */ + public int getIndexForFrequency(long frequency, IndexBoundaryPolicy indexBoundaryPolicy) + { + double offset = frequency - getCenterFrequency(); + + if(Math.abs(offset) < getHalfChannelBandwidth()) + { + return 0; //Center index + } + + if(offset > 0) + { + offset += getHalfChannelBandwidth(); + } + else + { + offset -= getHalfChannelBandwidth(); + } + + int indexOffset = (int)(offset / getChannelBandwidth()); + + if(indexOffset < 0) + { + indexOffset += getChannelCount(); + } + + if(indexBoundaryPolicy == IndexBoundaryPolicy.ADJUST_POSITIVE && + isOverlapFrequency(frequency, indexOffset, indexOffset + 1)) + { + indexOffset = normalize(indexOffset + 1); + } + else if(indexBoundaryPolicy == IndexBoundaryPolicy.ADJUST_NEGATIVE && + isOverlapFrequency(frequency, indexOffset - 1, indexOffset)) + { + indexOffset = normalize(indexOffset - 1); + } + + return indexOffset; + } + + /** + * Indicates if the frequency is an overlapping frequency between channel index 1 and channel index 2 where the + * maximum frequency of index 1 is the same as the minimum frequency of index 2 and both of these frequencies are + * the same as the frequency argument. + * + * Note: both index 1 and index 2 will be normalized to the range of 0 <> N-1 prior to determining overlap + * + * @param frequency to test for overlap between index 1 and 2 + * @param index1 which is the lower channel index located next to index 2. + * @param index2 which is the higher channel index located next to index 1. + * @return true if frequency = index1 maximum frequency = index2 minimum frequency + */ + public boolean isOverlapFrequency(long frequency, int index1, int index2) + { + index1 = normalize(index1); + index2 = normalize(index2); + + //Check that these two indexes are side by side + int delta = normalize(index2 - index1); + + if(delta != 1) + { + return false; + } + + long index1MaximumFrequency = (long)getIndexMaximumFrequency(index1, IndexBoundaryPolicy.ADJUST_POSITIVE); + long index2MinimumFrequency = (long)getIndexMinimumFrequency(index2, IndexBoundaryPolicy.ADJUST_NEGATIVE); + + if(index1 == getWrapAroundIndex()) + { + index1MaximumFrequency = (long)getIndexMaximumFrequency(index1, IndexBoundaryPolicy.ADJUST_NEGATIVE); + } + + if(index2 == getWrapAroundIndex()) + { + index2MinimumFrequency = (long)getIndexMinimumFrequency(index2, IndexBoundaryPolicy.ADJUST_POSITIVE); + } + + return frequency == index1MaximumFrequency && frequency == index2MinimumFrequency; + } + + /** + * Normalizes the index to the range of 0 <> N-1 channel indexes + */ + private int normalize(int index) + { + while(index < 0) + { + index += getChannelCount(); + } + + while(index >= getChannelCount()) + { + index -= getChannelCount(); + } + + return index; + } + + /** + * Calculates the center frequency for the channel identified by the index in the channel results array. + * + * @param index to determine the channel center frequency + * @return center frequency for the indexed channel + */ + public double getIndexCenterFrequency(int index, IndexBoundaryPolicy indexBoundaryPolicy) + { + if(index < 0 || index >= getChannelCount()) + { + throw new IllegalArgumentException("Illegal channel index"); + } + + int wrapAroundIndex = getWrapAroundIndex(); + + if(index == wrapAroundIndex) + { + if(indexBoundaryPolicy == IndexBoundaryPolicy.ADJUST_POSITIVE) + { + return getCenterFrequency() + (index * getChannelBandwidth()); + } + else + { + return getCenterFrequency() - (index * getChannelBandwidth()); + } + } + else if(index < wrapAroundIndex) + { + return getCenterFrequency() + (index * getChannelBandwidth()); + } + else + { + return getCenterFrequency() - ((getChannelCount() - index) * getChannelBandwidth()); + } + } + + /** + * Calculates the minimum frequency band edge for the channel identified by the index in the channel results + * array. + * @param index to calculate the minimum band edge frequency for the channel. + * @param indexBoundaryPolicy for the wrap around index to indicate if this method should return the minimum + * frequency of the upper spectrum half of the wrap around channel (ADJUST_POSITIVE) or the minimum frequency of the + * lower spectrum half of the wrap around channel (ADJUST_NEGATIVE) + * @return frequency of the minimum band edge + */ + public double getIndexMinimumFrequency(int index, IndexBoundaryPolicy indexBoundaryPolicy) + { + if(index < 0 || index >= getChannelCount()) + { + throw new IllegalArgumentException("Can't determine minimum frequency - illegal channel index [" + + index + "]"); + } + + int wrapAroundIndex = getWrapAroundIndex(); + + if(index == wrapAroundIndex) + { + if(indexBoundaryPolicy == IndexBoundaryPolicy.ADJUST_POSITIVE) + { + return getCenterFrequency() + ((double)index * getChannelBandwidth()) - getHalfChannelBandwidth(); + } + else + { + return getIndexCenterFrequency(index, indexBoundaryPolicy); + } + } + else if(index <= wrapAroundIndex) + { + return getCenterFrequency() + ((double)index * getChannelBandwidth()) - getHalfChannelBandwidth(); + } + else + { + return getCenterFrequency() - ((double)(getChannelCount() - index) * getChannelBandwidth()) - + getHalfChannelBandwidth(); + } + } + + /** + * Calculates the maximum frequency band edge for the channel identified by the index in the channel results + * array. + * @param index to calculate the maximum band edge frequency for the channel. + * @param indexBoundaryPolicy for the wrap around index to indicate if this method should return the maximum + * frequency of the upper spectrum half of the wrap around channel (ADJUST_POSITIVE) or the maximum frequency of the + * lower spectrum half of the wrap around channel (ADJUST_NEGATIVE) + * @return frequency of the maximum band edge + */ + public double getIndexMaximumFrequency(int index, IndexBoundaryPolicy indexBoundaryPolicy) + { + if(index < 0 || index >= getChannelCount()) + { + throw new IllegalArgumentException("Can't determine maximum frequency - illegal channel index [" + + index + "]"); + } + + int wrapAroundIndex = getWrapAroundIndex(); + + if(index == wrapAroundIndex) + { + if(indexBoundaryPolicy == IndexBoundaryPolicy.ADJUST_POSITIVE) + { + return getIndexCenterFrequency(index, indexBoundaryPolicy); + } + else + { + return getCenterFrequency() - ((double)index * getChannelBandwidth() - getHalfChannelBandwidth()); + } + } + else if(index <= wrapAroundIndex) + { + return getCenterFrequency() + ((double)index * getChannelBandwidth()) + getHalfChannelBandwidth(); + } + else + { + return getCenterFrequency() - ((double)(getChannelCount() - index) * getChannelBandwidth()) + + getHalfChannelBandwidth(); + } + } + + /** + * Calculates the center frequency for a channel that will be synthesized from the channel indexes. + * @param indexes that are contiguous in the spectrum + * @return center frequency in hertz + */ + public long getCenterFrequencyForIndexes(List indexes) + { + if(indexes.isEmpty()) + { + throw new IllegalArgumentException("Indexes cannot be empty"); + } + + int middleIndex = indexes.get(indexes.size() / 2); + + if(indexes.size() % 2 == 0) + { + //Even number of indexes + return (long)getIndexMinimumFrequency(middleIndex, IndexBoundaryPolicy.ADJUST_POSITIVE); + } + else + { + //Odd number of indexes + return (long)getIndexCenterFrequency(middleIndex, IndexBoundaryPolicy.ADJUST_POSITIVE); + } + } + + /** + * String representation of the channel calculator settings + */ + public String toString() + { + StringBuilder sb = new StringBuilder(); + sb.append("Channels:").append(getChannelCount()); + sb.append(" Sample Rate:").append(getSampleRate()); + sb.append(" Channel Bandwidth:").append(getChannelBandwidth()); + sb.append(" Channel Rate:").append(getChannelSampleRate()); + sb.append(" Min:").append(getMinimumFrequency()); + sb.append(" Max:").append(getMaximumFrequency()); + return sb.toString(); + } + + + public static void main(String[] args) + { + mLog.debug("Starting ...."); + + int channelBandwidth = 12500; + int sampleRate = 100000; //Airspy + int channelCount = sampleRate / channelBandwidth; + double centerFrequency = 455000000; + + ChannelCalculator calculator = new ChannelCalculator(sampleRate, channelCount, centerFrequency, 2.0); + + mLog.debug(calculator.toString()); + + DecimalFormat decimalFormat = new DecimalFormat("000,000,000.0"); + + int wrapAroundIndex = calculator.getWrapAroundIndex(); + + IndexBoundaryPolicy indexBoundaryPolicy = IndexBoundaryPolicy.ADJUST_NEGATIVE; + + for(int index = wrapAroundIndex; index < calculator.getChannelCount(); index++) + { + mLog.debug("Index:" + index + " Min:" + decimalFormat.format(calculator.getIndexMinimumFrequency(index, indexBoundaryPolicy)) + + " Center:" + decimalFormat.format(calculator.getIndexCenterFrequency(index, indexBoundaryPolicy)) + + " Max:" + decimalFormat.format(calculator.getIndexMaximumFrequency(index, indexBoundaryPolicy))); + } + + indexBoundaryPolicy = IndexBoundaryPolicy.ADJUST_POSITIVE; + + for(int index = 0; index <= wrapAroundIndex; index++) + { + mLog.debug("Index:" + index + " Min:" + decimalFormat.format(calculator.getIndexMinimumFrequency(index, indexBoundaryPolicy)) + + " Center:" + decimalFormat.format(calculator.getIndexCenterFrequency(index, indexBoundaryPolicy)) + + " Max:" + decimalFormat.format(calculator.getIndexMaximumFrequency(index, indexBoundaryPolicy))); + } + + mLog.debug("Finished"); + } +} diff --git a/src/source/tuner/channel/polyphase/ChannelizerViewer.java b/src/source/tuner/channel/polyphase/ChannelizerViewer.java new file mode 100644 index 00000000..b4dbcd30 --- /dev/null +++ b/src/source/tuner/channel/polyphase/ChannelizerViewer.java @@ -0,0 +1,343 @@ +/******************************************************************************* + * 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 source.tuner.channel.polyphase; + +import net.miginfocom.swing.MigLayout; +import org.slf4j.Logger; +import org.slf4j.LoggerFactory; +import sample.Listener; +import sample.SampleType; +import sample.complex.ComplexBuffer; +import settings.SettingsManager; +import source.ISourceEventProcessor; +import source.SourceEvent; +import source.SourceException; +import source.tuner.channel.TunerChannel; +import source.tuner.channel.TunerChannelSource; +import source.tuner.configuration.TunerConfigurationModel; +import source.tuner.test.TestTuner; +import spectrum.DFTProcessor; +import spectrum.DFTSize; +import spectrum.SpectrumPanel; +import spectrum.converter.ComplexDecibelConverter; + +import javax.swing.*; +import javax.swing.event.ChangeEvent; +import javax.swing.event.ChangeListener; +import java.awt.*; + +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 JPanel mControlPanel; + private PrimarySpectrumPanel mPrimarySpectrumPanel; + private ChannelArrayPanel mChannelPanel; + private int mChannelCount; + private int mChannelsPerRow; + private long mToneFrequency; + private DFTSize mMainPanelDFTSize = DFTSize.FFT32768; + private DFTSize mChannelPanelDFTSize = DFTSize.FFT01024; + private TestTuner mTestTuner; + + /** + * GUI Test utility for researching polyphase channelizers. + * + * @param channelsPerRow + */ + public ChannelizerViewer(int channelsPerRow) + { + mTestTuner = new TestTuner(); + + mChannelCount = (int)(mTestTuner.getTunerController().getFrequencyController().getSampleRate() / CHANNEL_BANDWIDTH); + mChannelsPerRow = channelsPerRow; + + mToneFrequency = mTestTuner.getTunerController().getFrequencyController().getFrequency() + 19200; + + mTestTuner.getTunerController().setToneFrequency(mToneFrequency); + + init(); + } + + private void init() + { + setTitle("TunerChannelizer Viewer"); + setSize(1000, 800); + 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]")); + mPrimaryPanel.add(getSpectrumPanel(), "wrap"); + mPrimaryPanel.add(getControlPanel(), "wrap"); + mPrimaryPanel.add(getChannelArrayPanel()); + } + + return mPrimaryPanel; + } + + private PrimarySpectrumPanel getSpectrumPanel() + { + if(mPrimarySpectrumPanel == null) + { + mPrimarySpectrumPanel = new PrimarySpectrumPanel(mSettingsManager, + mTestTuner.getTunerController().getFrequencyController().getSampleRate()); + mPrimarySpectrumPanel.setPreferredSize(new Dimension(1000, 200)); + mPrimarySpectrumPanel.setDFTSize(mMainPanelDFTSize); + mTestTuner.getSourceManager().addComplexBufferListener(mPrimarySpectrumPanel); + } + + return mPrimarySpectrumPanel; + } + + private JPanel getControlPanel() + { + if(mControlPanel == null) + { + mControlPanel = new JPanel(); + mControlPanel.setLayout(new MigLayout("insets 0 0 0 0", "", "")); + + mControlPanel.add(new JLabel("Tone:"), "align left"); + int maximumFrequencyOffset = (int)(mTestTuner.getTunerController().getFrequencyController().getSampleRate() / 2); + long minimumFrequency = mTestTuner.getTunerController().getFrequencyController().getFrequency() - maximumFrequencyOffset; + long maximumFrequency = mTestTuner.getTunerController().getFrequencyController().getFrequency() + maximumFrequencyOffset; + + SpinnerNumberModel model = new SpinnerNumberModel(mToneFrequency, minimumFrequency, maximumFrequency, + 100 ); + + model.addChangeListener(new ChangeListener() + { + @Override + public void stateChanged(ChangeEvent e) + { + int toneFrequency = model.getNumber().intValue(); + mTestTuner.getTunerController().setToneFrequency(toneFrequency); + } + }); + + JSpinner spinner = new JSpinner(model); + + mControlPanel.add(spinner); + mControlPanel.add(new JLabel("Hz")); + + mControlPanel.add(new JLabel("Sample Rate: " + + mTestTuner.getTunerController().getFrequencyController().getSampleRate() + " Hz"), "push,align right"); + + mControlPanel.add(new JLabel("Max: " + + (mTestTuner.getTunerController().getFrequencyController().getSampleRate() / 2) + " Hz"), "push,align right"); + mControlPanel.add(new JLabel("Channels: " + mChannelCount), "push,align right"); + } + + return mControlPanel; + } + + private ChannelArrayPanel getChannelArrayPanel() + { + if(mChannelPanel == null) + { + mChannelPanel = new ChannelArrayPanel(); + } + + return mChannelPanel; + } + + public class ChannelArrayPanel extends JPanel + { + public ChannelArrayPanel() + { + int bufferSize = CHANNEL_BANDWIDTH / CHANNEL_FFT_FRAME_RATE; + if(bufferSize % 2 == 1) + { + bufferSize++; + } + + init(); + } + + private void init() + { + setLayout(new MigLayout("insets 0 0 0 0", "fill", "fill")); + + double spectralBandwidth = mTestTuner.getTunerController().getFrequencyController().getSampleRate(); + double halfSpectralBandwidth = spectralBandwidth / 2.0; + + int channelToLog = -1; + + for(int x = 0; x < mChannelCount; x++) + { + //place the channels left and right of 10.0 MHz + long frequency = (long)(mTestTuner.getTunerController().getFrequencyController().getFrequency() + + ((x * CHANNEL_BANDWIDTH) - halfSpectralBandwidth) + (CHANNEL_BANDWIDTH / 2)); + + mLog.debug("Channel " + x + " Frequency: " + frequency); + + ChannelPanel channelPanel = new ChannelPanel(mSettingsManager, CHANNEL_BANDWIDTH * 2, frequency, CHANNEL_BANDWIDTH, (x == channelToLog)); + channelPanel.setDFTSize(mChannelPanelDFTSize); + + if(x % mChannelsPerRow == mChannelsPerRow - 1) + { + add(channelPanel, "grow,push,wrap 2px"); + } + else + { + add(channelPanel, "grow,push"); + } + } + } + } + + public class PrimarySpectrumPanel extends JPanel implements Listener, ISourceEventProcessor + { + private DFTProcessor mDFTProcessor = new DFTProcessor(SampleType.COMPLEX); + private ComplexDecibelConverter mComplexDecibelConverter = new ComplexDecibelConverter(); + private SpectrumPanel mSpectrumPanel; + + public PrimarySpectrumPanel(SettingsManager settingsManager, double sampleRate) + { + setLayout(new MigLayout("insets 0 0 0 0", "[grow,fill]", "[grow,fill]")); + mSpectrumPanel = new SpectrumPanel(settingsManager); + add(mSpectrumPanel); + + mDFTProcessor.addConverter(mComplexDecibelConverter); + mDFTProcessor.process(SourceEvent.sampleRateChange(sampleRate)); + mComplexDecibelConverter.addListener(mSpectrumPanel); + } + + public void setDFTSize(DFTSize dftSize) + { + mDFTProcessor.setDFTSize(dftSize); + } + + @Override + public void receive(ComplexBuffer complexBuffer) + { + mDFTProcessor.receive(complexBuffer); + } + + @Override + public void process(SourceEvent event) throws SourceException + { + mLog.debug("Source Event! Add handler support for this to channelizer viewer"); + } + } + + public class ChannelPanel extends JPanel implements Listener, ISourceEventProcessor + { + private TunerChannelSource mSource; + private DFTProcessor mDFTProcessor = new DFTProcessor(SampleType.COMPLEX); + private ComplexDecibelConverter mComplexDecibelConverter = new ComplexDecibelConverter(); + private SpectrumPanel mSpectrumPanel; + private long mFrequency; + + public ChannelPanel(SettingsManager settingsManager, double sampleRate, long frequency, int bandwidth, boolean enableLogging) + { + setLayout(new MigLayout("insets 0 0 0 0", "[grow,fill]", "[grow,fill]")); + mSpectrumPanel = new SpectrumPanel(settingsManager); + add(mSpectrumPanel); + + mDFTProcessor.addConverter(mComplexDecibelConverter); + mDFTProcessor.process(SourceEvent.sampleRateChange(sampleRate)); + + mComplexDecibelConverter.addListener(mSpectrumPanel); + + mFrequency = frequency; + + TunerChannel tunerChannel = new TunerChannel(frequency, bandwidth); + mSource = mTestTuner.getSourceManager().getSource(tunerChannel); + + if(mSource != null) + { + if(enableLogging) + { + mSource.setListener(new Listener() + { + @Override + public void receive(ComplexBuffer complexBuffer) + { + mLog.debug("Sending samples to DFT processor: " + complexBuffer.getSamples().length); + mDFTProcessor.receive(complexBuffer); + } + }); + } + else + { + mSource.setListener(mDFTProcessor); + } + + mSource.start(); + } + else + { + mLog.error("Couldn't get a source from the tuner for frequency: " + frequency); + } + } + + public TunerChannelSource getSource() + { + return mSource; + } + + public void setDFTSize(DFTSize dftSize) + { + mDFTProcessor.setDFTSize(dftSize); + } + + @Override + public void receive(ComplexBuffer complexBuffer) + { + mLog.debug("Got a buffer for the channel dft processor"); + mDFTProcessor.receive(complexBuffer); + } + + @Override + public void process(SourceEvent event) throws SourceException + { + mLog.debug("Source Event! Add handler support for this to channelizer viewer"); + } + } + + public static void main(String[] args) + { + int channelsPerRow = 4; + + final ChannelizerViewer frame = new ChannelizerViewer(channelsPerRow); + + EventQueue.invokeLater(new Runnable() + { + @Override + public void run() + { + frame.setVisible(true); + } + }); + } + +} diff --git a/src/source/tuner/channel/polyphase/PolyphaseChannelManager.java b/src/source/tuner/channel/polyphase/PolyphaseChannelManager.java new file mode 100644 index 00000000..c15b64a2 --- /dev/null +++ b/src/source/tuner/channel/polyphase/PolyphaseChannelManager.java @@ -0,0 +1,384 @@ +/******************************************************************************* + * 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 source.tuner.channel.polyphase; + +import dsp.filter.channelizer.ComplexPolyphaseChannelizerM2; +import dsp.filter.design.FilterDesignException; +import org.slf4j.Logger; +import org.slf4j.LoggerFactory; +import sample.Listener; +import sample.complex.IComplexBufferProvider; +import source.SourceEvent; +import source.SourceException; +import source.tuner.channel.ChannelManager; +import source.tuner.channel.TunerChannel; +import source.tuner.channel.TunerChannelSource; +import source.tuner.channel.polyphase.output.IPolyphaseChannelOutputProcessor; +import source.tuner.channel.polyphase.output.OneChannelOutputProcessor; +import source.tuner.channel.polyphase.output.TwoChannelOutputProcessor; +import source.tuner.channel.FrequencyController; + +import java.util.List; +import java.util.concurrent.CopyOnWriteArrayList; + +public class PolyphaseChannelManager extends ChannelManager +{ + private final static Logger mLog = LoggerFactory.getLogger(PolyphaseChannelManager.class); + private static final int CHANNEL_BANDWIDTH = 12500; + private static final double CHANNEL_OVERSAMPLING = 2.0; + private static final int CHANNEL_SAMPLE_RATE = (int)(CHANNEL_BANDWIDTH * CHANNEL_OVERSAMPLING); + private static final int POLYPHASE_FILTER_TAPS_PER_CHANNEL = 17; + + private IComplexBufferProvider mComplexBufferProvider; + private List mChannelSources = new CopyOnWriteArrayList<>(); + private ChannelCalculator mChannelCalculator; + private ComplexPolyphaseChannelizerM2 mPolyphaseChannelizer; + private ChannelSourceEventListener mChannelSourceEventListener = new ChannelSourceEventListener(); + private ScheduledBufferProcessor mBufferProcessor; + + /** + * Polyphase Channel Manager is a DDC channel manager and complex buffer queue/processor for a tuner. This class + * provides DDC polyphase channel sources and wraps a polyphase channelizer processing sample buffers produced by + * the tuner and distributing channelized sample buffers to each allocated DDC polyphase channel source. This + * class is responsible for monitoring the tuner for changes in center frequency and/or sample rate and updating + * active DDC polyphase channel sources accordingly. This class also monitors source event requests and + * notifications received from active DDC polyphase channel sources to adjust sample streams as required. + * + * Note: add this channel manager as a source event listener to the complex buffer provider to ensure this manager + * adapts to changes in source frequency and sample rate. + * + * @param frequencyController that can be controlled by this channel manager + * @param complexBufferProvider that supports register/deregister of this channel manager + */ + public PolyphaseChannelManager(FrequencyController frequencyController, IComplexBufferProvider complexBufferProvider) + { + super(frequencyController); + + if(complexBufferProvider == null) + { + throw new IllegalArgumentException("Complex buffer provider argument cannot be null"); + } + + mComplexBufferProvider = complexBufferProvider; + + //Ensure that sample rate is a multiple of channel sample rate since polyphase channelizer is M2 + if(getFrequencyController().getSampleRate() % (CHANNEL_SAMPLE_RATE) != 0) + { + throw new IllegalArgumentException("Tuner sample rate [" + getFrequencyController().getSampleRate() + + "] must be an even multiple of " + CHANNEL_SAMPLE_RATE + " Hz"); + } + + int channelCount = (int)(getFrequencyController().getSampleRate() / CHANNEL_BANDWIDTH); + + mChannelCalculator = new ChannelCalculator(getFrequencyController().getSampleRate(), channelCount, + getFrequencyController().getFrequency(), CHANNEL_OVERSAMPLING); + + mBufferProcessor = new ScheduledBufferProcessor(500, 100, 50, 50); + + updateChannelizer(getFrequencyController().getSampleRate(), null); + } + + @Override + protected long calculateCenterFrequency() + { + return 0; + } + + /** + * Provides a Digital Drop Channel (DDC) for the specified tuner channel or returns null if the channel can't be + * sourced due to the current center frequency and/or sample rate. + * + * @param tunerChannel specifying center frequency and bandwidth. + * @return source or null. + */ + @Override + protected TunerChannelSource getTunerChannelSource(TunerChannel tunerChannel) + { + PolyphaseChannelSource channelSource = null; + + try + { + List polyphaseIndexes = mChannelCalculator.getChannelIndexes(tunerChannel); + + IPolyphaseChannelOutputProcessor outputProcessor = getOutputProcessor(polyphaseIndexes); + + if(outputProcessor != null) + { + channelSource = new PolyphaseChannelSource(tunerChannel, outputProcessor, mChannelSourceEventListener, + CHANNEL_SAMPLE_RATE); + + channelSource.setFrequency(mChannelCalculator.getCenterFrequencyForIndexes(polyphaseIndexes)); + + mChannelSources.add(channelSource); + } + } + catch(IllegalArgumentException iae) + { + mLog.info("Can't provide DDC for " + tunerChannel.toString() + " due to channelizer frequency [" + + mChannelCalculator.getCenterFrequency() + "] and sample rate [" + + (mChannelCalculator.getChannelCount() * mChannelCalculator.getChannelBandwidth()) + "]"); + } + + return channelSource; + } + + /** + * Creates a processor to process the channelizer channel indexes into a composite output stream providing + * channelized complex sample buffers to a registered source listener. + * + * @param indexes to target by the output processor + * @return output processor compatible with the number of indexes to monitor + */ + private IPolyphaseChannelOutputProcessor getOutputProcessor(List indexes) + { + switch(indexes.size()) + { + case 1: + return new OneChannelOutputProcessor(CHANNEL_SAMPLE_RATE, indexes); + case 2: + return new TwoChannelOutputProcessor(CHANNEL_SAMPLE_RATE, indexes); + 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()); + return null; + } + } + + /** + * Starts/adds the channel source to receive channelized sample buffers, registering with the tuner to receive + * sample buffers when this is the first channel. + * + * @param channelSource to start + */ + private void startChannelSource(PolyphaseChannelSource channelSource) + { + synchronized(mBufferProcessor) + { + mPolyphaseChannelizer.addChannel(channelSource); + + if(mPolyphaseChannelizer.getRegisteredChannelCount() == 1) + { + mComplexBufferProvider.addComplexBufferListener(mBufferProcessor); + mBufferProcessor.start(); + } + } + } + + /** + * Stops/removes the channel source from receiving channelized sample buffers and deregisters from the tuner + * when this is the last channel being sourced. + * + * @param channelSource to stop + */ + private void stopChannelSource(PolyphaseChannelSource channelSource) + { + synchronized(mBufferProcessor) + { + super.releaseSource(channelSource); + + mChannelSources.remove(channelSource); + + if(mPolyphaseChannelizer.getRegisteredChannelCount() == 0) + { + mComplexBufferProvider.removeComplexBufferListener(mBufferProcessor); + mBufferProcessor.stop(); + } + } + } + + /** + * Implements the Listener interface for receiving notifications of frequency and sample rate + * changes from the tuner. + */ + @Override + public void receive(SourceEvent sourceEvent) + { + switch(sourceEvent.getEvent()) + { + case NOTIFICATION_FREQUENCY_CHANGE: + //Update the index calculator and each of the output processors to update each channel frequency + mChannelCalculator.setCenterFrequency(sourceEvent.getValue().longValue()); + updateOutputProcessors(sourceEvent); + break; + case NOTIFICATION_SAMPLE_RATE_CHANGE: + //Update the channelizer (and the output processors) for the new sample rate + updateChannelizer(sourceEvent.getValue().intValue(), sourceEvent); + break; + } + } + + /** + * Creates or updates the channelizer to process the incoming sample rate and updates any channel processors. + * + * @param sampleRate to use for the channelizer + * @param sourceEvent containing a notification of sample rate change (optional - can be null). This source + * event will be passed to all output processors to ensure the change is fully promulgated before restarting the + * buffer processor if it was already running. + */ + private void updateChannelizer(double sampleRate, SourceEvent sourceEvent) + { + mChannelCalculator.setSampleRate(sampleRate); + + //Lock on the buffer processor to block any channel create/start/stop requests while we configure + synchronized(mBufferProcessor) + { + //Get current processor state so we can restart it after we update the channelizer + boolean bufferProcessorWasRunning = mBufferProcessor.isRunning(); + + //Stop will be ignored if it is not currently running + mBufferProcessor.stop(); + + try + { + if(mPolyphaseChannelizer == null) + { + mPolyphaseChannelizer = new ComplexPolyphaseChannelizerM2(sampleRate, + POLYPHASE_FILTER_TAPS_PER_CHANNEL); + + mBufferProcessor.setListener(mPolyphaseChannelizer); + } + else + { + mPolyphaseChannelizer.setSampleRate(sampleRate); + } + } + catch(IllegalArgumentException iae) + { + mLog.error("Could not create polyphase channelizer for sample rate [" + sampleRate + "]", iae); + } + catch(FilterDesignException fde) + { + mLog.error("Could not create filter for polyphase channelizer for sample rate [" + sampleRate + "]", fde); + } + + if(sourceEvent != null && sourceEvent.getEvent() == SourceEvent.Event.NOTIFICATION_SAMPLE_RATE_CHANGE) + { + updateOutputProcessors(sourceEvent); + } + + if(bufferProcessorWasRunning) + { + mBufferProcessor.start(); + } + } + } + + /** + * Updates each of the output processors for any changes in the tuner's center frequency or sample rate, which + * would cause the output processors to change the polyphase channelizer results channel(s) that the processor is + * consuming + * + * @param sourceEvent that requires an update to the output processors + */ + private void updateOutputProcessors(SourceEvent sourceEvent) + { + for(PolyphaseChannelSource channelSource : mChannelSources) + { + updateOutputProcessor(channelSource); + + //Notify the channel source that the frequency or sample rate has changed so that it can reset + //any frequency correction value. + if(sourceEvent != null) + { + try + { + channelSource.process(sourceEvent); + } + catch(SourceException se) + { + mLog.error("Error while notifying polyphase channel source of a source event", se); + } + } + } + } + + /** + * Updates the polyphase channel source's output processor due to a change in the center frequency or sample + * rate for the source providing sample buffers to the polyphase channelizer, or whenever the DDC channel's + * center tuned frequency changes. + * + * @param channelSource that requires an update to its output processor + */ + private void updateOutputProcessor(PolyphaseChannelSource channelSource) + { + try + { + List indexes = mChannelCalculator.getChannelIndexes(channelSource.getTunerChannel()); + + long centerFrequency = mChannelCalculator.getCenterFrequencyForIndexes(indexes); + + //If the indexes size is the same then update the current processor, otherwise create a new one + if(channelSource.getPolyphaseChannelOutputProcessor().getInputChannelCount() == indexes.size()) + { + channelSource.getPolyphaseChannelOutputProcessor().setPolyphaseChannelIndices(indexes); + channelSource.setFrequency(centerFrequency); + } + else + { + channelSource.setPolyphaseChannelOutputProcessor(getOutputProcessor(indexes), centerFrequency); + } + } + catch(IllegalArgumentException iae) + { + mLog.error("Error updating polyphase channel source - can't determine output channel indexes for " + + "updated tuner center frequency and sample rate. Stopping channel source", iae); + + stopChannelSource(channelSource); + } + } + + /** + * Internal class for handling requests for start/stop sample stream from polyphase channel sources + */ + private class ChannelSourceEventListener implements Listener + { + @Override + public void receive(SourceEvent sourceEvent) + { + switch(sourceEvent.getEvent()) + { + case REQUEST_START_SAMPLE_STREAM: + if(sourceEvent.hasSource() && sourceEvent.getSource() instanceof PolyphaseChannelSource) + { + startChannelSource((PolyphaseChannelSource)sourceEvent.getSource()); + } + else + { + mLog.error("Request to start sample stream for unrecognized source: " + + (sourceEvent.hasSource() ? sourceEvent.getSource().getClass() : "null source")); + } + break; + case REQUEST_STOP_SAMPLE_STREAM: + if(sourceEvent.hasSource() && sourceEvent.getSource() instanceof PolyphaseChannelSource) + { + stopChannelSource((PolyphaseChannelSource)sourceEvent.getSource()); + } + else + { + mLog.error("Request to stop sample stream for unrecognized source: " + + (sourceEvent.hasSource() ? sourceEvent.getSource().getClass() : "null source")); + } + break; + default: + mLog.error("Received unrecognized source event from polyphase channel source [" + + sourceEvent.getEvent() + "]"); + } + } + } +} diff --git a/src/source/tuner/channel/polyphase/PolyphaseChannelSource.java b/src/source/tuner/channel/polyphase/PolyphaseChannelSource.java new file mode 100644 index 00000000..8b7babaa --- /dev/null +++ b/src/source/tuner/channel/polyphase/PolyphaseChannelSource.java @@ -0,0 +1,224 @@ +/******************************************************************************* + * 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 source.tuner.channel.polyphase; + +import source.tuner.channel.polyphase.output.IPolyphaseChannelOutputProcessor; +import org.slf4j.Logger; +import org.slf4j.LoggerFactory; +import sample.Listener; +import sample.complex.ComplexBuffer; +import sample.complex.ComplexToComplexBufferAssembler; +import source.SourceEvent; +import source.tuner.channel.TunerChannel; +import source.tuner.channel.TunerChannelSource; + +public class PolyphaseChannelSource extends TunerChannelSource +{ + private final static Logger mLog = LoggerFactory.getLogger(PolyphaseChannelSource.class); + + private ComplexToComplexBufferAssembler mComplexBufferAssembler = new ComplexToComplexBufferAssembler(2500); + private IPolyphaseChannelOutputProcessor mPolyphaseChannelOutputProcessor; + private double mChannelSampleRate; + private long mCenterFrequency; + private long mFrequencyCorrection; + + /** + * Polyphase channelizer tuner channel source implementation. Adapts the channel array output samples from the + * polyphase channelizer into a single channel, or a channel synthesized from two adjacent channels that is + * frequency translated and decimated to a single channel. + * + * @param tunerChannel describing the desired channel frequency and bandwidth/minimum sample rate + * @param outputProcessor - to process polyphase channelizer channel results into a channel stream + * @param producerSourceEventListener to receive source event requests (e.g. start/stop sample stream) + * @param channelSampleRate for the downstream sample output + */ + public PolyphaseChannelSource(TunerChannel tunerChannel, IPolyphaseChannelOutputProcessor outputProcessor, + Listener producerSourceEventListener, double channelSampleRate) + { + super(producerSourceEventListener, tunerChannel); + mPolyphaseChannelOutputProcessor = outputProcessor; + mChannelSampleRate = channelSampleRate; + } + + /** + * Registers the listener to receive complex sample buffers from this channel source + */ + @Override + public void setListener(final Listener listener) + { + mComplexBufferAssembler.setListener(listener); + } + + /** + * Removes the listener from receiving complex sample buffers from this channel source + */ + @Override + public void removeListener(Listener listener) + { + mComplexBufferAssembler.setListener(null); + } + + /** + * Channel output processor used by this channel source to convert polyphase channel results into a specific + * channel complex buffer output stream. + */ + public IPolyphaseChannelOutputProcessor getPolyphaseChannelOutputProcessor() + { + return mPolyphaseChannelOutputProcessor; + } + + /** + * Sets/updates the output processor for this channel source, replacing the existing output processor. + * + * @param outputProcessor to use + * @param frequency center for the channels processed by the output processor + */ + public void setPolyphaseChannelOutputProcessor(IPolyphaseChannelOutputProcessor outputProcessor, long frequency) + { + //Lock on the channel output processor to block the channel results processor thread from servicing the + //channel output processor's buffer while we change out the processors. + synchronized(mPolyphaseChannelOutputProcessor) + { + IPolyphaseChannelOutputProcessor existingProcessor = mPolyphaseChannelOutputProcessor; + + //Swap out the processor so that incoming samples can accumulate in the new channel output processor + mPolyphaseChannelOutputProcessor = outputProcessor; + + //Remove the buffer overflow listener and clear any previous buffer overflow state + existingProcessor.setSourceOverflowListener(null); + broadcastOverflowState(false); + + //Register to receive buffer overflow notifications so we can push them up through the source chain + mPolyphaseChannelOutputProcessor.setSourceOverflowListener(this); + + //Fully process the residual channel results buffer of the previous channel output processor + if(existingProcessor != null) + { + existingProcessor.processChannelResults(mComplexBufferAssembler); + } + + //Finally, setup the frequency offset for the output processor and reset the frequency correction value + //to allow consumers to adjust and calculate a new frequency correction value, if needed. + mCenterFrequency = frequency; + mFrequencyCorrection = 0; + mPolyphaseChannelOutputProcessor.setFrequencyOffset(getFrequencyOffset()); + broadcastConsumerSourceEvent(SourceEvent.frequencyCorrectionChange(mFrequencyCorrection)); + } + } + + /** + * Primary method for receiving channel results output from a polyphase channelizer. The results array will be + * queued for processing to extract the target channel samples, process them for frequency correction and/or + * channel aggregation, and dispatch the results to the downstream sample listener/consumer. + * + * @param channelResultsBuffer containing the polyphase channelizer output for a single complex sample period. + */ + public void receiveChannelResults(float[] channelResultsBuffer) + { + mPolyphaseChannelOutputProcessor.receiveChannelResults(channelResultsBuffer); + } + + /** + * Downstream channel sample rate + * + * @return sample rate in Hertz + */ + @Override + public double getSampleRate() + { + return mChannelSampleRate; + } + + /** + * Sets the downstream channel sample rate + * @param sampleRate in hertz + */ + @Override + protected void setSampleRate(double sampleRate) + { + mChannelSampleRate = sampleRate; + } + + @Override + public void dispose() + { + mLog.info("Polyphase channel source received request to dispose"); + } + + @Override + public long getFrequencyCorrection() + { + return mFrequencyCorrection; + } + + /** + * Adjusts the frequency correction value that is being applied to the channelized output stream by the + * polyphase channel output processor. + * + * @param value to apply for frequency correction in hertz + */ + protected void setFrequencyCorrection(long value) + { + mFrequencyCorrection = value; + updateFrequencyOffset(); + broadcastConsumerSourceEvent(SourceEvent.frequencyCorrectionChange(mFrequencyCorrection)); + } + + /** + * Sets the center frequency for the incoming sample stream channel results and resets frequency correction to zero. + * @param frequency in hertz + */ + @Override + protected void setFrequency(long frequency) + { + mCenterFrequency = frequency; + setFrequencyCorrection(0); + } + + @Override + protected void processSamples() + { + //Lock on the output processor so that it can't be changed out in the middle of processing + synchronized(mPolyphaseChannelOutputProcessor) + { + mPolyphaseChannelOutputProcessor.processChannelResults(mComplexBufferAssembler); + } + } + + /** + * Calculates the frequency offset required to mix the incoming signal to the desired output baseband + */ + private long getFrequencyOffset() + { + return getTunerChannel().getFrequency() - mCenterFrequency + mFrequencyCorrection; + } + + /** + * Updates the frequency offset being applied by the output processor. Calculates the offset using the center + * frequency of the incoming polyphase channel results and the desired output channel frequency adjusted by any + * requested frequency correction value. + */ + private void updateFrequencyOffset() + { + synchronized(mPolyphaseChannelOutputProcessor) + { + mPolyphaseChannelOutputProcessor.setFrequencyOffset(getFrequencyOffset()); + } + } +} diff --git a/src/source/tuner/channel/polyphase/ScheduledBufferProcessor.java b/src/source/tuner/channel/polyphase/ScheduledBufferProcessor.java new file mode 100644 index 00000000..810cb838 --- /dev/null +++ b/src/source/tuner/channel/polyphase/ScheduledBufferProcessor.java @@ -0,0 +1,171 @@ +/******************************************************************************* + * 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 source.tuner.channel.polyphase; + +import org.slf4j.Logger; +import org.slf4j.LoggerFactory; +import sample.Listener; +import sample.OverflowableTransferQueue; +import sample.real.IOverflowListener; +import util.ThreadPool; + +import java.util.ArrayList; +import java.util.List; +import java.util.concurrent.ScheduledFuture; +import java.util.concurrent.TimeUnit; +import java.util.concurrent.atomic.AtomicBoolean; + +public class ScheduledBufferProcessor implements Listener +{ + private final static Logger mLog = LoggerFactory.getLogger(ScheduledBufferProcessor.class); + + private OverflowableTransferQueue mQueue; + private Listener mListener; + private ScheduledFuture mScheduledFuture; + private long mDistributionInterval; + private int mMaxBuffersPerInterval; + private AtomicBoolean mRunning = new AtomicBoolean(); + + /** + * Scheduled Buffer Processor combines an internal overflowable buffer with a scheduled runnable processing task + * for periodically distributing internally queued elements to the registered listener. This processor provides + * a convenient way to create a thread-safe buffer for receiving elements from one thread/runnable and then + * distributing those elements to a registered listener where distribution occurs on a separate scheduled thread + * pool runnable thread. This allows the calling input thread to quickly return without incurring any subsequent + * processing workload. + * + * The internal queue is an overflowable queue implementation that allows a listener to be registered to receive + * notifications of overflow and reset state. Queue sizing parameters are specified in the constructor. + * + * @param maximumSize of the internal queue (overflow happens when this is exceeded) + * @param resetThreshold of the internal queue (overflow reset happens once queue size falls below this threshold + * @param distributionInterval in milliseconds specifying how often the internal scheduled thread pool buffer + * distributor should run + * @param maxBuffersPerInterval maximum number of buffers to pull from the internal queue during each distribution + * interval + */ + public ScheduledBufferProcessor(int maximumSize, int resetThreshold, long distributionInterval, int maxBuffersPerInterval) + { + mQueue = new OverflowableTransferQueue<>(maximumSize, resetThreshold); + + mQueue.setOverflowListener(new IOverflowListener() + { + @Override + public void sourceOverflow(boolean overflow) + { + mLog.error("Overflow state changed - overflow:" + overflow); + } + }); + mDistributionInterval = distributionInterval; + mMaxBuffersPerInterval = maxBuffersPerInterval; + } + + /** + * Sets the listener to receive notifications of buffer overflow and/or reset. Note: this method can also be + * invoked with a null argument to clear the previously registered overflow listener. + * + * @param listener to receive overflow/reset notifications. + */ + public void setOverflowListener(IOverflowListener listener) + { + mQueue.setOverflowListener(listener); + } + + /** + * Sets or changes the listener to receive buffers from this processor. + * @param listener to receive buffers + */ + public void setListener(Listener listener) + { + mListener = listener; + } + + /** + * Primary input method for adding buffers to this processor. Note: incoming buffers will be ignored if this + * processor is in a stopped state. You must invoke start() to allow incoming buffers and initiate buffer + * processing. + * + * @param e to enqueue for distribution to a registered listener + */ + @Override + public void receive(E e) + { + mQueue.offer(e); + } + + /** + * Starts this buffer processor and allows queuing of incoming buffers. + */ + public void start() + { + if(mRunning.compareAndSet(false, true)) + { + mScheduledFuture = ThreadPool.SCHEDULED.scheduleAtFixedRate(new Processor(), 0, mDistributionInterval, TimeUnit.MILLISECONDS); + } + } + + /** + * Stops this buffer processor, stops queuing of incoming buffers and clears the internal buffer queue. + */ + public void stop() + { + if(mRunning.compareAndSet(true, false)) + { + if(mScheduledFuture != null) + { + mScheduledFuture.cancel(true); + mScheduledFuture = null; + } + + mQueue.clear(); + } + } + + /** + * Indicates if this processor is currently running + */ + public boolean isRunning() + { + return mRunning.get(); + } + + /** + * Processor to service the buffer queue and distribute the buffers to the registered listener + */ + class Processor implements Runnable + { + private List mBuffers = new ArrayList<>(); + + @Override + public void run() + { + mQueue.drainTo(mBuffers, mMaxBuffersPerInterval); + + if(mListener != null) + { + for(E buffer: mBuffers) + { + mListener.receive(buffer); + } + } + + mBuffers.clear(); + } + } +} diff --git a/src/source/tuner/channel/polyphase/output/ChannelOutputProcessor.java b/src/source/tuner/channel/polyphase/output/ChannelOutputProcessor.java new file mode 100644 index 00000000..2a32baa4 --- /dev/null +++ b/src/source/tuner/channel/polyphase/output/ChannelOutputProcessor.java @@ -0,0 +1,170 @@ +/******************************************************************************* + * 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 source.tuner.channel.polyphase.output; + +import dsp.mixer.Oscillator; +import org.slf4j.Logger; +import org.slf4j.LoggerFactory; +import sample.OverflowableTransferQueue; +import sample.complex.Complex; +import sample.complex.ComplexSampleListener; +import sample.real.IOverflowListener; +import source.Source; + +import java.util.ArrayList; +import java.util.List; + +public abstract class ChannelOutputProcessor implements IPolyphaseChannelOutputProcessor +{ + private final static Logger mLog = LoggerFactory.getLogger(ChannelOutputProcessor.class); + + private OverflowableTransferQueue mChannelResultsQueue; + private List mChannelResultsToProcess = new ArrayList<>(); + private int mMaxResultsToProcess; + + private int mInputChannelCount; +//TODO: swap this out and use the LowPhaseNoiseOscillator + private Oscillator mFrequencyCorrectionMixer; + private boolean mFrequencyCorrectionEnabled; + + /** + * Base class for polyphase channelizer output channel processing. Provides built-in frequency translation + * oscillator support to apply frequency correction to the channel sample stream as requested by sample consumer. + * + * @param inputChannelCount is the number of input channels for this output processor + * @param sampleRate of the output channel. This is used to match the oscillator's sample rate to the output + * channel sample rate for frequency translation/correction. + */ + public ChannelOutputProcessor(int inputChannelCount, double sampleRate) + { + mInputChannelCount = inputChannelCount; + mFrequencyCorrectionMixer = new Oscillator(0, sampleRate); + mMaxResultsToProcess = (int)(sampleRate / 10) * 2; //process at 100 millis interval, twice the expected inflow rate + + mChannelResultsQueue = new OverflowableTransferQueue<>((int)(sampleRate * 3), (int)(sampleRate * 0.5)); + } + + /** + * 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) + { + mFrequencyCorrectionMixer.setFrequency(frequencyOffset); + mFrequencyCorrectionEnabled = (frequencyOffset != 0); + } + + @Override + public void receiveChannelResults(float[] channelResults) + { + mChannelResultsQueue.offer(channelResults); + } + + /** + * Processes all enqueued polyphase channelizer results until the internal queue is empty + * @param listener to receive the processed channel results + */ + @Override + public void processChannelResults(ComplexSampleListener listener) + { + try + { + int count = mChannelResultsQueue.drainTo(mChannelResultsToProcess, mMaxResultsToProcess); + + if(count > 0) + { + process(mChannelResultsToProcess, listener); + } + } + catch(Throwable throwable) + { + mLog.error("Error while processing polyphase channel samples", throwable); + } + + mChannelResultsToProcess.clear(); + } + + /** + * Sub-class implementation to process one polyphase channelizer result array. + * @param channelResults to process + * @param listener to receive the channel complex sample + */ + public abstract void process(List channelResults, ComplexSampleListener listener); + + + /** + * Sets the overflow listener to monitor the internal channelizer channel results queue overflow state + */ + public void setSourceOverflowListener(Source source) + { + mChannelResultsQueue.setSourceOverflowListener(source); + } + + /** + * Removes the overflow listener from monitoring the internal channelizer channel results queue overflow state + */ + public void removeOverflowListener(IOverflowListener listener) + { + mChannelResultsQueue.setOverflowListener(null); + } + + @Override + public int getInputChannelCount() + { + return mInputChannelCount; + } + + /** + * Provides frequency correction of the inphase component or simply returns the inphase argument value when no + * frequency correction value has been specified. + * + * @param inphase value of the sample to translate/correct + * @param quadrature value of the sample to translate/correct + * @return corrected inphase value + */ + protected float getFrequencyCorrectedInphase(float inphase, float quadrature) + { + return Complex.multiplyInphase(inphase, quadrature, mFrequencyCorrectionMixer.inphase(), + mFrequencyCorrectionMixer.quadrature()); + } + + /** + * Provides frequency correction of the quadrature component or simply returns the quadrature argument value when no + * frequency correction value has been applied, + * + * @param inphase value of the sample to translate/correct + * @param quadrature value of the sample to translate/correct + * @return translated inphase value + */ + protected float getFrequencyCorrectedQuadrature(float inphase, float quadrature) + { + return Complex.multiplyQuadrature(inphase, quadrature, mFrequencyCorrectionMixer.inphase(), + mFrequencyCorrectionMixer.quadrature()); + } + + /** + * Indicates if this channel has a frequency correction offset + */ + public boolean hasFrequencyCorrection() + { + return mFrequencyCorrectionEnabled; + } + +} diff --git a/src/source/tuner/channel/polyphase/output/IPolyphaseChannelOutputProcessor.java b/src/source/tuner/channel/polyphase/output/IPolyphaseChannelOutputProcessor.java new file mode 100644 index 00000000..f8348990 --- /dev/null +++ b/src/source/tuner/channel/polyphase/output/IPolyphaseChannelOutputProcessor.java @@ -0,0 +1,66 @@ +/******************************************************************************* + * 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 source.tuner.channel.polyphase.output; + +import sample.complex.ComplexSampleListener; +import source.Source; + +import java.util.List; + +public interface IPolyphaseChannelOutputProcessor +{ + /** + * Receive and enqueue output results from the polyphase analysis channelizer + * @param channelResults to enqueue + */ + void receiveChannelResults(float[] channelResults); + + /** + * Process the channel output channel results queue and deliver the output to the listener + * + * @param listener to receive the processed channel results + */ + void processChannelResults(ComplexSampleListener listener); + + /** + * Sets the desired frequency offset from center. The samples will be mixed with an oscillator set to this offset + * frequency to produce an output where the desired signal is centered in the passband. + * + * @param frequency in hertz + */ + void setFrequencyOffset(long frequency); + + /** + * Indicates the number of input channels processed by this output processor + * @return + */ + int getInputChannelCount(); + + /** + * Updates the input polyphase channel index(es) used by this output processor + * @param indexes + */ + void setPolyphaseChannelIndices(List indexes); + + /** + * Sets the listener to receive buffer overflow notifications + * @param source to receive overflow notifications + */ + void setSourceOverflowListener(Source source); +} diff --git a/src/source/tuner/channel/polyphase/output/OneChannelOutputProcessor.java b/src/source/tuner/channel/polyphase/output/OneChannelOutputProcessor.java new file mode 100644 index 00000000..44fd12f9 --- /dev/null +++ b/src/source/tuner/channel/polyphase/output/OneChannelOutputProcessor.java @@ -0,0 +1,96 @@ +/******************************************************************************* + * 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 source.tuner.channel.polyphase.output; + +import org.slf4j.Logger; +import org.slf4j.LoggerFactory; +import sample.complex.ComplexSampleListener; + +import java.util.List; + +public class OneChannelOutputProcessor extends ChannelOutputProcessor +{ + private final static Logger mLog = LoggerFactory.getLogger(OneChannelOutputProcessor.class); + + private int mChannelOffset; + + /** + * Processor to extract a single channel from a polyphase channelizer and produce an output I/Q complex sample + * from each polyphase channelizer output results array. + * + * @param sampleRate of the output sample stream. + * @param channelIndexes containing a single channel index. + */ + public OneChannelOutputProcessor(double sampleRate, List channelIndexes) + { + super(1, sampleRate); + setPolyphaseChannelIndices(channelIndexes); + mLog.debug("Sample Rate:" + sampleRate + " PolyPhase Channel Indexes:" + channelIndexes); + } + + /** + * Updates this processor to extract a single, specified channel index. + * @param indexes containing a single channel index value. + * @throws IllegalArgumentException if the list of indexes does not contain a single channel index. + */ + public void setPolyphaseChannelIndices(List indexes) + { + if(indexes.size() != 1) + { + throw new IllegalArgumentException("Single channel output processor requires a single index to " + + "process - provided indexes " + indexes.toString()); + } + + //Set the channelized output results offset to twice the channel index to account for each channel having + //an I/Q pair + mChannelOffset = indexes.get(0) * 2; + } + + /** + * Extract the channel from the channel results array, apply frequency translation, and deliver the + * extracted frequency-corrected channel I/Q sample set to the complex sample listener. + * + * @param channelResults to process containing a list of channel array of I/Q sample pairs (I0,Q0,I1,Q1...In,Qn) + * @param listener to receive the extracted, frequency-translated channel results + */ + @Override + public void process(List channelResults, ComplexSampleListener listener) + { + for(float[] channels: channelResults) + { + if(channels.length < mChannelOffset + 1) + { + throw new IllegalArgumentException("Polyphase channelizer output channels array is not large enough to " + + "cover this single channel output processor with channel offset [" + mChannelOffset + "]"); + } + + float i = channels[mChannelOffset]; + float q = channels[mChannelOffset + 1]; + + if(hasFrequencyCorrection()) + { + listener.receive(getFrequencyCorrectedInphase(i, q), getFrequencyCorrectedQuadrature(i, q)); + } + else + { + listener.receive(i, q); + } + } + } +} diff --git a/src/source/tuner/channel/polyphase/output/TwoChannelOutputProcessor.java b/src/source/tuner/channel/polyphase/output/TwoChannelOutputProcessor.java new file mode 100644 index 00000000..e96818e5 --- /dev/null +++ b/src/source/tuner/channel/polyphase/output/TwoChannelOutputProcessor.java @@ -0,0 +1,91 @@ +/******************************************************************************* + * 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 source.tuner.channel.polyphase.output; + +import org.slf4j.Logger; +import org.slf4j.LoggerFactory; +import sample.complex.ComplexSampleListener; + +import java.util.List; + +public class TwoChannelOutputProcessor extends ChannelOutputProcessor +{ + private final static Logger mLog = LoggerFactory.getLogger(TwoChannelOutputProcessor.class); + + private int mChannelOffset1; + private int mChannelOffset2; + + /** + * Processor to extract two channels from a polyphase channelizer, synthesize/recombine the channels, apply + * frequency translation and frequency correction, down-sample by a factor of two and output an I/Q complex sample. + * + * @param sampleRate of the output sample stream. + * @param channelIndexes containing two channel indices. + */ + public TwoChannelOutputProcessor(int sampleRate, List channelIndexes) + { + //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); + } + + /** + * Updates this processor to extract the two specified channel indexes. + * + * @param indexes containing a single channel index value. + * @throws IllegalArgumentException if the list of indexes does not contain a single channel index. + */ + public void setPolyphaseChannelIndices(List indexes) + { + if(indexes.size() != 2) + { + throw new IllegalArgumentException("Double channel output processor requires two indexes to " + + "process - provided indexes " + indexes.toString()); + } + + //Set the channelized output results offsets to twice the channel index to account for each channel having + //an I/Q pair + mChannelOffset1 = indexes.get(0) * 2; + mChannelOffset2 = indexes.get(1) * 2; + } + + /** + * Extract the channel from the channel results array, apply frequency translation, and deliver the + * extracted frequency-corrected channel I/Q sample set to the complex sample listener. + * + * @param channelResults to process containing an array of channel I/Q sample pairs (I0,Q0,I1,Q1...In,Qn) + * @param listener to receive the extracted, frequency-translated channel results + */ + @Override + public void process(List channelResults, ComplexSampleListener listener) + { + mLog.debug("I got channel results - doing nothing !!"); +// if(channels.length < mChannelOffset + 1) +// { +// throw new IllegalArgumentException("Polyphase channelizer output channels array is not large enough to " + +// "cover this single channel output processor with channel offset [" + mChannelOffset + "]"); +// } +// +// float i = channels[mChannelOffset]; +// float q = channels[mChannelOffset + 1]; + +// listener.receive(getFrequencyCorrectedInphase(i, q), getFrequencyCorrectedQuadrature(i, q)); + } +} From 236df8f3f42665a3be87eb76b6e5bf2125920ca4 Mon Sep 17 00:00:00 2001 From: Denny Date: Wed, 27 Dec 2017 10:10:51 -0500 Subject: [PATCH 38/87] Merges from master branch. --- src/audio/AudioModule.java | 188 - src/audio/broadcast/StreamManager.java | 233 -- src/buffer/FloatCircularBuffer.java | 126 - src/buffer/ReverseFloatCircularBuffer.java | 89 - .../state/AlwaysUnsquelchedDecoderState.java | 97 - src/channel/state/ChannelState.java | 639 --- src/channel/state/DecoderState.java | 222 - .../traffic/TrafficChannelManager.java | 395 -- src/controller/channel/Channel.java | 478 --- src/dsp/filter/FilterFactory.java | 910 ----- src/dsp/filter/Window.java | 439 -- src/dsp/filter/design/FilterView.java | 165 - src/dsp/filter/design/FilterViewer.java | 109 - .../filter/fir/FIRFilterSpecification.java | 1129 ----- .../fir/remez/RemezFIRFilterDesigner.java | 672 --- src/dsp/gain/AutomaticGainControl.java | 224 - .../gain/ComplexFeedForwardGainControl.java | 112 - src/dsp/mixer/Oscillator.java | 152 - src/dsp/psk/LSMDemodulator.java | 589 --- src/gui/control/JFrequencyControl.java | 506 --- .../tap/stream/BinaryTapViewPanel.java | 156 - .../io/github/dsheirer/audio/AudioModule.java | 4 +- .../audio/broadcast/StreamManager.java | 2 +- .../dsheirer/buffer/FloatCircularBuffer.java | 208 +- .../dsheirer/channel/heartbeat/Heartbeat.java | 31 - .../channel/heartbeat/IHeartbeatProvider.java | 28 - .../state/AlwaysUnsquelchedDecoderState.java | 4 +- .../dsheirer/channel/state/ChannelState.java | 34 +- .../traffic/TrafficChannelManager.java | 51 +- .../dsheirer/controller/channel/Channel.java | 28 +- .../dsheirer/dsp/filter/FilterFactory.java | 567 ++- .../io/github/dsheirer/dsp/filter/Window.java | 585 ++- .../AbstractComplexPolyphaseChannelizer.java | 13 +- .../channelizer}/ChannelCalculator.java | 26 +- .../channelizer}/ChannelizerViewer.java | 87 +- .../ComplexPolyphaseChannelizer.java | 12 +- .../ComplexPolyphaseChannelizerM2.java | 16 +- .../channelizer}/PolyphaseChannelManager.java | 87 +- .../channelizer}/PolyphaseChannelSource.java | 38 +- .../ScheduledBufferProcessor.java | 29 +- .../output/ChannelOutputProcessor.java | 14 +- .../IPolyphaseChannelOutputProcessor.java | 6 +- .../output/OneChannelOutputProcessor.java | 4 +- .../output/TwoChannelOutputProcessor.java | 4 +- .../filter/correction/IQCorrectionFilter.java | 4 +- .../filter/fir/FIRFilterSpecification.java | 2217 +++++----- .../remez/PolyphaseChannelizerDesigner.java | 4 +- .../PolyphaseChannelizerFilterFactory.java | 4 +- .../fir/remez/RemezFIRFilterDesigner.java | 9 +- .../fir/remez/RemezFIRFilterDesigner2.java | 6 +- .../RemezFIRFilterDesignerWithLagrange.java | 6 +- .../dsp/gain/AutomaticGainControl.java | 2 +- .../gain/ComplexFeedForwardGainControl.java | 2 +- .../dsp/mixer/LowPhaseNoiseOscillator.java | 6 +- .../github/dsheirer/dsp/mixer/Oscillator.java | 224 +- .../dsheirer/dsp/psk/LSMDemodulator.java | 12 +- .../gui/control/JFrequencyControl.java | 114 +- .../tap/stream/BinaryTapViewPanel.java | 7 +- .../io/github/dsheirer/module/Module.java | 6 +- .../dsheirer/module/ProcessingChain.java | 90 +- .../dsheirer/module/decode/Decoder.java | 18 +- .../module/decode/DecoderFactory.java | 23 +- .../dsheirer/module/decode/am/AMDecoder.java | 10 +- .../decode/fleetsync2/Fleetsync2Decoder.java | 16 - .../fleetsync2/Fleetsync2DecoderState.java | 34 +- .../module/decode/lj1200/LJ1200Decoder.java | 16 - .../decode/lj1200/LJ1200DecoderState.java | 35 +- .../module/decode/ltrnet/LTRNetDecoder.java | 11 - .../decode/ltrnet/LTRNetDecoderState.java | 35 +- .../ltrstandard/LTRStandardDecoder.java | 12 - .../ltrstandard/LTRStandardDecoderState.java | 36 +- .../module/decode/mdc1200/MDCDecoder.java | 22 - .../decode/mdc1200/MDCDecoderState.java | 35 +- .../module/decode/mpt1327/MPT1327Decoder.java | 13 - .../decode/mpt1327/MPT1327DecoderState.java | 35 +- .../module/decode/nbfm/NBFMDecoder.java | 4 +- .../module/decode/p25/C4FMSymbolFilter.java | 834 ++-- .../module/decode/p25/P25Decoder.java | 6 +- .../module/decode/p25/P25DecoderState.java | 28 +- .../module/decode/p25/P25_C4FMDecoder.java | 34 +- .../module/decode/p25/P25_LSMDecoder.java | 43 +- .../decode/p25/audio/P25AudioModule.java | 4 +- .../decode/passport/PassportDecoder.java | 11 - .../decode/passport/PassportDecoderState.java | 34 +- .../module/decode/tait/Tait1200Decoder.java | 22 - .../decode/tait/Tait1200DecoderState.java | 35 +- .../demodulate/am/AMDemodulatorModule.java | 144 +- .../audio/DemodulatedAudioFilterModule.java | 6 +- .../demodulate/fm/FMDemodulatorModule.java | 4 +- .../dsheirer/module/log/EventLogger.java | 22 +- .../github/dsheirer/record/AudioRecorder.java | 8 +- .../dsheirer/record/RecorderManager.java | 2 +- .../record/wave/AudioPacketWaveRecorder.java | 6 +- .../wave/ComplexBufferWaveRecorder.java | 33 +- .../record/wave/RealBufferWaveRecorder.java | 9 +- .../github/dsheirer/sample/Broadcaster.java | 142 +- .../sample/OverflowableTransferQueue.java | 27 +- .../io/github/dsheirer/sample/SampleType.java | 5 +- .../complex/IComplexBufferProvider.java | 4 +- .../github/dsheirer/source/ComplexSource.java | 9 +- .../source/ISourceEventListener.java | 4 +- .../source/ISourceEventProcessor.java | 2 +- .../source/ISourceEventProvider.java | 4 +- .../source/InvalidFrequencyException.java | 7 + .../io/github/dsheirer/source/RealSource.java | 9 +- .../io/github/dsheirer/source/Source.java | 65 +- .../github/dsheirer}/source/SourceEvent.java | 2 +- ...SourceEventListenerToProcessorAdapter.java | 7 +- .../source/config/SourceConfigRecording.java | 45 +- .../source/config/SourceConfigTuner.java | 9 +- .../dsheirer}/source/heartbeat/Heartbeat.java | 2 +- .../source/heartbeat/HeartbeatManager.java | 6 +- .../heartbeat/IHeartbeatListener.java | 2 +- .../source/heartbeat/IHeartbeatProvider.java | 2 +- .../source/mixer/RealMixerSource.java | 75 +- .../dsheirer/source/recording/Recording.java | 42 +- .../recording/RecordingSourceManager.java | 11 +- .../source/tuner/ITunerChannelProvider.java | 10 +- .../dsheirer/source/tuner/MixerTuner.java | 129 - .../github/dsheirer/source/tuner/Tuner.java | 275 +- .../dsheirer/source/tuner/TunerChannel.java | 122 - .../source/tuner/TunerChannelProvider.java | 27 - .../source/tuner/TunerChannelSource.java | 572 --- .../dsheirer/source/tuner/TunerClass.java | 3 +- .../source/tuner/TunerController.java | 120 +- .../dsheirer/source/tuner/TunerEditor.java | 155 +- .../dsheirer/source/tuner/TunerManager.java | 27 +- .../dsheirer/source/tuner/TunerModel.java | 495 ++- .../source/tuner/TunerSourceEditor.java | 20 +- .../dsheirer/source/tuner/TunerType.java | 1 + .../source/tuner/airspy/AirspyTuner.java | 66 +- .../tuner/airspy/AirspyTunerController.java | 58 +- .../tuner/airspy/AirspyTunerEditor.java | 416 +- .../tuner/channel}/CICTunerChannelSource.java | 60 +- .../source/tuner/channel/TunerChannel.java | 2 +- .../tuner/channel/TunerChannelProvider.java | 35 + .../tuner/channel/TunerChannelSource.java | 33 +- .../TunerConfigurationModel.java | 31 +- .../source/tuner/ettus/B100Tuner.java | 83 - .../tuner/ettus/B100TunerEditorPanel.java | 42 - .../dsheirer/source/tuner/fcd/FCDTuner.java | 84 +- .../source/tuner/fcd/FCDTunerController.java | 99 +- .../tuner/fcd/proV1/FCD1TunerController.java | 1268 +++--- .../tuner/fcd/proV1/FCD1TunerEditor.java | 543 +-- .../fcd/proplusV2/FCD2TunerController.java | 47 +- .../tuner/fcd/proplusV2/FCD2TunerEditor.java | 281 +- .../frequency/DCTrackingFrequencyControl.java | 224 +- .../tuner/frequency/FrequencyChangeEvent.java | 34 - .../FrequencyChangeProcessorWrapper.java | 44 - .../tuner/frequency/FrequencyController.java | 284 +- .../frequency/IFrequencyChangeListener.java | 34 - .../frequency/IFrequencyChangeProcessor.java | 8 - .../frequency/IFrequencyChangeProvider.java | 30 - .../source/tuner/hackrf/HackRFTuner.java | 142 +- .../tuner/hackrf/HackRFTunerController.java | 37 +- .../tuner/hackrf/HackRFTunerEditor.java | 595 +-- .../tuner/manager/AbstractSourceManager.java | 108 + .../tuner/manager/TunerSourceManager.java | 80 + .../source/tuner/rtl/RTL2832Tuner.java | 144 +- .../tuner/rtl/RTL2832TunerController.java | 155 +- .../tuner/rtl/e4k/E4KTunerController.java | 108 +- .../source/tuner/rtl/e4k/E4KTunerEditor.java | 570 +-- .../tuner/rtl/r820t/R820TTunerController.java | 105 +- .../tuner/rtl/r820t/R820TTunerEditor.java | 614 +-- .../source/tuner/test/SampleGenerator.java | 10 +- .../source/tuner/test/TestTuner.java | 8 +- .../tuner/test/TestTunerController.java | 12 +- .../tuner/usb/USBTransferProcessor.java | 36 +- .../source/tuner/usb/USBTunerController.java | 8 +- .../source/wave/ComplexWaveSource.java | 37 +- .../dsheirer/source/wave/RealWaveSource.java | 62 +- .../spectrum/ChannelSpectrumPanel.java | 815 ++-- .../dsheirer/spectrum/DFTProcessor.java | 29 +- .../dsheirer/spectrum/OverlayPanel.java | 22 +- .../spectrum/SpectralDisplayPanel.java | 112 +- src/module/Module.java | 76 - src/module/ProcessingChain.java | 821 ---- src/module/decode/Decoder.java | 100 - src/module/decode/am/AMDecoder.java | 53 - .../decode/fleetsync2/Fleetsync2Decoder.java | 177 - .../fleetsync2/Fleetsync2DecoderState.java | 301 -- src/module/decode/lj1200/LJ1200Decoder.java | 195 - .../decode/lj1200/LJ1200DecoderState.java | 138 - src/module/decode/ltrnet/LTRNetDecoder.java | 123 - .../decode/ltrnet/LTRNetDecoderState.java | 783 ---- .../ltrstandard/LTRStandardDecoder.java | 87 - .../ltrstandard/LTRStandardDecoderState.java | 466 --- src/module/decode/mdc1200/MDCDecoder.java | 205 - .../decode/mdc1200/MDCDecoderState.java | 253 -- src/module/decode/mpt1327/MPT1327Decoder.java | 300 -- .../decode/mpt1327/MPT1327DecoderState.java | 541 --- src/module/decode/nbfm/NBFMDecoder.java | 52 - src/module/decode/p25/C4FMSymbolFilter.java | 508 --- src/module/decode/p25/P25Decoder.java | 70 - src/module/decode/p25/P25DecoderState.java | 3622 ----------------- src/module/decode/p25/P25_C4FMDecoder.java | 225 - src/module/decode/p25/P25_LSMDecoder.java | 240 -- .../decode/p25/audio/P25AudioModule.java | 269 -- .../decode/passport/PassportDecoder.java | 112 - .../decode/passport/PassportDecoderState.java | 574 --- src/module/decode/tait/Tait1200Decoder.java | 193 - .../decode/tait/Tait1200DecoderState.java | 212 - .../demodulate/am/AMDemodulatorModule.java | 110 - .../audio/DemodulatedAudioFilterModule.java | 121 - .../demodulate/fm/FMDemodulatorModule.java | 115 - src/module/log/EventLogger.java | 138 - src/record/AudioRecorder.java | 336 -- src/record/RecorderManager.java | 249 -- .../wave/ComplexBufferWaveRecorder.java | 218 - src/record/wave/RealBufferWaveRecorder.java | 212 - src/sample/Broadcaster.java | 124 - src/sample/OverflowableTransferQueue.java | 151 - src/sample/SampleType.java | 7 - src/source/ComplexSource.java | 32 - src/source/InvalidFrequencyException.java | 64 - src/source/RealSource.java | 32 - src/source/Source.java | 89 - src/source/config/SourceConfigRecording.java | 70 - src/source/config/SourceConfigTuner.java | 68 - src/source/heartbeat/IHeartbeatListener.java | 26 - src/source/mixer/RealMixerSource.java | 306 -- src/source/recording/Recording.java | 117 - .../recording/RecordingSourceManager.java | 146 - src/source/tuner/ITunerChannelProvider.java | 46 - src/source/tuner/Tuner.java | 148 - src/source/tuner/TunerClass.java | 278 -- src/source/tuner/TunerController.java | 202 - src/source/tuner/TunerEditor.java | 356 -- src/source/tuner/TunerManager.java | 463 --- src/source/tuner/TunerModel.java | 292 -- src/source/tuner/TunerSourceEditor.java | 95 - src/source/tuner/TunerType.java | 49 - src/source/tuner/airspy/AirspyTuner.java | 74 - .../tuner/airspy/AirspyTunerController.java | 1191 ------ .../tuner/airspy/AirspyTunerEditor.java | 770 ---- src/source/tuner/channel/ChannelManager.java | 337 -- .../tuner/channel/FrequencyController.java | 344 -- .../FrequencyControllerLockedException.java | 35 - .../TunerConfigurationModel.java | 384 -- src/source/tuner/fcd/FCDTuner.java | 64 - src/source/tuner/fcd/FCDTunerController.java | 657 --- .../tuner/fcd/proV1/FCD1TunerController.java | 717 ---- .../tuner/fcd/proV1/FCD1TunerEditor.java | 528 --- .../fcd/proplusV2/FCD2TunerController.java | 238 -- .../tuner/fcd/proplusV2/FCD2TunerEditor.java | 328 -- .../frequency/DCTrackingSourceControl.java | 130 - src/source/tuner/hackrf/HackRFTuner.java | 79 - .../tuner/hackrf/HackRFTunerController.java | 884 ---- .../tuner/hackrf/HackRFTunerEditor.java | 464 --- src/source/tuner/rtl/RTL2832Tuner.java | 75 - .../tuner/rtl/RTL2832TunerController.java | 1555 ------- .../tuner/rtl/e4k/E4KTunerController.java | 2512 ------------ src/source/tuner/rtl/e4k/E4KTunerEditor.java | 499 --- .../tuner/rtl/r820t/R820TTunerController.java | 1296 ------ .../tuner/rtl/r820t/R820TTunerEditor.java | 559 --- .../tuner/usb/USBTransferProcessor.java | 434 -- src/source/wave/ComplexWaveSource.java | 285 -- src/source/wave/RealWaveSource.java | 290 -- src/spectrum/ChannelSpectrumPanel.java | 489 --- src/spectrum/DFTProcessor.java | 482 --- src/spectrum/OverlayPanel.java | 913 ----- src/spectrum/SpectralDisplayPanel.java | 941 ----- 262 files changed, 7701 insertions(+), 49122 deletions(-) delete mode 100644 src/audio/AudioModule.java delete mode 100644 src/audio/broadcast/StreamManager.java delete mode 100644 src/buffer/FloatCircularBuffer.java delete mode 100644 src/buffer/ReverseFloatCircularBuffer.java delete mode 100644 src/channel/state/AlwaysUnsquelchedDecoderState.java delete mode 100644 src/channel/state/ChannelState.java delete mode 100644 src/channel/state/DecoderState.java delete mode 100644 src/channel/traffic/TrafficChannelManager.java delete mode 100644 src/controller/channel/Channel.java delete mode 100644 src/dsp/filter/FilterFactory.java delete mode 100644 src/dsp/filter/Window.java delete mode 100644 src/dsp/filter/design/FilterView.java delete mode 100644 src/dsp/filter/design/FilterViewer.java delete mode 100644 src/dsp/filter/fir/FIRFilterSpecification.java delete mode 100644 src/dsp/filter/fir/remez/RemezFIRFilterDesigner.java delete mode 100644 src/dsp/gain/AutomaticGainControl.java delete mode 100644 src/dsp/gain/ComplexFeedForwardGainControl.java delete mode 100644 src/dsp/mixer/Oscillator.java delete mode 100644 src/dsp/psk/LSMDemodulator.java delete mode 100644 src/gui/control/JFrequencyControl.java delete mode 100644 src/instrument/tap/stream/BinaryTapViewPanel.java delete mode 100644 src/main/java/io/github/dsheirer/channel/heartbeat/Heartbeat.java delete mode 100644 src/main/java/io/github/dsheirer/channel/heartbeat/IHeartbeatProvider.java rename src/{ => main/java/io/github/dsheirer}/dsp/filter/channelizer/AbstractComplexPolyphaseChannelizer.java (93%) rename src/{source/tuner/channel/polyphase => main/java/io/github/dsheirer/dsp/filter/channelizer}/ChannelCalculator.java (96%) rename src/{source/tuner/channel/polyphase => main/java/io/github/dsheirer/dsp/filter/channelizer}/ChannelizerViewer.java (81%) rename src/{ => main/java/io/github/dsheirer}/dsp/filter/channelizer/ComplexPolyphaseChannelizer.java (94%) rename src/{ => main/java/io/github/dsheirer}/dsp/filter/channelizer/ComplexPolyphaseChannelizerM2.java (97%) rename src/{source/tuner/channel/polyphase => main/java/io/github/dsheirer/dsp/filter/channelizer}/PolyphaseChannelManager.java (84%) rename src/{source/tuner/channel/polyphase => main/java/io/github/dsheirer/dsp/filter/channelizer}/PolyphaseChannelSource.java (87%) rename src/{source/tuner/channel/polyphase => main/java/io/github/dsheirer/dsp/filter/channelizer}/ScheduledBufferProcessor.java (88%) rename src/{source/tuner/channel/polyphase => main/java/io/github/dsheirer/dsp/filter/channelizer}/output/ChannelOutputProcessor.java (94%) rename src/{source/tuner/channel/polyphase => main/java/io/github/dsheirer/dsp/filter/channelizer}/output/IPolyphaseChannelOutputProcessor.java (93%) rename src/{source/tuner/channel/polyphase => main/java/io/github/dsheirer/dsp/filter/channelizer}/output/OneChannelOutputProcessor.java (96%) rename src/{source/tuner/channel/polyphase => main/java/io/github/dsheirer/dsp/filter/channelizer}/output/TwoChannelOutputProcessor.java (96%) rename src/{ => main/java/io/github/dsheirer}/dsp/filter/correction/IQCorrectionFilter.java (93%) rename src/{ => main/java/io/github/dsheirer}/dsp/filter/fir/remez/PolyphaseChannelizerDesigner.java (92%) rename src/{ => main/java/io/github/dsheirer}/dsp/filter/fir/remez/PolyphaseChannelizerFilterFactory.java (97%) rename src/{ => main/java/io/github/dsheirer}/dsp/filter/fir/remez/RemezFIRFilterDesigner2.java (99%) rename src/{ => main/java/io/github/dsheirer}/dsp/filter/fir/remez/RemezFIRFilterDesignerWithLagrange.java (99%) rename src/{ => main/java/io/github/dsheirer}/dsp/mixer/LowPhaseNoiseOscillator.java (98%) rename src/{ => main/java/io/github/dsheirer}/sample/complex/IComplexBufferProvider.java (93%) rename src/{ => main/java/io/github/dsheirer}/source/ISourceEventListener.java (94%) rename src/{ => main/java/io/github/dsheirer}/source/ISourceEventProcessor.java (96%) rename src/{ => main/java/io/github/dsheirer}/source/ISourceEventProvider.java (93%) rename src/{ => main/java/io/github/dsheirer}/source/SourceEvent.java (99%) rename src/{ => main/java/io/github/dsheirer}/source/SourceEventListenerToProcessorAdapter.java (91%) rename src/{ => main/java/io/github/dsheirer}/source/heartbeat/Heartbeat.java (96%) rename src/{ => main/java/io/github/dsheirer}/source/heartbeat/HeartbeatManager.java (92%) rename src/main/java/io/github/dsheirer/{channel => source}/heartbeat/IHeartbeatListener.java (95%) rename src/{ => main/java/io/github/dsheirer}/source/heartbeat/IHeartbeatProvider.java (95%) delete mode 100644 src/main/java/io/github/dsheirer/source/tuner/MixerTuner.java delete mode 100644 src/main/java/io/github/dsheirer/source/tuner/TunerChannel.java delete mode 100644 src/main/java/io/github/dsheirer/source/tuner/TunerChannelProvider.java delete mode 100644 src/main/java/io/github/dsheirer/source/tuner/TunerChannelSource.java rename src/{source/tuner/channel/cic => main/java/io/github/dsheirer/source/tuner/channel}/CICTunerChannelSource.java (83%) rename src/{ => main/java/io/github/dsheirer}/source/tuner/channel/TunerChannel.java (98%) create mode 100644 src/main/java/io/github/dsheirer/source/tuner/channel/TunerChannelProvider.java rename src/{ => main/java/io/github/dsheirer}/source/tuner/channel/TunerChannelSource.java (92%) delete mode 100644 src/main/java/io/github/dsheirer/source/tuner/ettus/B100Tuner.java delete mode 100644 src/main/java/io/github/dsheirer/source/tuner/ettus/B100TunerEditorPanel.java delete mode 100644 src/main/java/io/github/dsheirer/source/tuner/frequency/FrequencyChangeEvent.java delete mode 100644 src/main/java/io/github/dsheirer/source/tuner/frequency/FrequencyChangeProcessorWrapper.java delete mode 100644 src/main/java/io/github/dsheirer/source/tuner/frequency/IFrequencyChangeListener.java delete mode 100644 src/main/java/io/github/dsheirer/source/tuner/frequency/IFrequencyChangeProcessor.java delete mode 100644 src/main/java/io/github/dsheirer/source/tuner/frequency/IFrequencyChangeProvider.java create mode 100644 src/main/java/io/github/dsheirer/source/tuner/manager/AbstractSourceManager.java create mode 100644 src/main/java/io/github/dsheirer/source/tuner/manager/TunerSourceManager.java rename src/{ => main/java/io/github/dsheirer}/source/tuner/test/SampleGenerator.java (96%) rename src/{ => main/java/io/github/dsheirer}/source/tuner/test/TestTuner.java (91%) rename src/{ => main/java/io/github/dsheirer}/source/tuner/test/TestTunerController.java (93%) rename src/{ => main/java/io/github/dsheirer}/source/tuner/usb/USBTunerController.java (92%) delete mode 100644 src/module/Module.java delete mode 100644 src/module/ProcessingChain.java delete mode 100644 src/module/decode/Decoder.java delete mode 100644 src/module/decode/am/AMDecoder.java delete mode 100644 src/module/decode/fleetsync2/Fleetsync2Decoder.java delete mode 100644 src/module/decode/fleetsync2/Fleetsync2DecoderState.java delete mode 100644 src/module/decode/lj1200/LJ1200Decoder.java delete mode 100644 src/module/decode/lj1200/LJ1200DecoderState.java delete mode 100644 src/module/decode/ltrnet/LTRNetDecoder.java delete mode 100644 src/module/decode/ltrnet/LTRNetDecoderState.java delete mode 100644 src/module/decode/ltrstandard/LTRStandardDecoder.java delete mode 100644 src/module/decode/ltrstandard/LTRStandardDecoderState.java delete mode 100644 src/module/decode/mdc1200/MDCDecoder.java delete mode 100644 src/module/decode/mdc1200/MDCDecoderState.java delete mode 100644 src/module/decode/mpt1327/MPT1327Decoder.java delete mode 100644 src/module/decode/mpt1327/MPT1327DecoderState.java delete mode 100644 src/module/decode/nbfm/NBFMDecoder.java delete mode 100644 src/module/decode/p25/C4FMSymbolFilter.java delete mode 100644 src/module/decode/p25/P25Decoder.java delete mode 100644 src/module/decode/p25/P25DecoderState.java delete mode 100644 src/module/decode/p25/P25_C4FMDecoder.java delete mode 100644 src/module/decode/p25/P25_LSMDecoder.java delete mode 100644 src/module/decode/p25/audio/P25AudioModule.java delete mode 100644 src/module/decode/passport/PassportDecoder.java delete mode 100644 src/module/decode/passport/PassportDecoderState.java delete mode 100644 src/module/decode/tait/Tait1200Decoder.java delete mode 100644 src/module/decode/tait/Tait1200DecoderState.java delete mode 100644 src/module/demodulate/am/AMDemodulatorModule.java delete mode 100644 src/module/demodulate/audio/DemodulatedAudioFilterModule.java delete mode 100644 src/module/demodulate/fm/FMDemodulatorModule.java delete mode 100644 src/module/log/EventLogger.java delete mode 100644 src/record/AudioRecorder.java delete mode 100644 src/record/RecorderManager.java delete mode 100644 src/record/wave/ComplexBufferWaveRecorder.java delete mode 100644 src/record/wave/RealBufferWaveRecorder.java delete mode 100644 src/sample/Broadcaster.java delete mode 100644 src/sample/OverflowableTransferQueue.java delete mode 100644 src/sample/SampleType.java delete mode 100644 src/source/ComplexSource.java delete mode 100644 src/source/InvalidFrequencyException.java delete mode 100644 src/source/RealSource.java delete mode 100644 src/source/Source.java delete mode 100644 src/source/config/SourceConfigRecording.java delete mode 100644 src/source/config/SourceConfigTuner.java delete mode 100644 src/source/heartbeat/IHeartbeatListener.java delete mode 100644 src/source/mixer/RealMixerSource.java delete mode 100644 src/source/recording/Recording.java delete mode 100644 src/source/recording/RecordingSourceManager.java delete mode 100644 src/source/tuner/ITunerChannelProvider.java delete mode 100644 src/source/tuner/Tuner.java delete mode 100644 src/source/tuner/TunerClass.java delete mode 100644 src/source/tuner/TunerController.java delete mode 100644 src/source/tuner/TunerEditor.java delete mode 100644 src/source/tuner/TunerManager.java delete mode 100644 src/source/tuner/TunerModel.java delete mode 100644 src/source/tuner/TunerSourceEditor.java delete mode 100644 src/source/tuner/TunerType.java delete mode 100644 src/source/tuner/airspy/AirspyTuner.java delete mode 100644 src/source/tuner/airspy/AirspyTunerController.java delete mode 100644 src/source/tuner/airspy/AirspyTunerEditor.java delete mode 100644 src/source/tuner/channel/ChannelManager.java delete mode 100644 src/source/tuner/channel/FrequencyController.java delete mode 100644 src/source/tuner/channel/FrequencyControllerLockedException.java delete mode 100644 src/source/tuner/configuration/TunerConfigurationModel.java delete mode 100644 src/source/tuner/fcd/FCDTuner.java delete mode 100644 src/source/tuner/fcd/FCDTunerController.java delete mode 100644 src/source/tuner/fcd/proV1/FCD1TunerController.java delete mode 100644 src/source/tuner/fcd/proV1/FCD1TunerEditor.java delete mode 100644 src/source/tuner/fcd/proplusV2/FCD2TunerController.java delete mode 100644 src/source/tuner/fcd/proplusV2/FCD2TunerEditor.java delete mode 100644 src/source/tuner/frequency/DCTrackingSourceControl.java delete mode 100644 src/source/tuner/hackrf/HackRFTuner.java delete mode 100644 src/source/tuner/hackrf/HackRFTunerController.java delete mode 100644 src/source/tuner/hackrf/HackRFTunerEditor.java delete mode 100644 src/source/tuner/rtl/RTL2832Tuner.java delete mode 100644 src/source/tuner/rtl/RTL2832TunerController.java delete mode 100644 src/source/tuner/rtl/e4k/E4KTunerController.java delete mode 100644 src/source/tuner/rtl/e4k/E4KTunerEditor.java delete mode 100644 src/source/tuner/rtl/r820t/R820TTunerController.java delete mode 100644 src/source/tuner/rtl/r820t/R820TTunerEditor.java delete mode 100644 src/source/tuner/usb/USBTransferProcessor.java delete mode 100644 src/source/wave/ComplexWaveSource.java delete mode 100644 src/source/wave/RealWaveSource.java delete mode 100644 src/spectrum/ChannelSpectrumPanel.java delete mode 100644 src/spectrum/DFTProcessor.java delete mode 100644 src/spectrum/OverlayPanel.java delete mode 100644 src/spectrum/SpectralDisplayPanel.java diff --git a/src/audio/AudioModule.java b/src/audio/AudioModule.java deleted file mode 100644 index f1cdf38f..00000000 --- a/src/audio/AudioModule.java +++ /dev/null @@ -1,188 +0,0 @@ -/******************************************************************************* - * 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 audio; - -import audio.squelch.ISquelchStateListener; -import audio.squelch.SquelchState; -import channel.metadata.Metadata; -import dsp.filter.design.FilterDesignException; -import dsp.filter.fir.FIRFilterSpecification; -import dsp.filter.fir.remez.RemezFIRFilterDesigner; -import dsp.filter.polyphase.PolyphaseFIRDecimatingFilter_RB; -import module.Module; -import org.slf4j.Logger; -import org.slf4j.LoggerFactory; -import sample.Listener; -import sample.real.IFilteredRealBufferListener; -import sample.real.RealBuffer; - -import java.util.concurrent.ScheduledExecutorService; - -/** - * Provides packaging of demodulated audio sample buffers into audio packets for - * broadcast to registered audio packet listeners. Includes audio packet - * metadata in constructed audio packets. - * - * Incorporates audio squelch state listener to control if audio packets are - * broadcast or ignored. - */ -public class AudioModule extends Module implements IAudioPacketProvider, IFilteredRealBufferListener, - ISquelchStateListener, Listener -{ - protected static final Logger mLog = LoggerFactory.getLogger(AudioModule.class); - - private static float[] mDecimationCoefficients; - - static - { - //Band-pass filter to both decimate (48kHz to 8kHz) and block 0 - 300 Hz (LTR signalling). - FIRFilterSpecification specification = FIRFilterSpecification.bandPassBuilder() - .sampleRate(48000) - .stopFrequency1(150) - .passFrequencyBegin(300) - .passFrequencyEnd(3500) - .stopFrequency2(4000) - .stopRipple(0.0004) - .passRipple(0.008) - .build(); - - RemezFIRFilterDesigner designer = new RemezFIRFilterDesigner(specification); - - try - { - if(!designer.isValid()) - { - throw new FilterDesignException("Couldn't design the audio decimation filter"); - } - - mDecimationCoefficients = designer.getImpulseResponse(); - } - catch(FilterDesignException e) - { - mLog.debug("Error designing filter", e); - } - } - - private SquelchStateListener mSquelchStateListener = new SquelchStateListener(); - private SquelchState mSquelchState = SquelchState.SQUELCH; - private Listener mAudioPacketListener; - private PolyphaseFIRDecimatingFilter_RB mAudioDecimationFilter; - private Metadata mMetadata; - - public AudioModule(Metadata metadata) - { - mMetadata = metadata; - - mAudioDecimationFilter = new PolyphaseFIRDecimatingFilter_RB(mDecimationCoefficients, 6, 2.0f); - mAudioDecimationFilter.setListener(new Listener() - { - @Override - public void receive(RealBuffer realBuffer) - { - if(mAudioPacketListener != null) - { - AudioPacket packet = new AudioPacket(realBuffer.getSamples(), mMetadata.copyOf()); - mAudioPacketListener.receive(packet); - } - } - }); - } - - @Override - public void dispose() - { - mSquelchStateListener = null; - mAudioPacketListener = null; - } - - @Override - public void reset() - { - } - - @Override - public void start() - { - /* No start operations provided */ - } - - @Override - public void stop() - { - /* Issue an end audio packet in case a recorder is still rolling */ - if(mAudioPacketListener != null) - { - mAudioPacketListener.receive(new AudioPacket(AudioPacket.Type.END, mMetadata.copyOf())); - } - } - - /** - * Processes demodulated audio samples into audio packets with current audio - * metadata and sends to the registered listener - */ - @Override - public void receive(RealBuffer buffer) - { - if(mAudioPacketListener != null && mSquelchState == SquelchState.UNSQUELCH) - { - mAudioDecimationFilter.receive(buffer); - } - } - - @Override - public Listener getFilteredRealBufferListener() - { - return this; - } - - @Override - public void setAudioPacketListener(Listener listener) - { - mAudioPacketListener = listener; - } - - @Override - public void removeAudioPacketListener() - { - mAudioPacketListener = null; - } - - @Override - public Listener getSquelchStateListener() - { - return mSquelchStateListener; - } - - /** - * Wrapper for squelch state listener - */ - public class SquelchStateListener implements Listener - { - @Override - public void receive(SquelchState state) - { - if(state == SquelchState.SQUELCH && mAudioPacketListener != null) - { - mAudioPacketListener.receive(new AudioPacket(AudioPacket.Type.END, mMetadata.copyOf())); - } - - mSquelchState = state; - } - } -} diff --git a/src/audio/broadcast/StreamManager.java b/src/audio/broadcast/StreamManager.java deleted file mode 100644 index 8e84ccdd..00000000 --- a/src/audio/broadcast/StreamManager.java +++ /dev/null @@ -1,233 +0,0 @@ -/******************************************************************************* - * 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 audio.broadcast; - -import audio.AudioPacket; -import org.slf4j.Logger; -import org.slf4j.LoggerFactory; -import record.AudioRecorder; -import sample.Listener; -import util.ThreadPool; -import util.TimeStamp; - -import java.nio.file.Files; -import java.nio.file.Path; -import java.util.ArrayList; -import java.util.HashMap; -import java.util.List; -import java.util.Map; -import java.util.concurrent.ScheduledFuture; -import java.util.concurrent.TimeUnit; -import java.util.concurrent.atomic.AtomicBoolean; -import java.util.concurrent.atomic.AtomicInteger; - -public class StreamManager implements Listener -{ - private final static Logger mLog = LoggerFactory.getLogger(StreamManager.class); - private static final long MAXIMUM_RECORDER_LIFESPAN_MILLIS = 30000; //30 seconds - - private static AtomicInteger sNextRecordingNumber = new AtomicInteger(); - - private Listener mAudioRecordingListener; - private BroadcastFormat mBroadcastFormat; - private Path mTempDirectory; - private Map mStreamRecorders = new HashMap<>(); - private Runnable mRecorderMonitor; - private ScheduledFuture mRecorderMonitorFuture; - private AtomicBoolean mRunning = new AtomicBoolean(); - - /** - * Stream manager processes all incoming audio packets and reassembles individual audio streams, converts audio - * to desired output format and persists each stream to disc. Each recording is capped at a maximum length to - * ensure that recordings don't run too long before they are streamed out and to ensure that inactive recordings - * are closed in a timely fashion. - * - * Completed streamable audio recordings are nominated to the output listener (for broadcast) upon completion - * - * @param listener to receive completed audio recordings - * @param tempDirectory where to store temporary audio recordings - */ - public StreamManager(Listener listener, BroadcastFormat broadcastFormat, Path tempDirectory) - { - assert (tempDirectory != null && Files.isDirectory(tempDirectory)); - - mAudioRecordingListener = listener; - mBroadcastFormat = broadcastFormat; - mTempDirectory = tempDirectory; - } - - /** - * Starts the stream manager. Schedules a processor to monitor for inactive temporary stream recorders - * every 2 seconds. - */ - public void start() - { - if(mRunning.compareAndSet(false, true)) - { - if(mRecorderMonitor == null) - { - mRecorderMonitor = new RecorderMonitor(); - } - - mRecorderMonitorFuture = ThreadPool.SCHEDULED.scheduleAtFixedRate(mRecorderMonitor, - 0,2, TimeUnit.SECONDS); - } - } - - /** - * Stops the stream manager. Stops the inactive temporary stream recorder monitoring thread. - */ - public void stop() - { - if(mRunning.compareAndSet(true, false)) - { - if(mRecorderMonitorFuture != null) - { - mRecorderMonitorFuture.cancel(true); - } - - synchronized(mStreamRecorders) - { - List streamKeys = new ArrayList<>(mStreamRecorders.keySet()); - - for(Integer streamKey : streamKeys) - { - removeRecorder(streamKey); - } - } - } - } - - @Override - public void receive(AudioPacket audioPacket) - { - if(mRunning.get() && audioPacket.hasMetadata()) - { - synchronized(mStreamRecorders) - { - int channelMetadataID = audioPacket.getMetadata().getMetadataID(); - - AudioPacket.Type type = audioPacket.getType(); - - if(type == AudioPacket.Type.AUDIO) - { - if(mStreamRecorders.containsKey(channelMetadataID)) - { - AudioRecorder recorder = mStreamRecorders.get(channelMetadataID); - - if(recorder != null) - { - recorder.receive(audioPacket); - } - } - else - { - AudioRecorder recorder = BroadcastFactory.getAudioRecorder(getTemporaryRecordingPath(), - mBroadcastFormat); - recorder.start(); - recorder.receive(audioPacket); - mStreamRecorders.put(channelMetadataID, recorder); - } - } - else if(type == AudioPacket.Type.END) - { - removeRecorder(channelMetadataID); - } - else - { - mLog.info("Unrecognized Audio Packet Type: " + type); - } - } - } - } - - /** - * Removes the recorder associated with the source channel ID. - * - * Note: this method invocation is not thread safe and must be invoked by a thread safe mechanism that protects the - * mStreamRecorders map. - * - * @param sourceChannelID identifying the recorder - */ - private void removeRecorder(Integer sourceChannelID) - { - if(mStreamRecorders.containsKey(sourceChannelID)) - { - AudioRecorder recorder = mStreamRecorders.remove(sourceChannelID); - - recorder.close(new Listener() - { - @Override - public void receive(AudioRecorder audioRecorder) - { - AudioRecording audioRecording = - new AudioRecording(audioRecorder.getPath(), audioRecorder.getMetadata(), - audioRecorder.getTimeRecordingStart(), audioRecorder.getRecordingLength()); - - if(mAudioRecordingListener != null) - { - mAudioRecordingListener.receive(audioRecording); - } - } - }); - - } - } - - /** - * Creates a temporary streaming recording file path - */ - private Path getTemporaryRecordingPath() - { - StringBuilder sb = new StringBuilder(); - sb.append(BroadcastModel.TEMPORARY_STREAM_FILE_SUFFIX); - sb.append(sNextRecordingNumber.incrementAndGet()).append("_"); - sb.append(TimeStamp.getLongTimeStamp("_")); - sb.append(mBroadcastFormat.getFileExtension()); - - Path temporaryRecordingPath = mTempDirectory.resolve(sb.toString()); - - return temporaryRecordingPath; - } - - /** - * Monitors recorders to ensure they don't exceed the maximum life-span allowed for a recording. - */ - public class RecorderMonitor implements Runnable - { - @Override - public void run() - { - synchronized(mStreamRecorders) - { - long now = System.currentTimeMillis(); - - mStreamRecorders.entrySet().stream() - .filter(entry -> entry.getValue().getTimeRecordingStart() + MAXIMUM_RECORDER_LIFESPAN_MILLIS < now) - .forEach(entry -> - { - mLog.info("cycling recorder - max temporary streaming recording time limit reached [" + - entry.getValue().getPath().toString() + "]"); - - removeRecorder(entry.getKey()); - }); - } - } - } -} diff --git a/src/buffer/FloatCircularBuffer.java b/src/buffer/FloatCircularBuffer.java deleted file mode 100644 index 4f6d600a..00000000 --- a/src/buffer/FloatCircularBuffer.java +++ /dev/null @@ -1,126 +0,0 @@ -/******************************************************************************* - * 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 buffer; - -import java.util.Arrays; - -/** - * Circular sample buffer - allocates a buffer and stores samples in a circular - * fashion, overwriting older samples with newly arrived samples. - * - * Can be used as a delay-type buffer, to delay samples by the 'size' amount - */ -public class FloatCircularBuffer -{ - float[] mBuffer; - int mBufferPointer = 0; - - public FloatCircularBuffer(int size) - { - mBuffer = new float[size]; - } - - public int getSize() - { - return mBuffer.length; - } - - public float[] getBuffer() - { - return mBuffer; - } - - /** - * Puts the new value into the buffer and returns the oldest buffer value - * that it replaced - * - * @param newValue - * @return - */ - public float putAndGet(float newValue) - { - float oldestSample = mBuffer[mBufferPointer]; - - put(newValue); - - return oldestSample; - } - - /** - * Adds the new value to this buffer. - */ - public void put(float newValue) - { - mBuffer[mBufferPointer] = newValue; - - mBufferPointer++; - - if(mBufferPointer >= mBuffer.length) - { - mBufferPointer = 0; - } - } - - /** - * Returns the sample at the specified index, where index 0 is the oldest sample and index (length - 1) is the - * newest sample. - * @param index in range of 0 to length-1 - * @return value at indexed position - */ - - public float get(int index) - { - if(index < mBuffer.length) - { - int pointer = mBufferPointer + index; - - if(pointer >= mBuffer.length) - { - pointer -= mBuffer.length; - } - - return mBuffer[pointer]; - } - else - { - throw new ArrayIndexOutOfBoundsException("Index [" + index + "] is not valid for the buffer size of [" + - mBuffer.length + "]"); - } - } - - /** - * Returns the contents of the circular buffer unwrapped where index 0 is the oldest sample and index length-1 - * is the newest sample. - * - * @return array of samples - */ - public float[] get() - { - float[] samples = new float[mBuffer.length]; - - System.arraycopy(mBuffer, mBufferPointer, samples, 0, mBuffer.length - mBufferPointer); - - if(mBufferPointer != 0) - { - System.arraycopy(mBuffer, 0, samples, mBuffer.length - mBufferPointer, mBufferPointer); - } - - return samples; - } -} diff --git a/src/buffer/ReverseFloatCircularBuffer.java b/src/buffer/ReverseFloatCircularBuffer.java deleted file mode 100644 index 3c3adb40..00000000 --- a/src/buffer/ReverseFloatCircularBuffer.java +++ /dev/null @@ -1,89 +0,0 @@ -/******************************************************************************* - * 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 buffer; - -/** - * Circular sample buffer - allocates a buffer and stores samples in a circular - * fashion, overwriting older samples with newly arrived samples. - */ -public class ReverseFloatCircularBuffer -{ - private float[] mBuffer; - private int mBufferPointer = 0; - - public ReverseFloatCircularBuffer(int size) - { - mBuffer = new float[size]; - mBufferPointer = mBuffer.length - 1; - } - - /** - * Size of the buffer - * @return - */ - public int getSize() - { - return mBuffer.length; - } - - public float[] getBuffer() - { - return mBuffer; - } - - /** - * Adds the new value to this buffer. - */ - public void put(float newValue) - { - mBuffer[mBufferPointer--] = newValue; - - if(mBufferPointer < 0) - { - mBufferPointer = mBuffer.length - 1; - } - } - - /** - * Returns the sample at the specified index, where index 0 is the newest sample and index (length - 1) is the - * oldest sample. - * @param index in range of 0 to length-1 - * @return value at indexed position - */ - - public float get(int index) - { - if(index < mBuffer.length) - { - int pointer = mBufferPointer + 1 + index; - - if(pointer >= mBuffer.length) - { - pointer -= mBuffer.length; - } - - return mBuffer[pointer]; - } - else - { - throw new ArrayIndexOutOfBoundsException("Index [" + index + "] is not valid for the buffer size of [" + - mBuffer.length + "]"); - } - } -} diff --git a/src/channel/state/AlwaysUnsquelchedDecoderState.java b/src/channel/state/AlwaysUnsquelchedDecoderState.java deleted file mode 100644 index b2a67cc9..00000000 --- a/src/channel/state/AlwaysUnsquelchedDecoderState.java +++ /dev/null @@ -1,97 +0,0 @@ -/******************************************************************************* - * SDR Trunk - * Copyright (C) 2014,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 - ******************************************************************************/ -package channel.state; - -import channel.metadata.Attribute; -import channel.metadata.AttributeChangeRequest; -import channel.state.DecoderStateEvent.Event; -import gui.SDRTrunk; -import message.Message; -import module.decode.DecoderType; -import org.slf4j.Logger; -import org.slf4j.LoggerFactory; - -import java.util.concurrent.ScheduledExecutorService; - -/** - * Basic decoder channel state - provides the minimum channel state functionality - * to support an always un-squelched audio decoder. - */ -public class AlwaysUnsquelchedDecoderState extends DecoderState -{ - private final static Logger mLog = LoggerFactory.getLogger(AlwaysUnsquelchedDecoderState.class); - - private DecoderType mDecoderType; - private String mChannelName; - - public AlwaysUnsquelchedDecoderState(DecoderType decoderType, String channelName) - { - super(null); - - mDecoderType = decoderType; - mChannelName = channelName; - } - - @Override - public void reset() - { - } - - @Override - public String getActivitySummary() - { - StringBuilder sb = new StringBuilder(); - - sb.append("Activity Summary\n"); - sb.append("\tDecoder:\t"); - sb.append(mDecoderType); - sb.append("\n\n"); - - return sb.toString(); - } - - @Override - public void receive(Message t) - { - /* Not implemented */ - } - - @Override - public void receiveDecoderStateEvent(DecoderStateEvent event) - { - if(event.getEvent() == Event.RESET) - { - //Each time we're reset, set the PRIMARY TO attribute back to the channel name, otherwise we won't have - //a primary ID for any audio produced by this state. - broadcast(new AttributeChangeRequest(Attribute.PRIMARY_ADDRESS_TO, mChannelName)); - } - } - - @Override - public DecoderType getDecoderType() - { - return mDecoderType; - } - - @Override - public void start() - { - broadcast(new AttributeChangeRequest(Attribute.PRIMARY_ADDRESS_TO, mChannelName)); - broadcast(new DecoderStateEvent(this, Event.ALWAYS_UNSQUELCH, State.IDLE)); - } -} diff --git a/src/channel/state/ChannelState.java b/src/channel/state/ChannelState.java deleted file mode 100644 index 3bb4d00b..00000000 --- a/src/channel/state/ChannelState.java +++ /dev/null @@ -1,639 +0,0 @@ -/******************************************************************************* - * 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 channel.state; - -import audio.squelch.ISquelchStateProvider; -import audio.squelch.SquelchState; -import source.heartbeat.Heartbeat; -import source.heartbeat.IHeartbeatListener; -import channel.metadata.Attribute; -import channel.metadata.AttributeChangeRequest; -import channel.metadata.IAttributeChangeRequestListener; -import channel.metadata.MutableMetadata; -import channel.state.DecoderStateEvent.Event; -import channel.traffic.TrafficChannelManager; -import controller.channel.Channel.ChannelType; -import module.Module; -import module.decode.DecoderType; -import module.decode.config.DecodeConfiguration; -import module.decode.event.CallEvent; -import module.decode.event.ICallEventProvider; -import org.slf4j.Logger; -import org.slf4j.LoggerFactory; -import sample.Listener; -import sample.real.IOverflowListener; -import source.SourceEvent; -import source.ISourceEventListener; - -import java.util.concurrent.ScheduledExecutorService; - -import static source.SourceEvent.Event.NOTIFICATION_FREQUENCY_CHANGE; - -public class ChannelState extends Module implements ICallEventProvider, IDecoderStateEventListener, - IDecoderStateEventProvider, ISourceEventListener, ISquelchStateProvider, IAttributeChangeRequestListener, - IOverflowListener, IHeartbeatListener -{ - private final static Logger mLog = LoggerFactory.getLogger(ChannelState.class); - - public static final long FADE_TIMEOUT_DELAY = 1200; - public static final long RESET_TIMEOUT_DELAY = 2000; - - private MutableMetadata mMutableMetadata = new MutableMetadata(); - private State mState = State.IDLE; - private Listener mCallEventListener; - private Listener mDecoderStateListener; - private Listener mSquelchStateListener; - private DecoderStateEventReceiver mDecoderStateEventReceiver = new DecoderStateEventReceiver(); - private HeartbeatReceiver mHeartbeatReceiver = new HeartbeatReceiver(); - private ChannelType mChannelType; - private TrafficChannelManager mTrafficChannelEndListener; - private CallEvent mTrafficChannelCallEvent; - private SourceChangeListener mSourceChangeListener; - - private boolean mSquelchLocked = false; - private boolean mSelected = false; - private boolean mSourceOverflow = false; - - private long mStandardChannelFadeTimeout = FADE_TIMEOUT_DELAY; - private long mTrafficChannelFadeTimeout = DecodeConfiguration.DEFAULT_CALL_TIMEOUT_SECONDS * 1000; - private long mFadeTimeout; - private long mEndTimeout; - - - /** - * Channel state tracks the overall state of all processing modules and decoders configured for the channel and - * provides squelch control and decoder state reset events. - * - * Uses a state enumeration that defines allowable channel state transitions in order to track a call or data decode - * event from start to finish. Uses a timer to monitor for inactivity and to provide a FADE period that indicates - * to the user that the activity has stopped while continuing to provide details about the call, before the state is - * reset to IDLE. - * - * State Descriptions: - * IDLE: Normal state. No voice or data call activity - * CALL/DATA/ENCRYPTED/CONTROL: Decoding states. - * FADE: The phase after a voice or data call when either an explicit call end has been received, or when no new - * signalling updates have been received, and the fade timer has expired. This phase allows for gui updates to - * signal to the user that the call is ended, while continuing to display the call details for the user - * TEARDOWN: Indicates a traffic channel that will be torn down for reuse. - */ - public ChannelState(ChannelType channelType) - { - mChannelType = channelType; - } - - - /** - * Metadata for this channel containing channel details, primary and secondary entities for call events and any - * decoded messages. This metadata is primarily intended to be used by graphical components to convey the - * details of the current channel state and to provide metadata backing any decoded audio packets produced by - * any audio streams for this channel. - */ - public MutableMetadata getMutableMetadata() - { - return mMutableMetadata; - } - - /** - * Resets this channel state and prepares it for reuse. - */ - @Override - public void reset() - { - broadcast(new DecoderStateEvent(this, Event.RESET, State.IDLE)); - - mState = State.IDLE; - mMutableMetadata.receive(new AttributeChangeRequest(Attribute.CHANNEL_STATE, mState)); - mSourceOverflow = false; - } - - @Override - public void start() - { - mMutableMetadata.receive(new AttributeChangeRequest(Attribute.CHANNEL_STATE, mState)); - - if(mTrafficChannelEndListener != null) - { - setState(State.CALL); - } - } - - @Override - public void stop() - { - mMutableMetadata.resetAllAttributes(); - - mTrafficChannelEndListener = null; - - if(mTrafficChannelCallEvent != null) - { - mTrafficChannelCallEvent.end(); - broadcast(mTrafficChannelCallEvent); - } - - mTrafficChannelCallEvent = null; - - mSquelchLocked = false; - } - - public void dispose() - { - mCallEventListener = null; - mDecoderStateListener = null; - mSquelchStateListener = null; - } - - @Override - public Listener getSourceEventListener() - { - if(mSourceChangeListener == null) - { - mSourceChangeListener = new SourceChangeListener(); - } - - return mSourceChangeListener; - } - - private boolean isStandardChannel() - { - return mChannelType == ChannelType.STANDARD; - } - - private boolean isTrafficChannel() - { - return mChannelType == ChannelType.TRAFFIC; - } - - public void setStandardChannelTimeout(long milliseconds) - { - mStandardChannelFadeTimeout = milliseconds; - - if(mChannelType == ChannelType.STANDARD) - { - mFadeTimeout = mStandardChannelFadeTimeout; - } - } - - public void setTrafficChannelTimeout(long milliseconds) - { - mTrafficChannelFadeTimeout = milliseconds; - - if(mChannelType == ChannelType.TRAFFIC) - { - mFadeTimeout = System.currentTimeMillis() + mTrafficChannelFadeTimeout; - } - } - - public void setSelected(boolean selected) - { - mSelected = selected; - } - - public boolean isSelected() - { - return mSelected; - } - - public State getState() - { - return mState; - } - - /** - * Updates the fade timeout threshold to the current time plus delay - */ - private void updateFadeTimeout() - { - if(isTrafficChannel()) - { - mFadeTimeout = System.currentTimeMillis() + mTrafficChannelFadeTimeout; - } - else - { - mFadeTimeout = System.currentTimeMillis() + mStandardChannelFadeTimeout; - } - - } - - /** - * Updates the reset timeout threshold to the current time plus delay - */ - private void updateResetTimeout() - { - if(isTrafficChannel()) - { - mEndTimeout = System.currentTimeMillis(); - } - else - { - mEndTimeout = System.currentTimeMillis() + RESET_TIMEOUT_DELAY; - } - } - - /** - * Broadcasts the squelch state to the registered listener - */ - protected void broadcast(SquelchState state) - { - if(mSquelchStateListener != null && !mSquelchLocked) - { - mSquelchStateListener.receive(state); - } - } - - /** - * Sets the squelch state listener - */ - @Override - public void setSquelchStateListener(Listener listener) - { - mSquelchStateListener = listener; - } - - /** - * Removes the squelch state listener - */ - @Override - public void removeSquelchStateListener() - { - mSquelchStateListener = null; - } - - /** - * Sets the channel state to the specified state, or updates the timeout values so that the state monitor will not - * change state. Broadcasts a squelch event when the state changes and the audio squelch state should change. Also - * broadcasts changed attribute and decoder state events so that external processes can maintain sync with this - * channel state. - */ - protected void setState(State state) - { - if(state == mState) - { - if(State.CALL_STATES.contains(state)) - { - updateFadeTimeout(); - } - } - else if(mState.canChangeTo(state)) - { - switch(state) - { - case CONTROL: - //Don't allow traffic channels to be control channels, otherwise they can't transition to teardown - if(isStandardChannel()) - { - broadcast(SquelchState.SQUELCH); - updateFadeTimeout(); - mState = state; - } - break; - case DATA: - case ENCRYPTED: - broadcast(SquelchState.SQUELCH); - updateFadeTimeout(); - mState = state; - break; - case CALL: - broadcast(SquelchState.UNSQUELCH); - updateFadeTimeout(); - mState = state; - break; - case FADE: - processFadeState(); - break; - case IDLE: - processIdleState(); - break; - case TEARDOWN: - processTeardownState(); - break; - case RESET: - mState = State.IDLE; - break; - default: - break; - } - - mMutableMetadata.receive(new AttributeChangeRequest(Attribute.CHANNEL_STATE, - mSourceOverflow ? State.OVERFLOW : mState)); - } - } - - /** - * Receiver inner class that implements the IHeartbeatListener interface to receive heartbeat messages. - */ - @Override - public Listener getHeartbeatListener() - { - return mHeartbeatReceiver; - } - - /** - * This method is invoked if the source buffer provider goes into overflow state. Since this is an external state, - * we use the mSourceOverflow variable to override the internal state reported to external listeners. - * @param overflow true to indicate an overflow state - */ - @Override - public void sourceOverflow(boolean overflow) - { - mSourceOverflow = overflow; - - mMutableMetadata.receive(new AttributeChangeRequest(Attribute.CHANNEL_STATE, - mSourceOverflow ? State.OVERFLOW : mState)); - } - - /** - * Sets the state and processes related actions - */ - private void processFadeState() - { - updateResetTimeout(); - mState = State.FADE; - - broadcast(SquelchState.SQUELCH); - mMutableMetadata.receive(new AttributeChangeRequest(Attribute.CHANNEL_STATE, mState)); - } - - private void processIdleState() - { - broadcast(SquelchState.SQUELCH); - - if(mState == State.FADE) - { - getMutableMetadata().resetTemporalAttributes(); - broadcast(new DecoderStateEvent(this, Event.RESET, State.IDLE)); - } - - mState = State.IDLE; - getMutableMetadata().receive(new AttributeChangeRequest(Attribute.CHANNEL_STATE, mState)); - } - - private void processTeardownState() - { - broadcast(SquelchState.SQUELCH); - - getMutableMetadata().resetTemporalAttributes(); - - mState = State.TEARDOWN; - - mMutableMetadata.receive(new AttributeChangeRequest(Attribute.CHANNEL_STATE, mState)); - - if(mTrafficChannelEndListener != null) - { - mTrafficChannelEndListener.callEnd(mTrafficChannelCallEvent.getChannel()); - } - } - - /** - * Broadcasts the call event to the registered listener - */ - protected void broadcast(CallEvent event) - { - if(mCallEventListener != null) - { - mCallEventListener.receive(event); - } - } - - @Override - public void addCallEventListener(Listener listener) - { - mCallEventListener = listener; - } - - @Override - public void removeCallEventListener(Listener listener) - { - mCallEventListener = null; - } - - - //NEW: - @Override - public Listener getAttributeChangeRequestListener() - { - return mMutableMetadata; - } - - /** - * Broadcasts a channel state event to any registered listeners - */ - protected void broadcast(DecoderStateEvent event) - { - if(mDecoderStateListener != null) - { - mDecoderStateListener.receive(event); - } - } - - /** - * Adds a decoder state event listener - */ - @Override - public void setDecoderStateListener(Listener listener) - { - mDecoderStateListener = listener; - } - - /** - * Removes the decoder state event listener - */ - @Override - public void removeDecoderStateListener() - { - mDecoderStateListener = null; - } - - @Override - public Listener getDecoderStateListener() - { - return mDecoderStateEventReceiver; - } - - /** - * Registers a listener to be notified when a traffic channel call event is - * completed, so that the listener can perform call tear-down - */ - public void configureAsTrafficChannel(TrafficChannelManager manager, CallEvent callEvent) - { - mTrafficChannelEndListener = manager; - - mTrafficChannelCallEvent = callEvent; - - /* Broadcast the call event details as metadata for the audio manager */ - String channel = mTrafficChannelCallEvent.getChannel(); - - if(channel != null) - { - mMutableMetadata.receive(new AttributeChangeRequest(Attribute.CHANNEL_FREQUENCY_LABEL, channel)); - } - - long frequency = mTrafficChannelCallEvent.getFrequency(); - - if(frequency > 0) - { - mMutableMetadata.receive(new AttributeChangeRequest(Attribute.CHANNEL_FREQUENCY, frequency)); - } - - mMutableMetadata.receive(new AttributeChangeRequest(Attribute.PRIMARY_DECODER_TYPE, - mTrafficChannelCallEvent.getDecoderType())); - - mMutableMetadata.receive(new AttributeChangeRequest(Attribute.CHANNEL_CONFIGURATION_NAME, "TRAFFIC")); - - String from = mTrafficChannelCallEvent.getFromID(); - - if(from != null) - { - mMutableMetadata.receive(new AttributeChangeRequest(Attribute.PRIMARY_ADDRESS_FROM, from, - mTrafficChannelCallEvent.getFromIDAlias())); - } - - String to = mTrafficChannelCallEvent.getToID(); - - if(to != null) - { - mMutableMetadata.receive(new AttributeChangeRequest(Attribute.PRIMARY_ADDRESS_TO, to, - mTrafficChannelCallEvent.getToIDAlias())); - } - - /* Rebroadcast the allocation event so that the internal decoder states - * can self-configure with the call event details */ - broadcast(mTrafficChannelCallEvent); - } - - /** - * Listener to receive frequency change events and rebroadcast them to the decoder states - */ - public class SourceChangeListener implements Listener - { - @Override - public void receive(SourceEvent sourceEvent) - { - if(sourceEvent.getEvent() == NOTIFICATION_FREQUENCY_CHANGE) - { - long frequency = sourceEvent.getValue().longValue(); - - broadcast(new DecoderStateEvent(this, Event.SOURCE_FREQUENCY, getState(), frequency)); - } - } - } - - /** - * DecoderStateEvent receiver wrapper - */ - public class DecoderStateEventReceiver implements Listener - { - @Override - public void receive(DecoderStateEvent event) - { - if(event.getSource() != this) - { - switch(event.getEvent()) - { - case ALWAYS_UNSQUELCH: - broadcast(SquelchState.UNSQUELCH); - mSquelchLocked = true; - break; - case CHANGE_CALL_TIMEOUT: - if(event instanceof ChangeChannelTimeoutEvent) - { - ChangeChannelTimeoutEvent timeout = (ChangeChannelTimeoutEvent) event; - - if(timeout.getChannelType() == ChannelType.STANDARD) - { - setStandardChannelTimeout(timeout.getCallTimeout()); - } - else - { - setTrafficChannelTimeout(timeout.getCallTimeout()); - } - } - case CONTINUATION: - case DECODE: - case START: - if(State.CALL_STATES.contains(event.getState())) - { - setState(event.getState()); - } - break; - case END: - if(isTrafficChannel()) - { - setState(State.TEARDOWN); - } - else - { - setState(State.FADE); - } - break; - case RESET: - /* Channel State does not respond to reset events */ - break; - default: - break; - } - } - } - } - - /** - * Processes periodic heartbeats received from the processing chain to perform state monitoring and cleanup - * functions. - * - * Monitors decoder state events to automatically transition the channel state to IDLE (standard channel) or to - * TEARDOWN (traffic channel) when decoding stops or the monitored channel returns to a no signal state. - * - * Provides a FADE transition state to allow for momentary decoding dropouts and to allow the user access to call - * details for a fade period upon call end. - */ - public class HeartbeatReceiver implements Listener - { - @Override - public void receive(Heartbeat heartbeat) - { - try - { - if(State.CALL_STATES.contains(mState) && mFadeTimeout <= System.currentTimeMillis()) - { - processFadeState(); - } - else if(mState == State.FADE && mEndTimeout <= System.currentTimeMillis()) - { - if(isTrafficChannel()) - { - processTeardownState(); - } - else - { - processIdleState(); - } - } - } - catch(Throwable e) - { - mLog.error("An error occurred while state monitor was running " + - "- state [" + getState() + - "] current [" + System.currentTimeMillis() + - "] mResetTimeout [" + mEndTimeout + - "] mFadeTimeout [" + mFadeTimeout + - "]", e); - } - } - } -} diff --git a/src/channel/state/DecoderState.java b/src/channel/state/DecoderState.java deleted file mode 100644 index 707f0241..00000000 --- a/src/channel/state/DecoderState.java +++ /dev/null @@ -1,222 +0,0 @@ -package channel.state; - -import alias.AliasList; -import channel.metadata.AttributeChangeRequest; -import channel.metadata.IAttributeChangeRequestProvider; -import message.IMessageListener; -import message.Message; -import module.Module; -import module.decode.DecoderType; -import module.decode.event.ActivitySummaryProvider; -import module.decode.event.CallEvent; -import module.decode.event.ICallEventProvider; -import sample.Broadcaster; -import sample.Listener; - -/** - * Channel state monitors the stream of decoded messages produced by the - * decoder and broadcasts call events as they occur within the decoded message activity. - * - * Provides access to a textual activity summary of events observed. - */ -public abstract class DecoderState extends Module implements ActivitySummaryProvider, Listener, - IAttributeChangeRequestProvider, ICallEventProvider, IDecoderStateEventListener, - IDecoderStateEventProvider, IMessageListener -{ - protected String DIVIDER1 = "======================================================\n"; - protected String DIVIDER2 = "------------------------------------------------------\n"; - - /* This has to be a broadcaster in order for references to persist */ - private Broadcaster mCallEventBroadcaster = new Broadcaster<>(); - private Broadcaster mAttributeChangeRequestBroadcaster = new Broadcaster<>(); - private Listener mDecoderStateListener; - - private DecoderStateEventListener mDecoderStateEventListener = new DecoderStateEventListener(); - - protected CallEvent mCurrentCallEvent; - - private AliasList mAliasList; - - public DecoderState(AliasList aliasList) - { - mAliasList = aliasList; - } - - public abstract DecoderType getDecoderType(); - - public AliasList getAliasList() - { - return mAliasList; - } - - public boolean hasAliasList() - { - return mAliasList != null; - } - - /** - * Provides subclass reference to the call event broadcaster - */ - protected Broadcaster getCallEventBroadcaster() - { - return mCallEventBroadcaster; - } - - /** - * Reset the decoder state to prepare for processing a different sample - * source - */ - public abstract void reset(); - - /** - * Allow the decoder to perform any setup actions - */ - public void init() - { - - } - - @Override - public void start() - { - } - - @Override - public void stop() - { - } - - /** - * Implements the IDecoderStateEventListener interface to receive state - * reset events. - */ - public abstract void receiveDecoderStateEvent(DecoderStateEvent event); - - /** - * Disposes any resources or pointers held by this instance to prepare for - * garbage collection - */ - public void dispose() - { - mCallEventBroadcaster.dispose(); - mCallEventBroadcaster = null; - mDecoderStateListener = null; - - mAliasList = null; - } - - /** - * Activity Summary - textual summary of activity observed by the channel state. - */ - public abstract String getActivitySummary(); - - /** - * Broadcasts a call event to any registered listeners - */ - protected void broadcast(CallEvent event) - { - mCallEventBroadcaster.broadcast(event); - } - - /** - * Adds a call event listener - */ - @Override - public void addCallEventListener(Listener listener) - { - mCallEventBroadcaster.addListener(listener); - } - - /** - * Removes the call event listener - */ - @Override - public void removeCallEventListener(Listener listener) - { - mCallEventBroadcaster.removeListener(listener); - } - - @Override - public Listener getDecoderStateListener() - { - return mDecoderStateEventListener; - } - - private class DecoderStateEventListener implements Listener - { - @Override - public void receive(DecoderStateEvent event) - { - receiveDecoderStateEvent(event); - } - } - - /** - * Broadcasts a channel state event to any registered listeners - */ - protected void broadcast(DecoderStateEvent event) - { - if(mDecoderStateListener != null) - { - mDecoderStateListener.receive(event); - } - } - - /** - * Adds a decoder state event listener - */ - @Override - public void setDecoderStateListener(Listener listener) - { - mDecoderStateListener = listener; - } - - /** - * Removes the decoder state event listener - */ - @Override - public void removeDecoderStateListener() - { - mDecoderStateListener = null; - } - - @Override - public Listener getMessageListener() - { - return this; - } - - /** - * Broadcasts the attribute change request to the registered listener - */ - protected void broadcast(AttributeChangeRequest request) - { - mAttributeChangeRequestBroadcaster.broadcast(request); - } - - /** - * Sets the listener to receive attribute change requests from this decoder state - */ - @Override - public void setAttributeChangeRequestListener(Listener listener) - { - mAttributeChangeRequestBroadcaster.addListener(listener); - } - - /** - * Removes any listener from receiving attribute change requests - */ - @Override - public void removeAttributeChangeRequestListener(Listener listener) - { - mAttributeChangeRequestBroadcaster.removeListener(listener); - } - - /** - * Broadaster so that Attribute monitors can broadcast attribute change requests - */ - protected Listener getAttributeChangeRequestListener() - { - return mAttributeChangeRequestBroadcaster; - } -} diff --git a/src/channel/traffic/TrafficChannelManager.java b/src/channel/traffic/TrafficChannelManager.java deleted file mode 100644 index f01bc492..00000000 --- a/src/channel/traffic/TrafficChannelManager.java +++ /dev/null @@ -1,395 +0,0 @@ -/******************************************************************************* - * 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 channel.traffic; - -import channel.state.DecoderStateEvent; -import channel.state.IDecoderStateEventListener; -import controller.channel.Channel; -import controller.channel.Channel.ChannelType; -import controller.channel.ChannelEvent; -import controller.channel.ChannelEvent.Event; -import controller.channel.ChannelModel; -import controller.channel.TrafficChannelEvent; -import module.Module; -import module.decode.config.DecodeConfiguration; -import module.decode.event.CallEvent; -import module.decode.event.CallEvent.CallEventType; -import module.decode.event.ICallEventProvider; -import org.slf4j.Logger; -import org.slf4j.LoggerFactory; -import record.config.RecordConfiguration; -import sample.Listener; -import source.config.SourceConfigTuner; -import source.tuner.channel.TunerChannel; - -import java.util.ArrayList; -import java.util.List; -import java.util.Map; -import java.util.concurrent.ConcurrentHashMap; -import java.util.concurrent.ScheduledExecutorService; - -public class TrafficChannelManager extends Module implements ICallEventProvider, IDecoderStateEventListener -{ - private final static Logger mLog = LoggerFactory.getLogger(TrafficChannelManager.class); - public static final String CHANNEL_START_REJECTED = "CHANNEL START REJECTED"; - public static final String NO_TUNER_AVAILABLE = "NO TUNER AVAILABLE"; - public static final String UNKNOWN_FREQUENCY = "UNKNOWN FREQUENCY"; - - private int mTrafficChannelPoolMaximumSize = DecodeConfiguration.TRAFFIC_CHANNEL_LIMIT_DEFAULT; - private List mTrafficChannelPool = new ArrayList(); - private Map mTrafficChannelsInUse = new ConcurrentHashMap(); - - private DecoderStateEventListener mEventListener = new DecoderStateEventListener(); - private Listener mCallEventListener; - - private ChannelModel mChannelModel; - private DecodeConfiguration mDecodeConfiguration; - private RecordConfiguration mRecordConfiguration; - private String mSystem; - private String mSite; - private String mAliasListName; - - /** - * Monitors call events and allocates traffic decoder channels in response - * to traffic channel allocation call events. Manages a pool of reusable - * traffic channel allocations. - * - * @param channelModel containing channels currently in use - * @param decodeConfiguration - decoder configuration to use for each - * traffic channel allocation. - * @param recordConfiguration - recording options for each traffic channel - * @param system label to use to describe the system - * @param site label to use to describe the site - * @param aliasListName designated for the channel - * @param trafficChannelPoolSize - maximum number of allocated traffic channels - * in the pool - */ - public TrafficChannelManager(ChannelModel channelModel, - DecodeConfiguration decodeConfiguration, - RecordConfiguration recordConfiguration, - String system, - String site, - String aliasListName, - int trafficChannelPoolSize) - { - mChannelModel = channelModel; - mDecodeConfiguration = decodeConfiguration; - mRecordConfiguration = recordConfiguration; - mSystem = system; - mSite = site; - mAliasListName = aliasListName; - mTrafficChannelPoolMaximumSize = trafficChannelPoolSize; - } - - @Override - public void dispose() - { - for(Channel trafficChannel : mTrafficChannelPool) - { - mChannelModel.broadcast(new ChannelEvent(trafficChannel, Event.REQUEST_DISABLE)); - } - - mTrafficChannelPool.clear(); - - mTrafficChannelsInUse.clear(); - - mCallEventListener = null; - mDecodeConfiguration = null; - } - - /** - * Provides a channel by either reusing an existing channel or constructing - * a new one, limited by the total number of constructed channels allowed. - * - * Note: you must enforce thread safety on the mTrafficChannelsInUse - * external to this method. - * - * @param channelNumber - * @param tunerChannel - * @return - */ - private Channel getChannel(String channelNumber, TunerChannel tunerChannel) - { - Channel channel = null; - - if(!mTrafficChannelsInUse.containsKey(channelNumber)) - { - for(Channel configuredChannel : mTrafficChannelPool) - { - if(!configuredChannel.getEnabled()) - { - channel = configuredChannel; - break; - } - } - - if(channel == null && mTrafficChannelPool.size() < mTrafficChannelPoolMaximumSize) - { - channel = new Channel("Traffic", ChannelType.TRAFFIC); - - channel.setDecodeConfiguration(mDecodeConfiguration); - - channel.setRecordConfiguration(mRecordConfiguration); - - channel.setAliasListName(mAliasListName); - - mChannelModel.addChannel(channel); - - mTrafficChannelPool.add(channel); - } - - /* If we have a configured channel, update metadata */ - if(channel != null) - { - channel.setSourceConfiguration(new SourceConfigTuner(tunerChannel)); - channel.setSystem(mSystem); - channel.setSite(mSite); - channel.setName(channelNumber); - - mChannelModel.broadcast(new ChannelEvent(channel, Event.NOTIFICATION_CONFIGURATION_CHANGE)); - } - } - - return channel; - } - - /** - * Processes the event and creates a traffic channel is resources are - * available - */ - private void process(TrafficChannelAllocationEvent event) - { - CallEvent callEvent = event.getCallEvent(); - - /* Check for duplicate events and suppress */ - synchronized(mTrafficChannelsInUse) - { - if(mTrafficChannelsInUse.containsKey(callEvent.getChannel())) - { - return; - } - - long frequency = callEvent.getFrequency(); - - if(frequency > 0) - { - Channel channel = getChannel(callEvent.getChannel(), new TunerChannel(frequency, - mDecodeConfiguration.getDecoderType().getChannelBandwidth())); - - if(channel != null) - { - TrafficChannelEvent trafficChannelEvent = - new TrafficChannelEvent(this, channel, Event.REQUEST_ENABLE, callEvent); - - //Request to enable the channel - mChannelModel.broadcast(trafficChannelEvent); - - if(channel.getEnabled()) - { - mTrafficChannelsInUse.put(callEvent.getChannel(), channel); - } - else - { - callEvent.setCallEventType(CallEventType.CALL_DETECT); - - String details = callEvent.getDetails(); - - if(details == null || details.isEmpty()) - { - callEvent.setDetails(CHANNEL_START_REJECTED); - } - else if(!details.contains(CHANNEL_START_REJECTED)) - { - callEvent.setDetails(new StringBuilder(CHANNEL_START_REJECTED).append(" : ") - .append(callEvent.getDetails()).toString()); - } - } - } - else - { - callEvent.setCallEventType(CallEventType.CALL_DETECT); - - String details = callEvent.getDetails(); - - if(details == null || details.isEmpty()) - { - callEvent.setDetails(NO_TUNER_AVAILABLE); - } - else if(!details.contains(NO_TUNER_AVAILABLE)) - { - callEvent.setDetails(new StringBuilder(NO_TUNER_AVAILABLE).append(" : ") - .append(callEvent.getDetails()).toString()); - } - } - } - else - { - callEvent.setCallEventType(CallEventType.CALL_DETECT); - - String details = callEvent.getDetails(); - - if(details == null || details.isEmpty()) - { - callEvent.setDetails(UNKNOWN_FREQUENCY); - } - else if(!details.contains(UNKNOWN_FREQUENCY)) - { - callEvent.setDetails(new StringBuilder(UNKNOWN_FREQUENCY).append(" : ") - .append(callEvent.getDetails()).toString()); - } - } - - final Listener listener = mCallEventListener; - - if(listener != null) - { - listener.receive(callEvent); - } - } - } - - /** - * Compares the call type, channel and to fields for equivalence and the - * from field for either both null, or equivalence. - * - * @param e1 - * @param e2 - * @return - */ - public static boolean isSameCallEvent(CallEvent e1, CallEvent e2) - { - if(e1 == null || e2 == null) - { - return false; - } - - if(e1.getCallEventType() != e2.getCallEventType()) - { - return false; - } - - if(e1.getChannel() == null || e2.getChannel() == null || !e1.getChannel().contentEquals(e2.getChannel())) - { - return false; - } - - if(e1.getToID() == null || e2.getToID() == null || !e1.getToID().contentEquals(e2.getToID())) - { - return false; - } - - if(e1.getFromID() == null || e2.getFromID() == null) - { - return (e1.getFromID() == null && e2.getFromID() == null); - } - else if(e1.getFromID().contentEquals(e2.getFromID())) - { - return true; - } - - return false; - } - - @Override - public Listener getDecoderStateListener() - { - return mEventListener; - } - - @Override - public void addCallEventListener(Listener listener) - { - mCallEventListener = listener; - } - - @Override - public void removeCallEventListener(Listener listener) - { - mCallEventListener = null; - } - - @Override - public void reset() - { - } - - @Override - public void start() - { - } - - @Override - public void stop() - { - if(!mTrafficChannelsInUse.isEmpty()) - { - List channels = new ArrayList<>(); - - /* Copy the keyset so we don't get concurrent modification of the map */ - channels.addAll(mTrafficChannelsInUse.keySet()); - - for(String channel : channels) - { - callEnd(channel); - } - } - } - - /** - * Callback used by the TrafficChannelStatusListener class to signal the - * end of a an allocated traffic channel call event - * - * @param channelNumber - channel number from the call event that signaled - * the start of a traffic channel allocation - */ - public void callEnd(String channelNumber) - { - synchronized(mTrafficChannelsInUse) - { - if(channelNumber != null && mTrafficChannelsInUse.containsKey(channelNumber)) - { - Channel channel = mTrafficChannelsInUse.get(channelNumber); - - mChannelModel.broadcast(new ChannelEvent(channel, Event.REQUEST_DISABLE)); - - mTrafficChannelsInUse.remove(channelNumber); - } - } - } - - /** - * Wrapper class for the decoder state event listener interface to catch - * traffic channel allocation requests - */ - public class DecoderStateEventListener implements Listener - { - @Override - public void receive(DecoderStateEvent event) - { - switch(event.getEvent()) - { - case TRAFFIC_CHANNEL_ALLOCATION: - process((TrafficChannelAllocationEvent) event); - break; - default: - break; - } - } - } -} diff --git a/src/controller/channel/Channel.java b/src/controller/channel/Channel.java deleted file mode 100644 index c099b517..00000000 --- a/src/controller/channel/Channel.java +++ /dev/null @@ -1,478 +0,0 @@ -/******************************************************************************* - * 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 controller.channel; - -import controller.config.Configuration; -import module.decode.DecoderFactory; -import module.decode.DecoderType; -import module.decode.config.AuxDecodeConfiguration; -import module.decode.config.DecodeConfiguration; -import module.log.EventLogType; -import module.log.config.EventLogConfiguration; -import record.RecorderType; -import record.config.RecordConfiguration; -import sample.Listener; -import source.SourceType; -import source.config.SourceConfigFactory; -import source.config.SourceConfigRecording; -import source.config.SourceConfigTuner; -import source.config.SourceConfiguration; -import source.tuner.channel.TunerChannel; -import source.SourceEvent; -import source.SourceEvent.Event; - -import javax.xml.bind.annotation.XmlAttribute; -import javax.xml.bind.annotation.XmlElement; -import javax.xml.bind.annotation.XmlRootElement; -import javax.xml.bind.annotation.XmlSeeAlso; -import javax.xml.bind.annotation.XmlTransient; - -@XmlSeeAlso({Configuration.class}) -@XmlRootElement(name = "channel") -public class Channel extends Configuration implements Listener -{ - // Standard channels are persisted and traffic channels are temporary - public enum ChannelType - { - STANDARD, TRAFFIC - } - - ; - - - // Static unique channel identifier tracking - private static int UNIQUE_ID = 0; - - private DecodeConfiguration mDecodeConfiguration = DecoderFactory.getDefaultDecodeConfiguration(); - private AuxDecodeConfiguration mAuxDecodeConfiguration = - new AuxDecodeConfiguration(); - private SourceConfiguration mSourceConfiguration = - SourceConfigFactory.getDefaultSourceConfiguration(); - private EventLogConfiguration mEventLogConfiguration = - new EventLogConfiguration(); - private RecordConfiguration mRecordConfiguration = new RecordConfiguration(); - - private String mAliasListName; - private String mSystem = "System"; - private String mSite = "Site"; - private String mName = "Channel"; - - private boolean mEnabled; - private boolean mSelected; - private TunerChannel mTunerChannel = null; - - private ChannelType mChannelType = ChannelType.STANDARD; - - private int mChannelID; - private int mChannelFrequencyCorrection = 0; - - /** - * Channel represents a complete set of configurations needed to setup and - * manage a processing chain and/or manage as a set of business objects that - * can be displayed in graphical components. - * - * @param channelName - * @param channelType - */ - public Channel(String channelName, ChannelType channelType) - { - this(channelName); - mChannelType = channelType; - } - - /** - * Constructs a new standard channel with the specified channel name - */ - public Channel(String channelName) - { - this(); - mName = channelName; - } - - /** - * Constructs a new standard channel with a default name of "Channel" - */ - public Channel() - { - mChannelID = UNIQUE_ID++; - } - - /** - * Creates a (new) deep copy of this channel - */ - public Channel copyOf() - { - Channel channel = new Channel(mName); - channel.setSystem(mSystem); - channel.setSite(mSite); - channel.setAliasListName(mAliasListName); - - AuxDecodeConfiguration aux = new AuxDecodeConfiguration(); - - for(DecoderType auxType : aux.getAuxDecoders()) - { - aux.addAuxDecoder(auxType); - } - - channel.setAuxDecodeConfiguration(aux); - - channel.setDecodeConfiguration(DecoderFactory.copy(mDecodeConfiguration)); - - EventLogConfiguration log = new EventLogConfiguration(); - - for(EventLogType logType : mEventLogConfiguration.getLoggers()) - { - log.addLogger(logType); - } - - channel.setEventLogConfiguration(log); - - RecordConfiguration record = new RecordConfiguration(); - - for(RecorderType recordType : mRecordConfiguration.getRecorders()) - { - record.addRecorder(recordType); - } - - channel.setRecordConfiguration(record); - - channel.setSourceConfiguration(SourceConfigFactory.copy(mSourceConfiguration)); - - return channel; - } - - /** - * Unique identifier for this channel. Value is transient (not persisted). - */ - @XmlTransient - public int getChannelID() - { - return mChannelID; - } - - /** - * Indicates if this channel has been selected for prioritized audio output - * and display of processing chain products - */ - @XmlTransient - public boolean isSelected() - { - return mSelected; - } - - /** - * Sets selection status of the channel. - * - * NOTE: this method is package private so that only the ChannelSelectionManager - * can toggle the selection state. Use the channel model to broadcast a - * request select/deselect channel event. - * - * @param selected - */ - void setSelected(boolean selected) - { - mSelected = selected; - } - - @XmlTransient - public ChannelType getChannelType() - { - return mChannelType; - } - - /** - * Returns the owning system for this channel. - */ - @XmlAttribute(name = "system") - public String getSystem() - { - return mSystem; - } - - public void setSystem(String system) - { - mSystem = system; - } - - public boolean hasSystem() - { - return mSystem != null; - } - - /** - * Returns the owning site for this channel. - */ - @XmlAttribute(name = "site") - public String getSite() - { - return mSite; - } - - public void setSite(String site) - { - mSite = site; - } - - public boolean hasSite() - { - return mSite != null; - } - - /** - * Returns the name of this channel. - */ - @XmlAttribute(name = "name") - public String getName() - { - return mName; - } - - /** - * Sets the name of this channel. - */ - public void setName(String name) - { - mName = name; - } - - /** - * Default display string for this channel: SYSTEM_SITE_NAME - */ - public String toString() - { - StringBuilder sb = new StringBuilder(); - sb.append(hasSystem() ? getSystem() : "SYSTEM"); - sb.append("_"); - sb.append(hasSite() ? getSite() : "SITE"); - sb.append("_"); - sb.append(getName()); - - return sb.toString(); - } - - /** - * Indicates if this channel is enabled for processing. - */ - @XmlAttribute(name = "enabled") - public boolean getEnabled() - { - return mEnabled; - } - - /** - * Indicates if this channel will automatically start processing on - * application startup or if this channel is currently processing after - * application startup. - * - * Note: this method is package private and is intended to be managed - * by a co-package central processing manager - * - * @see controller.channel.ChannelProcessingManager - */ - void setEnabled(boolean enabled) - { - mEnabled = enabled; - } - - /** - * Returns the alias list that is used by this channel for looking up alias - * values for the various identifiers produced by the decoder(s). - */ - @XmlElement(name = "alias_list_name") - public String getAliasListName() - { - return mAliasListName; - } - - /** - * Sets the alias list to be used for looking up the alias values for the - * various identifiers produced by the decoder - */ - public void setAliasListName(String name) - { - mAliasListName = name; - } - - /** - * Gets the primary decoder configuration used by this channel - */ - @XmlElement(name = "decode_configuration") - public DecodeConfiguration getDecodeConfiguration() - { - return mDecodeConfiguration; - } - - /** - * Sets the primary decoder configuration used by this channel - */ - public void setDecodeConfiguration(DecodeConfiguration config) - { - mDecodeConfiguration = config; - } - - /** - * Gets the aux decoder configuration used by this channel. Aux decoders - * operate on demodulated audio - */ - @XmlElement(name = "aux_decode_configuration") - public AuxDecodeConfiguration getAuxDecodeConfiguration() - { - return mAuxDecodeConfiguration; - } - - /** - * Sets the decoder configuration used by this channel - */ - public void setAuxDecodeConfiguration(AuxDecodeConfiguration config) - { - mAuxDecodeConfiguration = config; - } - - /** - * Returns the source configuration for this channel. A channel source - * identifies where the processing chain will get source sample data from. - */ - @XmlElement(name = "source_configuration") - public SourceConfiguration getSourceConfiguration() - { - return mSourceConfiguration; - } - - /** - * Sets the source configuration for this channel. - */ - public void setSourceConfiguration(SourceConfiguration config) - { - mSourceConfiguration = config; - - //Clear the tune channel object so that it can be recreated if the - //source configuration changes - mTunerChannel = null; - } - - /** - * Returns the event logger configuration for this channel. - */ - @XmlElement(name = "event_log_configuration") - public EventLogConfiguration getEventLogConfiguration() - { - return mEventLogConfiguration; - } - - /** - * Sets the event logger configuration for this channel. - */ - public void setEventLogConfiguration(EventLogConfiguration config) - { - mEventLogConfiguration = config; - } - - /** - * Returns the recorder configuration for this channel. - */ - @XmlElement(name = "record_configuration") - public RecordConfiguration getRecordConfiguration() - { - return mRecordConfiguration; - } - - /** - * Sets the recorder configuration for this channel. - */ - public void setRecordConfiguration(RecordConfiguration config) - { - mRecordConfiguration = config; - } - - /** - * Convenience method to construct a tuner channel object representing a - * tuner or recording source frequency and bandwidth that can be used by - * application components for graphically representing this channel. - * - * If the source configuration is not a tuner or recording, this method - * returns null. - */ - @XmlTransient - public TunerChannel getTunerChannel() - { - if(mTunerChannel == null) - { - if(mSourceConfiguration.getSourceType() == SourceType.TUNER) - { - SourceConfigTuner config = (SourceConfigTuner)mSourceConfiguration; - - mTunerChannel = new TunerChannel(config.getFrequency(), - mDecodeConfiguration.getDecoderType().getChannelBandwidth()); - } - else if(mSourceConfiguration.getSourceType() == SourceType.RECORDING) - { - SourceConfigRecording config = (SourceConfigRecording)mSourceConfiguration; - - mTunerChannel = new TunerChannel(config.getFrequency(), - mDecodeConfiguration.getDecoderType().getChannelBandwidth()); - } - } - - return mTunerChannel; - } - - /** - * Convenience method to indicate if any part of this channel is contained - * within the minimum and maximum frequency values. - */ - public boolean isWithin(long minimum, long maximum) - { - TunerChannel tunerChannel = getTunerChannel(); - - return tunerChannel != null && tunerChannel.overlaps(minimum, maximum); - } - - /** - * Convenience method to make the current channel frequency correction value - * available for use outside of a constructed processing chain while maintaining - * linkage to the source channel. This hack allows the correction value to - * be used to visually show in the spectral display. - */ - @Override - public void receive(SourceEvent event) - { - if(event.getEvent() == Event.NOTIFICATION_CHANNEL_FREQUENCY_CORRECTION_CHANGE) - { - mChannelFrequencyCorrection = event.getValue().intValue(); - } - } - - /** - * Current channel frequency correction value (when this channel is processing) - * @return - */ - @XmlTransient - public int getChannelFrequencyCorrection() - { - return mChannelFrequencyCorrection; - } - - /** - * Resets frequency correction value to 0 - */ - public void resetFrequencyCorrection() - { - mChannelFrequencyCorrection = 0; - } -} diff --git a/src/dsp/filter/FilterFactory.java b/src/dsp/filter/FilterFactory.java deleted file mode 100644 index 0a8103cc..00000000 --- a/src/dsp/filter/FilterFactory.java +++ /dev/null @@ -1,910 +0,0 @@ -/******************************************************************************* - * 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; - -import dsp.filter.Window.WindowType; -import dsp.filter.cic.ComplexPrimeCICDecimate; -import dsp.filter.design.FilterDesignException; -import dsp.filter.fir.FIRFilterSpecification; -import dsp.filter.fir.remez.RemezFIRFilterDesigner; -import dsp.filter.fir.remez.RemezFIRFilterDesignerWithLagrange; -import org.jtransforms.fft.FloatFFT_1D; -import org.slf4j.Logger; -import org.slf4j.LoggerFactory; - -import java.util.Set; -import java.util.TreeSet; - -/** - * Utility methods for designing various types of filters. - */ -public class FilterFactory -{ - private final static Logger mLog = LoggerFactory.getLogger(FilterFactory.class); - private static final double PERFECT_RECONSTRUCTION_GAIN_AT_BAND_EDGE = -6.020599842071533; //decibel(0.5, 0.0) - - /** - * Generates coefficients for a unity-gain, windowed low-pass filter - * - * @param sampleRate - hertz - * @param frequency - cutoff in hertz - * @param length - filter length - * @param window - to apply against the coefficients - * @return - */ - public static float[] getSinc(int sampleRate, long frequency, int length, WindowType window) - { - int evenLength = length % 2 == 0 ? length : length + 1; - - //Get unity response array (one element longer to align with IDFT size) - float[] frequencyResponse = getUnityResponseArray(sampleRate, frequency, evenLength); - - //Apply Inverse DFT against frequency response unity values, leaving the - //IDFT bin results in the frequency response array - FloatFFT_1D idft = new FloatFFT_1D(evenLength); - idft.realInverseFull(frequencyResponse, true); - - //Transfer the IDFT results to the return array - float[] coefficients = new float[length]; - int middleCoefficient = (int)(length / 2); - - if(length % 2 == 0) //Even length - { - //The remaining idft bins from 1 to (middle - 1) are the mirror image - //coefficients - for(int x = 0; x < middleCoefficient; x++) - { - coefficients[middleCoefficient + x] = frequencyResponse[2 * x]; - coefficients[middleCoefficient - x] = frequencyResponse[2 * x]; - } - } - else - { - //Bin 0 of the idft is our center coefficient - coefficients[middleCoefficient] = frequencyResponse[0]; - - //The remaining idft bins from 1 to (middle - 1) are the mirror image - //coefficients - for(int x = 1; x < middleCoefficient; x++) - { - coefficients[middleCoefficient + x] = frequencyResponse[2 * x]; - coefficients[middleCoefficient - x] = frequencyResponse[2 * x]; - } - } - - //Apply the window against the coefficients - coefficients = Window.apply(window, coefficients); - - return coefficients; - } - - /** - * Normalizes all filter coefficients to achieve unity (1) gain, by ensuring - * that the sum of the absolute value of all coefficients adds up to 1. - * - * @param coefficients - * @return - */ - public static float[] normalize(float[] coefficients) - { - float accumulator = 0; - - for(int x = 0; x < coefficients.length; x++) - { - accumulator += Math.abs(coefficients[x]); - } - - for(int x = 0; x < coefficients.length; x++) - { - coefficients[x] = coefficients[x] / accumulator; - } - - return coefficients; - } - - /** - * Constructs an array of unity (1) response values representing the - * desired (pre-windowing) frequency response, used by the sync function - * - * Returns an array twice the length, filled with unity (1) response values - * in the desired pass-band with the positive frequency response starting at - * the lower end of the array, and the negative frequency response at the - * higher end of the first half of the array. The remaining zero-valued - * indexes in the second half will store the results of the JTransforms - * inverse DFT operation - * - * @param sampleRate - * @param frequency - * @param length - * @return - */ - public static float[] getUnityResponseArray(int sampleRate, long frequency, int length) - { - float[] unityArray = new float[length * 2]; - - - if(length % 2 == 0) //even length - { - int binCount = (int)((Math.round((float)frequency / (float)sampleRate * (float)length))); - - for(int x = 0; x < binCount; x++) - { - unityArray[x] = 1.0f; - unityArray[length - 1 - x] = 1.0f; - } - } - else //odd length - { - int binCount = (int)((Math.round((float)frequency / (float)sampleRate * (float)length + 0.5))); - - unityArray[0] = 1.0f; - - for(int x = 1; x < binCount; x++) - { - unityArray[x] = 1.0f; - unityArray[length - x] = 1.0f; - } - } - - return unityArray; - } - - /** - * Applies a repeating sequence of 1, -1 to the coefficients to invert - * the frequency response of the filter. Used in converting a low-pass - * filter to a high-pass filter. - * - * Inverts the sign of all odd index coefficients ( 1, 3, 5, etc.) - * returning: - * Index 0: same - * Index 1: inverted - * Index 2: same - * ... - * Index length - 1: same - */ - public static float[] invert(float[] coefficients) - { - for(int x = 1; x < coefficients.length; x += 2) - { - coefficients[x] = -coefficients[x]; - } - - return coefficients; - } - - /** - * Generates filter coefficients for a unity-gain, odd-length, windowed, - * low pass filter with passband from 0-hertz to the cutoff frequency. - * - * @param sampleRate - hertz - * @param cutoff - frequency in hertz - * @param filterLength - odd filter length - * @param windowType - window to apply against the generated coefficients - * @return - */ - public static float[] getLowPass(int sampleRate, long cutoff, int filterLength, WindowType windowType) - { - return getSinc(sampleRate, cutoff, filterLength, windowType); - } - - /** - * Creates a low-pass filter with ~ 0.1 db ripple in the pass band - * - * Note: stop frequency - pass frequency defines the transition band. - */ - /** - * Creates a low-pass filter with ~0.1 db ripple in the pass band. The - * transition region is defined by the stop frequency minus the pass - * frequency. - * - * Requires: - * - passFrequency < stopFrequency - * - stopFrequency <= sampleRate/2 - */ - public static float[] getLowPass(int sampleRate, int passFrequency, int stopFrequency, int attenuation, - WindowType windowType, boolean forceOddLength) - { - if(stopFrequency < passFrequency || stopFrequency > (sampleRate / 2)) - { - throw new IllegalArgumentException("FilterFactory - low pass filter pass frequency [" + passFrequency + - "] must be less than the stop frequency [" + stopFrequency + "] and must be less than half [" + - (int)(sampleRate / 2) + "] of the sample rate [" + sampleRate + "]"); - } - - int tapCount = getTapCount(sampleRate, passFrequency, stopFrequency, attenuation); - - if(forceOddLength) - { - if(tapCount % 2 == 0) - { - tapCount--; - } - } - - return getLowPass(sampleRate, passFrequency, tapCount, windowType); - } - - /** - * Generates filter coefficients for a unity-gain, odd-length, windowed, - * high pass filter with passband from cutoff frequency to half the sample - * rate. - * - * @param sampleRate - hertz - * @param cutoff - frequency in hertz - * @param filterLength - odd filter length - * @param windowType - window to apply against the generated coefficients - * @return - */ - public static float[] getHighPass(int sampleRate, long cutoff, int filterLength, WindowType windowType) - { - //Convert the high frequency cutoff to its low frequency cutoff - //equivalent, so that when we generate the low pass filter, prior to - //inversion, its at the correct frequency - long convertedCutoff = sampleRate / 2 - cutoff; - - return invert(getSinc(sampleRate, convertedCutoff, filterLength, windowType)); - } - - public static float[] getHighPass(int sampleRate, long stopFrequency, long passFrequency, int attenuation, - WindowType windowType, boolean forceOddLength) - { - /* reverse the stop and pass frequency to get the low pass variant */ - int tapCount = getTapCount(sampleRate, stopFrequency, passFrequency, attenuation); - - if(forceOddLength) - { - if(tapCount % 2 == 0) - { - tapCount--; - } - } - - return invert(getLowPass(sampleRate, stopFrequency, tapCount, windowType)); - } - - /** - * Utility to log the arrays of doubles with line breaks - */ - public static String arrayToString(float[] array, boolean breaks) - { - StringBuilder sb = new StringBuilder(); - for(int x = 0; x < array.length; x++) - { - sb.append(x + ": " + array[x]); - - if(breaks) - { - sb.append("\n"); - } - else - { - if(x % 8 == 7) - { - sb.append("\n"); - } - else - { - sb.append("\t"); - } - } - } - - return sb.toString(); - } - - /** - * Determines the number of fir filter taps required to produce the specified frequency response with passband - * ripple near 0.1dB. - * - * Implements the algorithm from Understanding Digital Signal Processing, 3e, Lyons, section 5.10.5. - * - * @param sampleRate in hertz - * @param pass pass frequency in hertz - * @param stop stop frequency in hertz - * @param attenuation in dB - * @return - */ - public static int getTapCount(int sampleRate, long pass, long stop, int attenuation) - { - double frequency = ((double)stop - (double)pass) / (double)sampleRate; - - return (int)(Math.round((double)attenuation / (22.0d * frequency))); - } - - public static ComplexPrimeCICDecimate getDecimationFilter(int sampleRate, int decimatedRate, int order, - int passFrequency, int attenuation, WindowType windowType) - { - int decimationRate = (int)(sampleRate / decimatedRate); - - return new ComplexPrimeCICDecimate(decimationRate, order, - passFrequency, attenuation, windowType); - } - - /** - * Determines decimation rate(s) for a polyphase decimation filter. - * - * @param sampleRate - starting sample rate - * @param decimatedRate - final (decimated) output rate - * @return - set of integer decimation rates for a single or multi-stage - * polyphase filter decimation chain - * @throws - AssertionException if sample rate is not a multiple of 48 kHz - */ - public static int[] getPolyphaseDecimationRates(int sampleRate, int decimatedRate, long passFrequency, long stopFrequency) - { - int[] rates; - - if(sampleRate % decimatedRate != 0) - { - throw new IllegalArgumentException("Decimated rate must be an " - + "integer multiple of sample rate"); - } - - int decimation = (int)(sampleRate / decimatedRate); - - //Decimation rates below 20 will use single stage polyphase filter - if(decimation < 20) - { - rates = new int[1]; - rates[0] = decimation; - return rates; - } - else - { - int optimalStage1 = - getOptimalStageOneRate(sampleRate, decimation, passFrequency, stopFrequency); - - Set factors = getFactors(decimation); - - int stage1 = findClosest(optimalStage1, factors); - - if(stage1 == decimation || stage1 == 1) - { - rates = new int[1]; - rates[0] = decimation; - return rates; - } - else - { - rates = new int[2]; - - int stage2 = (int)(decimation / stage1); - - if(stage1 > stage2) - { - rates[0] = stage1; - rates[1] = stage2; - } - else - { - rates[0] = stage2; - rates[1] = stage1; - } - return rates; - } - } - } - - /** - * Finds the factor that is closest to the desired factor, from an ordered list of factors. - */ - private static int findClosest(int desiredFactor, Set factors) - { - int bestFactor = 1; - int bestDelta = desiredFactor; - - for(Integer factor : factors) - { - int testDelta = Math.abs(desiredFactor - factor); - - if(testDelta < bestDelta) - { - bestDelta = testDelta; - bestFactor = factor; - } - } - - return bestFactor; - } - - /** - * Determines the factors that make up an integer. Uses a brute force method to iterate all integers from 1 to - * value, determining which factors are evenly divisible into the value. - * - * @param value - integer decimation value - * @return - ordered set of factors for value - */ - private static Set getFactors(int value) - { - Set factors = new TreeSet(); - - /* Brute force */ - for(int x = 1; x <= value; x++) - { - int remainder = (int)(value / x); - - if(remainder * x == value) - { - factors.add(x); - } - } - - return factors; - } - - /** - * Determines the optimal decimation rate for stage 1 of a two-stage poly-phase decimation filter chain, to produce - * a final sample rate of 48 kHz using a pass bandwidth of 25 kHz. Use for total decimation rates of 20 or higher. - * - * Implements the algorithm described in Lyons, Understanding Digital Signal Processing, 3e, section 10.2.1, page 511. - * - * @param sampleRate - * @param decimation - * @param passFrequency frequency of the pass band - * @return optimum integer decimation rate for the first stage decimation - * filter - */ - public static int getOptimalStageOneRate(int sampleRate, int decimation, long passFrequency, long stopFrequency) - { - double ratio = getBandwidthRatio(passFrequency, stopFrequency); - - double numerator = 1.0d - (Math.sqrt((double)decimation * ratio / (2.0d - ratio))); - - double denominator = 2.0d - (ratio * (decimation + 1.0d)); - - int retVal = (int)(2.0d * decimation * (numerator / denominator)); - - return retVal; - } - - /** - * Determines the F ratio as described in Lyons, Understanding Digital Signal Processing, 3e, section 10.2.1, page 511 - * Used in conjunction with the optimal stage one decimation rate method above. - */ - private static double getBandwidthRatio(long passFrequency, long stopFrequency) - { - assert (passFrequency < stopFrequency); - - return ((double)(stopFrequency - passFrequency) / - (double)stopFrequency); - } - - /** - * Assumes that the pass band is 1/4 of the output sample rate. - * - * Assumes the stop band is: pass + (pass * .25). - * - * @param outputSampleRate - * @return - */ - public static float[] getCICCleanupFilter(int outputSampleRate, int passFrequency, int attenuation, WindowType window) - { - int taps = getTapCount(outputSampleRate, passFrequency, passFrequency + 1500, - attenuation); - - /* Make tap count odd */ - if(taps % 2 == 0) - { - taps++; - } - - float[] frequencyResponse = - getCICResponseArray(outputSampleRate, passFrequency, taps); - - //Apply Inverse DFT against frequency response unity values, leaving the - //IDFT bin results in the frequency response array - FloatFFT_1D idft = new FloatFFT_1D(taps); - idft.realInverseFull(frequencyResponse, false); - - //Transfer the IDFT results to the odd length return array - float[] coefficients = new float[taps]; - int middleCoefficient = (int)(taps / 2); - - //Bin 0 of the idft is our center coefficient - coefficients[middleCoefficient] = frequencyResponse[0]; - - //The remaining idft bins from 1 to (middle - 1) are the mirror image - //coefficients - for(int x = 1; x <= middleCoefficient; x++) - { - coefficients[middleCoefficient + x] = frequencyResponse[2 * x]; - coefficients[middleCoefficient - x] = frequencyResponse[2 * x]; - } - - //Apply the window against the coefficients -// coefficients = Window.apply( window, coefficients ); - - normalize(coefficients); - - return coefficients; - } - - public static float[] getCICResponseArray(int sampleRate, int frequency, int length) - { - float[] cicArray = new float[length * 2]; - - int binCount = (int)((Math.round( - (double)frequency / (double)sampleRate * 2.0d * (double)length))); - - cicArray[0] = 1.0f; - - float unityResponse = (float)(Math.sin(1.0d / (double)length) / - (1.0d / (double)length)); - - for(int x = 1; x <= binCount; x++) - { - /* Calculate unity response plus amplification for droop */ - float compensated = 1.0f + (unityResponse - (float)(Math.sin((double)x / (double)length) / ((double)x / (double)length))); - - cicArray[x] = compensated; - cicArray[length - x] = compensated; - } - - return cicArray; - } - - /** - * Creates root raised cosine filter coefficients with a tap count equal to the symbols x samplesPerSymbol + 1. - * - * Symbol count should be an even integer. - * Ported to java from gnuradio/filter/firdes.cc - * - * For 40db attenuation, calculate the number of symbols based on the following formula: - * Symbols = -44 * alpha + 33 - * - * Polyphase Channelizer notes: - * -Set samples Per Symbol at 2 (or more) * number of channels - * -Set symbolCount to sufficient size to produce required attenuation - * -Set symbol rate in hertz - * -Set alpha = (desired channel bandwidth / (symbol rate * samples per symbol) - 1.0 - * -Channel bandwidth must be larger than symbol rate * samples per symbol, in order for there to be a positive alpha - * - * @param samplesPerSymbol - number of samples per symbol - * @param symbolCount - number of symbol intervals for the filter. This directly impacts the filter size - * @param alpha - roll-off factor. - * @return - filter coefficients - */ - public static float[] getRootRaisedCosine(int samplesPerSymbol, int symbolCount, float alpha) - { - int taps = samplesPerSymbol * symbolCount; - - float scale = 0; - - float[] coefficients = new float[taps]; - - for(int x = 0; x < taps; x++) - { - float index = (float)x - ((float)taps / 2.0f); - - float x1 = (float)Math.PI * index / (float)samplesPerSymbol; - float x2 = 4.0f * alpha * index / (float)samplesPerSymbol; - float x3 = x2 * x2 - 1.0f; - - float numerator, denominator; - - if(Math.abs(x3) >= 0.000001) - { - if(x != taps / 2) - { - numerator = (float)Math.cos((1.0 + alpha) * x1) + - (float)Math.sin((1.0f - alpha) * x1) / (4.0f * alpha * index / (float)samplesPerSymbol); - } - else - { - numerator = (float)Math.cos((1.0f + alpha) * x1) + (1.0f - alpha) * (float)Math.PI / (4.0f * alpha); - } - - denominator = x3 * (float)Math.PI; - } - else - { - if(alpha == 1.0f) - { - coefficients[x] = -1.0f; - continue; - } - - x3 = (1.0f - alpha) * x1; - x2 = (1.0f + alpha) * x1; - - numerator = (float)(Math.sin(x2) * (1.0f + alpha) * Math.PI - Math.cos(x3) * ((1.0 - alpha) * Math.PI * - (double)samplesPerSymbol) / (4.0 * alpha * index) + Math.sin(x3) * (double)samplesPerSymbol * - (double)samplesPerSymbol / (4.0 * alpha * index * index)); - - denominator = (float)(-32.0 * Math.PI * alpha * alpha * index / (double)samplesPerSymbol); - } - - coefficients[x] = 4.0f * alpha * numerator / denominator; - - scale += coefficients[x]; - } - - /** - * Normalize (scale) coefficients to 1.0 sum and apply gain - */ - for(int x = 0; x < taps; x++) - { - coefficients[x] = coefficients[x] / scale; - } - - return coefficients; - } - - /** - * Creates a filter from the filter specification using the remez exchange design algorithm - * - * @param specification - * @return filter coefficients - * @throws FilterDesignException if the filter cannot be designed - */ - public static float[] getTaps(FIRFilterSpecification specification) throws FilterDesignException - { - RemezFIRFilterDesigner designer = new RemezFIRFilterDesigner(specification); - - if(designer.isValid()) - { - return designer.getImpulseResponse(); - } - - return null; - } - - /** - * Evaluates the filter to determine the magnitude response (dB) at the specified frequency. - * - * @param filter to evaluate - * @param frequency to calculate (0 <> 0.5) - * @return magnitude frequency response in decibels - */ - public static double evaluate(float[] filter, double frequency) - { - double real = 0.0; - double imag = 0.0; - - for(int x = 0; x < filter.length; x++) - { - real += filter[x] * Math.cos(Math.PI * frequency * (double)x); - imag += filter[x] * Math.sin(Math.PI * frequency * (double)x); - } - - return decibel(real, imag); - } - - /** - * Calculates the decibel magnitude of the real and imaginary complex sample - * @param real (x-axis) value - * @param imag (y-axis) value - * @return magnitude in decibels - */ - public static double decibel(double real, double imag) - { - return (float)(10.0 * Math.log10(Math.pow(real, 2.0) + Math.pow(imag, 2.0))); - } - - - public static float[] getRemezChannelizer(int channelBandwidth, int channels, int tapsPerChannel, double alpha, - double passRipple, double stopRipple) throws FilterDesignException - { - FIRFilterSpecification specification = FIRFilterSpecification.channelizerBuilder() - .sampleRate(channels * channelBandwidth * 4) - .channels(channels) - .channelBandwidth(channelBandwidth) - .tapsPerChannel(tapsPerChannel) - .alpha(alpha) - .passRipple(passRipple) - .stopRipple(stopRipple) - .build(); - - RemezFIRFilterDesignerWithLagrange designer = new RemezFIRFilterDesignerWithLagrange(specification); - - float[] taps = designer.getImpulseResponse(); - - double bandEdgeFrequency = (double)channelBandwidth / (double)(channels * channelBandwidth * 2); - - double response = decibel(designer.getFrequencyResponse(Math.cos(bandEdgeFrequency * Math.PI)), 0.0); - mLog.debug("Frequency Response at 1.0: " + response); - response = decibel(designer.getFrequencyResponse(Math.cos(bandEdgeFrequency * Math.PI * 2.0)), 0.0); - mLog.debug("Frequency Response at 2.0: " + response); - - return taps; - } - - /** - * 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 - * adjusted to achieve a -6.02 dB attenuation at the channel band edge to enable perfect reconstruction of adjacent - * channels. - * - * The odd length sync filter that meets the design objectives will be increased in length by pre-padding a zero - * coefficient to produce a filter with a length that is an integral of the channel count. - * - * Note: the tapsPerChannel value will be iteratively increased (up to 10x) if a filter cannot be designed to meet - * the channel band edge attenuation objective. - * - * Note: a channelizer filter designed using a Blackman-Harris-7 window and 18-21 taps per channel creates a filter - * with attenuation in range of -60 to -80 dB at the edge of the 2x sampled channel. - * - * @param channelBandwidth - full bandwidth of the channel in hertz - * @param channels - number of channels - * @param tapsPerChannel - desired filter tap count per channel (may be increased to meet objective) - * @param windowType - to use when designing the windowed-sync filter - * @param logResults - to log a debug summary of the filter characteristics - * - * @return filter coefficients - * - * @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, - 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 bandEdge = cutoffFrequency; - double increment = cutoffFrequency * 0.1; - - //Get an initial filter and band edge frequency response using the channel bandwidth as a cutoff - float[] taps = FilterFactory.getSinc(cutoffFrequency, filterLength, windowType); - 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; - - //Iteratively evaluate filters to find the filter with the highest cutoff frequency that meets the objective - while(increment > incrementThreshold) - { - //If the current cutoff meets the objective, test if a higher cutoff also meets the objective - if(matchesObjective(response, PERFECT_RECONSTRUCTION_GAIN_AT_BAND_EDGE) && - (cutoffFrequency + increment <= bandEdge)) - { - float[] higherCutoffTaps = FilterFactory.getSinc(cutoffFrequency + increment, filterLength, windowType); - double higherCutoffResponse = FilterFactory.evaluate(higherCutoffTaps, bandEdge); - - if(matchesObjective(higherCutoffResponse, PERFECT_RECONSTRUCTION_GAIN_AT_BAND_EDGE)) - { - cutoffFrequency += increment; - taps = higherCutoffTaps; - response = higherCutoffResponse; - } - else - { - increment /= 2.0; - } - } - //If the current cutoff meets the objective, decrease the increment to test for smaller resolution cutoffs - else if(matchesObjective(response, PERFECT_RECONSTRUCTION_GAIN_AT_BAND_EDGE)) - { - increment /= 2.0; - } - //Decrease the cutoff frequency - we can't meet the objective - else - { - cutoffFrequency -= increment; - - if(cutoffFrequency <= 0) - { - if(logResults) - { - mLog.debug("Warning: cannot design channelizer filter with tap count [" + currentTapsPerChannel + - "] increasing tap count and starting over"); - } - - currentTapsPerChannel++; - - //After we've attempted increasing the taps per channel 10x, give up - if(currentTapsPerChannel > (tapsPerChannel + 10)) - { - throw new FilterDesignException("Couldn't design filter with taps per channel count in the " + - "range of " + tapsPerChannel + " - " + (tapsPerChannel + 10)); - } - - filterLength = channels * currentTapsPerChannel - 1; - cutoffFrequency = ((double)channelBandwidth / (double)sampleRate); - increment = cutoffFrequency * 0.1; - } - - taps = FilterFactory.getSinc(cutoffFrequency, filterLength, windowType); - response = FilterFactory.evaluate(taps, bandEdge); - } - } - - //This will probably never happen, but ... - if(!matchesObjective(response, PERFECT_RECONSTRUCTION_GAIN_AT_BAND_EDGE)) - { - throw new FilterDesignException("Cannot design filter to specifications"); - } - - //This is an odd length filter - increase the length by 1 by pre-padding a zero coefficient - float[] extendedTaps = new float[taps.length + 1]; - System.arraycopy(taps, 0, extendedTaps, 1, taps.length); - - if(logResults) - { - mLog.debug("Polyphase Channelizer Filter Design Summary"); - mLog.debug("-----------------------------------------------------"); - mLog.debug("Input Sample Rate: " + sampleRate); - mLog.debug("Channel Bandwidth: " + channelBandwidth); - mLog.debug("Channels: " + channels); - mLog.debug("Window Type: " + windowType.name()); - mLog.debug("Taps Per Channel - Requested:" + tapsPerChannel + " Actual:" + ((double)extendedTaps.length / (double)channels)); - mLog.debug("Filter Length: " + (extendedTaps.length)); - mLog.debug("Requested Cutoff Frequency: " + (sampleRate * bandEdge)); - mLog.debug("Actual Cutoff Frequency: " + (sampleRate * cutoffFrequency)); - mLog.debug("Attenuation at 1.0 OBJECTIVE: " + PERFECT_RECONSTRUCTION_GAIN_AT_BAND_EDGE); - mLog.debug("Attenuation at 1.00 Channels: " + evaluate(taps, bandEdge * 1.00)); - mLog.debug("Attenuation at 1.25 Channels: " + evaluate(taps, bandEdge * 1.25)); - mLog.debug("Attenuation at 1.50 Channels: " + evaluate(taps, bandEdge * 1.50)); - mLog.debug("Attenuation at 1.75 Channels: " + evaluate(taps, bandEdge * 1.75)); - mLog.debug("Attenuation at 2.00 Channels: " + evaluate(taps, bandEdge * 2.00)); - } - - return extendedTaps; - } - - /** - * Creates a windowed-sync (Nyquist) filter. - * - * @param cutoff frequency (0.0 <> 0.5) - * @param length of the filter - must be odd-length - * @param windowType to use in designing the filter - * @return filter coefficients. - * @throws FilterDesignException if the requested length is not odd - */ - public static float[] getSinc(double cutoff, int length, WindowType windowType) throws FilterDesignException - { - if(length % 2 == 0) - { - throw new FilterDesignException("Sinc filters must be odd-length"); - } - - float[] coefficients = new float[length]; - int half = length / 2; - - double[] window = Window.getWindow(windowType, length); - - double scalor = 2.0 * cutoff; - double piScalor = Math.PI * scalor; - - coefficients[half] = (float)(1.0 * scalor); - - for(int x = 1; x <= half; x++) - { - double a = piScalor * x; - double coefficient = scalor * Math.sin(a) / a; - - coefficient *= window[half + x]; - coefficients[half + x] = (float)coefficient; - coefficients[half - x] = (float)coefficient; - } - - return coefficients; - } - - /** - * Compares two doubles for equals and avoids any rounding error that are present. - * @param a value to compare - * @param objective value to compare - * @return true if they are equivalent within the margin of error - */ - private static boolean matchesObjective(double a, double objective) - { - return Math.abs(a - objective) < 0.0000000001; - } -} diff --git a/src/dsp/filter/Window.java b/src/dsp/filter/Window.java deleted file mode 100644 index 6d1d5ac5..00000000 --- a/src/dsp/filter/Window.java +++ /dev/null @@ -1,439 +0,0 @@ -/******************************************************************************* - * 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; - -import org.slf4j.Logger; -import org.slf4j.LoggerFactory; - -import java.util.Arrays; - -/** - * Window factory and corresponding utility methods. - */ -public class Window -{ - private final static Logger mLog = LoggerFactory.getLogger(Window.class); - - private static final double TWO_PI = Math.PI * 2.0d; - private static final double FOUR_PI = Math.PI * 4.0d; - private static final double SIX_PI = Math.PI * 6.0d; - private static final double EIGHT_PI = Math.PI * 8.0d; - - /** - * Creates a window of the specified type and length. - * - * @param type of window - * @param length of window - * @return window of the specified type and length - */ - public static double[] getWindow(WindowType type, int length) - { - switch(type) - { - case BLACKMAN: - return getBlackman(length); - case BLACKMAN_HARRIS_4: - return getBlackmanHarris4(length); - case BLACKMAN_HARRIS_7: - return getBlackmanHarris7(length); - case BLACKMAN_NUTALL: - return getBlackmanNutall(length); - case COSINE: - return getCosine(length); - case FLAT_TOP: - return getFlatTop(length); - case HAMMING: - return getHamming(length); - case HANN: - return getHann(length); - case NUTALL: - return getNutall(length); - case NONE: - default: - return getRectangular(length); - } - } - - /** - * Creates an all-pass or rectangular window. - * - * @param length of the window - * @return rectangular window - */ - public static double[] getRectangular(int length) - { - double[] coefficients = new double[length]; - - Arrays.fill(coefficients, 1.0d); - - return coefficients; - } - - /** - * Creates a cosine window of the specified length. - * - * @param length of the window - * @return cosine window - */ - public static double[] getCosine(int length) - { - double[] coefficients = new double[length]; - - if(length % 2 == 0) //Even length - { - int half = (int)((length - 1) / 2); - - for(int x = -half; x < length / 2 + 1; x++) - { - coefficients[x + half] = Math.cos( - ((double)x * Math.PI) / ((double)length + 1.0d)); - } - } - else //Odd length - { - int half = (int)length / 2; - - for(int x = -half; x < half + 1; x++) - { - coefficients[x + half] = Math.cos( - ((double)x * Math.PI) / ((double)length + 1.0d)); - } - } - - return coefficients; - } - - /** - * Creates a 3-term Blackman window. - * - * @param length of the window - * @return Blackman window - */ - public static double[] getBlackman(int length) - { - double[] coefficients = new double[length]; - - double denominator = length - 1; - - double a0 = 0.426590713672; - double a1 = 0.496560619089; - double a2 = 0.0768486672399; - - for(int x = 0; x < length; x++) - { - coefficients[x] = a0 - - (a1 * Math.cos((TWO_PI * (double)x) / denominator)) + - (a2 * Math.cos((FOUR_PI * (double)x) / denominator)); - } - - return coefficients; - } - - /** - * Creates a 4-term Nutall window - * - * @param length of the window - * @return Nutall window - */ - public static double[] getNutall(int length) - { - double[] coefficients = new double[length]; - - double denominator = length - 1; - double a0 = 0.355768; - double a1 = 0.487396; - double a2 = 0.144232; - double a3 = 0.012604; - - for(int x = 0; x < length; x++) - { - coefficients[x] = a0 - - (a1 * Math.cos(TWO_PI * (double)x / denominator)) + - (a2 * Math.cos(FOUR_PI * (double)x / denominator)) - - (a3 * Math.cos(SIX_PI * (double)x / denominator)); - } - - return coefficients; - } - - /** - * Creates a 4-term Blackman-Nutall window - * - * @param length of the window - * @return Blackman-Nutall window - */ - public static double[] getBlackmanNutall(int length) - { - double[] coefficients = new double[length]; - - double denominator = length - 1; - double a0 = 0.3635819; - double a1 = 0.4891775; - double a2 = 0.1365995; - double a3 = 0.0106411; - - for(int x = 0; x < length; x++) - { - coefficients[x] = a0 - - (a1 * Math.cos(TWO_PI * (double)x / denominator)) + - (a2 * Math.cos(FOUR_PI * (double)x / denominator)) - - (a3 * Math.cos(SIX_PI * (double)x / denominator)); - } - - return coefficients; - } - - /** - * Creates a 4-term Blackman-Harris window - * - * @param length of the window - * @return Blackman-Harris window - */ - public static double[] getBlackmanHarris4(int length) - { - double[] coefficients = new double[length]; - - double denominator = length - 1; - double a0 = 0.35875; - double a1 = 0.48829; - double a2 = 0.14128; - double a3 = 0.01168; - - for(int x = 0; x < length; x++) - { - coefficients[x] = a0 - - (a1 * Math.cos(TWO_PI * (double)x / denominator)) + - (a2 * Math.cos(FOUR_PI * (double)x / denominator)) - - (a3 * Math.cos(SIX_PI * (double)x / denominator)); - } - - return coefficients; - } - - /** - * Creates a 7-term Blackman-Harris window - * - * @param length of the window - * @return Blackman-Harris window - */ - public static double[] getBlackmanHarris7(int length) - { - double[] coefficients = new double[length]; - - double denominator = length - 1; - - double a0 = 0.27105140069342; - double a1 = 0.43329793923448; - double a2 = 0.21812299954311; - double a3 = 0.06592544638803; - double a4 = 0.01081174209837; - double a5 = 0.00077658482522; - double a6 = 0.00001388721735; - - - for(int x = 0; x < length; x++) - { - double w = TWO_PI * (double)x / denominator; - - coefficients[x] = a0 - - (a1 * Math.cos(w)) + - (a2 * Math.cos(2.0 * w)) - - (a3 * Math.cos(3.0 * w)) + - (a4 * Math.cos(4.0 * w)) - - (a5 * Math.cos(5.0 * w)) + - (a6 * Math.cos(6.0 * w)); - } - - return coefficients; - } - - /** - * Creates a Hamming window - * - * @param length of the window - * @return Hamming window - */ - public static double[] getHamming(int length) - { - double[] coefficients = new double[length]; - - double a0 = 0.54; - double a1 = 0.46; - double denominator = length; - - for(int x = 0; x < length; x++) - { - coefficients[x] = a0 - (a1 * Math.cos(TWO_PI * (double)x / denominator)); - } - - return coefficients; - } - - /** - * Creates a Hann window - * - * @param length of the window - * @return Hann window - */ - public static double[] getHann(int length) - { - double[] coefficients = new double[length]; - - double a0 = 0.5; - double a1 = 0.5; - double denominator = length; - - for(int x = 0; x < length; x++) - { - coefficients[x] = a0 - (a1 * Math.cos(TWO_PI * (double)x / denominator)); - } - - return coefficients; - } - - /** - * Creates a 4-term Blackman-Harris window - * - * @param length of the window - * @return Blackman-Harris window - */ - public static double[] getFlatTop(int length) - { - double[] coefficients = new double[length]; - - double denominator = length - 1; - - double a0 = 0.215578948; - double a1 = 0.41663158; - double a2 = 0.277263158; - double a3 = 0.083578947; - double a4 = 0.006947368; - - for(int x = 0; x < length; x++) - { - coefficients[x] = a0 - - (a1 * Math.cos(TWO_PI * (double)x / denominator)) + - (a2 * Math.cos(FOUR_PI * (double)x / denominator)) - - (a3 * Math.cos(SIX_PI * (double)x / denominator)) + - (a4 * Math.cos(EIGHT_PI * (double)x / denominator)); - } - - return coefficients; - } - - - private static double getKaiserBeta(double attenuation) - { - if(attenuation > 50.0) - { - return 0.1102 * (attenuation - 8.7); - } - else if(attenuation >= 21.0) - { - return (0.5842 * Math.pow(attenuation - 21.0, 0.4)) + (0.07886 * (attenuation - 21.0)); - } - else - { - return 0.0; - } - } - - /** - * Apply the window against an array of float-type samples - */ - public static float[] apply(double[] coefficients, float[] samples) - { - for(int x = 0; x < coefficients.length; x++) - { - samples[x] = (float)(samples[x] * coefficients[x]); - } - - return samples; - } - - /** - * Apply the window type against the float array of samples - * - * @param type of window to use - * @param samples to be windowed - * @return windowed samples - */ - public static float[] apply(WindowType type, float[] samples) - { - double[] coefficients = getWindow(type, samples.length); - - return apply(coefficients, samples); - } - - /** - * Apply the window against an array of double-type samples - */ - public static double[] apply(double[] coefficients, double[] samples) - { - for(int x = 0; x < coefficients.length; x++) - { - samples[x] = samples[x] * coefficients[x]; - } - - return samples; - } - - /** - * Apply the window type against the double array of samples - * - * @param type of window to use - * @param samples to be windowed - * @return windowed samples - */ - public static double[] apply(WindowType type, double[] samples) - { - double[] coefficients = getWindow(type, samples.length); - - return apply(coefficients, samples); - } - - /** - * Window types - */ - public enum WindowType - { - BLACKMAN("Blackman"), - BLACKMAN_HARRIS_4("Blackman-Harris 4"), - BLACKMAN_HARRIS_7("Blackman-Harris 7"), - BLACKMAN_NUTALL("Blackman-Nutall"), - COSINE("Cosine"), - FLAT_TOP("Flat Top"), - HAMMING("Hamming"), - HANN("Hann"), - NUTALL("Nutall"), - NONE("None"); - - private String mLabel; - - WindowType(String label) - { - mLabel = label; - } - - public String toString() - { - return mLabel; - } - } -} diff --git a/src/dsp/filter/design/FilterView.java b/src/dsp/filter/design/FilterView.java deleted file mode 100644 index de30c8e3..00000000 --- a/src/dsp/filter/design/FilterView.java +++ /dev/null @@ -1,165 +0,0 @@ -package dsp.filter.design; - -import gui.control.CurveFittedAreaChart; -import javafx.collections.ObservableList; -import javafx.scene.chart.NumberAxis; -import javafx.scene.chart.XYChart; -import javafx.scene.chart.XYChart.Series; -import javafx.scene.layout.BorderPane; -import javafx.scene.layout.HBox; -import javafx.scene.layout.Priority; -import org.jtransforms.fft.FloatFFT_1D; -import org.slf4j.Logger; -import org.slf4j.LoggerFactory; - -public class FilterView extends BorderPane -{ - 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 = 2048; - private FloatFFT_1D mFFT = new FloatFFT_1D(FFT_SIZE); - - public FilterView(float[] taps) - { - init(); - - 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, 50.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 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(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> list = mImpulseChart.getData(); - list.add(series); - } - - public void setDFTTaps(float[] taps) - { - double smallest = 0.0; - - final XYChart.Series 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(x, taps[x])); - } - - ObservableList> 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; - double magnitude = Math.sqrt(Math.pow(dft[index], 2.0) + Math.pow(dft[index + 1], 2.0)); - decibels[x] = (float)(20.0 * Math.log10(magnitude)); - } - - return decibels; - } -} diff --git a/src/dsp/filter/design/FilterViewer.java b/src/dsp/filter/design/FilterViewer.java deleted file mode 100644 index 14db2b9c..00000000 --- a/src/dsp/filter/design/FilterViewer.java +++ /dev/null @@ -1,109 +0,0 @@ -package dsp.filter.design; - -import dsp.filter.FilterFactory; -import dsp.filter.Window; -import javafx.application.Application; -import javafx.scene.Scene; -import javafx.stage.Stage; -import org.slf4j.Logger; -import org.slf4j.LoggerFactory; - -public class FilterViewer extends Application -{ - private final static Logger mLog = LoggerFactory.getLogger(FilterViewer.class); - - /** - * Developer tool to visualize filter designs - */ - public FilterViewer() - { - } - - @Override - public void start(Stage primaryStage) throws Exception - { - float[] taps = getFilter(); - - if(taps != null) - { - Scene scene = new Scene(new FilterView(taps)); - - primaryStage.setTitle("Filter Viewer"); - primaryStage.setScene(scene); - primaryStage.show(); - } - else - { - throw new Exception("Couldn't start application - filter error"); - } - } - - public static void main(String[] args) - { - launch(args); - } - - /** - * Provides the filter to visualize. Modify this method to visualize your filter design. - */ - private float[] getFilter() - { - float[] taps = null; - -// FIRFilterSpecification specification = FIRFilterSpecification.lowPassBuilder() -// .sampleRate(1000) -// .gridDensity(16) -// .passBandAmplitude(1.0) -// .passBandCutoff(100) -// .passBandRipple(0.01) -// .stopBandAmplitude(0.0) -// .stopBandRipple(0.0001) -// .stopBandStart(150) -// .build(); - -// FIRFilterSpecification specification = FIRFilterSpecification.highPassBuilder() -// .sampleRate( 24000 ) -// .stopBandCutoff( 800 ) -// .stopBandAmplitude( 0.0 ) -// .stopBandRipple( 0.03 ) -// .passBandStart( 1000 ) -// .passBandAmplitude( 1.0 ) -// .passBandRipple( 0.08 ) -// .build(); - -// FIRFilterSpecification specification = FIRFilterSpecification.bandPassBuilder() -// .sampleRate( 48000 ) -// .stopFrequency1( 200 ) -// .passFrequencyBegin( 400 ) -// .passFrequencyEnd( 3200 ) -// .stopFrequency2( 3400 ) -// .stopRipple( 0.0003 ) -// .passRipple( 0.008) -// .build(); - -// try -// { -// RemezFIRFilterDesignerWithLagrange remez = new RemezFIRFilterDesignerWithLagrange(specification); -// taps = remez.getImpulseResponse(); -// } -// catch(Exception e) -// { -// mLog.error("Error designing filter", e); -// } - -// taps = FilterFactory.getSinc(0.1, 51, Window.WindowType.BLACKMAN_HARRIS_7); - try - { -// taps = FilterFactory.getSincChannelizer(12500, 50, 14, -// Window.WindowType.BLACKMAN_HARRIS_7, true); - taps = FilterFactory.getRemezChannelizer(12500, 4, 22, 0.4, - 0.01, 0.0001); - } - catch(Exception e) - { - mLog.error("Couldn't design filter", e); - } - - return taps; - } -} diff --git a/src/dsp/filter/fir/FIRFilterSpecification.java b/src/dsp/filter/fir/FIRFilterSpecification.java deleted file mode 100644 index 2ffc10a0..00000000 --- a/src/dsp/filter/fir/FIRFilterSpecification.java +++ /dev/null @@ -1,1129 +0,0 @@ -/******************************************************************************* - * 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.fir; - -import dsp.filter.FilterFactory; -import dsp.filter.fir.remez.FIRLinearPhaseFilterType; -import org.apache.commons.lang3.Validate; -import org.slf4j.Logger; -import org.slf4j.LoggerFactory; - -import java.text.DecimalFormat; -import java.util.ArrayList; -import java.util.Collection; -import java.util.List; - -public class FIRFilterSpecification -{ - private final static Logger mLog = LoggerFactory.getLogger(FIRFilterSpecification.class); - - private static final double PERFECT_RECONSTRUCTION_GAIN_AT_BAND_EDGE = -6.020599842071533; //0.5 magnitude - private static final DecimalFormat DECIMAL_FORMATTER = new DecimalFormat("0.00000"); - - private FIRLinearPhaseFilterType mRemezFilterType; - private int mOrder; - private int mGridDensity = 16; - private List mFrequencyBands = new ArrayList<>(); - - /** - * Constructs a Remez filter specification. Use one of the filter builders to construct a Remez - * filter specification. - * - * @param type of filter - * @param order of the filter - * @param gridDensity to use when calculating the coefficients - */ - protected FIRFilterSpecification(FIRLinearPhaseFilterType type, int order, int gridDensity) - { - mRemezFilterType = type; - mOrder = order; - mGridDensity = gridDensity; - } - - public String toString() - { - StringBuilder sb = new StringBuilder(); - sb.append("Type: "); - sb.append(mRemezFilterType.name()); - sb.append(" Order: "); - sb.append(mOrder); - sb.append(" Extrema: "); - sb.append(getExtremaCount()); - sb.append(" Grid Density: "); - sb.append(mGridDensity); - sb.append(" Grid Size: "); - sb.append(getGridSize()); - - for(FrequencyBand band : mFrequencyBands) - { - sb.append("\n"); - sb.append(band.toString()); - } - - return sb.toString(); - } - - public void addFrequencyBand(FrequencyBand band) - { - mFrequencyBands.add(band); - - updateGridSize(); - } - - public void addFrequencyBands(Collection bands) - { - mFrequencyBands.addAll(bands); - - updateGridSize(); - } - - /** - * Removes all frequency bands from this filter specification - */ - public void clearFrequencyBands() - { - mFrequencyBands.clear(); - } - - public List getFrequencyBands() - { - return mFrequencyBands; - } - - /** - * Returns the total bandwidth of the defined frequency bands in the specification. - * - * @return total bandwidth -- should be less than or equal to 0.5 - */ - public double getTotalBandwidth() - { - double bandwidth = 0.0; - - for(FrequencyBand band : mFrequencyBands) - { - bandwidth += band.getBandWidth(); - } - - return bandwidth; - } - - /** - * Creates a Low-Pass FIR filter specification builder that allows you to define the filter - * parameters and create a filter specification. - */ - public static LowPassBuilder lowPassBuilder() - { - return new LowPassBuilder(); - } - - /** - * Creates a High-Pass FIR filter specification builder that allows you to define the filter - * parameters and create a filter specification. - */ - public static HighPassBuilder highPassBuilder() - { - return new HighPassBuilder(); - } - - /** - * Creates a Band-Pass FIR filter specification builder that allows you to define the filter - * parameters and create a filter specification. - */ - public static BandPassBuilder bandPassBuilder() - { - return new BandPassBuilder(); - } - - /** - * Creates a Polyphase TunerChannelizer filter specification builder that allows you to define the filter parameters and - * create a filter specification - */ - public static ChannelizerBuilder channelizerBuilder() - { - return new ChannelizerBuilder(); - } - - public FIRLinearPhaseFilterType getFilterType() - { - return mRemezFilterType; - } - - public boolean isSymmetrical() - { - return mRemezFilterType == FIRLinearPhaseFilterType.TYPE_1_ODD_LENGTH_EVEN_ORDER_SYMMETRICAL || - mRemezFilterType == FIRLinearPhaseFilterType.TYPE_2_EVEN_LENGTH_ODD_ORDER_SYMMETRICAL; - } - - public boolean isAntiSymmetrical() - { - return mRemezFilterType == FIRLinearPhaseFilterType.TYPE_3_ODD_LENGTH_EVEN_ORDER_ANTI_SYMMETRICAL || - mRemezFilterType == FIRLinearPhaseFilterType.TYPE_4_EVEN_LENGTH_ODD_ORDER_ANTI_SYMMETRICAL; - } - - public int getOrder() - { - return mOrder; - } - - public boolean isEvenOrder() - { - return mOrder % 2 == 0; - } - - public boolean isOddOrder() - { - return mOrder % 2 == 1; - } - - public int getFilterLength() - { - return mOrder + 1; - } - - public int getBandCount() - { - return mFrequencyBands.size(); - } - - /** - * Grid density setting. Default value is 16. - */ - public int getGridDensity() - { - return mGridDensity; - } - - /** - * Sets the grid density setting. Default value is 16. - */ - public void setGridDensity(int density) - { - mGridDensity = density; - - updateGridSize(); - } - - /** - * Maximum number of alternations/extrema (L + 2 or M + 2) - */ - public int getExtremaCount() - { - return getHalfFilterOrder() + 2; - } - - /** - * Half of the filter order. - * - * This value is referred to as L or M, depending on the source. - */ - public int getHalfFilterOrder() - { - switch(mRemezFilterType) - { - case TYPE_1_ODD_LENGTH_EVEN_ORDER_SYMMETRICAL: - return (mOrder) / 2; - case TYPE_2_EVEN_LENGTH_ODD_ORDER_SYMMETRICAL: - return (mOrder - 1) / 2; - case TYPE_3_ODD_LENGTH_EVEN_ORDER_ANTI_SYMMETRICAL: - return (mOrder - 2) / 2; - case TYPE_4_EVEN_LENGTH_ODD_ORDER_ANTI_SYMMETRICAL: - return (mOrder - 1) / 2; - default: - return 0; - } - } - - /** - * Determines the max band ripple from across the frequency bands in this specification. - */ - public double getMaxBandAmplitude() - { - double maxRippleAmplitude = 0.0; - - for(FrequencyBand band : mFrequencyBands) - { - double bandRippleAmplitude = band.getRippleAmplitude(); - - if(bandRippleAmplitude > maxRippleAmplitude) - { - maxRippleAmplitude = bandRippleAmplitude; - } - } - - return maxRippleAmplitude; - } - - public int getNominalGridSize() - { - return (getExtremaCount() - 1) * getGridDensity() + 1; - } - - public int getGridSize() - { - int gridSize = 0; - - for(FrequencyBand band : mFrequencyBands) - { - gridSize += band.getGridSize(); - } - - return gridSize; - } - - private void updateGridSize() - { - if(!mFrequencyBands.isEmpty()) - { - int gridSize = getNominalGridSize(); - - double totalBandwidth = getTotalBandwidth(); - - for(FrequencyBand band : mFrequencyBands) - { - band.setGridSize(gridSize, totalBandwidth); - } - } - } - - /** - * Dense frequency spacing for extrema count * grid density over a frequency range of 0.0 - 1.0 - */ - public double getGridFrequencyInterval() - { - return getTotalBandwidth() / (double)((getGridSize() - mFrequencyBands.size())); - } - - /** - * Low-pass FIR filter specification builder - */ - public static class LowPassBuilder - { - private int mSampleRate; - private int mOrder; - private int mGridDensity = 16; - private int mPassBandEndFrequency; - private int mStopBandStartFrequency; - private double mPassBandRipple; - private double mStopBandRipple; - private double mPassBandAmplitude = 1.0; - private double mStopBandAmplitude = 0.0; - - public LowPassBuilder() - { - } - - public LowPassBuilder sampleRate(int sampleRateHz) - { - mSampleRate = sampleRateHz; - return this; - } - - /** - * Sets the filter order. If the order is not specified, it will be calculated from the - * other parameters. - */ - public LowPassBuilder order(int order) - { - mOrder = order; - return this; - } - - public LowPassBuilder gridDensity(int density) - { - mGridDensity = density; - return this; - } - - public LowPassBuilder passBandCutoff(int frequencyHz) - { - mPassBandEndFrequency = frequencyHz; - return this; - } - - public LowPassBuilder stopBandStart(int frequencyHz) - { - mStopBandStartFrequency = frequencyHz; - return this; - } - - public LowPassBuilder passBandRipple(double rippleDb) - { - mPassBandRipple = rippleDb; - return this; - } - - public LowPassBuilder stopBandRipple(double rippleDb) - { - mStopBandRipple = rippleDb; - return this; - } - - public LowPassBuilder passBandAmplitude(double amplitude) - { - mPassBandAmplitude = amplitude; - return this; - } - - public LowPassBuilder stopBandAmplitude(double amplitude) - { - mStopBandAmplitude = amplitude; - return this; - } - - public FIRFilterSpecification build() - { - if(mOrder < 6) - { - mOrder = estimateFilterOrder(mSampleRate, mPassBandEndFrequency, - mStopBandStartFrequency, mPassBandRipple, mStopBandRipple); - } - - FIRLinearPhaseFilterType type = (mOrder % 2 == 0 - ? FIRLinearPhaseFilterType.TYPE_1_ODD_LENGTH_EVEN_ORDER_SYMMETRICAL - : FIRLinearPhaseFilterType.TYPE_2_EVEN_LENGTH_ODD_ORDER_SYMMETRICAL); - - FIRFilterSpecification spec = new FIRFilterSpecification(type, mOrder, mGridDensity); - - FrequencyBand passBand = new FrequencyBand(mSampleRate, 0, mPassBandEndFrequency, - mPassBandAmplitude, mPassBandRipple); - spec.addFrequencyBand(passBand); - - FrequencyBand stopBand = new FrequencyBand(mSampleRate, mStopBandStartFrequency, - mSampleRate / 2, mStopBandAmplitude, mStopBandRipple); - spec.addFrequencyBand(stopBand); - - return spec; - } - } - - /** - * Remez high-pass FIR filter specification builder - */ - public static class HighPassBuilder - { - private int mSampleRate; - private int mOrder = -1; - private int mGridDensity = 16; - private int mPassBandStartFrequency; - private int mStopBandEndFrequency; - private double mPassBandRipple; - private double mStopBandRipple; - private double mPassBandAmplitude = 1.0; - private double mStopBandAmplitude = 0.0; - - public HighPassBuilder() - { - } - - public HighPassBuilder sampleRate(int sampleRateHz) - { - mSampleRate = sampleRateHz; - return this; - } - - /** - * Sets the filter order. If the order is not specified, it will be calculated from the - * other parameters. - */ - public HighPassBuilder order(int order) - { - mOrder = order; - return this; - } - - public HighPassBuilder gridDensity(int density) - { - mGridDensity = density; - return this; - } - - public HighPassBuilder passBandStart(int frequencyHz) - { - mPassBandStartFrequency = frequencyHz; - return this; - } - - public HighPassBuilder stopBandCutoff(int frequencyHz) - { - mStopBandEndFrequency = frequencyHz; - return this; - } - - public HighPassBuilder passBandRipple(double rippleDb) - { - mPassBandRipple = rippleDb; - return this; - } - - public HighPassBuilder stopBandRipple(double rippleDb) - { - mStopBandRipple = rippleDb; - return this; - } - - public HighPassBuilder passBandAmplitude(double amplitude) - { - mPassBandAmplitude = amplitude; - return this; - } - - public HighPassBuilder stopBandAmplitude(double amplitude) - { - mStopBandAmplitude = amplitude; - return this; - } - - public FIRFilterSpecification build() - { - if(mOrder < 6) - { - mOrder = estimateFilterOrder(mSampleRate, mStopBandEndFrequency, - mPassBandStartFrequency, mPassBandRipple, mStopBandRipple); - } - - // High pass is a Type 1 filter -- ensure order is even - if(mOrder % 2 == 1) - { - mOrder++; - } - - FIRFilterSpecification spec = new FIRFilterSpecification( - FIRLinearPhaseFilterType.TYPE_1_ODD_LENGTH_EVEN_ORDER_SYMMETRICAL, mOrder, - mGridDensity); - - FrequencyBand stopBand = new FrequencyBand(mSampleRate, 0, mStopBandEndFrequency, - mStopBandAmplitude, mStopBandRipple); - spec.addFrequencyBand(stopBand); - - FrequencyBand passBand = new FrequencyBand(mSampleRate, mPassBandStartFrequency, - mSampleRate / 2, mPassBandAmplitude, mPassBandRipple); - spec.addFrequencyBand(passBand); - - return spec; - } - } - - /** - * Band-pass or band-stop FIR filter specification builder - */ - public static class BandPassBuilder - { - private int mOrder; - private int mGridDensity = 16; - private int mSampleRate; - private int mStopFrequency1; - private int mPassFrequencyBegin; - private int mPassFrequencyEnd; - private int mStopFrequency2; - private double mStopRipple = 0.01; - private double mPassRipple = 0.01; - private double mStopAmplitude = 0.0; - private double mPassAmplitude = 1.0; - - public BandPassBuilder() - { - } - - /** - * Sets the filter order. If the order is not specified, it will be calculated from the - * other parameters. If you specify and odd-length order, it will be corrected (+1) to make - * it an even order number. - */ - public BandPassBuilder order(int order) - { - mOrder = order; - return this; - } - - /** - * Sets the frequency grid density. Default is 16. - */ - public BandPassBuilder gridDensity(int density) - { - mGridDensity = density; - return this; - } - - /** - * Sets the filter sample rate in hertz - */ - public BandPassBuilder sampleRate(int sampleRate) - { - mSampleRate = sampleRate; - return this; - } - - /** - * Specifies the ending frequency of the first stop band - */ - public BandPassBuilder stopFrequency1(int stopFrequency) - { - mStopFrequency1 = stopFrequency; - return this; - } - - /** - * Specifies the beginning frequency of the second stop band - */ - public BandPassBuilder stopFrequency2(int stopFrequency) - { - mStopFrequency2 = stopFrequency; - return this; - } - - /** - * Specifies the pass band beginning frequency - */ - public BandPassBuilder passFrequencyBegin(int passFrequency) - { - mPassFrequencyBegin = passFrequency; - return this; - } - - /** - * Specifies the pass band ending frequency - */ - public BandPassBuilder passFrequencyEnd(int passFrequency) - { - mPassFrequencyEnd = passFrequency; - return this; - } - - /** - * Specifies the ripple in dB for both stop bands - */ - public BandPassBuilder stopRipple(double rippleDb) - { - mStopRipple = rippleDb; - return this; - } - - /** - * Specifies the ripple in dB for the pass band - */ - public BandPassBuilder passRipple(double rippleDb) - { - mPassRipple = rippleDb; - return this; - } - - /** - * Specifies the amplitude of the stop bands. Default is 0.0 - */ - public BandPassBuilder stopAmplitude(double amplitude) - { - mStopAmplitude = amplitude; - return this; - } - - /** - * Specifies the amplitude of the pass band. Default is 1.0 - */ - public BandPassBuilder passAmplitude(double amplitude) - { - mPassAmplitude = amplitude; - return this; - } - - /** - * Validates the frequency bands to test for overlap and completeness. - */ - private void validateFrequencyBands() - { - if(mStopFrequency1 >= mPassFrequencyBegin) - { - throw new IllegalArgumentException("Stop band 1 ending frequency [" - + mStopFrequency1 + "] must be less than the pass band start frequency [" - + mPassFrequencyBegin + "]"); - } - - if(mPassFrequencyBegin >= mPassFrequencyEnd) - { - throw new IllegalArgumentException("Pass band begin frequency [" - + mPassFrequencyBegin + "] must be less than the pass band end frequency [" - + mPassFrequencyEnd + "]"); - } - - if(mPassFrequencyEnd >= mStopFrequency2) - { - throw new IllegalArgumentException("Pass band end frequency [" + mPassFrequencyEnd - + "] must be less than stop band 2 beginning frequency [" + mStopFrequency2 - + "]"); - } - - if(mStopFrequency2 >= (mSampleRate / 2)) - { - throw new IllegalArgumentException("Stop band 2 beginning frequency [" - + mStopFrequency2 + "] must be less than half of the sample rate [" - + (mSampleRate / 2) + "]"); - } - } - - public FIRFilterSpecification build() - { - validateFrequencyBands(); - - FIRLinearPhaseFilterType type = FIRLinearPhaseFilterType.TYPE_1_ODD_LENGTH_EVEN_ORDER_SYMMETRICAL; - - if(mOrder < 10) - { - mOrder = estimateBandPassOrder(mSampleRate, mPassFrequencyBegin, mPassFrequencyEnd, - mPassRipple, mStopRipple); - } - - // Ensure even order since we're designing a Type 1 filter - if(mOrder % 2 == 1) - { - mOrder++; - } - - FIRFilterSpecification spec = new FIRFilterSpecification(type, mOrder, mGridDensity); - - spec.addFrequencyBand(new FrequencyBand(mSampleRate, 0, mStopFrequency1, - mStopAmplitude, mStopRipple)); - spec.addFrequencyBand(new FrequencyBand(mSampleRate, mPassFrequencyBegin, - mPassFrequencyEnd, mPassAmplitude, mPassRipple)); - spec.addFrequencyBand(new FrequencyBand(mSampleRate, mStopFrequency2, - (int)(mSampleRate / 2), mStopAmplitude, mStopRipple)); - - return spec; - } - } - - /** - * Builder for creating polyphase channelizer low-pass prototype filter. - */ - public static class ChannelizerBuilder - { - private int mGridDensity = 16; - private int mSampleRate; - private int mChannelCount; - private int mChannelBandwidth; - private int mTapsPerChannel; - private double mStopRipple = 0.001; - private double mPassRipple = 0.01; - private double mAlpha = 0.2; - - public ChannelizerBuilder() - { - } - - /** - * Sets the filter sample rate in hertz - */ - public ChannelizerBuilder sampleRate(int sampleRate) - { - mSampleRate = sampleRate; - return this; - } - - /** - * Number of Channels - * @param channels - must be a multiple of 2 - * @return - */ - public ChannelizerBuilder channels(int channels) - { - Validate.isTrue(channels % 2 == 0, "Channel count must be a multiple of 2"); - mChannelCount = channels; - return this; - } - - /** - * Sets the channel bandwidth in hertz - */ - public ChannelizerBuilder channelBandwidth(int channelBandwidth) - { - mChannelBandwidth = channelBandwidth; - return this; - } - - /** - * Number of filter taps per channel. Note: total filter length will be channels * tapsPerChannel - */ - public ChannelizerBuilder tapsPerChannel(int tapsPerChannel) - { - mTapsPerChannel = tapsPerChannel; - return this; - } - - /** - * Sets the frequency grid density. Default is 16. - */ - public ChannelizerBuilder gridDensity(int density) - { - mGridDensity = density; - return this; - } - - /** - * Specifies the ripple in dB for both stop bands - */ - public ChannelizerBuilder stopRipple(double rippleDb) - { - mStopRipple = rippleDb; - return this; - } - - /** - * Specifies the ripple in dB for the pass band - */ - public ChannelizerBuilder passRipple(double rippleDb) - { - mPassRipple = rippleDb; - return this; - } - - /** - * Rolloff factor for the start band and stop band. - * @param alpha - value in range 0.0 - 1.0 - */ - public ChannelizerBuilder alpha(double alpha) - { - Validate.isTrue(0.0 <= alpha && alpha <= 1.0); - mAlpha = alpha; - - return this; - } - - public FIRFilterSpecification build() - { - FIRLinearPhaseFilterType type = FIRLinearPhaseFilterType.TYPE_2_EVEN_LENGTH_ODD_ORDER_SYMMETRICAL; - - int passFrequencyEnd = (int)(mChannelBandwidth * (1.0 - mAlpha)); - int stopFrequencyBegin = (int)(mChannelBandwidth * (1.0 + mAlpha)); - - int estimatedOrder = estimateFilterOrder(mSampleRate, passFrequencyEnd, stopFrequencyBegin, mPassRipple, - mStopRipple); - - // Ensure odd order since we're designing a Type 2 filter - if(estimatedOrder % 2 == 0) - { - estimatedOrder++; - } - - int order = mChannelCount * mTapsPerChannel - 1; - - mLog.debug("Order:" + order + " Sample Rate:" + mSampleRate + " Estimated Order:" + estimatedOrder + " Requested Order:" + order); - - FIRFilterSpecification spec = new FIRFilterSpecification(type, order, mGridDensity); - - double bandEdge = (double)mChannelBandwidth / (double)mSampleRate; - double passEnd = bandEdge * (1.0 - mAlpha); - double stopStart = bandEdge * (1.0 + mAlpha); - - mLog.debug("Pass End:" + passEnd + " Band Edge:" + bandEdge + " Stop Begin:" + stopStart); - mLog.debug("Pass End:" + (int)(mSampleRate * passEnd) + - " Band Edge:" + (int)(mSampleRate * bandEdge) + - " Stop Begin:" + (int)(mSampleRate * stopStart)); - - FrequencyBand passBand = new FrequencyBand(0.0, passEnd, 1.0, mPassRipple); - FrequencyBand edgeBand = new FrequencyBand(bandEdge, bandEdge, 0.5, mPassRipple); - edgeBand.setWeight(8.0 * edgeBand.getWeight(mPassRipple)); - FrequencyBand stopBand = new FrequencyBand(stopStart, 0.5, 0.0, mStopRipple); - - spec.addFrequencyBand(passBand); - spec.addFrequencyBand(edgeBand); - spec.addFrequencyBand(stopBand); - - mLog.debug(spec.toString()); - return spec; - } - } - - // /** - // * Converts the set of band amplitude values into a set of weights - // * normalized to the largest band amplitude value - // */ - // private static double[] getBandWeights( double... amplitudes ) - // { - // double maximum = 0.0; - // - // for ( double amplitude : amplitudes ) - // { - // if ( amplitude > maximum ) - // { - // maximum = amplitude; - // } - // } - // - // double[] weights = new double[ amplitudes.length ]; - // - // for ( int x = 0; x < amplitudes.length; x++ ) - // { - // weights[ x ] = maximum / amplitudes[ x ]; - // } - // - // return weights; - // } - - /** - * Calculates filter order from filter length - */ - private static int getFilterOrder(double length) - { - return (int)(Math.ceil(length) - 1); - } - - /** - * Estimates filter length for a two-band (high or low pass) FIR filter. - * - * Note: this estimator doesn't work well if the transition frequency is near 0 or sampleRate/2 - * - * From Herrmann et al (1973), Practical design rules for optimum finite impulse response - * filters. Bell System Technical J., 52, 769-99 - * - * @param sampleRate in hertz - * @param frequency1 in hertz - * @param frequency2 in hertz - * @param passBandRipple pass band ripple in dB - * @param stopBandRipple stop band ripple in dB - * @return estimated filter length - */ - public static int estimateFilterOrder(int sampleRate, int frequency1, int frequency2, - double passBandRipple, double stopBandRipple) - { - double df = Math.abs(frequency2 - frequency1) / (double)sampleRate; - - double ddp = Math - .log10(stopBandRipple <= passBandRipple ? passBandRipple : stopBandRipple); - double dds = Math - .log10(stopBandRipple <= passBandRipple ? stopBandRipple : passBandRipple); - - double a1 = 5.309e-3; - double a2 = 7.114e-2; - double a3 = -4.761e-1; - double a4 = -2.66e-3; - double a5 = -5.941e-1; - double a6 = -4.278e-1; - - double b1 = 11.01217; - double b2 = 0.5124401; - - double t1 = a1 * ddp * ddp; - double t2 = a2 * ddp; - double t3 = a4 * ddp * ddp; - double t4 = a5 * ddp; - - double dinf = ((t1 + t2 + a3) * dds) + (t3 + t4 + a6); - double ff = b1 + b2 * (ddp - dds); - double n = dinf / df - ff * df + 1.0; - - return (int)Math.ceil(n); - } - - /** - * Estimates band pass filter length; - * - * @param passBandStart frequency normalized to sample rate - * @param passBandEnd frequency normalized to sample rate - * @param passBandRippleDb - * @param stopBandRippleDb - */ - public static int estimateBandPassOrder(int sampleRate, int passBandStart, int passBandEnd, - double passBandRippleDb, double stopBandRippleDb) - { - double df = (double)Math.abs(passBandEnd - passBandStart) / (double)sampleRate; - double ddp = (double)Math.log10(passBandRippleDb); - double dds = (double)Math.log10(stopBandRippleDb); - - double a1 = 0.01201; - double a2 = 0.09664; - double a3 = -0.51325; - double a4 = 0.00203; - double a5 = -0.57054; - double a6 = -0.44314; - - double t1 = a1 * ddp * ddp; - double t2 = a2 * ddp; - double t3 = a4 * ddp * ddp; - double t4 = a5 * ddp; - - double cinf = dds * (t1 + t2 + a3) + t3 + t4 + a6; - double ginf = -14.6f * (double)Math.log10(passBandRippleDb / stopBandRippleDb) - 16.9; - double n = cinf / df + ginf * df + 1.0; - - return (int)Math.ceil(n); - } - - /** - * Frequency band - */ - public static class FrequencyBand - { - private int mGridSize; - private double mStart; - private double mEnd; - private double mAmplitude; - private double mRippleDB; - private Double mWeight; - - /** - * Constructs a frequency band where the start and end frequencies are normalized to a - * sample rate of 1. - * - * @param start edge of the frequency band (0.0 - 1.0) - * @param end edge of the frequency band (0.0 - 1.0) - * @param amplitude of the frequency band on the unity scale (0.0 - 1.0) - * @param ripple of the frequency band specified as db ripple - */ - public FrequencyBand(double start, double end, double amplitude, double ripple) - { - assert (0.0 <= mStart); - assert (mStart <= mEnd); - assert (mEnd <= 1.0); - assert (0.0 <= amplitude && amplitude <= 1.0); - assert (0.0 <= ripple); - - mStart = start; - mEnd = end; - mAmplitude = amplitude; - mRippleDB = ripple; - } - - /** - * Constructs a frequency band for the sample rate and start and end frequencies. - * - * @param sampleRate in hertz - * @param start edge of the frequency band in hertz - * @param end edge of the frequency band in hertz - * @param amplitude of the frequency band relative to unity (0.0 - 1.0) - * @param ripple of the frequency band specified as db ripple - */ - public FrequencyBand(int sampleRate, int start, int end, double amplitude, double ripple) - { - this((double)start / (double)sampleRate, (double)end / (double)sampleRate, - amplitude, ripple); - } - - public FrequencyBand(int sampleRate, int start, int end, double amplitude, double ripple, double weight) - { - this(sampleRate, start, end, amplitude, ripple); - mWeight = weight; - } - - /** - * Explicitly sets the weight of this frequency band. - */ - public void setWeight(double weight) - { - mWeight = weight; - } - - public int getGridSize() - { - return mGridSize; - } - - public void setGridSize(int totalGridSize, double totalBandwidth) - { - mGridSize = Math.max(1, (int)Math.ceil((double)totalGridSize * (getBandWidth() / totalBandwidth))); - } - - public String toString() - { - StringBuilder sb = new StringBuilder(); - - sb.append("Band Start: "); - sb.append(mStart); - sb.append(" End:"); - sb.append(mEnd); - sb.append(" Amplitude: "); - sb.append(mAmplitude); - sb.append(" Ripple dB: "); - sb.append(mRippleDB); - sb.append(" Ripple Amplitude: "); - sb.append(DECIMAL_FORMATTER.format(getRippleAmplitude())); - sb.append(" Grid Size: "); - sb.append(mGridSize); - sb.append(" Weight: "); - sb.append(getWeight(mRippleDB)); - - return sb.toString(); - } - - /** - * Start frequency edge for this band normalized to 1 Hz. - */ - public double getStart() - { - return mStart; - } - - /** - * End frequency edge for this band normalized to 1 Hz. - */ - public double getEnd() - { - return mEnd; - } - - /** - * Desired amplitude response for this frequency band - * - * @return amplitude (0.0 - 1.0) - */ - public double getAmplitude() - { - return mAmplitude; - } - - /** - * Frequency band ripple in decibels - */ - public double getRippleDB() - { - return mRippleDB; - } - - /** - * Frequency band ripple as an amplitude value - */ - public double getRippleAmplitude() - { - return (Math.pow(10.0, (mRippleDB / 20)) - 1) - / (Math.pow(10.0, (mRippleDB / 20)) + 1); - } - - /** - * Calculates the weighting of this frequency band's ripple relative to the max ripple for a - * filter specification. - * - * @param maxRippleAmplitude for the filter specification - * @return weight of this frequency band - */ - public double getWeight(double maxRippleAmplitude) - { - if(mWeight == null) - { - return 1.0 / (getRippleAmplitude() / maxRippleAmplitude); - } - - return mWeight; - } - - /** - * Double-sided bandwidth of this frequency band - */ - public double getBandWidth() - { - return mEnd - mStart; - } - } -} diff --git a/src/dsp/filter/fir/remez/RemezFIRFilterDesigner.java b/src/dsp/filter/fir/remez/RemezFIRFilterDesigner.java deleted file mode 100644 index dba1fd76..00000000 --- a/src/dsp/filter/fir/remez/RemezFIRFilterDesigner.java +++ /dev/null @@ -1,672 +0,0 @@ -package dsp.filter.fir.remez; - -import java.text.DecimalFormat; -import java.util.ArrayList; -import java.util.Iterator; -import java.util.List; - -import org.slf4j.Logger; -import org.slf4j.LoggerFactory; - -import dsp.filter.design.FilterDesignException; -import dsp.filter.fir.FIRFilterSpecification; - -public class RemezFIRFilterDesigner -{ - private final static Logger mLog = LoggerFactory.getLogger( RemezFIRFilterDesigner.class ); - - private static final double CONVERGENCE_THRESHOLD = 0.0001; - public static final int MAXIMUM_ITERATION_COUNT = 40; - public static final double TWO_PI = 2.0 * Math.PI; - - private static final DecimalFormat DECIMAL_FORMATTER = new DecimalFormat( "0.00000000" ); - - private FIRFilterSpecification mSpecification; - private Grid mGrid; - private List mExtremalIndices; - - private double[] mD; - private double[] mGridErrors; - private double[] mGridFrequencyResponse; - private double[] mIdealFrequencyResponse; - - private double mDelta; - - private boolean mConverged; - - /** - * Linear FIR filter designer using the Parks-McClelland Remez exchange algorithm. - * - * Construct a filter specification using the builder methods in the FIRFilterSpecification - * class and construct an instance of this class with the specification. Check the isValid() - * method after construction to see if the filter design was successful. If successful, use the - * getFrequencyResponse() and getImpulseResponse() methods to access the filter taps as the - * specified length. - * - * If the filter cannot be designed from the specification the isValid() method will return - * false and any attempts to access the getFrequencyResponse() or getImpulseResponse() methods - * will throw a FilterDesignException. - * - * @param specification that defines a Type 1-4 linear phase FIR Filter - */ - public RemezFIRFilterDesigner( FIRFilterSpecification specification ) - { - mSpecification = specification; - - design(); - } - - /** - * Designs the filter according to the specification - */ - private void design() - { - mGrid = new Grid( mSpecification ); - mExtremalIndices = getInitialExtremalIndices(); - - int iterationCount = 0; - - try - { - do - { - calculateGridFrequencyResponse(); - calculateGridError(); - findExtremalIndices(); - checkConvergence(); - - iterationCount++; - } - while( !mConverged && iterationCount < MAXIMUM_ITERATION_COUNT ); - } - catch( FilterDesignException fde ) - { - mLog.error( "Filter design error - couldn't find extremal indices at count [" + - iterationCount + "] try changing filter order up/down from [" + - mSpecification.getOrder() + "]" ); - - mConverged = false; - } - - if( mConverged ) - { - //Update frequency response using the final set of extremal indices - calculateGridFrequencyResponse(); - } - } - - /** - * Indicates if the filter was successfully created from the filter specification. Check this - * method before accessing frequency response or filter response, otherwise a FilterDesignException - * is thrown. - * - * @return true if the filter was successfully designed. - */ - public boolean isValid() - { - return mConverged; - } - - /** - * Frequency response of the designed filter with double precision - * - * @return half ( length / 2 ) of filter frequency response sampled at 0.0 - PI radians - * @throws FilterDesignException if the specified filter cannot be designed - */ - public double[] getFrequencyResponse() throws FilterDesignException - { - if( !mConverged ) - { - throw new FilterDesignException( "Can't create filter from specification - failed " - + "to converge" ); - } - - //Resample the final polynomial at the desired filter length - double[] resampled = resample(); - - //Apply Parks/McClelland frequency response corrections according to the filter type - return correctFrequencyResponse( resampled ); - } - - /** - * Impulse response of the designed filter with floating point precision. - * - * @return filter impulse response - * @throws FilterDesignException if the specified filter cannot be designed - */ - public float[] getImpulseResponse() throws FilterDesignException - { - return convertToFloatArray( getImpulseResponseDoubles() ); - } - - /** - * Impulse response of the designed filter with double precision - */ - public double[] getImpulseResponseDoubles() throws FilterDesignException - { - double[] frequencyResponse = getFrequencyResponse(); - - int length = mSpecification.getFilterLength(); - - double[] impulseResponse = new double[ length ]; - - double M; - - switch( mSpecification.getFilterType() ) - { - case TYPE_1_ODD_LENGTH_EVEN_ORDER_SYMMETRICAL: - M = ( (double)length - 1.0 ) / 2.0; - - for( int n = 0; n < length; n++ ) - { - double accumulator = frequencyResponse[ 0 ]; - - double frequency = TWO_PI * ( n - M ) / length; - - for( int k = 1; k <= M; k++ ) - { - accumulator += 2.0 * frequencyResponse[ k ] * Math.cos( frequency * (double)k ); - } - - impulseResponse[ n ] = accumulator / (double)length; - } - break; - case TYPE_2_EVEN_LENGTH_ODD_ORDER_SYMMETRICAL: - double offset = (double)( length - 1 ) / 2.0; - - for( int n = 0; n < length; n++ ) - { - double accumulator = frequencyResponse[ 0 ]; - - double frequency = TWO_PI * ( (double)n - offset ) / (double)length; - - for( int k = 1; k < frequencyResponse.length; k++ ) - { - accumulator += 2.0 * frequencyResponse[ k ] * Math.cos( frequency * (double)k ); - } - - impulseResponse[ n ] = accumulator / (double)length; - } - break; - } - - return impulseResponse; - } - - /** - * Calculates the frequency response of this filter at the specified cosine of the frequency - * - * Implements Oppenheim/Schafer Discrete Time Signal Processing, 3e, 2016, equation 116a to - * determine the actual response of the specified frequency cosine using Lagrange - * interpolation of the polynomial formed at the extremal index set. - * - * @param cosineOfFrequency to calculate the frequency response - * - * @return actual frequency response for the specified frequency with the current set of - * extreaml indices - */ - private double getFrequencyResponse( double cosineOfFrequency ) - { - double numerator = 0.0; - double denominator = 0.0; - - for( int k = 0; k < mExtremalIndices.size() - 1; k++ ) - { - double cosineDelta = cosineOfFrequency - mGrid.getCosineFrequencyGrid()[ mExtremalIndices.get( k ) ]; - - //If this frequency is close to one of the polynomial points, use the polynomial point for the response - if( Math.abs( cosineDelta ) < 1.0e-7 ) - { - return mIdealFrequencyResponse[ k ]; - } - else - { - double dkOverCosineDelta = mD[ k ] / cosineDelta; - - numerator += dkOverCosineDelta * mIdealFrequencyResponse[ k ]; - denominator += dkOverCosineDelta; - } - } - - return numerator / denominator; - } - - /** - * Calculated Delta value - * - * @return delta value - */ - public double getDelta() - { - return mDelta; - } - - /** - * Creates initial array of extremal frequency indices spaced at intervals of the grid frequency - * set for all frequencies where a frequency band weighting is specified. - */ - private List getInitialExtremalIndices() - { - int count = mSpecification.getExtremaCount(); - - int density = mSpecification.getGridDensity(); - - List extremalIndices = new ArrayList<>(); - - for( int i = 0; i < count; i++ ) - { - extremalIndices.add( i * density ); - } - - return extremalIndices; - } - - /** - * Calculates the actual frequency response of a dense frequency grid using the polynomial - * represented by the extremal indices. - */ - private void calculateGridFrequencyResponse() - { - double[] b = calculateB(); - - calculateDelta( b ); - - calculateC(); - - calculateD( b ); - - updateGridFrequencyResponse(); - } - - /** - * Updates the frequency response of frequencies specified in the dense frequency grid - */ - private void updateGridFrequencyResponse() - { - mGridFrequencyResponse = new double[ mGrid.getSize() ]; - - double[] gridFrequencyCosines = mGrid.getCosineFrequencyGrid(); - - for( int i = 0; i < mGridFrequencyResponse.length; i++ ) - { - mGridFrequencyResponse[ i ] = getFrequencyResponse( gridFrequencyCosines[ i ] ); - } - } - - /** - * Calculates the value of b across the set of extremal indices. - * - * Implements Oppenheim/Schafer Discrete Time Signal Processing, 3e, 2016, equation 115 - * - * @return the array of b values of length L+2 - */ - private double[] calculateB() - { - int length = mExtremalIndices.size(); - - double[] b = new double[ length ]; - - for( int k = 0; k < length; k++ ) - { - b[ k ] = 1.0; - - double xk = mGrid.getCosineFrequencyGrid()[ mExtremalIndices.get( k ) ]; - - for( int i = 0; i < length; i++ ) - { - if( i != k ) - { - double xi = mGrid.getCosineFrequencyGrid()[ mExtremalIndices.get( i ) ]; - - double denominator = xk - xi; - - if( Math.abs( denominator ) < 0.00001 ) - { - denominator = 0.00001; - } - - b[ k ] *= 1.0 / denominator; - } - } - } - - return b; - } - - /** - * Calculates the value of delta that represents the maximum ripple for the current set of - * extremal indices (L+2) - * - * Implements Oppenheim/Schafer Discrete Time Signal Processing, 3e, 2016, equation 114 - * - * @param b calculated for the grid from equation 115 - */ - private void calculateDelta( double[] b ) - { - double numerator = 0.0; - double denominator = 0.0; - - double sign = 1.0; - - for( int k = 0; k < b.length; k++ ) - { - if( k < mExtremalIndices.size() ) - { - int extremalIndex = mExtremalIndices.get( k ); - - numerator += ( b[ k ] * mGrid.getDesiredResponse()[ extremalIndex ] ); - denominator += b[ k ] * sign / mGrid.getWeight()[ extremalIndex ]; - sign = -sign; - } - else - { - mLog.error( "Something went wrong -- the length of b exceeds the set of extremal indices" ); - } - } - - mDelta = numerator / denominator; - } - - /** - * Calculates the ideal frequency response (C) of the current extremal index set. This reference - * is used to determine the level of error in the set of actual frequency responses calculated - * from this set of extremal indices to determine convergence. - * - * Implements Oppenheim/Schafer Discrete Time Signal Processing, 3e, 2016, equation 116b - */ - private void calculateC() - { - int length = mSpecification.getExtremaCount() - 1; - - mIdealFrequencyResponse = new double[ length ]; - - double sign = 1.0; - - for( int k = 0; k < length; k++ ) - { - if( k < mExtremalIndices.size() ) - { - int index = mExtremalIndices.get( k ); - - mIdealFrequencyResponse[ k ] = mGrid.getDesiredResponse()[ index ] - - ( sign * mDelta / mGrid.getWeight()[ index ] ); - - sign = -sign; - } - } - } - - /** - * Calculates the set of D values for the current extremal index set. - * - * Implements Oppenheim/Schafer Discrete Time Signal Processing, 3e, 2016, equation 116c - */ - private void calculateD( double[] b ) - { - int length = mExtremalIndices.size() - 1; - - mD = new double[ length ]; - - for( int k = 0; k < length; k++ ) - { - //Note: extremalCosines array is one index longer than d array, so we use length as - //a pointer to the final (L+2) extremalCosine index - mD[k] = b[k] * ( mGrid.getCosineFrequencyGrid()[ mExtremalIndices.get( k ) ] - - mGrid.getCosineFrequencyGrid()[ mExtremalIndices.get( length ) ] ); - } - } - - /** - * Calculates the weighted error between the specification desired frequency response and the - * actual frequency response of the current extremal index set across the frequencies in the - * dense frequency grid. - * - * Implements Oppenheim/Schafer Discrete Time Signal Processing, 3e, 2016, equation 112 - */ - public void calculateGridError() - { - int length = mGridFrequencyResponse.length; - - mGridErrors = new double[ length ]; - - for( int i = 0; i < length; i++ ) - { - mGridErrors[ i ] = mGrid.getWeight()[ i ] * - ( mGrid.getDesiredResponse()[ i ] - mGridFrequencyResponse[ i ] ); - } - } - - /** - * Identifies the indexes of the extremal error values using the Alternation theorem - * - * @throws FilterDesignException if the full set of extremal indices can't be identified - */ - private void findExtremalIndices() throws FilterDesignException - { - mExtremalIndices.clear(); - - //Check for extremum at error index 0 - if( ( ( mGridErrors[ 0 ] > 0.0 && mGridErrors[ 0 ] > mGridErrors[ 1 ] ) || - ( mGridErrors[ 0 ] < 0.0 && mGridErrors[ 0 ] < mGridErrors[ 1 ] ) ) && - isGTEDelta( mGridErrors[ 0 ] ) ) - { - mExtremalIndices.add( 0 ); - } - - //Check for extrema in middle error indices ) - for( int x = 1; x < mGridErrors.length - 1; x++ ) - { - if( ( ( mGridErrors[ x ] > 0.0 && ( mGridErrors[ x - 1 ] <= mGridErrors[ x ] && mGridErrors[ x ] > mGridErrors[ x + 1 ] ) ) || - ( mGridErrors[ x ] < 0.0 && ( mGridErrors[ x - 1 ] >= mGridErrors[ x ] && mGridErrors[ x ] < mGridErrors[ x + 1 ] ) ) ) && - isGTEDelta( mGridErrors[ x ] ) ) - { - mExtremalIndices.add( x ); - } - } - - //Check for extremum at final error index - int last = mGridErrors.length - 1; - - if( ( ( mGridErrors[ last ] > 0.0 && ( mGridErrors[ last ] > mGridErrors[ last - 1 ] ) ) || - ( mGridErrors[ last ] < 0.0 && ( mGridErrors[ last ] < mGridErrors[ last - 1 ] ) ) ) && - isGTEDelta( mGridErrors[ last ] ) ) - { - mExtremalIndices.add( last ); - } - - //Ensure we have the minimum of extremals before we continue - if( mExtremalIndices.size() < mSpecification.getExtremaCount() ) - { - throw new FilterDesignException( "Couldn't find the minimum extremal frequencies in " - + "error set" ); - } - - //Enforce alternation theory -- only one extremal for each transition about the zero axis - List indicesToRemove = new ArrayList<>(); - - Iterator it = mExtremalIndices.iterator(); - Integer current = it.next(); - Integer next; - - boolean positiveAxis = mGridErrors[ current ] > 0.0; - - while( it.hasNext() ) - { - next = it.next(); - - //Check for consecutive (redundant) indices on same side of zero axis - retain largest - if( !( positiveAxis ^ ( mGridErrors[ next ] > 0.0 ) ) ) - { - if( Math.abs( mGridErrors[ next ] ) <= Math.abs( mGridErrors[ current ] )) - { - //Remove next error index that is less than current index and on - it.remove(); - next = current; - } - else - { - //Since we can't remove the current index with the iterator, queue for later removal - indicesToRemove.add( current ); - } - } - else - { - positiveAxis = !positiveAxis; - } - - current = next; - } - - //Remove redundant extremal indices that couldn't be removed via the iterator - mExtremalIndices.removeAll( indicesToRemove ); - - //Truncate the list to one larger than needed by removing excess tailing indices - while( mExtremalIndices.size() > mSpecification.getExtremaCount() ) - { - mExtremalIndices.remove( mExtremalIndices.size() - 1 ); - } - - //If we have one too many indices, delete the smaller of the first or last - if( mExtremalIndices.size() > mSpecification.getExtremaCount() ) - { - int lastIndex = mExtremalIndices.size() - 1; - - if( Math.abs( mGridErrors[ mExtremalIndices.get( 0 )] ) > - Math.abs( mGridErrors[ mExtremalIndices.get( lastIndex )] ) ) - { - mExtremalIndices.remove( lastIndex ); - } - else - { - mExtremalIndices.remove( 0 ); - } - } - - //Detect if we have too few indices - if( mExtremalIndices.size() < mSpecification.getExtremaCount() ) - { - throw new FilterDesignException( "Couldn't find the minimum extremal frequencies in " - + "error set" ); - } - } - - /** - * Indicates if the absolute value of the argument is greater than or equal to the delta - * value with accuracy to 14 digits of precision. This avoids rounding errors at 15 digits or - * precision or greater. - * - * @param value to evaluate - * - * @return true if the absolute value is greater than or equal to the absolute delta value - */ - private boolean isGTEDelta( double value ) - { - return Math.abs( value ) - Math.abs( mDelta ) > -1.0e-5; - } - - /** - * Checks for convergence of the frequency response of the current set of extremal indices to - * the filter specification by comparing the maximum absolute error value against the delta - * value to determine if these two values are within the convergence threshold. - */ - public void checkConvergence() - { - double maximum = Math.abs( mGridErrors[ mExtremalIndices.get( 0 ) ] ); - - for( int i = 1; i < mExtremalIndices.size(); i++ ) - { - double current = Math.abs( mGridErrors[ mExtremalIndices.get( i ) ] ); - - if( current > maximum ) - { - maximum = current; - } - } - - double convergence = maximum - Math.abs( mDelta ); - - mConverged = convergence < CONVERGENCE_THRESHOLD; - } - - /** - * Coverts/casts the double array to a float array - */ - private static float[] convertToFloatArray( double[] samples ) - { - float[] converted = new float[ samples.length ]; - - for( int x = 0; x < samples.length; x++ ) - { - converted[ x ] = (float)samples[ x ]; - } - - return converted; - } - - /** - * Resamples the designed filter at half of the filter length at evenly spaced intervals of the - * frequency spectrum from 0 to PI radians. - * - * @return frequency response of the filter sampled the desired filter length and order - */ - private double[] resample() - { - int length = mSpecification.getFilterLength(); - - if( length % 2 == 0 ) - { - length--; - } - - double half = (double)length / 2.0; - - double[] resampled = new double[ (int)Math.ceil( half ) ] ; - - for( int x = 0; x < resampled.length; x++ ) - { - resampled[ x ] = getFrequencyResponse( Math.cos( Math.PI * (double)x / half ) ); - } - - return resampled; - } - - /** - * Applies Parks/McClelland filter type correction to the final frequency response array - * - * @param frequencyResponse of the resampled filter design from 0 <> ~ PI radians - * - * @return corrected frequency response - */ - private double[] correctFrequencyResponse( double[] frequencyResponse ) - { - double filterLength = mSpecification.getFilterLength(); - - switch( mSpecification.getFilterType() ) - { - case TYPE_1_ODD_LENGTH_EVEN_ORDER_SYMMETRICAL: - //No correction needed - break; - case TYPE_2_EVEN_LENGTH_ODD_ORDER_SYMMETRICAL: - //No correction needed - break; - case TYPE_3_ODD_LENGTH_EVEN_ORDER_ANTI_SYMMETRICAL: - for( int x = 0; x < frequencyResponse.length; x++ ) - { - double frequencyRadians = Math.PI * ( ( (double)x ) / filterLength ); - frequencyResponse[ x ] *= Math.sin( TWO_PI * frequencyRadians ); - } - break; - case TYPE_4_EVEN_LENGTH_ODD_ORDER_ANTI_SYMMETRICAL: - for( int x = 0; x < frequencyResponse.length; x++ ) - { - double frequencyRadians = Math.PI * ( ( (double)x ) / filterLength ); - frequencyResponse[ x ] *= Math.sin( Math.PI * frequencyRadians ); - } - break; - default: - break; - } - - return frequencyResponse; - } -} diff --git a/src/dsp/gain/AutomaticGainControl.java b/src/dsp/gain/AutomaticGainControl.java deleted file mode 100644 index b46dd105..00000000 --- a/src/dsp/gain/AutomaticGainControl.java +++ /dev/null @@ -1,224 +0,0 @@ -package dsp.gain; - -import java.util.concurrent.atomic.AtomicBoolean; - -import buffer.DoubleCircularBuffer; -import buffer.FloatCircularBuffer; - -/******************************************************************************* - * SDR Trunk - * Copyright (C) 2014 Dennis Sheirer - * Copyright (C) 2011 Alex Csete - * Copyright (C) 2010 Moe Wheatley - * - * 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 - * - * --------------------------------------------------- - * Ported from Alex Csete's gqrx source code at: - * https://github.com/csete/gqrx/blob/74ba2742c89b0dfb66854fc9f72d19e5c6e355b6/dsp/agc_impl.cpp - * - * This Software is released under the "Simplified BSD License" + + + - * Copyright 2010 Moe Wheatley. All rights reserved. - * - * Redistribution and use in source and binary forms, with or without - * modification, are permitted provided that the following conditions are - * met: - * - * 1. Redistributions of source code must retain the above copyright notice, - * this list of conditions and the following disclaimer. - * - * 2. Redistributions in binary form must reproduce the above copyright - * notice, this list of conditions and the following disclaimer in the - * documentation and/or other materials provided with the distribution. - * - * THIS SOFTWARE IS PROVIDED BY Moe Wheatley ``AS IS'' AND ANY EXPRESS OR - * IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE IMPLIED WARRANTIES - * OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE DISCLAIMED. - * IN NO EVENT SHALL Moe Wheatley OR CONTRIBUTORS BE LIABLE FOR ANY DIRECT, - * INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES - * (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR - * SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) - * HOWEVER CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, - * STRICT LIABILITY, OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN - * ANY WAY OUT OF THE USE OF THIS SOFTWARE, EVEN IF ADVISED OF THE - * POSSIBILITY OF SUCH DAMAGE. - * - * The views and conclusions contained in the software and documentation are - * those of the authors and should not be interpreted as representing - * official policies, either expressed or implied, of Moe Wheatley. - ******************************************************************************/ -public class AutomaticGainControl -{ - private static final double SAMPLE_RATE = 48000; - - /* Signal delay line - time delay in seconds */ -// private static final double DELAY_TIME_CONSTANT = 0.015; - private static final double DELAY_TIME_CONSTANT = 0.01; - - /* Peak detector window - time delay in seconds */ - private static final double WINDOW_TIME_CONSTANT = 0.018; - - /* Attack time constants in seconds */ - private static final double ATTACK_RISE_TIME_CONSTANT = 0.002; - private static final double ATTACK_FALL_TIME_CONSTANT = 0.005; - - private static final double ATTACK_RISE_ALPHA = 1.0 - - Math.exp( -1.0 / SAMPLE_RATE * ATTACK_RISE_TIME_CONSTANT ); - - private static final double ATTACK_FALL_ALPHA = 1.0 - - Math.exp( -1.0 / SAMPLE_RATE * ATTACK_FALL_TIME_CONSTANT ); - - /* AGC decay value in milliseconds (20 to 5000) */ - private static final double DECAY = 200; - - /* Ratio between rise and fall times of decay time constants - adjust for - * best action with SSB */ - private static final double DECAY_RISEFALL_RATIO = 0.3; - - private static final double DECAY_RISE_ALPHA = 1.0 - - Math.exp( -1.0 / ( SAMPLE_RATE * DECAY * .001 * DECAY_RISEFALL_RATIO ) ); - - private static final double DECAY_FALL_ALPHA = 1.0 - - Math.exp( -1.0 / ( SAMPLE_RATE * DECAY * .001 ) ); - - /* Hang timer release decay time constant in seconds */ - @SuppressWarnings( "unused" ) - private static final double RELEASE_TIME_CONSTANT = 0.05; - - /* Specifies the AGC Knee in dB if AGC is active (nominal range -160 to 0 dB) */ - private static final double THRESHOLD = -100; - - /* Limit output to about 3db of maximum */ - private static final double AGC_OUT_SCALE = 0.7; - - /* Keep max input and output the same */ - private static final double MAX_AMPLITUDE = 32767.0; //1.0; - private static final double MAX_MANUAL_AMPLITUDE = 32767.0; //1.0; - - /* Specifies AGC manual gain in dB if AGC is not active ( 0 to 100 dB) */ - private static final double MANUAL_GAIN = 0.0; - - private static final double MANUAL_AGC_GAIN = MAX_MANUAL_AMPLITUDE * - Math.pow( 10.0, MANUAL_GAIN / 20.0 ); - - /* Specifies dB reduction in output at knee from max output level (0 - 10dB) */ - private static final double SLOPE_FACTOR = 2.0; - - private static final double KNEE = THRESHOLD / 20.0; - - private static final double GAIN_SLOPE = SLOPE_FACTOR / 100.0; - - private static final double FIXED_GAIN = AGC_OUT_SCALE * - Math.pow( 10.0, KNEE * ( GAIN_SLOPE - 1.0 ) ); - - /* Constant for calc log() so that a value of 0 magnitude = -8 */ - private static final double MIN_CONSTANT = 3.2767E-4; - - private AtomicBoolean mAGCEnabled = new AtomicBoolean( true ); - - private double mPeakMagnitude = 0.0; - private double mAttackAverage = 0.0; - private double mDecayAverage = 0.0; - - private FloatCircularBuffer mDelayBuffer = - new FloatCircularBuffer( (int)( SAMPLE_RATE * DELAY_TIME_CONSTANT ) ); - private DoubleCircularBuffer mMagnitudeBuffer = - new DoubleCircularBuffer( (int)( SAMPLE_RATE * WINDOW_TIME_CONSTANT ) ); - - public AutomaticGainControl() - { - } - - public float process( float currentSample ) - { - float delayedSample = mDelayBuffer.putAndGet( currentSample ); - - double gain = MANUAL_AGC_GAIN; - - if( mAGCEnabled.get() ) - { - double currentMagnitude = Math.log10( Math.abs( currentSample ) + - MIN_CONSTANT ) - Math.log10( MAX_AMPLITUDE ); - - double delayedMagnitude = mMagnitudeBuffer.get( currentMagnitude ); - - if( currentMagnitude > mPeakMagnitude ) - { - /* Use current magnitude as peak if it's larger */ - mPeakMagnitude = currentMagnitude; - } - else if( delayedMagnitude == mPeakMagnitude ) - { - /* If delayed magnitude is the current peak, then find a new peak */ - mPeakMagnitude = mMagnitudeBuffer.max(); - } - - /* Exponential decay mode */ - if( mPeakMagnitude > mAttackAverage ) - { - mAttackAverage = ( ( 1.0 - ATTACK_RISE_ALPHA ) * mAttackAverage ) + - ( ATTACK_RISE_ALPHA * mPeakMagnitude ); - } - else - { - mAttackAverage = ( ( 1.0 - ATTACK_FALL_ALPHA ) * mAttackAverage ) + - ( ATTACK_FALL_ALPHA * mPeakMagnitude ); - } - - if( mPeakMagnitude > mDecayAverage ) - { - mDecayAverage = ( ( 1.0 - DECAY_RISE_ALPHA ) * mDecayAverage ) + - ( DECAY_RISE_ALPHA * mPeakMagnitude ); - } - else - { - mDecayAverage = ( ( 1.0 - DECAY_FALL_ALPHA ) * mDecayAverage ) + - ( DECAY_RISE_ALPHA * mPeakMagnitude ); - } - - double magnitude = ( mAttackAverage > mDecayAverage ) ? - mAttackAverage : mDecayAverage; - - if( magnitude < KNEE ) - { - gain = FIXED_GAIN; - } - else - { - gain = AGC_OUT_SCALE * Math.pow( 10.0, - magnitude * ( GAIN_SLOPE - 1.0 ) ); - } - - } - - return (float)( delayedSample * gain ); - } - - /** - * Enables or disables Automatic Gain Control (AGC). - */ - public void setAGCEnabled( boolean enabled ) - { - mAGCEnabled.set( enabled ); - } - - /** - * Indicates if AGC is enabled - * @return true=AGC, false=MANUAL GAIN - */ - public boolean isAGCEnabled() - { - return mAGCEnabled.get(); - } -} diff --git a/src/dsp/gain/ComplexFeedForwardGainControl.java b/src/dsp/gain/ComplexFeedForwardGainControl.java deleted file mode 100644 index 20449301..00000000 --- a/src/dsp/gain/ComplexFeedForwardGainControl.java +++ /dev/null @@ -1,112 +0,0 @@ -package dsp.gain; - -import sample.complex.Complex; -import sample.complex.ComplexSampleListener; -import buffer.FloatCircularBuffer; - -/******************************************************************************* - * 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 - * - * This class is modeled using concepts detailed in the GNURadio - * implementation of feed forward AGC class located at: - * https://github.com/gnuradio/gnuradio/blob/master/gr-analog/lib/ - * feedforward_agc_cc_impl.cc - * - ******************************************************************************/ -public class ComplexFeedForwardGainControl implements ComplexSampleListener -{ - public static final float OBJECTIVE_ENVELOPE = 1.0f; - public static final float MINIMUM_ENVELOPE = 0.0001f; - - private ComplexSampleListener mListener; - - private FloatCircularBuffer mEnvelopeHistory; - - private float mMaxEnvelope = 0.0f; - private float mGain = 1.0f; - - /** - * Dynamic gain control for incoming sample stream to amplify or attenuate - * all samples toward an objective unity)gain, using the maximum envelope - * value detected in the stream history window. - * - * Uses the specified damping factor to limit gain swings. Damping factor - * is applied against the delta between current gain value and a recalculated - * gain value to limit how quickly the gain value will increase or decrease. - * - * @param window - history size to use in detecting maximum envelope value - * @param damping - a value between 0 < damping <= 1.0; - */ - public ComplexFeedForwardGainControl( int window ) - { - mEnvelopeHistory = new FloatCircularBuffer( window ); - } - - public void dispose() - { - mListener = null; - } - - @Override - public void receive( float inphase, float quadrature ) - { - float envelope = Complex.envelope( inphase, quadrature ); - - if( envelope > mMaxEnvelope ) - { - mMaxEnvelope = envelope; - - adjustGain(); - } - - /* Replace oldest envelope value with current envelope value */ - float oldestEnvelope = mEnvelopeHistory.putAndGet( envelope ); - - /* If the oldest envelope value was the max envelope value, then we - * have to rediscover the max value from the envelope history */ - if( mMaxEnvelope == oldestEnvelope && mMaxEnvelope != envelope ) - { - mMaxEnvelope = MINIMUM_ENVELOPE; - - for( float value: mEnvelopeHistory.getBuffer() ) - { - if( value > mMaxEnvelope ) - { - mMaxEnvelope = value; - } - } - - adjustGain(); - } - - /* Apply current gain value to the sample and send to the listener */ - if( mListener != null ) - { - mListener.receive( inphase *= mGain, quadrature *= mGain ); - } - } - - private void adjustGain() - { - mGain = OBJECTIVE_ENVELOPE / mMaxEnvelope; - } - - public void setListener( ComplexSampleListener listener ) - { - mListener = listener; - } -} diff --git a/src/dsp/mixer/Oscillator.java b/src/dsp/mixer/Oscillator.java deleted file mode 100644 index b711c4ab..00000000 --- a/src/dsp/mixer/Oscillator.java +++ /dev/null @@ -1,152 +0,0 @@ -/******************************************************************************* - * 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 sample.complex.Complex; -import sample.complex.ComplexBuffer; - -public class Oscillator -{ - 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 - * - * @param frequency - positive or negative frequency in hertz - * @param sampleRate - in hertz - */ - public Oscillator(long 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(); - } - - /** - * Steps the current angle by the angle per sample amount - */ - public void rotate() - { - mCurrentAngle.multiply(mAnglePerSample); - mCurrentAngle.fastNormalize(); - } - - public float inphase() - { - return mCurrentAngle.inphase(); - } - - public float quadrature() - { - return mCurrentAngle.quadrature(); - } - - /** - * Get next complex sample - */ - public Complex getComplex() - { - 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); - } -} diff --git a/src/dsp/psk/LSMDemodulator.java b/src/dsp/psk/LSMDemodulator.java deleted file mode 100644 index 11c5de0d..00000000 --- a/src/dsp/psk/LSMDemodulator.java +++ /dev/null @@ -1,589 +0,0 @@ -package dsp.psk; - -/******************************************************************************* - * SDR Trunk - * Copyright (C) 2014, 2015 Dennis Sheirer - * - * Gardner detector, Costas loop and Interpolator derived from: - * - * OP25 - gardner_costas_cc_impl.cc - * Copyright 2010, 2011, 2012, 2013 KA1RBI (gardner_costas_cc_impl.cc) - * - * OP25 - scope.py - * Copyright 2008-2011 Steve Glass - * Copyright 2011, 2012, 2013 KA1RBI - * - * GNURadio - control_loop and mpsk_receiver_cc - * Copyright 2011,2013 Free Software Foundation, Inc. - * - * 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 - ******************************************************************************/ - -import instrument.Instrumentable; -import instrument.tap.Tap; -import instrument.tap.TapGroup; -import instrument.tap.stream.EyeDiagramData; -import instrument.tap.stream.EyeDiagramDataTap; - -import java.util.ArrayList; -import java.util.Arrays; -import java.util.List; - -import sample.Listener; -import sample.complex.Complex; -import sample.complex.ComplexSampleListener; -import source.ISourceEventListener; -import buffer.FloatAveragingBuffer; -import dsp.filter.interpolator.RealInterpolator; -import dsp.symbol.Dibit; - -/** - * Implements a LSM (Pi/4) demodulator using a Gardner Detector to determine - * optimal sampling timing and a Costas Loop as a phase locked loop synchronized - * with the incoming signal carrier frequency. - * - * Sample Rate: 48000 - * Symbol Rate: 4800 - */ -public class LSMDemodulator implements Instrumentable, ComplexSampleListener -{ - private List mAvailableTaps; - - /* 45 degree rotation angle */ - public static final float THETA = (float)( Math.PI / 4.0d ); - - /* 45 degree point */ - public static final Complex POINT_45_DEGREES = - new Complex( (float)Math.sin( THETA ), (float)Math.cos( THETA ) ); - - private Listener mSymbolListener; - - private GardnerSymbolTiming mGardnerDetector = new GardnerSymbolTiming(); - - private CostasLoop mCostasLoop = new CostasLoop(); - - private ISourceEventListener mFrequencyChangeListener; - - private EyeDiagramDataTap mEyeDiagramDataTap; - - public LSMDemodulator() - { - } - - public void dispose() - { - mSymbolListener = null; - mFrequencyChangeListener = null; - } - - public void setSymbolListener( Listener listener ) - { - mSymbolListener = listener; - } - - public void removeSymbolListener() - { - mSymbolListener = null; - } - - @Override - public void receive( float inphase, float quadrature ) - { - mGardnerDetector.receive( inphase, quadrature ); - } - - /** - * Applies a phase correction value to the costas loop to correct when a - * phase lock error is detected in the binary output stream. - * - * @param correction - value in radians - */ - public void correctPhaseError( double correction ) - { - mCostasLoop.correctPhaseError( correction ); - } - - /** - * Constrains value to the range of ( -maximum <> maximum ) - */ - public static float clip( float value, float maximum ) - { - if( value > maximum ) - { - return maximum; - } - else if( value < -maximum ) - { - return -maximum; - } - - return value; - } - - /** - * Constrains timing error to +/- the maximum value and corrects any - * floating point invalid numbers - */ - private float normalize( float error, float maximum ) - { - if( Float.isNaN( error ) ) - { - return 0.0f; - } - else - { - return clip( error, maximum ); - } - } - - public void addListener( ISourceEventListener listener ) - { - mFrequencyChangeListener = listener; - } - - public void removeListener( ISourceEventListener listener ) - { - mFrequencyChangeListener = null; - } - - /** - * Processes the current loop frequency of the costas loop and broadcasts - * tuner frequency offset adjustments 10 times a second as needed to keep - * the frequency offset below half of the maximum frequency - */ - public class FrequencyControl - { - /* Set the trigger threshold at half of the maximum costas loop control - * frequency */ - private static final float CORRECTION_THRESHOLD_FREQUENCY = - ( 2.0f * (float)Math.PI * 600.0f ) / 48000.0f; - - private FloatAveragingBuffer mBuffer = new FloatAveragingBuffer( 40 ); - - private long mFrequencyError = 0; - - private int mCounter = 0; - - public void receive( float frequency ) - { -// mCounter++; -// -// if( mCounter >= 480 ) -// { -// float average = mBuffer.get( frequency ); -// mLog.debug( "Avg:" + average + " Threshold:" + CORRECTION_THRESHOLD_FREQUENCY); -// -// boolean correctionNeeded = false; -// -// if( average > CORRECTION_THRESHOLD_FREQUENCY ) -// { -// mFrequencyError--; -// -// correctionNeeded = true; -// } -// else if( average < -CORRECTION_THRESHOLD_FREQUENCY ) -// { -// mFrequencyError++; -// -// correctionNeeded = true; -// } -// -// if( correctionNeeded && mFrequencyChangeListener != null ) -// { -// mLog.debug( "Issuing Frequency Correction: " + mFrequencyError ); -// mFrequencyChangeListener.process( -// new SourceEvent( Attribute.FREQUENCY_ERROR, -// mFrequencyError ) ); -// } -// -// mCounter = 0; -// } -// else -// { -// mBuffer.get( frequency ); -// } - } - } - - /** - * Gardner symbol timing detector - */ - public class GardnerSymbolTiming implements ComplexSampleListener - { - public static final int HALF_SAMPLES_PER_SYMBOL = 5; - public static final int SAMPLES_PER_SYMBOL = 10; - public static final int TWICE_SAMPLES_PER_SYMBOL = 20; - - private float[] mDelayLineInphase = new float[ 2 * TWICE_SAMPLES_PER_SYMBOL ]; - private float[] mDelayLineQuadrature = new float[ 2 * TWICE_SAMPLES_PER_SYMBOL ]; - - private int mDelayLinePointer = 0; - - /* Sampling point */ - private float mMu = 10.0f; - private float mGainMu = 0.05f; - - /* Samples per symbol */ - private float mOmega = 10.0f; - private float mGainOmega = 0.1f * mGainMu * mGainMu; - private float mOmegaRel = 0.005f; - private float mOmegaMid = 10.0f; - - private RealInterpolator mInterpolator = new RealInterpolator( 1.0f ); - - private Complex mPreviousSample = new Complex( 0, 0 ); - private Complex mPreviousMiddleSample = new Complex( 0, 0 ); - private Complex mPreviousSymbol = new Complex( 0, 0 ); - - /** - * Provides symbol sampling timing control - */ - public GardnerSymbolTiming() - { - } - - @Override - public void receive( float inphase, float quadrature ) - { - /* Count down samples per symbol until we calculate the symbol */ - mMu--; - - mCostasLoop.increment(); - - /* Mix incoming sample with costas loop to remove any rotation - * that is present from a mis-tuned carrier frequency */ - - Complex costas = mCostasLoop.getCurrentVector(); - - float derotatedInphase = Complex.multiplyInphase( inphase, quadrature, costas.inphase(), costas.quadrature() ); - float derotatedQuadrature = Complex.multiplyQuadrature( inphase, quadrature, costas.inphase(), costas.quadrature() ); - - /* Fill up the delay line to use with the interpolator */ - mDelayLineInphase[ mDelayLinePointer ] = derotatedInphase; - mDelayLineInphase[ mDelayLinePointer + TWICE_SAMPLES_PER_SYMBOL ] = derotatedInphase; - - mDelayLineQuadrature[ mDelayLinePointer ] = derotatedQuadrature; - mDelayLineQuadrature[ mDelayLinePointer + TWICE_SAMPLES_PER_SYMBOL ] = derotatedQuadrature; - - /* Increment pointer and keep pointer in bounds */ - mDelayLinePointer = ( mDelayLinePointer + 1 ) % TWICE_SAMPLES_PER_SYMBOL; - - /* Calculate the symbol once we've stored enough samples */ - if( mMu <= 1.0f ) - { - float half_omega = mOmega / 2.0f; - int half_sps = (int)Math.floor( half_omega ); - float half_mu = mMu + half_omega - (float)half_sps; - - if( half_mu > 1.0 ) - { - half_mu -= 1.0; - half_sps += 1; - } - - /* Calculate interpolated middle sample and current sample */ - float middleSampleInphase = mInterpolator.filter( - mDelayLineInphase, mDelayLinePointer, mMu ); - float middleSampleQuadrature = mInterpolator.filter( - mDelayLineQuadrature, mDelayLinePointer, mMu ); - - int index = mDelayLinePointer + half_sps; - - float currentSampleInphase = mInterpolator.filter( - mDelayLineInphase, index, half_mu ); - float currentSampleQuadrature = mInterpolator.filter( - mDelayLineQuadrature, index, half_mu ); - - /* Multiply current sample and conjugate (negated quadrature) of - * previous sample to get symbols to use for gardner error feedback */ - Complex middleSymbol = Complex.multiply( middleSampleInphase, - middleSampleQuadrature, mPreviousMiddleSample.conjugate() ); - Complex currentSymbol = Complex.multiply( currentSampleInphase, - currentSampleQuadrature, mPreviousSample.conjugate() ); - - /* Set gain to unity */ - middleSymbol.normalize(); - currentSymbol.normalize(); - - /* Gardner timing error calculations */ - float errorInphase = ( mPreviousSymbol.inphase() - - currentSymbol.inphase() ) * middleSymbol.inphase(); - - float errorQuadrature = ( mPreviousSymbol.quadrature() - - currentSymbol.quadrature() ) * middleSymbol.quadrature(); - - float gardnerError = normalize( errorInphase + errorQuadrature, 1.0f ); - - if( mEyeDiagramDataTap != null ) - { - mEyeDiagramDataTap.receive( - new EyeDiagramData( - Arrays.copyOfRange( mDelayLineInphase, 0, 20 ), - Arrays.copyOfRange( mDelayLineQuadrature, 0, 20 ), - mMu, (float)half_sps + half_mu, gardnerError ) ); - } - - /* mOmega is samples per symbol and is constrained to floating - * between +/- .005 of the nominal 10.0 samples per symbol */ - mOmega = mOmega + mGainOmega * gardnerError; - mOmega = mOmegaMid + clip( mOmega - mOmegaMid, mOmegaRel ); - - /* Adjust sample timing based on error of current sample */ - mMu += mOmega + ( mGainMu * gardnerError ); - - /* Store current samples/symbols to use for the next period */ - mPreviousSample.setValues( currentSampleInphase, currentSampleQuadrature ); - mPreviousMiddleSample.setValues( middleSampleInphase, middleSampleQuadrature ); - mPreviousSymbol = currentSymbol; - - /* Update costas loop using phase error present in current - * symbol. The symbol is rotated from star orientation to polar - * orientation to simplify error calculation */ - mCostasLoop.receive( currentSymbol ); -// mCostasLoop.receive( -// ComplexSample.multiply( currentSymbol, POINT_45_DEGREES ) ); - - /* Dispatch the differentiated symbol to the registered listener */ - if( mSymbolListener != null ) - { - mSymbolListener.receive( currentSymbol ); - } - } - } - } - - /** - * Costas Loop - phase locked loop synchronized to the frequency offset of - * the incoming signal to enable the signal to be mixed down to zero offset - * for proper synchronization. The mFrequency value indicates the detected - * frequency offset. We attempt to keep that value close to zero by issuing - * frequency adjustments to the tuner channel source. - * - * Most of the costas loop code was ported from gnuradio/control_loop and - * gnuradio/mpsk_receiver_cc using initialization values from KA1RBI's - * OP25/cqpsk.py - */ - public class CostasLoop implements Listener - { - public static final double TWO_PI = 2.0 * Math.PI; - - private static final float MAXIMUM_FREQUENCY = - ( 2400.0f * (float)TWO_PI ) / 48000.0f; - - /* http://www.trondeau.com/blog/2011/8/13/control-loop-gain-values.html */ - private float mDamping = (float)Math.sqrt( 2.0 ) / 2.0f; - - /* Use denominator between 100 (most) and 400 (least) to adjust costas - * loop control level */ - private float mLoopBandwidth = (float)( TWO_PI / 400.0d ); - - private float mAlphaGain = ( 4.0f * mDamping * mLoopBandwidth ) / - ( 1.0f + ( 2.0f * mDamping * mLoopBandwidth ) + - ( mLoopBandwidth * mLoopBandwidth ) ); - - private float mBetaGain = ( 4.0f * mLoopBandwidth * mLoopBandwidth ) / - ( 1.0f + ( 2.0f * mDamping * mLoopBandwidth ) + - ( mLoopBandwidth * mLoopBandwidth ) ); - - private float mLoopPhase = 0.0f; - - private float mLoopFrequency = 0.0f; - - private FrequencyControl mFrequencyControl = new FrequencyControl(); - - public CostasLoop() - { - } - - /** - * Applies a phase correction value to the current phasor. Use this - * method to apply correction when a +/- 90, or 180 degree phase error - * is detected in the binary output stream. - * - * If the supplied correction value places the loop frequency outside - * of the max frequency, then the frequency will be corrected 360 - * degrees in the opposite direction to maintain within the max - * frequency bounds. - * - * @param correction - correction value in radians - */ - public void correctPhaseError( double correction ) - { - mLoopFrequency += correction; - - if( mLoopFrequency > MAXIMUM_FREQUENCY ) - { - mLoopFrequency -= 2.0d * MAXIMUM_FREQUENCY; - } - - if( mLoopFrequency < -MAXIMUM_FREQUENCY ) - { - mLoopFrequency += 2.0d * MAXIMUM_FREQUENCY; - } - } - - /** - * Increments the phase of the loop - */ - public void increment() - { - mLoopPhase += mLoopFrequency; - - /* Keep the loop phase in bounds */ - unwrapPhase(); - } - - private void unwrapPhase() - { - while( mLoopPhase > TWO_PI ) - { - mLoopPhase -= TWO_PI; - } - - while( mLoopPhase < -TWO_PI ) - { - mLoopPhase += TWO_PI; - } - } - - /** - * Current vector of the loop rotated by THETA degrees. - */ - public Complex getCurrentVector() - { -// return ComplexSample.fromAngle( mLoopPhase + THETA ); - return Complex.fromAngle( mLoopPhase ); - } - - @Override - public void receive( Complex complex ) - { - /* Calculate phase error */ - float phaseError = getPhaseError( complex ); - - Dibit d = QPSKPolarSlicer.decide( complex ); -// mLog.debug( "Dibit: " + d.name() + " Error: " + phaseError ); - - /* Adjust for phase error */ - adjust( phaseError ); - } - - /** - * Updates the costas loop frequency and phase to adjust for the phase - * error value - * - * @param phase_error - (-)= late and (+)= early - */ - private void adjust( float phase_error ) - { - mLoopFrequency += mBetaGain * phase_error; - mLoopPhase += mLoopFrequency + mAlphaGain * phase_error; - - /* Maintain phase between +/- 2 * PI */ - unwrapPhase(); - - /* Limit frequency to +/- maximum loop frequency */ - limitFrequency(); - } - - /** - * Constrains the frequency within the bounds of +/- loop frequency - */ - private void limitFrequency() - { - /* Check for and issue tuner offset correction */ - mFrequencyControl.receive( mLoopFrequency ); - - if( mLoopFrequency > MAXIMUM_FREQUENCY ) - { - mLoopFrequency = MAXIMUM_FREQUENCY; - } - - if( mLoopFrequency < -MAXIMUM_FREQUENCY ) - { - mLoopFrequency = -MAXIMUM_FREQUENCY; - } - } - - /** - * Detects rotational error present when the costas loop is not aligned - * with the carrier frequency. Provides error feedback to adjust - * mixer frequency. - */ - public float getPhaseError( Complex complex ) - { - float phase_error = 0; - - if( Math.abs( complex.inphase() ) > Math.abs( complex.quadrature() ) ) - { - if( complex.inphase() > 0 ) - { - phase_error = -complex.quadrature(); - } - else - { - phase_error = complex.quadrature(); - } - } - else - { - if( complex.quadrature() > 0 ) - { - phase_error = complex.inphase(); - } - else - { - phase_error = -complex.inphase(); - } - } - - return phase_error; - } - } - - @Override - public List getTapGroups() - { - if( mAvailableTaps == null ) - { - mAvailableTaps = new ArrayList<>(); - - TapGroup group = new TapGroup( "LSM Demodulator" ); - - group.add( new EyeDiagramDataTap( "Eye Diagram", 0, 4800 ) ); - - mAvailableTaps.add( group ); - } - - return mAvailableTaps; - } - - @Override - public void registerTap( Tap tap ) - { - if( tap instanceof EyeDiagramDataTap ) - { - mEyeDiagramDataTap = (EyeDiagramDataTap)tap; - } - } - - @Override - public void unregisterTap( Tap tap ) - { - if( tap instanceof EyeDiagramDataTap ) - { - mEyeDiagramDataTap = null; - } - } -} diff --git a/src/gui/control/JFrequencyControl.java b/src/gui/control/JFrequencyControl.java deleted file mode 100644 index aed75ab0..00000000 --- a/src/gui/control/JFrequencyControl.java +++ /dev/null @@ -1,506 +0,0 @@ -/******************************************************************************* - * 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 gui.control; - -import net.miginfocom.swing.MigLayout; -import org.slf4j.Logger; -import org.slf4j.LoggerFactory; -import source.ISourceEventProcessor; -import source.InvalidFrequencyException; -import source.SourceEvent; -import source.SourceEvent.Event; -import source.SourceException; - -import javax.swing.*; -import java.awt.*; -import java.awt.event.KeyEvent; -import java.awt.event.KeyListener; -import java.awt.event.MouseEvent; -import java.awt.event.MouseListener; -import java.awt.event.MouseWheelEvent; -import java.awt.event.MouseWheelListener; -import java.text.ParseException; -import java.util.ArrayList; -import java.util.HashMap; -import java.util.Iterator; -import java.util.List; - -public class JFrequencyControl extends JPanel implements ISourceEventProcessor -{ - private static final long serialVersionUID = 1L; - private final static Logger mLog = LoggerFactory.getLogger(JFrequencyControl.class); - - private List mProcessors = new ArrayList<>(); - - private Color mHighlightColor = Color.YELLOW; - - private long mFrequency; - - private Cursor mBlankCursor; - - private HashMap mDigits = new HashMap(); - - public JFrequencyControl() - { - init(); - } - - public JFrequencyControl(long value) - { - this(); - mFrequency = value; - } - - private void init() - { - setLayout(new MigLayout("insets 0 0 0 0", "[]0[]", "")); - - Font font = new Font(Font.MONOSPACED, Font.BOLD, 30); - - for(int x = 9; x >= 0; x--) - { - Digit digit = null; - - try - { - digit = new Digit(x); - } - catch(ParseException e) - { - mLog.error("JFrequencyControl - parse exception " - + "constructing a digit - " + e); - } - - if(digit != null) - { - mDigits.put(x, digit); - - add(digit); - - digit.setFont(font); - - if(x == 6) - { - JLabel period = new JLabel("."); - - add(period); - } - } - } - - add(new JLabel(" MHz")); - - /** - * Create a blank cursor to use when the mouse is over the digits - */ - //Create an empty byte array - byte[] imageByte = new byte[0]; - - //Create image for cursor using empty array - Image cursorImage = Toolkit.getDefaultToolkit().createImage(imageByte); - - mBlankCursor = Toolkit.getDefaultToolkit() - .createCustomCursor(cursorImage, new Point(0, 0), "cursor"); - - revalidate(); - } - - @Override - public void setEnabled(boolean enabled) - { - super.setEnabled(enabled); - - for(Digit digit : mDigits.values()) - { - digit.setEnabled(enabled); - } - } - - /** - * Receives a frequency change event invoked by another control. We don't - * rebroadcast this event, just set the control to indicate the new frequency. - */ - @Override - public void process(SourceEvent event) - { - if(event.getEvent() == Event.NOTIFICATION_FREQUENCY_CHANGE) - { - setFrequency(event.getValue().longValue(), false); - } - } - - /** - * Loads the frequency into the display and optionally fires a change event - * to all registered listeners - */ - public void setFrequency(long frequency, boolean fireChangeEvent) - { - mFrequency = frequency; - - for(Digit digit : mDigits.values()) - { - digit.setFrequency(frequency, fireChangeEvent); - } - } - - public long getFrequency() - { - return mFrequency; - } - - private void updateFrequency() - { - long frequency = 0; - - for(Digit digit : mDigits.values()) - { - frequency += digit.getFrequency(); - } - - mFrequency = frequency; - } - - private void fireSourceChanged() throws SourceException - { - updateFrequency(); - - Iterator it = mProcessors.iterator(); - - SourceEvent event = SourceEvent.frequencyRequest(mFrequency); - - while(it.hasNext()) - { - it.next().process(event); - } - } - - public void addListener(ISourceEventProcessor processor) - { - mProcessors.add(processor); - } - - public void removeListener(ISourceEventProcessor processor) - { - mProcessors.remove(processor); - } - - public class Digit extends JTextField - { - private static final long serialVersionUID = 1L; - private int mPower = 0; - private long mValue = 0; - - private Digit(int position) throws ParseException - { - super("0"); - - mPower = position; - - Listener listener = new Listener(); - - this.addKeyListener(listener); - this.addMouseListener(listener); - this.addMouseWheelListener(listener); - } - - /** - * Sets this digit to the value of the column in frequency that corresponds - * to the power (ie column) set for this digit. Optionally, fires a - * value change event to all listeners. - */ - public void setFrequency(long frequency, boolean fireChangeEvent) - { - //Strip the digits higher than this one - long lower = frequency % (long)(Math.pow(10, mPower + 1)); - - //Set the value to int value of dividing by 10 to this power - long value = (long)(lower / (long)(Math.pow(10, mPower))); - - set(value, fireChangeEvent); - } - - public long getFrequency() - { - return mValue * (long)Math.pow(10, mPower); - } - - public void increment() - { - increment(true); - } - - public void increment(boolean fireChangeEvent) - { - if(isEnabled()) - { - set(mValue + 1, fireChangeEvent); - } - } - - public void decrement() - { - decrement(true); - } - - public void decrement(boolean fireChangeEvent) - { - if(isEnabled()) - { - set(mValue - 1, fireChangeEvent); - } - } - - /** - * Convenience wrapper to change amount and fire change event - */ - private void set(long amount) - { - set(amount, true); - } - - /** - * Changes the value and optionally fires change event to listeners - */ - private void set(long amount, boolean fireChangeEvent) - { - long previous = mValue; - boolean higherDigitIncremented = false; - boolean higherDigitDecremented = false; - - mValue = amount; - - - while(mValue < 0) - { - mValue += 10; - - Digit nextHigherDigit = mDigits.get(mPower + 1); - - if(nextHigherDigit != null) - { - nextHigherDigit.decrement(false); - higherDigitDecremented = true; - } - - } - - while(mValue > 9) - { - mValue -= 10; - - Digit nextHigherDigit = mDigits.get(mPower + 1); - - if(nextHigherDigit != null) - { - nextHigherDigit.increment(false); - higherDigitIncremented = true; - } - } - - if(fireChangeEvent) - { - try - { - fireSourceChanged(); - } - catch(SourceException se) - { - mValue = previous; - - if(higherDigitIncremented) - { - Digit nextHigherDigit = mDigits.get(mPower + 1); - - if(nextHigherDigit != null) - { - nextHigherDigit.decrement(false); - } - } - - if(higherDigitDecremented) - { - Digit nextHigherDigit = mDigits.get(mPower + 1); - - if(nextHigherDigit != null) - { - nextHigherDigit.increment(false); - } - } - - if(se instanceof InvalidFrequencyException) - { - InvalidFrequencyException ife = (InvalidFrequencyException)se; - - JOptionPane.showMessageDialog(this, "Frequency [" + ife.getInvalidFrequency() + - "] exceeds the frequency limit [" + ife.getValidFrequency() + "] for this tuner."); - } - } - } - - - setText(String.valueOf(mValue)); - - repaint(); - } - - private class Listener implements KeyListener, - MouseListener, - MouseWheelListener - { - @Override - public void keyReleased(KeyEvent e) - { - int key = e.getKeyCode(); - - switch(key) - { - case KeyEvent.VK_0: - case KeyEvent.VK_NUMPAD0: - set(0); - Digit.this.transferFocus(); - break; - case KeyEvent.VK_1: - case KeyEvent.VK_NUMPAD1: - set(1); - Digit.this.transferFocus(); - break; - case KeyEvent.VK_2: - case KeyEvent.VK_NUMPAD2: - set(2); - Digit.this.transferFocus(); - break; - case KeyEvent.VK_3: - case KeyEvent.VK_NUMPAD3: - set(3); - Digit.this.transferFocus(); - break; - case KeyEvent.VK_4: - case KeyEvent.VK_NUMPAD4: - set(4); - Digit.this.transferFocus(); - break; - case KeyEvent.VK_5: - case KeyEvent.VK_NUMPAD5: - set(5); - Digit.this.transferFocus(); - break; - case KeyEvent.VK_6: - case KeyEvent.VK_NUMPAD6: - set(6); - Digit.this.transferFocus(); - break; - case KeyEvent.VK_7: - case KeyEvent.VK_NUMPAD7: - set(7); - Digit.this.transferFocus(); - break; - case KeyEvent.VK_8: - case KeyEvent.VK_NUMPAD8: - set(8); - Digit.this.transferFocus(); - break; - case KeyEvent.VK_9: - case KeyEvent.VK_NUMPAD9: - set(9); - Digit.this.transferFocus(); - break; - case KeyEvent.VK_LEFT: - Digit.this.transferFocusBackward(); - break; - case KeyEvent.VK_RIGHT: - Digit.this.transferFocus(); - break; - case KeyEvent.VK_UP: - increment(); - break; - case KeyEvent.VK_DOWN: - decrement(); - break; - case KeyEvent.VK_TAB: - break; - default: - set(mValue); - break; - } - - repaint(); - } - - @Override - public void keyPressed(KeyEvent e) - { - } - - @Override - public void keyTyped(KeyEvent e) - { - } - - @Override - public void mouseClicked(MouseEvent e) - { - switch(e.getButton()) - { - case MouseEvent.BUTTON1: - increment(); - break; - case MouseEvent.BUTTON2: - break; - case MouseEvent.BUTTON3: - decrement(); - break; - } - } - - public void mousePressed(MouseEvent e) - { - } - - public void mouseReleased(MouseEvent e) - { - } - - @Override - public void mouseEntered(MouseEvent e) - { - Digit.this.setBackground(mHighlightColor); - - setCursor(mBlankCursor); - - repaint(); - } - - @Override - public void mouseExited(MouseEvent e) - { - Digit.this.setBackground(Color.WHITE); - - setCursor(Cursor.getDefaultCursor()); - - repaint(); - } - - @Override - public void mouseWheelMoved(MouseWheelEvent e) - { - set(mValue - e.getWheelRotation()); - } - } - } -} diff --git a/src/instrument/tap/stream/BinaryTapViewPanel.java b/src/instrument/tap/stream/BinaryTapViewPanel.java deleted file mode 100644 index 03ae5fdf..00000000 --- a/src/instrument/tap/stream/BinaryTapViewPanel.java +++ /dev/null @@ -1,156 +0,0 @@ -/******************************************************************************* - * SDR Trunk - * Copyright (C) 2014 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 instrument.tap.stream; - -import instrument.gui.SampleModel; -import instrument.tap.Tap; -import instrument.tap.TapViewPanel; - -import java.awt.Color; -import java.awt.FontMetrics; -import java.awt.Graphics; -import java.awt.Graphics2D; -import java.awt.geom.Path2D; -import java.awt.geom.Rectangle2D; -import java.util.List; -import java.util.Observable; - -public class BinaryTapViewPanel extends TapViewPanel -{ - private static final long serialVersionUID = 1L; - - private Tap mTap; - private List mSamples; - private int mSampleCount; - - public BinaryTapViewPanel( BinaryTap tap ) - { - super( new SampleModel(), tap.getName() ); - - mTap = tap; - mTap.addListener( getModel() ); - - getModel().setSampleCount( - (int)( getModel().getSampleCount() * tap.getSampleRateRatio() ) ); - - getModel().setDelay( tap.getDelay() ); - } - - @Override - public void update( Observable arg0, Object arg1 ) - { - mSamples = (List)getModel().getSamples(); - mSampleCount = getModel().getSampleCount(); - repaint(); - } - - public void paintComponent( Graphics g ) - { - super.paintComponent( g ); - - Graphics2D g2 = (Graphics2D)g; - - int height = getHeight(); - int width = getWidth(); - - g2.setColor( Color.DARK_GRAY ); - - float middle = (float)height / 2.0f; - - g2.drawLine( 0, (int)middle, width, (int)middle ); - - if( mSamples != null && mSamples.size() > 0 ) - { - g2.setColor( getForeground() ); - - g2.drawString( mLabel, 5 , 15 ); - - for( int x = 1; x < mSamples.size(); x++ ) - { - paintBit( g, x, mSamples.get( x ) ); - } - } - - g2.dispose(); - } - - /** - * Paints a polyline representing a boolean value with a 0 or 1 label in - * the middle of it - * - * @param graphics object - * @param index - index of the sample int the sample buffer - * @param value - bit value, 0 or 1 - */ - private void paintBit( Graphics graphics, int index, boolean value ) - { - Graphics2D g2 = (Graphics2D)graphics; - - Path2D.Float polyline = - new Path2D.Float( Path2D.Float.WIND_EVEN_ODD, mSamples.size() ); - - float middleY = ( (float)getHeight() / 2.0f ); - - float indexWidth = (float)( getWidth() ) / (float)mSampleCount; - - float startX = (float)index * indexWidth; - float stopX = (float)(index + 1 ) * indexWidth; - float fivePercent = indexWidth * .05f; - float leftX = startX + fivePercent; - float rightX = stopX - fivePercent; - - /* Start */ - polyline.moveTo( startX, middleY ); - - /* Left horizonal segment */ - polyline.lineTo( leftX, middleY ); - - float y = ( value ? ( middleY - ( middleY * 0.5f) ) : - ( middleY + ( middleY * 0.5f) ) ); - - /* Left vertical segment */ - polyline.lineTo( leftX, y ); - - /* Middle horizontal segment */ - polyline.lineTo( rightX, y ); - - /* Right vertical segment */ - polyline.lineTo( rightX, middleY ); - - /* Right horizontal segment */ - polyline.lineTo( stopX, middleY ); - - g2.draw( polyline ); - - /* Draw the bit value label */ - String bit = value ? "1" : "0"; - - FontMetrics fontMetrics = graphics.getFontMetrics( this.getFont() ); - - Rectangle2D label = fontMetrics.getStringBounds( bit, graphics ); - - float offsetX = (float)label.getWidth() / 2.0f; - float offsetY = (float)label.getWidth() / 2.0f; - - float middleX = startX + ( indexWidth / 2.0f ); - - graphics.drawString( bit, - (int)( middleX - offsetX ), - (int)( middleY - offsetY ) ); - } -} diff --git a/src/main/java/io/github/dsheirer/audio/AudioModule.java b/src/main/java/io/github/dsheirer/audio/AudioModule.java index 369dd8ed..81f3451c 100644 --- a/src/main/java/io/github/dsheirer/audio/AudioModule.java +++ b/src/main/java/io/github/dsheirer/audio/AudioModule.java @@ -32,8 +32,6 @@ import io.github.dsheirer.sample.real.RealBuffer; import org.slf4j.Logger; import org.slf4j.LoggerFactory; -import java.util.concurrent.ScheduledExecutorService; - /** * Provides packaging of demodulated audio sample buffers into audio packets for * broadcast to registered audio packet listeners. Includes audio packet @@ -117,7 +115,7 @@ public class AudioModule extends Module implements IAudioPacketProvider, IFilter } @Override - public void start(ScheduledExecutorService executor) + public void start() { /* No start operations provided */ } diff --git a/src/main/java/io/github/dsheirer/audio/broadcast/StreamManager.java b/src/main/java/io/github/dsheirer/audio/broadcast/StreamManager.java index ce98e155..c7c7b7ad 100644 --- a/src/main/java/io/github/dsheirer/audio/broadcast/StreamManager.java +++ b/src/main/java/io/github/dsheirer/audio/broadcast/StreamManager.java @@ -140,7 +140,7 @@ public class StreamManager implements Listener { AudioRecorder recorder = BroadcastFactory.getAudioRecorder(getTemporaryRecordingPath(), mBroadcastFormat); - recorder.start(ThreadPool.SCHEDULED); + recorder.start(); recorder.receive(audioPacket); mStreamRecorders.put(channelMetadataID, recorder); } diff --git a/src/main/java/io/github/dsheirer/buffer/FloatCircularBuffer.java b/src/main/java/io/github/dsheirer/buffer/FloatCircularBuffer.java index ce3d0bd3..855a7e2a 100644 --- a/src/main/java/io/github/dsheirer/buffer/FloatCircularBuffer.java +++ b/src/main/java/io/github/dsheirer/buffer/FloatCircularBuffer.java @@ -1,118 +1,124 @@ /******************************************************************************* - * SDR Trunk - * Copyright (C) 2014 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 + * 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 io.github.dsheirer.buffer; -import java.util.Arrays; - /** * Circular sample buffer - allocates a buffer and stores samples in a circular - * fashion, overwriting older samples with newly arrived samples. Initially - * fills buffer with 0-valued samples - * - * Can be used as a delay-type buffer, to delay samples by the 'size' amount - * @author denny + * fashion, overwriting older samples with newly arrived samples. * + * Can be used as a delay-type buffer, to delay samples by the 'size' amount */ public class FloatCircularBuffer { - float[] mBuffer; - int mBufferPointer = 0; - - public FloatCircularBuffer( int size ) - { - mBuffer = new float[ size ]; - - Arrays.fill( mBuffer, (float)0.0 ); - } - - public int getSize() - { - return mBuffer.length; - } - - public float[] getBuffer() - { - return mBuffer; - } + float[] mBuffer; + int mBufferPointer = 0; - /** - * Puts the new value into the buffer and returns the oldest buffer value - * that it replaced - * @param newValue - * @return - */ - public float get( float newValue ) - { - float oldestSample = mBuffer[ mBufferPointer ]; + public FloatCircularBuffer(int size) + { + mBuffer = new float[size]; + } - mBuffer[ mBufferPointer ] = newValue; - - mBufferPointer++; - - if( mBufferPointer >= mBuffer.length ) - { - mBufferPointer = 0; - } - - return oldestSample; - } + public int getSize() + { + return mBuffer.length; + } - /** - * Convenience method to put a new value in the buffer, while discarding - * the old value - */ - public void put( float newValue ) - { - get( newValue ); - } + public float[] getBuffer() + { + return mBuffer; + } - /** - * Returns an array of arraySize, loaded with sampleCount of the - * oldest samples, loaded into the array starting at index 0. - * - * This is useful for fetching an array filled half way with samples to - * use in FFT processing. - * @param arraySize - size of the array (must be larger than sampleCount) - * @param sampleCount - number of the oldest samples in the buffer to load - * @return - */ - public float[] get( int arraySize, int sampleCount ) - { - float[] samples = new float[ arraySize ]; + /** + * Puts the new value into the buffer and returns the oldest buffer value + * that it replaced + * + * @param newValue + * @return + */ + public float putAndGet(float newValue) + { + float oldestSample = mBuffer[mBufferPointer]; - if( arraySize > sampleCount ) - { - int tempCounter = mBufferPointer; + put(newValue); - for( int x = 0; x < sampleCount; x++ ) - { - samples[ x ] = mBuffer[ tempCounter ]; - - tempCounter++; - - if( tempCounter > mBuffer.length ) - { - tempCounter = 0; - } - } - } - - return samples; - } - + return oldestSample; + } + + /** + * Adds the new value to this buffer. + */ + public void put(float newValue) + { + mBuffer[mBufferPointer] = newValue; + + mBufferPointer++; + + if(mBufferPointer >= mBuffer.length) + { + mBufferPointer = 0; + } + } + + /** + * Returns the sample at the specified index, where index 0 is the oldest sample and index (length - 1) is the + * newest sample. + * @param index in range of 0 to length-1 + * @return value at indexed position + */ + + public float get(int index) + { + if(index < mBuffer.length) + { + int pointer = mBufferPointer + index; + + if(pointer >= mBuffer.length) + { + pointer -= mBuffer.length; + } + + return mBuffer[pointer]; + } + else + { + throw new ArrayIndexOutOfBoundsException("Index [" + index + "] is not valid for the buffer size of [" + + mBuffer.length + "]"); + } + } + + /** + * Returns the contents of the circular buffer unwrapped where index 0 is the oldest sample and index length-1 + * is the newest sample. + * + * @return array of samples + */ + public float[] get() + { + float[] samples = new float[mBuffer.length]; + + System.arraycopy(mBuffer, mBufferPointer, samples, 0, mBuffer.length - mBufferPointer); + + if(mBufferPointer != 0) + { + System.arraycopy(mBuffer, 0, samples, mBuffer.length - mBufferPointer, mBufferPointer); + } + + return samples; + } } diff --git a/src/main/java/io/github/dsheirer/channel/heartbeat/Heartbeat.java b/src/main/java/io/github/dsheirer/channel/heartbeat/Heartbeat.java deleted file mode 100644 index 72946df0..00000000 --- a/src/main/java/io/github/dsheirer/channel/heartbeat/Heartbeat.java +++ /dev/null @@ -1,31 +0,0 @@ -/******************************************************************************* - * 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 io.github.dsheirer.channel.heartbeat; - -public class Heartbeat -{ - /** - * Periodic pulse that is broadcast to subscribing modules within the demodulating chain so that - * monitoring of state and other attributes can occur on the primary demodulation and decoding - * thread. - */ - public Heartbeat() - { - } -} diff --git a/src/main/java/io/github/dsheirer/channel/heartbeat/IHeartbeatProvider.java b/src/main/java/io/github/dsheirer/channel/heartbeat/IHeartbeatProvider.java deleted file mode 100644 index 8d2ab3c3..00000000 --- a/src/main/java/io/github/dsheirer/channel/heartbeat/IHeartbeatProvider.java +++ /dev/null @@ -1,28 +0,0 @@ -/******************************************************************************* - * 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 io.github.dsheirer.channel.heartbeat; - -import io.github.dsheirer.sample.Listener; - -public interface IHeartbeatProvider -{ - public void setHeartbeatListener(Listener listener); - - public void removeHeartbeatListener(); -} diff --git a/src/main/java/io/github/dsheirer/channel/state/AlwaysUnsquelchedDecoderState.java b/src/main/java/io/github/dsheirer/channel/state/AlwaysUnsquelchedDecoderState.java index bd2432fb..1d3795e8 100644 --- a/src/main/java/io/github/dsheirer/channel/state/AlwaysUnsquelchedDecoderState.java +++ b/src/main/java/io/github/dsheirer/channel/state/AlwaysUnsquelchedDecoderState.java @@ -25,8 +25,6 @@ import io.github.dsheirer.module.decode.DecoderType; import org.slf4j.Logger; import org.slf4j.LoggerFactory; -import java.util.concurrent.ScheduledExecutorService; - /** * Basic decoder channel state - provides the minimum channel state functionality * to support an always un-squelched audio decoder. @@ -94,7 +92,7 @@ public class AlwaysUnsquelchedDecoderState extends DecoderState } @Override - public void start(ScheduledExecutorService executor) + public void start() { broadcast(new AttributeChangeRequest(Attribute.PRIMARY_ADDRESS_TO, NO_SQUELCH)); broadcast(new DecoderStateEvent(this, Event.ALWAYS_UNSQUELCH, State.IDLE)); diff --git a/src/main/java/io/github/dsheirer/channel/state/ChannelState.java b/src/main/java/io/github/dsheirer/channel/state/ChannelState.java index ae83ec65..2ba741e4 100644 --- a/src/main/java/io/github/dsheirer/channel/state/ChannelState.java +++ b/src/main/java/io/github/dsheirer/channel/state/ChannelState.java @@ -20,8 +20,6 @@ package io.github.dsheirer.channel.state; import io.github.dsheirer.audio.squelch.ISquelchStateProvider; import io.github.dsheirer.audio.squelch.SquelchState; -import io.github.dsheirer.channel.heartbeat.Heartbeat; -import io.github.dsheirer.channel.heartbeat.IHeartbeatListener; import io.github.dsheirer.channel.metadata.Attribute; import io.github.dsheirer.channel.metadata.AttributeChangeRequest; import io.github.dsheirer.channel.metadata.IAttributeChangeRequestListener; @@ -36,17 +34,17 @@ import io.github.dsheirer.module.decode.event.CallEvent; import io.github.dsheirer.module.decode.event.ICallEventProvider; import io.github.dsheirer.sample.Listener; import io.github.dsheirer.sample.real.IOverflowListener; -import io.github.dsheirer.source.tuner.frequency.FrequencyChangeEvent; -import io.github.dsheirer.source.tuner.frequency.IFrequencyChangeListener; +import io.github.dsheirer.source.ISourceEventListener; +import io.github.dsheirer.source.SourceEvent; +import io.github.dsheirer.source.heartbeat.Heartbeat; +import io.github.dsheirer.source.heartbeat.IHeartbeatListener; import org.slf4j.Logger; import org.slf4j.LoggerFactory; -import java.util.concurrent.ScheduledExecutorService; - -import static io.github.dsheirer.source.tuner.frequency.FrequencyChangeEvent.Event.NOTIFICATION_FREQUENCY_CHANGE; +import static io.github.dsheirer.source.SourceEvent.Event.NOTIFICATION_FREQUENCY_CHANGE; public class ChannelState extends Module implements ICallEventProvider, IDecoderStateEventListener, - IDecoderStateEventProvider, IFrequencyChangeListener, IHeartbeatListener, ISquelchStateProvider, + IDecoderStateEventProvider, ISourceEventListener, IHeartbeatListener, ISquelchStateProvider, IAttributeChangeRequestListener, IOverflowListener { private final static Logger mLog = LoggerFactory.getLogger(ChannelState.class); @@ -64,7 +62,7 @@ public class ChannelState extends Module implements ICallEventProvider, IDecoder private ChannelType mChannelType; private TrafficChannelManager mTrafficChannelEndListener; private CallEvent mTrafficChannelCallEvent; - private FrequencyChangeListener mFrequencyChangeListener; + private SourceChangeListener mSourceChangeListener; private boolean mSquelchLocked = false; private boolean mSelected = false; @@ -124,7 +122,7 @@ public class ChannelState extends Module implements ICallEventProvider, IDecoder } @Override - public void start(ScheduledExecutorService executor) + public void start() { mMutableMetadata.receive(new AttributeChangeRequest(Attribute.CHANNEL_STATE, mState)); @@ -160,14 +158,14 @@ public class ChannelState extends Module implements ICallEventProvider, IDecoder } @Override - public Listener getFrequencyChangeListener() + public Listener getSourceEventListener() { - if(mFrequencyChangeListener == null) + if(mSourceChangeListener == null) { - mFrequencyChangeListener = new FrequencyChangeListener(); + mSourceChangeListener = new SourceChangeListener(); } - return mFrequencyChangeListener; + return mSourceChangeListener; } private boolean isStandardChannel() @@ -520,14 +518,14 @@ public class ChannelState extends Module implements ICallEventProvider, IDecoder /** * Listener to receive frequency change events and rebroadcast them to the decoder states */ - public class FrequencyChangeListener implements Listener + public class SourceChangeListener implements Listener { @Override - public void receive(FrequencyChangeEvent frequencyChangeEvent) + public void receive(SourceEvent sourceEvent) { - if(frequencyChangeEvent.getEvent() == NOTIFICATION_FREQUENCY_CHANGE) + if(sourceEvent.getEvent() == NOTIFICATION_FREQUENCY_CHANGE) { - long frequency = frequencyChangeEvent.getValue().longValue(); + long frequency = sourceEvent.getValue().longValue(); broadcast(new DecoderStateEvent(this, Event.SOURCE_FREQUENCY, getState(), frequency)); } diff --git a/src/main/java/io/github/dsheirer/channel/traffic/TrafficChannelManager.java b/src/main/java/io/github/dsheirer/channel/traffic/TrafficChannelManager.java index dcd12ce5..6c284f3c 100644 --- a/src/main/java/io/github/dsheirer/channel/traffic/TrafficChannelManager.java +++ b/src/main/java/io/github/dsheirer/channel/traffic/TrafficChannelManager.java @@ -1,19 +1,16 @@ /******************************************************************************* - * sdrtrunk + * sdr-trunk * 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 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 + * You should have received a copy of the GNU General Public License along with this program. + * If not, see * ******************************************************************************/ package io.github.dsheirer.channel.traffic; @@ -29,13 +26,12 @@ import io.github.dsheirer.controller.channel.TrafficChannelEvent; import io.github.dsheirer.module.Module; import io.github.dsheirer.module.decode.config.DecodeConfiguration; import io.github.dsheirer.module.decode.event.CallEvent; +import io.github.dsheirer.module.decode.event.CallEvent.CallEventType; import io.github.dsheirer.module.decode.event.ICallEventProvider; -import io.github.dsheirer.module.log.config.EventLogConfiguration; import io.github.dsheirer.record.config.RecordConfiguration; import io.github.dsheirer.sample.Listener; import io.github.dsheirer.source.config.SourceConfigTuner; -import io.github.dsheirer.source.tuner.TunerChannel; -import io.github.dsheirer.source.tuner.TunerChannel.Type; +import io.github.dsheirer.source.tuner.channel.TunerChannel; import org.slf4j.Logger; import org.slf4j.LoggerFactory; @@ -43,7 +39,6 @@ import java.util.ArrayList; import java.util.List; import java.util.Map; import java.util.concurrent.ConcurrentHashMap; -import java.util.concurrent.ScheduledExecutorService; public class TrafficChannelManager extends Module implements ICallEventProvider, IDecoderStateEventListener { @@ -61,7 +56,6 @@ public class TrafficChannelManager extends Module implements ICallEventProvider, private ChannelModel mChannelModel; private DecodeConfiguration mDecodeConfiguration; - private EventLogConfiguration mEventLogConfiguration; private RecordConfiguration mRecordConfiguration; private String mSystem; private String mSite; @@ -75,7 +69,6 @@ public class TrafficChannelManager extends Module implements ICallEventProvider, * @param channelModel containing channels currently in use * @param decodeConfiguration - decoder configuration to use for each * traffic channel allocation. - * @param eventLogConfiguration - logging configuration to use for traffic channels * @param recordConfiguration - recording options for each traffic channel * @param system label to use to describe the system * @param site label to use to describe the site @@ -85,7 +78,6 @@ public class TrafficChannelManager extends Module implements ICallEventProvider, */ public TrafficChannelManager(ChannelModel channelModel, DecodeConfiguration decodeConfiguration, - EventLogConfiguration eventLogConfiguration, RecordConfiguration recordConfiguration, String system, String site, @@ -94,7 +86,6 @@ public class TrafficChannelManager extends Module implements ICallEventProvider, { mChannelModel = channelModel; mDecodeConfiguration = decodeConfiguration; - mEventLogConfiguration = eventLogConfiguration; mRecordConfiguration = recordConfiguration; mSystem = system; mSite = site; @@ -150,8 +141,6 @@ public class TrafficChannelManager extends Module implements ICallEventProvider, channel.setDecodeConfiguration(mDecodeConfiguration); - channel.setEventLogConfiguration(mEventLogConfiguration); - channel.setRecordConfiguration(mRecordConfiguration); channel.setAliasListName(mAliasListName); @@ -161,7 +150,7 @@ public class TrafficChannelManager extends Module implements ICallEventProvider, mTrafficChannelPool.add(channel); } - /* If we have a configured channel, update metadata */ + /* If we have a configured channel, update metadata */ if(channel != null) { channel.setSourceConfiguration(new SourceConfigTuner(tunerChannel)); @@ -184,7 +173,7 @@ public class TrafficChannelManager extends Module implements ICallEventProvider, { CallEvent callEvent = event.getCallEvent(); - /* Check for duplicate events and suppress */ + /* Check for duplicate events and suppress */ synchronized(mTrafficChannelsInUse) { if(mTrafficChannelsInUse.containsKey(callEvent.getChannel())) @@ -196,8 +185,8 @@ public class TrafficChannelManager extends Module implements ICallEventProvider, if(frequency > 0) { - Channel channel = getChannel(callEvent.getChannel(), new TunerChannel(Type.TRAFFIC, - frequency, mDecodeConfiguration.getDecoderType().getChannelBandwidth())); + Channel channel = getChannel(callEvent.getChannel(), new TunerChannel(frequency, + mDecodeConfiguration.getDecoderType().getChannelBandwidth())); if(channel != null) { @@ -213,7 +202,7 @@ public class TrafficChannelManager extends Module implements ICallEventProvider, } else { - callEvent.setCallEventType(CallEvent.CallEventType.CALL_DETECT); + callEvent.setCallEventType(CallEventType.CALL_DETECT); String details = callEvent.getDetails(); @@ -230,7 +219,7 @@ public class TrafficChannelManager extends Module implements ICallEventProvider, } else { - callEvent.setCallEventType(CallEvent.CallEventType.CALL_DETECT); + callEvent.setCallEventType(CallEventType.CALL_DETECT); String details = callEvent.getDetails(); @@ -247,7 +236,7 @@ public class TrafficChannelManager extends Module implements ICallEventProvider, } else { - callEvent.setCallEventType(CallEvent.CallEventType.CALL_DETECT); + callEvent.setCallEventType(CallEventType.CALL_DETECT); String details = callEvent.getDetails(); @@ -337,7 +326,7 @@ public class TrafficChannelManager extends Module implements ICallEventProvider, } @Override - public void start(ScheduledExecutorService executor) + public void start() { } @@ -348,7 +337,7 @@ public class TrafficChannelManager extends Module implements ICallEventProvider, { List channels = new ArrayList<>(); - /* Copy the keyset so we don't get concurrent modification of the map */ + /* Copy the keyset so we don't get concurrent modification of the map */ channels.addAll(mTrafficChannelsInUse.keySet()); for(String channel : channels) @@ -399,4 +388,4 @@ public class TrafficChannelManager extends Module implements ICallEventProvider, } } } -} +} \ No newline at end of file diff --git a/src/main/java/io/github/dsheirer/controller/channel/Channel.java b/src/main/java/io/github/dsheirer/controller/channel/Channel.java index 9c0cee7f..4f66c37f 100644 --- a/src/main/java/io/github/dsheirer/controller/channel/Channel.java +++ b/src/main/java/io/github/dsheirer/controller/channel/Channel.java @@ -31,18 +31,16 @@ import io.github.dsheirer.module.log.config.EventLogConfiguration; import io.github.dsheirer.record.RecorderType; import io.github.dsheirer.record.config.RecordConfiguration; import io.github.dsheirer.sample.Listener; +import io.github.dsheirer.source.SourceEvent; import io.github.dsheirer.source.SourceType; import io.github.dsheirer.source.config.SourceConfigFactory; import io.github.dsheirer.source.config.SourceConfigRecording; import io.github.dsheirer.source.config.SourceConfigTuner; import io.github.dsheirer.source.config.SourceConfiguration; -import io.github.dsheirer.source.tuner.TunerChannel; -import io.github.dsheirer.source.tuner.TunerChannel.Type; -import io.github.dsheirer.source.tuner.frequency.FrequencyChangeEvent; -import io.github.dsheirer.source.tuner.frequency.FrequencyChangeEvent.Event; +import io.github.dsheirer.source.tuner.channel.TunerChannel; @JacksonXmlRootElement(localName = "channel") -public class Channel extends Configuration implements Listener +public class Channel extends Configuration implements Listener { // Standard channels are persisted and traffic channels are temporary public enum ChannelType {STANDARD, TRAFFIC} @@ -51,12 +49,9 @@ public class Channel extends Configuration implements Listener (sampleRate / 2)) { - throw new IllegalArgumentException("FilterFactory - low pass " - + "filter pass frequency [" + passFrequency + "] must be " - + "less than the stop frequency [" + stopFrequency + "] " - + "and must be less than half [" + (int) (sampleRate / 2) + - "] of the sample rate [" + sampleRate + "]"); + throw new IllegalArgumentException("FilterFactory - low pass filter pass frequency [" + passFrequency + + "] must be less than the stop frequency [" + stopFrequency + "] and must be less than half [" + + (int)(sampleRate / 2) + "] of the sample rate [" + sampleRate + "]"); } - int tapCount = getTapCount(sampleRate, - passFrequency, - stopFrequency, - attenuation); + int tapCount = getTapCount(sampleRate, passFrequency, stopFrequency, attenuation); if(forceOddLength) { @@ -289,34 +254,21 @@ public class FilterFactory * @param windowType - window to apply against the generated coefficients * @return */ - public static float[] getHighPass(int sampleRate, - long cutoff, - int filterLength, - Window.WindowType windowType) + public static float[] getHighPass(int sampleRate, long cutoff, int filterLength, Window.WindowType windowType) { //Convert the high frequency cutoff to its low frequency cutoff //equivalent, so that when we generate the low pass filter, prior to //inversion, its at the correct frequency long convertedCutoff = sampleRate / 2 - cutoff; - return invert(getSinc(sampleRate, - convertedCutoff, - filterLength, - windowType)); + return invert(getSinc(sampleRate, convertedCutoff, filterLength, windowType)); } - public static float[] getHighPass(int sampleRate, - long stopFrequency, - long passFrequency, - int attenuation, - Window.WindowType windowType, - boolean forceOddLength) + public static float[] getHighPass(int sampleRate, long stopFrequency, long passFrequency, int attenuation, + Window.WindowType windowType, boolean forceOddLength) { /* reverse the stop and pass frequency to get the low pass variant */ - int tapCount = getTapCount(sampleRate, - stopFrequency, - passFrequency, - attenuation); + int tapCount = getTapCount(sampleRate, stopFrequency, passFrequency, attenuation); if(forceOddLength) { @@ -360,11 +312,10 @@ public class FilterFactory } /** - * Determines the number of fir filter taps required to produce the - * specified frequency response with passband ripple near .1dB. + * Determines the number of fir filter taps required to produce the specified frequency response with passband + * ripple near 0.1dB. * - * Implements the algorithm from Understanding Digital Signal Processing, 3e - * , Lyons, section 5.10.5. + * Implements the algorithm from Understanding Digital Signal Processing, 3e, Lyons, section 5.10.5. * * @param sampleRate in hertz * @param pass pass frequency in hertz @@ -372,21 +323,17 @@ public class FilterFactory * @param attenuation in dB * @return */ - public static int getTapCount(int sampleRate, - long pass, - long stop, - int attenuation) + public static int getTapCount(int sampleRate, long pass, long stop, int attenuation) { - double frequency = ((double) stop - (double) pass) / (double) sampleRate; + double frequency = ((double)stop - (double)pass) / (double)sampleRate; - return (int) (Math.round((double) attenuation / (22.0d * frequency))); + return (int)(Math.round((double)attenuation / (22.0d * frequency))); } - public static ComplexPrimeCICDecimate getDecimationFilter(int sampleRate, - int decimatedRate, int order, int passFrequency, int attenuation, - Window.WindowType windowType) + public static ComplexPrimeCICDecimate getDecimationFilter(int sampleRate, int decimatedRate, int order, + int passFrequency, int attenuation, Window.WindowType windowType) { - int decimationRate = (int) (sampleRate / decimatedRate); + int decimationRate = (int)(sampleRate / decimatedRate); return new ComplexPrimeCICDecimate(decimationRate, order, passFrequency, attenuation, windowType); @@ -401,10 +348,7 @@ public class FilterFactory * polyphase filter decimation chain * @throws - AssertionException if sample rate is not a multiple of 48 kHz */ - public static int[] getPolyphaseDecimationRates(int sampleRate, - int decimatedRate, - long passFrequency, - long stopFrequency) + public static int[] getPolyphaseDecimationRates(int sampleRate, int decimatedRate, long passFrequency, long stopFrequency) { int[] rates; @@ -414,7 +358,7 @@ public class FilterFactory + "integer multiple of sample rate"); } - int decimation = (int) (sampleRate / decimatedRate); + int decimation = (int)(sampleRate / decimatedRate); //Decimation rates below 20 will use single stage polyphase filter if(decimation < 20) @@ -432,11 +376,6 @@ public class FilterFactory int stage1 = findClosest(optimalStage1, factors); -// mLog.info( "Decimation rate [" + decimation + -// "] stage1 optimal [" + optimalStage1 + -// "] stage1 actual [" + stage1 + -// "]"); - if(stage1 == decimation || stage1 == 1) { rates = new int[1]; @@ -447,7 +386,7 @@ public class FilterFactory { rates = new int[2]; - int stage2 = (int) (decimation / stage1); + int stage2 = (int)(decimation / stage1); if(stage1 > stage2) { @@ -465,8 +404,7 @@ public class FilterFactory } /** - * Finds the factor that is closest to the desired factor, from an ordered - * list of factors. + * Finds the factor that is closest to the desired factor, from an ordered list of factors. */ private static int findClosest(int desiredFactor, Set factors) { @@ -488,9 +426,8 @@ public class FilterFactory } /** - * Determines the factors that make up an integer. Uses a brute force - * method to iterate all integers from 1 to value, determining which factors - * are evenly divisible into the value. + * Determines the factors that make up an integer. Uses a brute force method to iterate all integers from 1 to + * value, determining which factors are evenly divisible into the value. * * @param value - integer decimation value * @return - ordered set of factors for value @@ -498,11 +435,11 @@ public class FilterFactory private static Set getFactors(int value) { Set factors = new TreeSet(); - + /* Brute force */ for(int x = 1; x <= value; x++) { - int remainder = (int) (value / x); + int remainder = (int)(value / x); if(remainder * x == value) { @@ -514,14 +451,10 @@ public class FilterFactory } /** - * Determines the optimal decimation rate for stage 1 of a two-stage - * poly-phase decimation filter chain, to produce a final sample rate of - * 48 kHz using a pass bandwidth of 25 kHz. + * Determines the optimal decimation rate for stage 1 of a two-stage poly-phase decimation filter chain, to produce + * a final sample rate of 48 kHz using a pass bandwidth of 25 kHz. Use for total decimation rates of 20 or higher. * - * Use for total decimation rates of 20 or higher. - * - * Implements the algorithm described in Lyons, Understanding Digital Signal - * Processing, 3e, section 10.2.1, page 511. + * Implements the algorithm described in Lyons, Understanding Digital Signal Processing, 3e, section 10.2.1, page 511. * * @param sampleRate * @param decimation @@ -529,42 +462,29 @@ public class FilterFactory * @return optimum integer decimation rate for the first stage decimation * filter */ - public static int getOptimalStageOneRate(int sampleRate, - int decimation, - long passFrequency, - long stopFrequency) + public static int getOptimalStageOneRate(int sampleRate, int decimation, long passFrequency, long stopFrequency) { double ratio = getBandwidthRatio(passFrequency, stopFrequency); - double numerator = 1.0d - ( - Math.sqrt((double) decimation * ratio / (2.0d - ratio))); + double numerator = 1.0d - (Math.sqrt((double)decimation * ratio / (2.0d - ratio))); double denominator = 2.0d - (ratio * (decimation + 1.0d)); - int retVal = (int) (2.0d * decimation * (numerator / denominator)); - -// mLog.info( "Optimal Stage 1 Decimation - rate [" + sampleRate + -// "] pass [" + passFrequency + -// "] bw ratio [" + ratio + -// "] optimal [" + retVal + -// "]" ); + int retVal = (int)(2.0d * decimation * (numerator / denominator)); return retVal; } /** - * Determines the F ratio as described in Lyons, Understanding Digital - * Signal Processing, 3e, section 10.2.1, page 511 - * - * Used in conjunction with the optimal stage one decimation rate method - * above. + * Determines the F ratio as described in Lyons, Understanding Digital Signal Processing, 3e, section 10.2.1, page 511 + * Used in conjunction with the optimal stage one decimation rate method above. */ private static double getBandwidthRatio(long passFrequency, long stopFrequency) { assert (passFrequency < stopFrequency); - return ((double) (stopFrequency - passFrequency) / - (double) stopFrequency); + return ((double)(stopFrequency - passFrequency) / + (double)stopFrequency); } /** @@ -575,14 +495,11 @@ public class FilterFactory * @param outputSampleRate * @return */ - public static float[] getCICCleanupFilter(int outputSampleRate, - int passFrequency, - int attenuation, - Window.WindowType window) + public static float[] getCICCleanupFilter(int outputSampleRate, int passFrequency, int attenuation, Window.WindowType window) { int taps = getTapCount(outputSampleRate, passFrequency, passFrequency + 1500, attenuation); - + /* Make tap count odd */ if(taps % 2 == 0) { @@ -599,7 +516,7 @@ public class FilterFactory //Transfer the IDFT results to the odd length return array float[] coefficients = new float[taps]; - int middleCoefficient = (int) (taps / 2); + int middleCoefficient = (int)(taps / 2); //Bin 0 of the idft is our center coefficient coefficients[middleCoefficient] = frequencyResponse[0]; @@ -620,26 +537,22 @@ public class FilterFactory return coefficients; } - public static float[] getCICResponseArray(int sampleRate, - int frequency, - int length) + public static float[] getCICResponseArray(int sampleRate, int frequency, int length) { float[] cicArray = new float[length * 2]; - int binCount = (int) ((Math.round( - (double) frequency / (double) sampleRate * 2.0d * (double) length))); + int binCount = (int)((Math.round( + (double)frequency / (double)sampleRate * 2.0d * (double)length))); cicArray[0] = 1.0f; - float unityResponse = (float) (Math.sin(1.0d / (double) length) / - (1.0d / (double) length)); + float unityResponse = (float)(Math.sin(1.0d / (double)length) / + (1.0d / (double)length)); for(int x = 1; x <= binCount; x++) { - /* Calculate unity response plus amplification for droop */ - float compensated = 1.0f + (unityResponse - - (float) (Math.sin((double) x / (double) length) / - ((double) x / (double) length))); + /* Calculate unity response plus amplification for droop */ + float compensated = 1.0f + (unityResponse - (float)(Math.sin((double)x / (double)length) / ((double)x / (double)length))); cicArray[x] = compensated; cicArray[length - x] = compensated; @@ -649,28 +562,29 @@ public class FilterFactory } /** - * Creates root raised cosine filter coefficients with a tap count equal - * to the symbols x samplesPerSymbol + 1. + * Creates root raised cosine filter coefficients with a tap count equal to the symbols x samplesPerSymbol + 1. * * Symbol count should be an even integer. - * * Ported to java from gnuradio/filter/firdes.cc * - * For 40db attenuation, calculate the number of symbols based on the - * following formula: + * For 40db attenuation, calculate the number of symbols based on the following formula: + * Symbols = -44 * alpha + 33 * - * Symbols = -44 * alpha + 33 + * Polyphase Channelizer notes: + * -Set samples Per Symbol at 2 (or more) * number of channels + * -Set symbolCount to sufficient size to produce required attenuation + * -Set symbol rate in hertz + * -Set alpha = (desired channel bandwidth / (symbol rate * samples per symbol) - 1.0 + * -Channel bandwidth must be larger than symbol rate * samples per symbol, in order for there to be a positive alpha * * @param samplesPerSymbol - number of samples per symbol - * @param symbols - number of symbols - must be even - * @param alpha - roll-off factor + * @param symbolCount - number of symbol intervals for the filter. This directly impacts the filter size + * @param alpha - roll-off factor. * @return - filter coefficients */ - public static float[] getRootRaisedCosine(int samplesPerSymbol, - int symbols, - float alpha) + public static float[] getRootRaisedCosine(int samplesPerSymbol, int symbolCount, float alpha) { - int taps = samplesPerSymbol * symbols + 1; + int taps = samplesPerSymbol * symbolCount; float scale = 0; @@ -678,10 +592,10 @@ public class FilterFactory for(int x = 0; x < taps; x++) { - float index = (float) x - ((float) taps / 2.0f); + float index = (float)x - ((float)taps / 2.0f); - float x1 = (float) Math.PI * index / (float) samplesPerSymbol; - float x2 = 4.0f * alpha * index / (float) samplesPerSymbol; + float x1 = (float)Math.PI * index / (float)samplesPerSymbol; + float x2 = 4.0f * alpha * index / (float)samplesPerSymbol; float x3 = x2 * x2 - 1.0f; float numerator, denominator; @@ -690,17 +604,15 @@ public class FilterFactory { if(x != taps / 2) { - numerator = (float) Math.cos((1.0 + alpha) * x1) + - (float) Math.sin((1.0f - alpha) * x1) / - (4.0f * alpha * index / (float) samplesPerSymbol); + numerator = (float)Math.cos((1.0 + alpha) * x1) + + (float)Math.sin((1.0f - alpha) * x1) / (4.0f * alpha * index / (float)samplesPerSymbol); } else { - numerator = (float) Math.cos((1.0f + alpha) * x1) + - (1.0f - alpha) * (float) Math.PI / (4.0f * alpha); + numerator = (float)Math.cos((1.0f + alpha) * x1) + (1.0f - alpha) * (float)Math.PI / (4.0f * alpha); } - denominator = x3 * (float) Math.PI; + denominator = x3 * (float)Math.PI; } else { @@ -713,14 +625,11 @@ public class FilterFactory x3 = (1.0f - alpha) * x1; x2 = (1.0f + alpha) * x1; - numerator = (float) (Math.sin(x2) * (1.0f + alpha) * Math.PI - - Math.cos(x3) * ((1.0 - alpha) * Math.PI * - (double) samplesPerSymbol) / (4 * alpha * index) + - Math.sin(x3) * (double) samplesPerSymbol * - (double) samplesPerSymbol / (4.0 * alpha * index * index)); + numerator = (float)(Math.sin(x2) * (1.0f + alpha) * Math.PI - Math.cos(x3) * ((1.0 - alpha) * Math.PI * + (double)samplesPerSymbol) / (4.0 * alpha * index) + Math.sin(x3) * (double)samplesPerSymbol * + (double)samplesPerSymbol / (4.0 * alpha * index * index)); - denominator = (float) (-32.0 * Math.PI * alpha * alpha * index / - (double) samplesPerSymbol); + denominator = (float)(-32.0 * Math.PI * alpha * alpha * index / (double)samplesPerSymbol); } coefficients[x] = 4.0f * alpha * numerator / denominator; @@ -740,10 +649,10 @@ public class FilterFactory } /** - * Creates a filter from the filter specification + * Creates a filter from the filter specification using the remez exchange design algorithm * * @param specification - * @return + * @return filter coefficients * @throws FilterDesignException if the filter cannot be designed */ public static float[] getTaps(FIRFilterSpecification specification) throws FilterDesignException @@ -757,4 +666,248 @@ public class FilterFactory return null; } + + /** + * Evaluates the filter to determine the magnitude response (dB) at the specified frequency. + * + * @param filter to evaluate + * @param frequency to calculate (0 <> 0.5) + * @return magnitude frequency response in decibels + */ + public static double evaluate(float[] filter, double frequency) + { + double real = 0.0; + double imag = 0.0; + + for(int x = 0; x < filter.length; x++) + { + real += filter[x] * Math.cos(Math.PI * frequency * (double)x); + imag += filter[x] * Math.sin(Math.PI * frequency * (double)x); + } + + return decibel(real, imag); + } + + /** + * Calculates the decibel magnitude of the real and imaginary complex sample + * @param real (x-axis) value + * @param imag (y-axis) value + * @return magnitude in decibels + */ + public static double decibel(double real, double imag) + { + return (float)(10.0 * Math.log10(Math.pow(real, 2.0) + Math.pow(imag, 2.0))); + } + + + public static float[] getRemezChannelizer(int channelBandwidth, int channels, int tapsPerChannel, double alpha, + double passRipple, double stopRipple) throws FilterDesignException + { + FIRFilterSpecification specification = FIRFilterSpecification.channelizerBuilder() + .sampleRate(channels * channelBandwidth * 4) + .channels(channels) + .channelBandwidth(channelBandwidth) + .tapsPerChannel(tapsPerChannel) + .alpha(alpha) + .passRipple(passRipple) + .stopRipple(stopRipple) + .build(); + + RemezFIRFilterDesignerWithLagrange designer = new RemezFIRFilterDesignerWithLagrange(specification); + + float[] taps = designer.getImpulseResponse(); + + double bandEdgeFrequency = (double)channelBandwidth / (double)(channels * channelBandwidth * 2); + + double response = decibel(designer.getFrequencyResponse(Math.cos(bandEdgeFrequency * Math.PI)), 0.0); + mLog.debug("Frequency Response at 1.0: " + response); + response = decibel(designer.getFrequencyResponse(Math.cos(bandEdgeFrequency * Math.PI * 2.0)), 0.0); + mLog.debug("Frequency Response at 2.0: " + response); + + return taps; + } + + /** + * 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 + * adjusted to achieve a -6.02 dB attenuation at the channel band edge to enable perfect reconstruction of adjacent + * channels. + * + * The odd length sync filter that meets the design objectives will be increased in length by pre-padding a zero + * coefficient to produce a filter with a length that is an integral of the channel count. + * + * Note: the tapsPerChannel value will be iteratively increased (up to 10x) if a filter cannot be designed to meet + * the channel band edge attenuation objective. + * + * Note: a channelizer filter designed using a Blackman-Harris-7 window and 18-21 taps per channel creates a filter + * with attenuation in range of -60 to -80 dB at the edge of the 2x sampled channel. + * + * @param channelBandwidth - full bandwidth of the channel in hertz + * @param channels - number of channels + * @param tapsPerChannel - desired filter tap count per channel (may be increased to meet objective) + * @param windowType - to use when designing the windowed-sync filter + * @param logResults - to log a debug summary of the filter characteristics + * + * @return filter coefficients + * + * @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 + { + int currentTapsPerChannel = tapsPerChannel; + int sampleRate = (int)(channelBandwidth * (double)channels * 2.0); + int filterLength = (channels * currentTapsPerChannel) - 1; + + double cutoffFrequency = channelBandwidth / (double)sampleRate; + double bandEdge = cutoffFrequency; + double increment = cutoffFrequency * 0.1; + + //Get an initial filter and band edge frequency response using the channel bandwidth as a cutoff + float[] taps = FilterFactory.getSinc(cutoffFrequency, filterLength, windowType); + 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; + + //Iteratively evaluate filters to find the filter with the highest cutoff frequency that meets the objective + while(increment > incrementThreshold) + { + //If the current cutoff meets the objective, test if a higher cutoff also meets the objective + if(matchesObjective(response, PERFECT_RECONSTRUCTION_GAIN_AT_BAND_EDGE) && + (cutoffFrequency + increment <= bandEdge)) + { + float[] higherCutoffTaps = FilterFactory.getSinc(cutoffFrequency + increment, filterLength, windowType); + double higherCutoffResponse = FilterFactory.evaluate(higherCutoffTaps, bandEdge); + + if(matchesObjective(higherCutoffResponse, PERFECT_RECONSTRUCTION_GAIN_AT_BAND_EDGE)) + { + cutoffFrequency += increment; + taps = higherCutoffTaps; + response = higherCutoffResponse; + } + else + { + increment /= 2.0; + } + } + //If the current cutoff meets the objective, decrease the increment to test for smaller resolution cutoffs + else if(matchesObjective(response, PERFECT_RECONSTRUCTION_GAIN_AT_BAND_EDGE)) + { + increment /= 2.0; + } + //Decrease the cutoff frequency - we can't meet the objective + else + { + cutoffFrequency -= increment; + + if(cutoffFrequency <= 0) + { + if(logResults) + { + mLog.debug("Warning: cannot design channelizer filter with tap count [" + currentTapsPerChannel + + "] increasing tap count and starting over"); + } + + currentTapsPerChannel++; + + //After we've attempted increasing the taps per channel 10x, give up + if(currentTapsPerChannel > (tapsPerChannel + 10)) + { + throw new FilterDesignException("Couldn't design filter with taps per channel count in the " + + "range of " + tapsPerChannel + " - " + (tapsPerChannel + 10)); + } + + filterLength = channels * currentTapsPerChannel - 1; + cutoffFrequency = ((double)channelBandwidth / (double)sampleRate); + increment = cutoffFrequency * 0.1; + } + + taps = FilterFactory.getSinc(cutoffFrequency, filterLength, windowType); + response = FilterFactory.evaluate(taps, bandEdge); + } + } + + //This will probably never happen, but ... + if(!matchesObjective(response, PERFECT_RECONSTRUCTION_GAIN_AT_BAND_EDGE)) + { + throw new FilterDesignException("Cannot design filter to specifications"); + } + + //This is an odd length filter - increase the length by 1 by pre-padding a zero coefficient + float[] extendedTaps = new float[taps.length + 1]; + System.arraycopy(taps, 0, extendedTaps, 1, taps.length); + + if(logResults) + { + mLog.debug("Polyphase Channelizer Filter Design Summary"); + mLog.debug("-----------------------------------------------------"); + mLog.debug("Input Sample Rate: " + sampleRate); + mLog.debug("Channel Bandwidth: " + channelBandwidth); + mLog.debug("Channels: " + channels); + mLog.debug("Window Type: " + windowType.name()); + mLog.debug("Taps Per Channel - Requested:" + tapsPerChannel + " Actual:" + ((double)extendedTaps.length / (double)channels)); + mLog.debug("Filter Length: " + (extendedTaps.length)); + mLog.debug("Requested Cutoff Frequency: " + (sampleRate * bandEdge)); + mLog.debug("Actual Cutoff Frequency: " + (sampleRate * cutoffFrequency)); + mLog.debug("Attenuation at 1.0 OBJECTIVE: " + PERFECT_RECONSTRUCTION_GAIN_AT_BAND_EDGE); + mLog.debug("Attenuation at 1.00 Channels: " + evaluate(taps, bandEdge * 1.00)); + mLog.debug("Attenuation at 1.25 Channels: " + evaluate(taps, bandEdge * 1.25)); + mLog.debug("Attenuation at 1.50 Channels: " + evaluate(taps, bandEdge * 1.50)); + mLog.debug("Attenuation at 1.75 Channels: " + evaluate(taps, bandEdge * 1.75)); + mLog.debug("Attenuation at 2.00 Channels: " + evaluate(taps, bandEdge * 2.00)); + } + + return extendedTaps; + } + + /** + * Creates a windowed-sync (Nyquist) filter. + * + * @param cutoff frequency (0.0 <> 0.5) + * @param length of the filter - must be odd-length + * @param windowType to use in designing the filter + * @return filter coefficients. + * @throws FilterDesignException if the requested length is not odd + */ + public static float[] getSinc(double cutoff, int length, Window.WindowType windowType) throws FilterDesignException + { + if(length % 2 == 0) + { + throw new FilterDesignException("Sinc filters must be odd-length"); + } + + float[] coefficients = new float[length]; + int half = length / 2; + + double[] window = Window.getWindow(windowType, length); + + double scalor = 2.0 * cutoff; + double piScalor = Math.PI * scalor; + + coefficients[half] = (float)(1.0 * scalor); + + for(int x = 1; x <= half; x++) + { + double a = piScalor * x; + double coefficient = scalor * Math.sin(a) / a; + + coefficient *= window[half + x]; + coefficients[half + x] = (float)coefficient; + coefficients[half - x] = (float)coefficient; + } + + return coefficients; + } + + /** + * Compares two doubles for equals and avoids any rounding error that are present. + * @param a value to compare + * @param objective value to compare + * @return true if they are equivalent within the margin of error + */ + private static boolean matchesObjective(double a, double objective) + { + return Math.abs(a - objective) < 0.0000000001; + } } diff --git a/src/main/java/io/github/dsheirer/dsp/filter/Window.java b/src/main/java/io/github/dsheirer/dsp/filter/Window.java index 5ca75acd..ceb68afb 100644 --- a/src/main/java/io/github/dsheirer/dsp/filter/Window.java +++ b/src/main/java/io/github/dsheirer/dsp/filter/Window.java @@ -1,234 +1,439 @@ /******************************************************************************* - * SDR Trunk - * Copyright (C) 2014 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 + * 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 io.github.dsheirer.dsp.filter; +import org.slf4j.Logger; +import org.slf4j.LoggerFactory; + import java.util.Arrays; /** - * Window creator - * - * Blackman, Hamming and Hanning windows - * based on windowing methods described at: - * http://www.mstarlabs.com/dsp/goertzel/goertzel.html - * and - * Understanding Digital Signal Processing 3e, Lyons, p.91 - * and - * Digital Filters for Everyone, Allred, p. 132 - * + * Window factory and corresponding utility methods. */ public class Window { - public static double[] getWindow( WindowType type, int length ) + private final static Logger mLog = LoggerFactory.getLogger(Window.class); + + private static final double TWO_PI = Math.PI * 2.0d; + private static final double FOUR_PI = Math.PI * 4.0d; + private static final double SIX_PI = Math.PI * 6.0d; + private static final double EIGHT_PI = Math.PI * 8.0d; + + /** + * Creates a window of the specified type and length. + * + * @param type of window + * @param length of window + * @return window of the specified type and length + */ + public static double[] getWindow(WindowType type, int length) { - switch( type ) - { - case BLACKMAN: - return getBlackmanWindow( length ); - case COSINE: - return getCosineWindow( length ); - case HAMMING: - return getHammingWindow( length ); - case HANNING: - return getHanningWindow( length ); - case NONE: - default: - return getRectangularWindow( length ); - } + switch(type) + { + case BLACKMAN: + return getBlackman(length); + case BLACKMAN_HARRIS_4: + return getBlackmanHarris4(length); + case BLACKMAN_HARRIS_7: + return getBlackmanHarris7(length); + case BLACKMAN_NUTALL: + return getBlackmanNutall(length); + case COSINE: + return getCosine(length); + case FLAT_TOP: + return getFlatTop(length); + case HAMMING: + return getHamming(length); + case HANN: + return getHann(length); + case NUTALL: + return getNutall(length); + case NONE: + default: + return getRectangular(length); + } } - public static double[] getRectangularWindow( int length ) + /** + * Creates an all-pass or rectangular window. + * + * @param length of the window + * @return rectangular window + */ + public static double[] getRectangular(int length) { - double[] coefficients = new double[ length ]; + double[] coefficients = new double[length]; - Arrays.fill( coefficients, 1.0d ); - - return coefficients; - } - - public static double[] getCosineWindow( int length ) - { - double[] coefficients = new double[ length ]; + Arrays.fill(coefficients, 1.0d); - - if( length % 2 == 0 ) //Even length - { - int half = (int)( ( length - 1 ) / 2 ); - - for( int x = -half; x < length / 2 + 1; x++ ) - { - coefficients[ x + half ] = Math.cos( - ( (double)x * Math.PI ) / ( (double)length + 1.0d ) ); - } - } - else //Odd length - { - int half = (int) length / 2; - - for( int x = -half; x < half + 1; x++ ) - { - coefficients[ x + half ] = Math.cos( - ( (double)x * Math.PI ) / ( (double)length + 1.0d ) ); - } - } - - return coefficients; + return coefficients; } - public static double[] getBlackmanWindow( int length ) + /** + * Creates a cosine window of the specified length. + * + * @param length of the window + * @return cosine window + */ + public static double[] getCosine(int length) { - double[] coefficients = new double[ length ]; - - for( int x = 0; x < length; x++ ) - { - coefficients[ x ] = .426591D - - ( .496561D * Math.cos( ( Math.PI * 2.0D * (double)x ) / (double)( length - 1 ) ) + - ( .076848D * Math.cos( ( Math.PI * 4.0D * (double)x ) / (double)( length - 1 ) ) ) ); - } + double[] coefficients = new double[length]; - return coefficients; - } - - public static double[] getHammingWindow( int length ) - { - double[] coefficients = new double[ length ]; - - if( length % 2 == 0 ) //Even length - { - for( int x = 0; x < length; x++ ) - { - coefficients[ x ] = .54D - ( .46D * - Math.cos( ( Math.PI * ( 2.0D * (double)x + 1.0d ) ) / (double)( length - 1 ) ) ); - } - } - else //Odd length - { - for( int x = 0; x < length; x++ ) - { - coefficients[ x ] = .54D - ( .46D * - Math.cos( ( 2.0D * Math.PI * (double)x ) / (double)( length - 1 ) ) ); - } - } + if(length % 2 == 0) //Even length + { + int half = (int)((length - 1) / 2); - return coefficients; + for(int x = -half; x < length / 2 + 1; x++) + { + coefficients[x + half] = Math.cos( + ((double)x * Math.PI) / ((double)length + 1.0d)); + } + } + else //Odd length + { + int half = (int)length / 2; + + for(int x = -half; x < half + 1; x++) + { + coefficients[x + half] = Math.cos( + ((double)x * Math.PI) / ((double)length + 1.0d)); + } + } + + return coefficients; } - public static double[] getHanningWindow( int length ) + /** + * Creates a 3-term Blackman window. + * + * @param length of the window + * @return Blackman window + */ + public static double[] getBlackman(int length) { - double[] coefficients = new double[ length ]; + double[] coefficients = new double[length]; - if( length % 2 == 0 ) //Even length - { - for( int x = 0; x < length; x++ ) - { - coefficients[ x ] = .5D - ( .5D * - Math.cos( ( Math.PI * ( 2.0D * (double)x + 1 ) ) / (double)( length - 1 ) ) ); - } - } - else //Odd length - { - for( int x = 0; x < length; x++ ) - { - coefficients[ x ] = .5D - ( .5D * - Math.cos( ( 2.0D * Math.PI * (double)x ) / (double)( length - 1 ) ) ); - } - } + double denominator = length - 1; + + double a0 = 0.426590713672; + double a1 = 0.496560619089; + double a2 = 0.0768486672399; + + for(int x = 0; x < length; x++) + { + coefficients[x] = a0 - + (a1 * Math.cos((TWO_PI * (double)x) / denominator)) + + (a2 * Math.cos((FOUR_PI * (double)x) / denominator)); + } + + return coefficients; + } + + /** + * Creates a 4-term Nutall window + * + * @param length of the window + * @return Nutall window + */ + public static double[] getNutall(int length) + { + double[] coefficients = new double[length]; + + double denominator = length - 1; + double a0 = 0.355768; + double a1 = 0.487396; + double a2 = 0.144232; + double a3 = 0.012604; + + for(int x = 0; x < length; x++) + { + coefficients[x] = a0 - + (a1 * Math.cos(TWO_PI * (double)x / denominator)) + + (a2 * Math.cos(FOUR_PI * (double)x / denominator)) - + (a3 * Math.cos(SIX_PI * (double)x / denominator)); + } + + return coefficients; + } + + /** + * Creates a 4-term Blackman-Nutall window + * + * @param length of the window + * @return Blackman-Nutall window + */ + public static double[] getBlackmanNutall(int length) + { + double[] coefficients = new double[length]; + + double denominator = length - 1; + double a0 = 0.3635819; + double a1 = 0.4891775; + double a2 = 0.1365995; + double a3 = 0.0106411; + + for(int x = 0; x < length; x++) + { + coefficients[x] = a0 - + (a1 * Math.cos(TWO_PI * (double)x / denominator)) + + (a2 * Math.cos(FOUR_PI * (double)x / denominator)) - + (a3 * Math.cos(SIX_PI * (double)x / denominator)); + } + + return coefficients; + } + + /** + * Creates a 4-term Blackman-Harris window + * + * @param length of the window + * @return Blackman-Harris window + */ + public static double[] getBlackmanHarris4(int length) + { + double[] coefficients = new double[length]; + + double denominator = length - 1; + double a0 = 0.35875; + double a1 = 0.48829; + double a2 = 0.14128; + double a3 = 0.01168; + + for(int x = 0; x < length; x++) + { + coefficients[x] = a0 - + (a1 * Math.cos(TWO_PI * (double)x / denominator)) + + (a2 * Math.cos(FOUR_PI * (double)x / denominator)) - + (a3 * Math.cos(SIX_PI * (double)x / denominator)); + } + + return coefficients; + } + + /** + * Creates a 7-term Blackman-Harris window + * + * @param length of the window + * @return Blackman-Harris window + */ + public static double[] getBlackmanHarris7(int length) + { + double[] coefficients = new double[length]; + + double denominator = length - 1; + + double a0 = 0.27105140069342; + double a1 = 0.43329793923448; + double a2 = 0.21812299954311; + double a3 = 0.06592544638803; + double a4 = 0.01081174209837; + double a5 = 0.00077658482522; + double a6 = 0.00001388721735; - return coefficients; + for(int x = 0; x < length; x++) + { + double w = TWO_PI * (double)x / denominator; + + coefficients[x] = a0 - + (a1 * Math.cos(w)) + + (a2 * Math.cos(2.0 * w)) - + (a3 * Math.cos(3.0 * w)) + + (a4 * Math.cos(4.0 * w)) - + (a5 * Math.cos(5.0 * w)) + + (a6 * Math.cos(6.0 * w)); + } + + return coefficients; + } + + /** + * Creates a Hamming window + * + * @param length of the window + * @return Hamming window + */ + public static double[] getHamming(int length) + { + double[] coefficients = new double[length]; + + double a0 = 0.54; + double a1 = 0.46; + double denominator = length; + + for(int x = 0; x < length; x++) + { + coefficients[x] = a0 - (a1 * Math.cos(TWO_PI * (double)x / denominator)); + } + + return coefficients; + } + + /** + * Creates a Hann window + * + * @param length of the window + * @return Hann window + */ + public static double[] getHann(int length) + { + double[] coefficients = new double[length]; + + double a0 = 0.5; + double a1 = 0.5; + double denominator = length; + + for(int x = 0; x < length; x++) + { + coefficients[x] = a0 - (a1 * Math.cos(TWO_PI * (double)x / denominator)); + } + + return coefficients; + } + + /** + * Creates a 4-term Blackman-Harris window + * + * @param length of the window + * @return Blackman-Harris window + */ + public static double[] getFlatTop(int length) + { + double[] coefficients = new double[length]; + + double denominator = length - 1; + + double a0 = 0.215578948; + double a1 = 0.41663158; + double a2 = 0.277263158; + double a3 = 0.083578947; + double a4 = 0.006947368; + + for(int x = 0; x < length; x++) + { + coefficients[x] = a0 - + (a1 * Math.cos(TWO_PI * (double)x / denominator)) + + (a2 * Math.cos(FOUR_PI * (double)x / denominator)) - + (a3 * Math.cos(SIX_PI * (double)x / denominator)) + + (a4 * Math.cos(EIGHT_PI * (double)x / denominator)); + } + + return coefficients; + } + + + private static double getKaiserBeta(double attenuation) + { + if(attenuation > 50.0) + { + return 0.1102 * (attenuation - 8.7); + } + else if(attenuation >= 21.0) + { + return (0.5842 * Math.pow(attenuation - 21.0, 0.4)) + (0.07886 * (attenuation - 21.0)); + } + else + { + return 0.0; + } } /** * Apply the window against an array of float-type samples */ - public static float[] apply( double[] coefficients, float[] samples ) + public static float[] apply(double[] coefficients, float[] samples) { - for( int x = 0; x < coefficients.length; x++ ) - { - samples[ x ] = (float)( samples[ x ] * coefficients[ x ] ); - } - - return samples; - } - - public static float[] apply( WindowType type, float[] samples ) - { - double[] coefficients = getWindow( type, samples.length ); - - return apply( coefficients, samples ); - } - - /** - * Apply the window against an array of float-type samples - */ - public static double[] apply( double[] coefficients, double[] samples ) - { - for( int x = 0; x < coefficients.length; x++ ) - { - samples[ x ] = samples[ x ] * coefficients[ x ]; - } - - return samples; + for(int x = 0; x < coefficients.length; x++) + { + samples[x] = (float)(samples[x] * coefficients[x]); + } + + return samples; } - public static double[] apply( WindowType type, double[] samples ) + /** + * Apply the window type against the float array of samples + * + * @param type of window to use + * @param samples to be windowed + * @return windowed samples + */ + public static float[] apply(WindowType type, float[] samples) { - double[] coefficients = getWindow( type, samples.length ); - - return apply( coefficients, samples ); + double[] coefficients = getWindow(type, samples.length); + + return apply(coefficients, samples); } - - - + + /** + * Apply the window against an array of double-type samples + */ + public static double[] apply(double[] coefficients, double[] samples) + { + for(int x = 0; x < coefficients.length; x++) + { + samples[x] = samples[x] * coefficients[x]; + } + + return samples; + } + + /** + * Apply the window type against the double array of samples + * + * @param type of window to use + * @param samples to be windowed + * @return windowed samples + */ + public static double[] apply(WindowType type, double[] samples) + { + double[] coefficients = getWindow(type, samples.length); + + return apply(coefficients, samples); + } + /** * Window types */ public enum WindowType { - BLACKMAN, COSINE, HAMMING, HANNING, NONE; - } - - /** - * Utility to log the double arrays - */ - public static String arrayToString( double[] array, boolean breaks ) - { - StringBuilder sb = new StringBuilder(); - for( int x = 0; x < array.length; x++ ) - { - sb.append( x + ":" + array[ x ] ); + BLACKMAN("Blackman"), + BLACKMAN_HARRIS_4("Blackman-Harris 4"), + BLACKMAN_HARRIS_7("Blackman-Harris 7"), + BLACKMAN_NUTALL("Blackman-Nutall"), + COSINE("Cosine"), + FLAT_TOP("Flat Top"), + HAMMING("Hamming"), + HANN("Hann"), + NUTALL("Nutall"), + NONE("None"); - if( breaks ) - { - sb.append( "\n" ); - } - else - { - if( x % 8 == 7 ) - { - sb.append( "\n" ); - } - else - { - sb.append( "\t" ); - } - } - } - - return sb.toString(); - } + private String mLabel; + + WindowType(String label) + { + mLabel = label; + } + + public String toString() + { + return mLabel; + } + } } diff --git a/src/dsp/filter/channelizer/AbstractComplexPolyphaseChannelizer.java b/src/main/java/io/github/dsheirer/dsp/filter/channelizer/AbstractComplexPolyphaseChannelizer.java similarity index 93% rename from src/dsp/filter/channelizer/AbstractComplexPolyphaseChannelizer.java rename to src/main/java/io/github/dsheirer/dsp/filter/channelizer/AbstractComplexPolyphaseChannelizer.java index 0505c1b0..85b2e832 100644 --- a/src/dsp/filter/channelizer/AbstractComplexPolyphaseChannelizer.java +++ b/src/main/java/io/github/dsheirer/dsp/filter/channelizer/AbstractComplexPolyphaseChannelizer.java @@ -16,16 +16,15 @@ * along with this program. If not, see * ******************************************************************************/ -package dsp.filter.channelizer; +package io.github.dsheirer.dsp.filter.channelizer; +import io.github.dsheirer.sample.Broadcaster; +import io.github.dsheirer.sample.Listener; +import io.github.dsheirer.sample.complex.ComplexBuffer; +import io.github.dsheirer.source.ISourceEventListener; +import io.github.dsheirer.source.SourceEvent; import org.slf4j.Logger; import org.slf4j.LoggerFactory; -import sample.Broadcaster; -import sample.Listener; -import sample.complex.ComplexBuffer; -import source.ISourceEventListener; -import source.SourceEvent; -import source.tuner.channel.polyphase.PolyphaseChannelSource; import java.util.List; import java.util.concurrent.CopyOnWriteArrayList; diff --git a/src/source/tuner/channel/polyphase/ChannelCalculator.java b/src/main/java/io/github/dsheirer/dsp/filter/channelizer/ChannelCalculator.java similarity index 96% rename from src/source/tuner/channel/polyphase/ChannelCalculator.java rename to src/main/java/io/github/dsheirer/dsp/filter/channelizer/ChannelCalculator.java index a1b0940c..1210e45a 100644 --- a/src/source/tuner/channel/polyphase/ChannelCalculator.java +++ b/src/main/java/io/github/dsheirer/dsp/filter/channelizer/ChannelCalculator.java @@ -1,26 +1,24 @@ /******************************************************************************* - * sdrtrunk + * ********************************************************************************************************************* + * sdr-trunk * 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 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 + * 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 source.tuner.channel.polyphase; +package io.github.dsheirer.dsp.filter.channelizer; +import io.github.dsheirer.source.tuner.channel.TunerChannel; import org.slf4j.Logger; import org.slf4j.LoggerFactory; -import source.tuner.channel.TunerChannel; import java.text.DecimalFormat; import java.util.ArrayList; diff --git a/src/source/tuner/channel/polyphase/ChannelizerViewer.java b/src/main/java/io/github/dsheirer/dsp/filter/channelizer/ChannelizerViewer.java similarity index 81% rename from src/source/tuner/channel/polyphase/ChannelizerViewer.java rename to src/main/java/io/github/dsheirer/dsp/filter/channelizer/ChannelizerViewer.java index b4dbcd30..e6d01e0c 100644 --- a/src/source/tuner/channel/polyphase/ChannelizerViewer.java +++ b/src/main/java/io/github/dsheirer/dsp/filter/channelizer/ChannelizerViewer.java @@ -1,46 +1,49 @@ /******************************************************************************* - * sdrtrunk + * ********************************************************************************************************************* + * sdr-trunk * 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 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 + * 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 source.tuner.channel.polyphase; +package io.github.dsheirer.dsp.filter.channelizer; +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.ISourceEventProcessor; +import io.github.dsheirer.source.SourceEvent; +import io.github.dsheirer.source.SourceException; +import io.github.dsheirer.source.tuner.channel.TunerChannel; +import io.github.dsheirer.source.tuner.channel.TunerChannelSource; +import io.github.dsheirer.source.tuner.configuration.TunerConfigurationModel; +import io.github.dsheirer.source.tuner.test.TestTuner; +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 net.miginfocom.swing.MigLayout; import org.slf4j.Logger; import org.slf4j.LoggerFactory; -import sample.Listener; -import sample.SampleType; -import sample.complex.ComplexBuffer; -import settings.SettingsManager; -import source.ISourceEventProcessor; -import source.SourceEvent; -import source.SourceException; -import source.tuner.channel.TunerChannel; -import source.tuner.channel.TunerChannelSource; -import source.tuner.configuration.TunerConfigurationModel; -import source.tuner.test.TestTuner; -import spectrum.DFTProcessor; -import spectrum.DFTSize; -import spectrum.SpectrumPanel; -import spectrum.converter.ComplexDecibelConverter; -import javax.swing.*; +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.*; +import java.awt.Dimension; +import java.awt.EventQueue; public class ChannelizerViewer extends JFrame { @@ -70,10 +73,10 @@ public class ChannelizerViewer extends JFrame { mTestTuner = new TestTuner(); - mChannelCount = (int)(mTestTuner.getTunerController().getFrequencyController().getSampleRate() / CHANNEL_BANDWIDTH); + mChannelCount = (int)(mTestTuner.getTunerController().getSampleRate() / CHANNEL_BANDWIDTH); mChannelsPerRow = channelsPerRow; - mToneFrequency = mTestTuner.getTunerController().getFrequencyController().getFrequency() + 19200; + mToneFrequency = mTestTuner.getTunerController().getFrequency() + 19200; mTestTuner.getTunerController().setToneFrequency(mToneFrequency); @@ -109,7 +112,7 @@ public class ChannelizerViewer extends JFrame if(mPrimarySpectrumPanel == null) { mPrimarySpectrumPanel = new PrimarySpectrumPanel(mSettingsManager, - mTestTuner.getTunerController().getFrequencyController().getSampleRate()); + mTestTuner.getTunerController().getSampleRate()); mPrimarySpectrumPanel.setPreferredSize(new Dimension(1000, 200)); mPrimarySpectrumPanel.setDFTSize(mMainPanelDFTSize); mTestTuner.getSourceManager().addComplexBufferListener(mPrimarySpectrumPanel); @@ -126,9 +129,9 @@ public class ChannelizerViewer extends JFrame mControlPanel.setLayout(new MigLayout("insets 0 0 0 0", "", "")); mControlPanel.add(new JLabel("Tone:"), "align left"); - int maximumFrequencyOffset = (int)(mTestTuner.getTunerController().getFrequencyController().getSampleRate() / 2); - long minimumFrequency = mTestTuner.getTunerController().getFrequencyController().getFrequency() - maximumFrequencyOffset; - long maximumFrequency = mTestTuner.getTunerController().getFrequencyController().getFrequency() + maximumFrequencyOffset; + int maximumFrequencyOffset = (int)(mTestTuner.getTunerController().getSampleRate() / 2); + long minimumFrequency = mTestTuner.getTunerController().getFrequency() - maximumFrequencyOffset; + long maximumFrequency = mTestTuner.getTunerController().getFrequency() + maximumFrequencyOffset; SpinnerNumberModel model = new SpinnerNumberModel(mToneFrequency, minimumFrequency, maximumFrequency, 100 ); @@ -148,11 +151,9 @@ public class ChannelizerViewer extends JFrame mControlPanel.add(spinner); mControlPanel.add(new JLabel("Hz")); - mControlPanel.add(new JLabel("Sample Rate: " + - mTestTuner.getTunerController().getFrequencyController().getSampleRate() + " Hz"), "push,align right"); + mControlPanel.add(new JLabel("Sample Rate: " + mTestTuner.getTunerController().getSampleRate() + " Hz"), "push,align right"); - mControlPanel.add(new JLabel("Max: " + - (mTestTuner.getTunerController().getFrequencyController().getSampleRate() / 2) + " Hz"), "push,align right"); + mControlPanel.add(new JLabel("Max: " + (mTestTuner.getTunerController().getSampleRate() / 2) + " Hz"), "push,align right"); mControlPanel.add(new JLabel("Channels: " + mChannelCount), "push,align right"); } @@ -186,7 +187,7 @@ public class ChannelizerViewer extends JFrame { setLayout(new MigLayout("insets 0 0 0 0", "fill", "fill")); - double spectralBandwidth = mTestTuner.getTunerController().getFrequencyController().getSampleRate(); + double spectralBandwidth = mTestTuner.getTunerController().getSampleRate(); double halfSpectralBandwidth = spectralBandwidth / 2.0; int channelToLog = -1; @@ -194,7 +195,7 @@ public class ChannelizerViewer extends JFrame for(int x = 0; x < mChannelCount; x++) { //place the channels left and right of 10.0 MHz - long frequency = (long)(mTestTuner.getTunerController().getFrequencyController().getFrequency() + + long frequency = (long)(mTestTuner.getTunerController().getFrequency() + ((x * CHANNEL_BANDWIDTH) - halfSpectralBandwidth) + (CHANNEL_BANDWIDTH / 2)); mLog.debug("Channel " + x + " Frequency: " + frequency); @@ -340,4 +341,4 @@ public class ChannelizerViewer extends JFrame }); } -} +} \ No newline at end of file diff --git a/src/dsp/filter/channelizer/ComplexPolyphaseChannelizer.java b/src/main/java/io/github/dsheirer/dsp/filter/channelizer/ComplexPolyphaseChannelizer.java similarity index 94% rename from src/dsp/filter/channelizer/ComplexPolyphaseChannelizer.java rename to src/main/java/io/github/dsheirer/dsp/filter/channelizer/ComplexPolyphaseChannelizer.java index dca0f4ca..9ab818b9 100644 --- a/src/dsp/filter/channelizer/ComplexPolyphaseChannelizer.java +++ b/src/main/java/io/github/dsheirer/dsp/filter/channelizer/ComplexPolyphaseChannelizer.java @@ -16,16 +16,16 @@ * along with this program. If not, see * ******************************************************************************/ -package dsp.filter.channelizer; +package io.github.dsheirer.dsp.filter.channelizer; -import dsp.filter.FilterFactory; -import dsp.filter.Window; -import dsp.filter.fir.real.RealFIRFilter; -import dsp.mixer.Oscillator; +import io.github.dsheirer.dsp.filter.FilterFactory; +import io.github.dsheirer.dsp.filter.Window; +import io.github.dsheirer.dsp.filter.fir.real.RealFIRFilter; +import io.github.dsheirer.dsp.mixer.Oscillator; +import io.github.dsheirer.sample.complex.ComplexBuffer; import org.jtransforms.fft.FloatFFT_1D; import org.slf4j.Logger; import org.slf4j.LoggerFactory; -import sample.complex.ComplexBuffer; public class ComplexPolyphaseChannelizer extends AbstractComplexPolyphaseChannelizer { diff --git a/src/dsp/filter/channelizer/ComplexPolyphaseChannelizerM2.java b/src/main/java/io/github/dsheirer/dsp/filter/channelizer/ComplexPolyphaseChannelizerM2.java similarity index 97% rename from src/dsp/filter/channelizer/ComplexPolyphaseChannelizerM2.java rename to src/main/java/io/github/dsheirer/dsp/filter/channelizer/ComplexPolyphaseChannelizerM2.java index d3bdded6..3e0995dc 100644 --- a/src/dsp/filter/channelizer/ComplexPolyphaseChannelizerM2.java +++ b/src/main/java/io/github/dsheirer/dsp/filter/channelizer/ComplexPolyphaseChannelizerM2.java @@ -16,17 +16,15 @@ * along with this program. If not, see * ******************************************************************************/ -package dsp.filter.channelizer; +package io.github.dsheirer.dsp.filter.channelizer; -import dsp.filter.FilterFactory; -import dsp.filter.Window; -import dsp.filter.design.FilterDesignException; +import io.github.dsheirer.dsp.filter.FilterFactory; +import io.github.dsheirer.dsp.filter.Window.WindowType; +import io.github.dsheirer.dsp.filter.design.FilterDesignException; +import io.github.dsheirer.sample.complex.ComplexBuffer; import org.jtransforms.fft.FloatFFT_1D; import org.slf4j.Logger; import org.slf4j.LoggerFactory; -import sample.complex.ComplexBuffer; - -import java.text.DecimalFormat; public class ComplexPolyphaseChannelizerM2 extends AbstractComplexPolyphaseChannelizer { @@ -83,7 +81,7 @@ public class ComplexPolyphaseChannelizerM2 extends AbstractComplexPolyphaseChann super(sampleRate, (int)(sampleRate / DEFAULT_MINIMUM_CHANNEL_BANDWIDTH)); float[] filterTaps = FilterFactory.getSincChannelizer(getChannelSampleRate(), getChannelCount(), - tapsPerFilter, Window.WindowType.BLACKMAN_HARRIS_7, true); + tapsPerFilter, WindowType.BLACKMAN_HARRIS_7, true); init(filterTaps); } @@ -101,7 +99,7 @@ public class ComplexPolyphaseChannelizerM2 extends AbstractComplexPolyphaseChann super.setSampleRate(sampleRate); float[] filterTaps = FilterFactory.getSincChannelizer(getChannelSampleRate(), getChannelCount(), - 15, Window.WindowType.BLACKMAN_HARRIS_7, true); + 15, WindowType.BLACKMAN_HARRIS_7, true); init(filterTaps); } diff --git a/src/source/tuner/channel/polyphase/PolyphaseChannelManager.java b/src/main/java/io/github/dsheirer/dsp/filter/channelizer/PolyphaseChannelManager.java similarity index 84% rename from src/source/tuner/channel/polyphase/PolyphaseChannelManager.java rename to src/main/java/io/github/dsheirer/dsp/filter/channelizer/PolyphaseChannelManager.java index c15b64a2..2d673111 100644 --- a/src/source/tuner/channel/polyphase/PolyphaseChannelManager.java +++ b/src/main/java/io/github/dsheirer/dsp/filter/channelizer/PolyphaseChannelManager.java @@ -1,43 +1,37 @@ /******************************************************************************* - * sdrtrunk + * sdr-trunk * 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 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 + * You should have received a copy of the GNU General Public License along with this program. + * If not, see * ******************************************************************************/ -package source.tuner.channel.polyphase; +package io.github.dsheirer.dsp.filter.channelizer; -import dsp.filter.channelizer.ComplexPolyphaseChannelizerM2; -import dsp.filter.design.FilterDesignException; +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; +import io.github.dsheirer.dsp.filter.design.FilterDesignException; +import io.github.dsheirer.sample.Listener; +import io.github.dsheirer.sample.complex.IComplexBufferProvider; +import io.github.dsheirer.source.Source; +import io.github.dsheirer.source.SourceEvent; +import io.github.dsheirer.source.SourceException; +import io.github.dsheirer.source.tuner.channel.TunerChannel; import org.slf4j.Logger; import org.slf4j.LoggerFactory; -import sample.Listener; -import sample.complex.IComplexBufferProvider; -import source.SourceEvent; -import source.SourceException; -import source.tuner.channel.ChannelManager; -import source.tuner.channel.TunerChannel; -import source.tuner.channel.TunerChannelSource; -import source.tuner.channel.polyphase.output.IPolyphaseChannelOutputProcessor; -import source.tuner.channel.polyphase.output.OneChannelOutputProcessor; -import source.tuner.channel.polyphase.output.TwoChannelOutputProcessor; -import source.tuner.channel.FrequencyController; import java.util.List; import java.util.concurrent.CopyOnWriteArrayList; -public class PolyphaseChannelManager extends ChannelManager +public class PolyphaseChannelManager implements Listener { private final static Logger mLog = LoggerFactory.getLogger(PolyphaseChannelManager.class); private static final int CHANNEL_BANDWIDTH = 12500; @@ -63,13 +57,12 @@ public class PolyphaseChannelManager extends ChannelManager * Note: add this channel manager as a source event listener to the complex buffer provider to ensure this manager * adapts to changes in source frequency and sample rate. * - * @param frequencyController that can be controlled by this channel manager * @param complexBufferProvider that supports register/deregister of this channel manager + * @param frequency of the provided complex buffer samples + * @param sampleRate of the provided complex buffer samples */ - public PolyphaseChannelManager(FrequencyController frequencyController, IComplexBufferProvider complexBufferProvider) + public PolyphaseChannelManager(IComplexBufferProvider complexBufferProvider, long frequency, int sampleRate) { - super(frequencyController); - if(complexBufferProvider == null) { throw new IllegalArgumentException("Complex buffer provider argument cannot be null"); @@ -78,37 +71,28 @@ public class PolyphaseChannelManager extends ChannelManager mComplexBufferProvider = complexBufferProvider; //Ensure that sample rate is a multiple of channel sample rate since polyphase channelizer is M2 - if(getFrequencyController().getSampleRate() % (CHANNEL_SAMPLE_RATE) != 0) + if(sampleRate % (CHANNEL_SAMPLE_RATE) != 0) { - throw new IllegalArgumentException("Tuner sample rate [" + getFrequencyController().getSampleRate() + - "] must be an even multiple of " + CHANNEL_SAMPLE_RATE + " Hz"); + throw new IllegalArgumentException("Tuner sample rate [" + sampleRate + "] must be an even multiple of " + + CHANNEL_SAMPLE_RATE + " Hz"); } - int channelCount = (int)(getFrequencyController().getSampleRate() / CHANNEL_BANDWIDTH); + int channelCount = sampleRate / CHANNEL_BANDWIDTH; - mChannelCalculator = new ChannelCalculator(getFrequencyController().getSampleRate(), channelCount, - getFrequencyController().getFrequency(), CHANNEL_OVERSAMPLING); + mChannelCalculator = new ChannelCalculator(sampleRate, channelCount, frequency, CHANNEL_OVERSAMPLING); mBufferProcessor = new ScheduledBufferProcessor(500, 100, 50, 50); - updateChannelizer(getFrequencyController().getSampleRate(), null); - } - - @Override - protected long calculateCenterFrequency() - { - return 0; + updateChannelizer(sampleRate, null); } /** * Provides a Digital Drop Channel (DDC) for the specified tuner channel or returns null if the channel can't be * sourced due to the current center frequency and/or sample rate. - * * @param tunerChannel specifying center frequency and bandwidth. * @return source or null. */ - @Override - protected TunerChannelSource getTunerChannelSource(TunerChannel tunerChannel) + public Source getChannel(TunerChannel tunerChannel) { PolyphaseChannelSource channelSource = null; @@ -123,8 +107,6 @@ public class PolyphaseChannelManager extends ChannelManager channelSource = new PolyphaseChannelSource(tunerChannel, outputProcessor, mChannelSourceEventListener, CHANNEL_SAMPLE_RATE); - channelSource.setFrequency(mChannelCalculator.getCenterFrequencyForIndexes(polyphaseIndexes)); - mChannelSources.add(channelSource); } } @@ -141,7 +123,6 @@ public class PolyphaseChannelManager extends ChannelManager /** * Creates a processor to process the channelizer channel indexes into a composite output stream providing * channelized complex sample buffers to a registered source listener. - * * @param indexes to target by the output processor * @return output processor compatible with the number of indexes to monitor */ @@ -190,8 +171,6 @@ public class PolyphaseChannelManager extends ChannelManager { synchronized(mBufferProcessor) { - super.releaseSource(channelSource); - mChannelSources.remove(channelSource); if(mPolyphaseChannelizer.getRegisteredChannelCount() == 0) @@ -231,7 +210,7 @@ public class PolyphaseChannelManager extends ChannelManager * event will be passed to all output processors to ensure the change is fully promulgated before restarting the * buffer processor if it was already running. */ - private void updateChannelizer(double sampleRate, SourceEvent sourceEvent) + private void updateChannelizer(int sampleRate, SourceEvent sourceEvent) { mChannelCalculator.setSampleRate(sampleRate); @@ -288,7 +267,7 @@ public class PolyphaseChannelManager extends ChannelManager */ private void updateOutputProcessors(SourceEvent sourceEvent) { - for(PolyphaseChannelSource channelSource : mChannelSources) + for(PolyphaseChannelSource channelSource: mChannelSources) { updateOutputProcessor(channelSource); @@ -381,4 +360,4 @@ public class PolyphaseChannelManager extends ChannelManager } } } -} +} \ No newline at end of file diff --git a/src/source/tuner/channel/polyphase/PolyphaseChannelSource.java b/src/main/java/io/github/dsheirer/dsp/filter/channelizer/PolyphaseChannelSource.java similarity index 87% rename from src/source/tuner/channel/polyphase/PolyphaseChannelSource.java rename to src/main/java/io/github/dsheirer/dsp/filter/channelizer/PolyphaseChannelSource.java index 8b7babaa..e2985c2a 100644 --- a/src/source/tuner/channel/polyphase/PolyphaseChannelSource.java +++ b/src/main/java/io/github/dsheirer/dsp/filter/channelizer/PolyphaseChannelSource.java @@ -1,32 +1,30 @@ /******************************************************************************* - * sdrtrunk + * ********************************************************************************************************************* + * sdr-trunk * 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 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 + * 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 source.tuner.channel.polyphase; +package io.github.dsheirer.dsp.filter.channelizer; -import source.tuner.channel.polyphase.output.IPolyphaseChannelOutputProcessor; +import io.github.dsheirer.dsp.filter.channelizer.output.IPolyphaseChannelOutputProcessor; +import io.github.dsheirer.sample.Listener; +import io.github.dsheirer.sample.complex.ComplexBuffer; +import io.github.dsheirer.sample.complex.ComplexToComplexBufferAssembler; +import io.github.dsheirer.source.SourceEvent; +import io.github.dsheirer.source.tuner.channel.TunerChannel; +import io.github.dsheirer.source.tuner.channel.TunerChannelSource; import org.slf4j.Logger; import org.slf4j.LoggerFactory; -import sample.Listener; -import sample.complex.ComplexBuffer; -import sample.complex.ComplexToComplexBufferAssembler; -import source.SourceEvent; -import source.tuner.channel.TunerChannel; -import source.tuner.channel.TunerChannelSource; public class PolyphaseChannelSource extends TunerChannelSource { diff --git a/src/source/tuner/channel/polyphase/ScheduledBufferProcessor.java b/src/main/java/io/github/dsheirer/dsp/filter/channelizer/ScheduledBufferProcessor.java similarity index 88% rename from src/source/tuner/channel/polyphase/ScheduledBufferProcessor.java rename to src/main/java/io/github/dsheirer/dsp/filter/channelizer/ScheduledBufferProcessor.java index 810cb838..f9b3563f 100644 --- a/src/source/tuner/channel/polyphase/ScheduledBufferProcessor.java +++ b/src/main/java/io/github/dsheirer/dsp/filter/channelizer/ScheduledBufferProcessor.java @@ -1,29 +1,26 @@ /******************************************************************************* - * sdrtrunk + * sdr-trunk * 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 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 + * You should have received a copy of the GNU General Public License along with this program. + * If not, see * ******************************************************************************/ -package source.tuner.channel.polyphase; +package io.github.dsheirer.dsp.filter.channelizer; +import io.github.dsheirer.sample.Listener; +import io.github.dsheirer.sample.OverflowableTransferQueue; +import io.github.dsheirer.sample.real.IOverflowListener; +import io.github.dsheirer.util.ThreadPool; import org.slf4j.Logger; import org.slf4j.LoggerFactory; -import sample.Listener; -import sample.OverflowableTransferQueue; -import sample.real.IOverflowListener; -import util.ThreadPool; import java.util.ArrayList; import java.util.List; diff --git a/src/source/tuner/channel/polyphase/output/ChannelOutputProcessor.java b/src/main/java/io/github/dsheirer/dsp/filter/channelizer/output/ChannelOutputProcessor.java similarity index 94% rename from src/source/tuner/channel/polyphase/output/ChannelOutputProcessor.java rename to src/main/java/io/github/dsheirer/dsp/filter/channelizer/output/ChannelOutputProcessor.java index 2a32baa4..272f69d0 100644 --- a/src/source/tuner/channel/polyphase/output/ChannelOutputProcessor.java +++ b/src/main/java/io/github/dsheirer/dsp/filter/channelizer/output/ChannelOutputProcessor.java @@ -16,16 +16,16 @@ * along with this program. If not, see * ******************************************************************************/ -package source.tuner.channel.polyphase.output; +package io.github.dsheirer.dsp.filter.channelizer.output; -import dsp.mixer.Oscillator; +import io.github.dsheirer.dsp.mixer.Oscillator; +import io.github.dsheirer.sample.OverflowableTransferQueue; +import io.github.dsheirer.sample.complex.Complex; +import io.github.dsheirer.sample.complex.ComplexSampleListener; +import io.github.dsheirer.sample.real.IOverflowListener; +import io.github.dsheirer.source.Source; import org.slf4j.Logger; import org.slf4j.LoggerFactory; -import sample.OverflowableTransferQueue; -import sample.complex.Complex; -import sample.complex.ComplexSampleListener; -import sample.real.IOverflowListener; -import source.Source; import java.util.ArrayList; import java.util.List; diff --git a/src/source/tuner/channel/polyphase/output/IPolyphaseChannelOutputProcessor.java b/src/main/java/io/github/dsheirer/dsp/filter/channelizer/output/IPolyphaseChannelOutputProcessor.java similarity index 93% rename from src/source/tuner/channel/polyphase/output/IPolyphaseChannelOutputProcessor.java rename to src/main/java/io/github/dsheirer/dsp/filter/channelizer/output/IPolyphaseChannelOutputProcessor.java index f8348990..ce56168a 100644 --- a/src/source/tuner/channel/polyphase/output/IPolyphaseChannelOutputProcessor.java +++ b/src/main/java/io/github/dsheirer/dsp/filter/channelizer/output/IPolyphaseChannelOutputProcessor.java @@ -16,10 +16,10 @@ * along with this program. If not, see * ******************************************************************************/ -package source.tuner.channel.polyphase.output; +package io.github.dsheirer.dsp.filter.channelizer.output; -import sample.complex.ComplexSampleListener; -import source.Source; +import io.github.dsheirer.sample.complex.ComplexSampleListener; +import io.github.dsheirer.source.Source; import java.util.List; diff --git a/src/source/tuner/channel/polyphase/output/OneChannelOutputProcessor.java b/src/main/java/io/github/dsheirer/dsp/filter/channelizer/output/OneChannelOutputProcessor.java similarity index 96% rename from src/source/tuner/channel/polyphase/output/OneChannelOutputProcessor.java rename to src/main/java/io/github/dsheirer/dsp/filter/channelizer/output/OneChannelOutputProcessor.java index 44fd12f9..e8adb854 100644 --- a/src/source/tuner/channel/polyphase/output/OneChannelOutputProcessor.java +++ b/src/main/java/io/github/dsheirer/dsp/filter/channelizer/output/OneChannelOutputProcessor.java @@ -16,11 +16,11 @@ * along with this program. If not, see * ******************************************************************************/ -package source.tuner.channel.polyphase.output; +package io.github.dsheirer.dsp.filter.channelizer.output; +import io.github.dsheirer.sample.complex.ComplexSampleListener; import org.slf4j.Logger; import org.slf4j.LoggerFactory; -import sample.complex.ComplexSampleListener; import java.util.List; diff --git a/src/source/tuner/channel/polyphase/output/TwoChannelOutputProcessor.java b/src/main/java/io/github/dsheirer/dsp/filter/channelizer/output/TwoChannelOutputProcessor.java similarity index 96% rename from src/source/tuner/channel/polyphase/output/TwoChannelOutputProcessor.java rename to src/main/java/io/github/dsheirer/dsp/filter/channelizer/output/TwoChannelOutputProcessor.java index e96818e5..350567a3 100644 --- a/src/source/tuner/channel/polyphase/output/TwoChannelOutputProcessor.java +++ b/src/main/java/io/github/dsheirer/dsp/filter/channelizer/output/TwoChannelOutputProcessor.java @@ -16,11 +16,11 @@ * along with this program. If not, see * ******************************************************************************/ -package source.tuner.channel.polyphase.output; +package io.github.dsheirer.dsp.filter.channelizer.output; +import io.github.dsheirer.sample.complex.ComplexSampleListener; import org.slf4j.Logger; import org.slf4j.LoggerFactory; -import sample.complex.ComplexSampleListener; import java.util.List; diff --git a/src/dsp/filter/correction/IQCorrectionFilter.java b/src/main/java/io/github/dsheirer/dsp/filter/correction/IQCorrectionFilter.java similarity index 93% rename from src/dsp/filter/correction/IQCorrectionFilter.java rename to src/main/java/io/github/dsheirer/dsp/filter/correction/IQCorrectionFilter.java index 14f84de8..ca219ac5 100644 --- a/src/dsp/filter/correction/IQCorrectionFilter.java +++ b/src/main/java/io/github/dsheirer/dsp/filter/correction/IQCorrectionFilter.java @@ -16,9 +16,9 @@ * along with this program. If not, see * ******************************************************************************/ -package dsp.filter.correction; +package io.github.dsheirer.dsp.filter.correction; -import sample.complex.ComplexBuffer; +import io.github.dsheirer.sample.complex.ComplexBuffer; public class IQCorrectionFilter { diff --git a/src/main/java/io/github/dsheirer/dsp/filter/fir/FIRFilterSpecification.java b/src/main/java/io/github/dsheirer/dsp/filter/fir/FIRFilterSpecification.java index 661038df..d26483dd 100644 --- a/src/main/java/io/github/dsheirer/dsp/filter/fir/FIRFilterSpecification.java +++ b/src/main/java/io/github/dsheirer/dsp/filter/fir/FIRFilterSpecification.java @@ -1,6 +1,9 @@ package io.github.dsheirer.dsp.filter.fir; import io.github.dsheirer.dsp.filter.fir.remez.FIRLinearPhaseFilterType; +import org.apache.commons.lang3.Validate; +import org.slf4j.Logger; +import org.slf4j.LoggerFactory; import java.text.DecimalFormat; import java.util.ArrayList; @@ -9,1123 +12,1099 @@ import java.util.List; public class FIRFilterSpecification { - private static final DecimalFormat DECIMAL_FORMATTER = new DecimalFormat( "0.00000" ); - - private FIRLinearPhaseFilterType mRemezFilterType; - private int mOrder; - private int mGridDensity = 16; - private List mFrequencyBands = new ArrayList<>(); - - /** - * Constructs a Remez filter specification. Use one of the filter builders to construct a Remez - * filter specification. - * - * @param order of the filter - * @param frequencyBands containing the start and stop values for each frequency band normalized - * to the sample rate where each frequency value is in the range (0.0 <> 0.5) - * @param amplitudes of each frequency band - * @param weights of the band ripples normalized to the largest ripple value - */ - private FIRFilterSpecification( FIRLinearPhaseFilterType type, int order, int gridDensity ) - { - mRemezFilterType = type; - mOrder = order; - mGridDensity = gridDensity; - } - - public String toString() - { - StringBuilder sb = new StringBuilder(); - sb.append( "Type: " ); - sb.append( mRemezFilterType.name() ); - sb.append( " Order: " ); - sb.append( mOrder ); - sb.append( " Extrema: " ); - sb.append( getExtremaCount() ); - sb.append( " Grid Density: " ); - sb.append( mGridDensity ); - sb.append( " Grid Size: " ); - sb.append( getGridSize() ); - - for ( FrequencyBand band : mFrequencyBands ) - { - sb.append( "\n" ); - sb.append( band.toString() ); - } - - return sb.toString(); - } - - public void addFrequencyBand( FrequencyBand band ) - { - mFrequencyBands.add( band ); - - updateGridSize(); - } - - public void addFrequencyBands( Collection bands ) - { - mFrequencyBands.addAll( bands ); - - updateGridSize(); - } - - public List getFrequencyBands() - { - return mFrequencyBands; - } - - /** - * Returns the total bandwidth of the defined frequency bands in the specification. - * - * @return total bandwidth -- should be less than or equal to 0.5 - */ - public double getTotalBandwidth() - { - double bandwidth = 0.0; - - for ( FrequencyBand band : mFrequencyBands ) - { - bandwidth += band.getBandWidth(); - } - - return bandwidth; - } - - /** - * Creates a Low-Pass FIR filter specification builder that allows you to define the filter - * parameters and create a filter specification. - */ - public static LowPassBuilder lowPassBuilder() - { - return new LowPassBuilder(); - } - - /** - * Creates a High-Pass FIR filter specification builder that allows you to define the filter - * parameters and create a filter specification. - */ - public static HighPassBuilder highPassBuilder() - { - return new HighPassBuilder(); - } - - /** - * Creates a Band-Pass FIR filter specification builder that allows you to define the filter - * parameters and create a filter specification. - */ - public static BandPassBuilder bandPassBuilder() - { - return new BandPassBuilder(); - } - - public FIRLinearPhaseFilterType getFilterType() - { - return mRemezFilterType; - } - - public boolean isSymmetrical() - { - return mRemezFilterType == FIRLinearPhaseFilterType.TYPE_1_ODD_LENGTH_EVEN_ORDER_SYMMETRICAL || - mRemezFilterType == FIRLinearPhaseFilterType.TYPE_2_EVEN_LENGTH_ODD_ORDER_SYMMETRICAL; - } - - public boolean isAntiSymmetrical() - { - return mRemezFilterType == FIRLinearPhaseFilterType.TYPE_3_ODD_LENGTH_EVEN_ORDER_ANTI_SYMMETRICAL || - mRemezFilterType == FIRLinearPhaseFilterType.TYPE_4_EVEN_LENGTH_ODD_ORDER_ANTI_SYMMETRICAL; - } - - public int getOrder() - { - return mOrder; - } - - public boolean isEvenOrder() - { - return mOrder % 2 == 0; - } - - public boolean isOddOrder() - { - return mOrder % 2 == 1; - } - - public int getFilterLength() - { - return mOrder + 1; - } - - public int getBandCount() - { - return mFrequencyBands.size(); - } - - /** - * Grid density setting. Default value is 16. - */ - public int getGridDensity() - { - return mGridDensity; - } - - /** - * Sets the grid density setting. Default value is 16. - */ - public void setGridDensity( int density ) - { - mGridDensity = density; - - updateGridSize(); - } - - /** - * Maximum number of alternations/extrema (L + 2 or M + 2) - */ - public int getExtremaCount() - { - return getHalfFilterOrder() + 2; - } - - /** - * Half of the filter order. - * - * This value is referred to as L or M, depending on the source. - */ - public int getHalfFilterOrder() - { - switch ( mRemezFilterType ) - { - case TYPE_1_ODD_LENGTH_EVEN_ORDER_SYMMETRICAL: - return ( mOrder ) / 2; - case TYPE_2_EVEN_LENGTH_ODD_ORDER_SYMMETRICAL: - return ( mOrder - 1 ) / 2; - case TYPE_3_ODD_LENGTH_EVEN_ORDER_ANTI_SYMMETRICAL: - return ( mOrder - 2 ) / 2; - case TYPE_4_EVEN_LENGTH_ODD_ORDER_ANTI_SYMMETRICAL: - return ( mOrder - 1 ) / 2; - default: - return 0; - } - } - - /** - * Determines the max band ripple from across the frequency bands in this specification. - */ - public double getMaxBandAmplitude() - { - double maxRippleAmplitude = 0.0; - - for ( FrequencyBand band : mFrequencyBands ) - { - double bandRippleAmplitude = band.getRippleAmplitude(); - - if ( bandRippleAmplitude > maxRippleAmplitude ) - { - maxRippleAmplitude = bandRippleAmplitude; - } - } - - return maxRippleAmplitude; - } - - public int getNominalGridSize() - { - return ( getExtremaCount() - 1 ) * getGridDensity() + 1; - } - - public int getGridSize() - { - int gridSize = 0; - - for ( FrequencyBand band : mFrequencyBands ) - { - gridSize += band.getGridSize(); - } - - return gridSize; - } - - private void updateGridSize() - { - if ( !mFrequencyBands.isEmpty() ) - { - int gridSize = getNominalGridSize(); - - double totalBandwidth = getTotalBandwidth(); - - for ( FrequencyBand band : mFrequencyBands ) - { - band.setGridSize( gridSize, totalBandwidth ); - } - } - } - - // /** - // * Determines the grid size needed to represent the percentage of weighted - // frequency bands - // * over the entire frequency band. - // */ - // public int getGridSize() - // { - // int gridSize = 0; - // - // int maxGridSize = getMaxGridSize(); - // - // //Add the proportional index counts from each weighted band - // for( FrequencyBand band: mFrequencyBands ) - // { - // gridSize += band.getBandGridSize( maxGridSize ); - // } - // - // if( getSymmetry() == Symmetry.NEGATIVE ) - // { - // gridSize--; - // } - // - // return gridSize; - // } - - /** - * Dense frequency spacing for extrema count * grid density over a frequency range of 0.0 - 1.0 - */ - public double getGridFrequencyInterval() - { - return getTotalBandwidth() / (double) ( ( getGridSize() - mFrequencyBands.size() ) ); - } - - // /** - // * ''' FIR order estimator (lowpass, highpass, bandpass, mulitiband). - // * - // * (n, fo, ao, w) = remezord (f, a, dev) (n, fo, ao, w) = remezord (f, a, - // * dev, fs) - // * - // * (n, fo, ao, w) = remezord (f, a, dev) finds the approximate order, - // * normalized frequency band edges, frequency band amplitudes, and weights - // * that meet input specifications f, a, and dev, to use with the remez - // * command. - // * - // * f is a sequence of frequency band edges (between 0 and Fs/2, where Fs - // is - // * the sampling frequency), and a is a sequence specifying the desired - // * amplitude on the bands defined by f. The length of f is twice the - // length - // * of a, minus 2. The desired function is piecewise constant. - // * - // * dev is a sequence the same size as a that specifies the maximum - // allowable - // * deviation or ripples between the frequency response and the desired - // * amplitude of the output filter, for each band. - // * - // * Use remez with the resulting order n, frequency sequence fo, amplitude - // * response sequence ao, and weights w to design the filter b which - // * approximately meets the specifications given by remezord input - // parameters - // * f, a, and dev: - // * - // * b = remez (n, fo, ao, w) - // * - // * (n, fo, ao, w) = remezord (f, a, dev, Fs) specifies a sampling - // frequency - // * Fs. - // * - // * Fs defaults to 2 Hz, implying a Nyquist frequency of 1 Hz. You can - // * therefore specify band edges scaled to a particular applications - // sampling - // * frequency. - // * - // * In some cases remezord underestimates the order n. If the filter does - // not - // * meet the specifications, try a higher order such as n+1 or n+2. ''' - // * - // * @param fcuts - // * @param mags - // * @param devs - // * @param fsamp - // */ - // // TODO: rename to calculateFilterOrder() - // public static FIRFilterSpecification remezOrd( int[] fcutoffs, - // double[] mags, double[] devs, int fsamp ) - // { - // // def remezord (fcuts, mags, devs, fsamp = 2): - // - // double[] fcuts = new double[ fcutoffs.length ]; - // - // for ( int x = 0; x < fcuts.length; x++ ) - // { - // fcuts[ x ] = (double) fcutoffs[ x ] / (double) fsamp; - // } - // - // int nf = fcuts.length; - // int nm = mags.length; - // int nd = devs.length; - // int nbands = nm; - // - // if ( mags.length != devs.length ) - // { - // throw new IllegalArgumentException( - // "Length of magnitudes and deviations must be equal" ); - // } - // - // if ( fcuts.length != ( 2 * ( mags.length - 1 ) ) ) - // { - // throw new IllegalArgumentException( - // "Length of frequency band edges must be 2 * mags length - 2" ); - // } - // - // for ( int x = 0; x < mags.length; x++ ) - // { - // if ( mags[ x ] != 0 ) - // { - // devs[ x ] = devs[ x ] / mags[ x ]; - // } - // } - // - // // Separate the passband and stopband edges - // double[] f1 = new double[ fcuts.length / 2 ]; - // double[] f2 = new double[ fcuts.length / 2 ]; - // - // for ( int x = 0; x < fcuts.length / 2; x++ ) - // { - // f1[ x ] = fcuts[ 2 * x ]; - // f2[ x ] = fcuts[ 2 * x + 1 ]; - // } - // - // int n = 0; - // - // double min_delta = 2.0; - // - // for ( int x = 0; x < f1.length; x++ ) - // { - // if ( f2[ x ] - f1[ x ] < min_delta ) - // { - // n = x; - // min_delta = f2[ x ] - f1[ x ]; - // } - // } - // - // double l; - // - // if ( nbands == 2 ) - // { - // // lowpass or highpass case (use formula) - // l = lporder( f1[ n ], f2[ n ], devs[ 0 ], devs[ 1 ] ); - // } else - // { - // // bandpass or multipass case - // // try different lowpasses and take the worst one that - // // goes through the BP specs - // l = 0; - // - // for ( int x = 1; x < nbands - 1; x++ ) - // { - // double l1 = lporder( f1[ x - 1 ], f2[ x - 1 ], devs[ x ], - // devs[ x - 1 ] ); - // double l2 = lporder( f1[ x ], f2[ x ], devs[ x ], - // devs[ x + 1 ] ); - // l = Math.max( l1, l2 ); - // } - // } - // - // n = (int) ( Math.ceil( l ) - 1 ); // need order, not length for remez - // - // // // cook up remez compatible result - // double[] ff = new double[ fcuts.length + 2 ]; - // ff[ 0 ] = 0.0; - // ff[ ff.length - 1 ] = 1.0; - // - // System.arraycopy( fcuts, 0, ff, 1, fcuts.length ); - // - // for ( int x = 1; x < ff.length - 1; x++ ) - // { - // ff[ x ] *= 2; - // } - // - // double[] aa = new double[ mags.length * 2 ]; - // - // for ( int x = 0; x < mags.length; x++ ) - // { - // aa[ 2 * x ] = mags[ x ]; - // aa[ 2 * x + 1 ] = mags[ x ]; - // } - // - // double max_dev = 0; - // - // for ( double dev : devs ) - // { - // if ( dev > max_dev ) - // { - // max_dev = dev; - // } - // } - // - // double[] wts = new double[ devs.length ]; - // - // for ( int x = 0; x < devs.length; x++ ) - // { - // wts[ x ] = max_dev / devs[ x ]; - // } - // - // return null; - // // return new RemezFilterSpecification( RemezFilterType.BANDPASS, n, ff, - // // aa, wts, 16); - // } - - /** - * Low-pass FIR filter specification builder - */ - public static class LowPassBuilder - { - private int mSampleRate; - private int mOrder; - private int mGridDensity = 16; - private int mPassBandEndFrequency; - private int mStopBandStartFrequency; - private double mPassBandRipple; - private double mStopBandRipple; - private double mPassBandAmplitude = 1.0; - private double mStopBandAmplitude = 0.0; - - public LowPassBuilder() - { - } - - public LowPassBuilder sampleRate( int sampleRateHz ) - { - mSampleRate = sampleRateHz; - return this; - } - - /** - * Sets the filter order. If the order is not specified, it will be calculated from the - * other parameters. - */ - public LowPassBuilder order( int order ) - { - mOrder = order; - return this; - } - - public LowPassBuilder gridDensity( int density ) - { - mGridDensity = density; - return this; - } - - public LowPassBuilder passBandCutoff( int frequencyHz ) - { - mPassBandEndFrequency = frequencyHz; - return this; - } - - public LowPassBuilder stopBandStart( int frequencyHz ) - { - mStopBandStartFrequency = frequencyHz; - return this; - } - - public LowPassBuilder passBandRipple( double rippleDb ) - { - mPassBandRipple = rippleDb; - return this; - } - - public LowPassBuilder stopBandRipple( double rippleDb ) - { - mStopBandRipple = rippleDb; - return this; - } - - public LowPassBuilder passBandAmplitude( double amplitude ) - { - mPassBandAmplitude = amplitude; - return this; - } - - public LowPassBuilder stopBandAmplitude( double amplitude ) - { - mStopBandAmplitude = amplitude; - return this; - } - - public FIRFilterSpecification build() - { - if ( mOrder < 6 ) - { - mOrder = estimateFilterOrder( mSampleRate, mPassBandEndFrequency, - mStopBandStartFrequency, mPassBandRipple, mStopBandRipple ); - } - - FIRLinearPhaseFilterType type = ( mOrder % 2 == 0 - ? FIRLinearPhaseFilterType.TYPE_1_ODD_LENGTH_EVEN_ORDER_SYMMETRICAL - : FIRLinearPhaseFilterType.TYPE_2_EVEN_LENGTH_ODD_ORDER_SYMMETRICAL ); - - FIRFilterSpecification spec = new FIRFilterSpecification( type, mOrder, mGridDensity ); - - FrequencyBand passBand = new FrequencyBand( mSampleRate, 0, mPassBandEndFrequency, - mPassBandAmplitude, mPassBandRipple ); - spec.addFrequencyBand( passBand ); - - FrequencyBand stopBand = new FrequencyBand( mSampleRate, mStopBandStartFrequency, - mSampleRate / 2, mStopBandAmplitude, mStopBandRipple ); - spec.addFrequencyBand( stopBand ); - - return spec; - } - } - - /** - * Remez high-pass FIR filter specification builder - */ - public static class HighPassBuilder - { - private int mSampleRate; - private int mOrder = -1; - private int mGridDensity = 16; - private int mPassBandStartFrequency; - private int mStopBandEndFrequency; - private double mPassBandRipple; - private double mStopBandRipple; - private double mPassBandAmplitude = 1.0; - private double mStopBandAmplitude = 0.0; - - public HighPassBuilder() - { - } - - public HighPassBuilder sampleRate( int sampleRateHz ) - { - mSampleRate = sampleRateHz; - return this; - } - - /** - * Sets the filter order. If the order is not specified, it will be calculated from the - * other parameters. - */ - public HighPassBuilder order( int order ) - { - mOrder = order; - return this; - } - - public HighPassBuilder gridDensity( int density ) - { - mGridDensity = density; - return this; - } - - public HighPassBuilder passBandStart( int frequencyHz ) - { - mPassBandStartFrequency = frequencyHz; - return this; - } - - public HighPassBuilder stopBandCutoff( int frequencyHz ) - { - mStopBandEndFrequency = frequencyHz; - return this; - } - - public HighPassBuilder passBandRipple( double rippleDb ) - { - mPassBandRipple = rippleDb; - return this; - } - - public HighPassBuilder stopBandRipple( double rippleDb ) - { - mStopBandRipple = rippleDb; - return this; - } - - public HighPassBuilder passBandAmplitude( double amplitude ) - { - mPassBandAmplitude = amplitude; - return this; - } - - public HighPassBuilder stopBandAmplitude( double amplitude ) - { - mStopBandAmplitude = amplitude; - return this; - } - - public FIRFilterSpecification build() - { - if ( mOrder < 6 ) - { - mOrder = estimateFilterOrder( mSampleRate, mStopBandEndFrequency, - mPassBandStartFrequency, mPassBandRipple, mStopBandRipple ); - } - - // High pass is a Type 1 filter -- ensure order is even - if ( mOrder % 2 == 1 ) - { - mOrder++; - } - - FIRFilterSpecification spec = new FIRFilterSpecification( - FIRLinearPhaseFilterType.TYPE_1_ODD_LENGTH_EVEN_ORDER_SYMMETRICAL, mOrder, - mGridDensity ); - - FrequencyBand stopBand = new FrequencyBand( mSampleRate, 0, mStopBandEndFrequency, - mStopBandAmplitude, mStopBandRipple ); - spec.addFrequencyBand( stopBand ); - - FrequencyBand passBand = new FrequencyBand( mSampleRate, mPassBandStartFrequency, - mSampleRate / 2, mPassBandAmplitude, mPassBandRipple ); - spec.addFrequencyBand( passBand ); - - return spec; - } - } - - /** - * Band-pass or band-stop FIR filter specification builder - */ - public static class BandPassBuilder - { - private int mOrder; - private int mGridDensity = 16; - private int mSampleRate; - private int mStopFrequency1; - private int mPassFrequencyBegin; - private int mPassFrequencyEnd; - private int mStopFrequency2; - private double mStopRipple = 0.01; - private double mPassRipple = 0.01; - private double mStopAmplitude = 0.0; - private double mPassAmplitude = 1.0; - - public BandPassBuilder() - { - } - - /** - * Sets the filter order. If the order is not specified, it will be calculated from the - * other parameters. If you specify and odd-length order, it will be corrected (+1) to make - * it an even order number. - */ - public BandPassBuilder order( int order ) - { - mOrder = order; - return this; - } - - /** - * Sets the frequency grid density. Default is 16. - */ - public BandPassBuilder gridDensity( int density ) - { - mGridDensity = density; - return this; - } - - /** - * Sets the filter sample rate in hertz - */ - public BandPassBuilder sampleRate( int sampleRate ) - { - mSampleRate = sampleRate; - return this; - } - - /** - * Specifies the ending frequency of the first stop band - */ - public BandPassBuilder stopFrequency1( int stopFrequency ) - { - mStopFrequency1 = stopFrequency; - return this; - } - - /** - * Specifies the beginning frequency of the second stop band - */ - public BandPassBuilder stopFrequency2( int stopFrequency ) - { - mStopFrequency2 = stopFrequency; - return this; - } - - /** - * Specifies the pass band beginning frequency - */ - public BandPassBuilder passFrequencyBegin( int passFrequency ) - { - mPassFrequencyBegin = passFrequency; - return this; - } - - /** - * Specifies the pass band ending frequency - */ - public BandPassBuilder passFrequencyEnd( int passFrequency ) - { - mPassFrequencyEnd = passFrequency; - return this; - } - - /** - * Specifies the ripple in dB for both stop bands - */ - public BandPassBuilder stopRipple( double rippleDb ) - { - mStopRipple = rippleDb; - return this; - } - - /** - * Specifies the ripple in dB for the pass band - */ - public BandPassBuilder passRipple( double rippleDb ) - { - mPassRipple = rippleDb; - return this; - } - - /** - * Specifies the amplitude of the stop bands. Default is 0.0 - */ - public BandPassBuilder stopAmplitude( double amplitude ) - { - mStopAmplitude = amplitude; - return this; - } - - /** - * Specifies the amplitude of the pass band. Default is 1.0 - */ - public BandPassBuilder passAmplitude( double amplitude ) - { - mPassAmplitude = amplitude; - return this; - } - - /** - * Validates the frequency bands to test for overlap and completeness. - */ - private void validateFrequencyBands() - { - if ( mStopFrequency1 >= mPassFrequencyBegin ) - { - throw new IllegalArgumentException( "Stop band 1 ending frequency [" - + mStopFrequency1 + "] must be less than the pass band start frequency [" - + mPassFrequencyBegin + "]" ); - } - - if ( mPassFrequencyBegin >= mPassFrequencyEnd ) - { - throw new IllegalArgumentException( "Pass band begin frequency [" - + mPassFrequencyBegin + "] must be less than the pass band end frequency [" - + mPassFrequencyEnd + "]" ); - } - - if ( mPassFrequencyEnd >= mStopFrequency2 ) - { - throw new IllegalArgumentException( "Pass band end frequency [" + mPassFrequencyEnd - + "] must be less than stop band 2 beginning frequency [" + mStopFrequency2 - + "]" ); - } - - if ( mStopFrequency2 >= ( mSampleRate / 2 ) ) - { - throw new IllegalArgumentException( "Stop band 2 beginning frequency [" - + mStopFrequency2 + "] must be less than half of the sample rate [" - + ( mSampleRate / 2 ) + "]" ); - } - } - - public FIRFilterSpecification build() - { - validateFrequencyBands(); - - FIRLinearPhaseFilterType type = FIRLinearPhaseFilterType.TYPE_1_ODD_LENGTH_EVEN_ORDER_SYMMETRICAL; - - if ( mOrder < 10 ) - { - mOrder = estimateBandPassOrder( mSampleRate, mPassFrequencyBegin, mPassFrequencyEnd, - mPassRipple, mStopRipple ); - } - - // Ensure even order since we're designing a Type 1 filter - if ( mOrder % 2 == 1 ) - { - mOrder++; - } - - FIRFilterSpecification spec = new FIRFilterSpecification( type, mOrder, mGridDensity ); - - spec.addFrequencyBand( new FrequencyBand( mSampleRate, 0, mStopFrequency1, - mStopAmplitude, mStopRipple ) ); - spec.addFrequencyBand( new FrequencyBand( mSampleRate, mPassFrequencyBegin, - mPassFrequencyEnd, mPassAmplitude, mPassRipple ) ); - spec.addFrequencyBand( new FrequencyBand( mSampleRate, mStopFrequency2, - (int) ( mSampleRate / 2 ), mStopAmplitude, mStopRipple ) ); - - return spec; - } - } - - // /** - // * Converts the set of band amplitude values into a set of weights - // * normalized to the largest band amplitude value - // */ - // private static double[] getBandWeights( double... amplitudes ) - // { - // double maximum = 0.0; - // - // for ( double amplitude : amplitudes ) - // { - // if ( amplitude > maximum ) - // { - // maximum = amplitude; - // } - // } - // - // double[] weights = new double[ amplitudes.length ]; - // - // for ( int x = 0; x < amplitudes.length; x++ ) - // { - // weights[ x ] = maximum / amplitudes[ x ]; - // } - // - // return weights; - // } - - /** - * Calculates filter order from filter length - */ - private static int getFilterOrder( double length ) - { - return (int) ( Math.ceil( length ) - 1 ); - } - - /** - * Estimates filter length for a two-band (high or low pass) FIR filter. - * - * Note: this estimator doesn't work well if the transition frequency is near 0 or sampleRate/2 - * - * From Herrmann et al (1973), Practical design rules for optimum finite impulse response - * filters. Bell System Technical J., 52, 769-99 - * - * @param sampleRate in hertz - * @param frequency1 in hertz - * @param frequency2 in hertz - * @param passBandRipple pass band ripple in dB - * @param stopBandRipple stop band ripple in dB - * - * @return estimated filter length - */ - private static int estimateFilterOrder( int sampleRate, int frequency1, int frequency2, - double passBandRipple, double stopBandRipple ) - { - double df = Math.abs( frequency2 - frequency1 ) / (double) sampleRate; - - double ddp = Math - .log10( stopBandRipple <= passBandRipple ? passBandRipple : stopBandRipple ); - double dds = Math - .log10( stopBandRipple <= passBandRipple ? stopBandRipple : passBandRipple ); - - double a1 = 5.309e-3; - double a2 = 7.114e-2; - double a3 = -4.761e-1; - double a4 = -2.66e-3; - double a5 = -5.941e-1; - double a6 = -4.278e-1; - - double b1 = 11.01217; - double b2 = 0.5124401; - - double t1 = a1 * ddp * ddp; - double t2 = a2 * ddp; - double t3 = a4 * ddp * ddp; - double t4 = a5 * ddp; - - double dinf = ( ( t1 + t2 + a3 ) * dds ) + ( t3 + t4 + a6 ); - double ff = b1 + b2 * ( ddp - dds ); - double n = dinf / df - ff * df + 1.0; - - return (int) Math.ceil( n ); - } - - /** - * Estimates band pass filter length; - * - * @param passBandStart frequency normalized to sample rate - * @param passBandEnd frequency normalized to sample rate - * @param passBandRippleDb - * @param stopBandRippleDb - */ - public static int estimateBandPassOrder( int sampleRate, int passBandStart, int passBandEnd, - double passBandRippleDb, double stopBandRippleDb ) - { - double df = (double) Math.abs( passBandEnd - passBandStart ) / (double) sampleRate; - double ddp = (double) Math.log10( passBandRippleDb ); - double dds = (double) Math.log10( stopBandRippleDb ); - - double a1 = 0.01201; - double a2 = 0.09664; - double a3 = -0.51325; - double a4 = 0.00203; - double a5 = -0.57054; - double a6 = -0.44314; - - double t1 = a1 * ddp * ddp; - double t2 = a2 * ddp; - double t3 = a4 * ddp * ddp; - double t4 = a5 * ddp; - - double cinf = dds * ( t1 + t2 + a3 ) + t3 + t4 + a6; - double ginf = -14.6f * (double) Math.log10( passBandRippleDb / stopBandRippleDb ) - 16.9; - double n = cinf / df + ginf * df + 1.0; - - return (int) Math.ceil( n ); - } - - /** - * Frequency band - */ - public static class FrequencyBand - { - private int mGridSize; - private double mStart; - private double mEnd; - private double mAmplitude; - private double mRippleDB; - - /** - * Constructs a frequency band where the start and end frequencies are normalized to a - * sample rate of 1. - * - * @param start edge of the frequency band (0.0 - 1.0) - * @param end edge of the frequency band (0.0 - 1.0) - * @param amplitude of the frequency band on the unity scale (0.0 - 1.0) - * @param ripple of the frequency band specified as db ripple - */ - public FrequencyBand( double start, double end, double amplitude, double ripple ) - { - assert ( 0.0 <= mStart ); - assert ( mStart < mEnd ); - assert ( mEnd <= 1.0 ); - assert ( 0.0 <= amplitude && amplitude <= 1.0 ); - assert ( 0.0 <= ripple ); - - mStart = start; - mEnd = end; - mAmplitude = amplitude; - mRippleDB = ripple; - } - - /** - * Constructs a frequency band for the sample rate and start and end frequencies. - * - * @param sampleRate in hertz - * @param start edge of the frequency band in hertz - * @param end edge of the frequency band in hertz - * @param amplitude of the frequency band relative to unity (0.0 - 1.0) - * @param ripple of the frequency band specified as db ripple - */ - public FrequencyBand( int sampleRate, int start, int end, double amplitude, double ripple ) - { - this( (double) start / (double) sampleRate, (double) end / (double) sampleRate, - amplitude, ripple ); - } - - public int getGridSize() - { - return mGridSize; - } - - public void setGridSize( int totalGridSize, double totalBandwidth ) - { - mGridSize = (int) Math - .ceil( (double) totalGridSize * ( getBandWidth() / totalBandwidth ) ); - } - - public String toString() - { - StringBuilder sb = new StringBuilder(); - - sb.append( "Band Start: " ); - sb.append( mStart ); - sb.append( " End:" ); - sb.append( mEnd ); - sb.append( " Amplitude: " ); - sb.append( mAmplitude ); - sb.append( " Ripple dB: " ); - sb.append( mRippleDB ); - sb.append( " Ripple Amplitude: " ); - sb.append( DECIMAL_FORMATTER.format( getRippleAmplitude() ) ); - sb.append( " Grid Size: " ); - sb.append( mGridSize ); - - return sb.toString(); - } - - /** - * Start frequency edge for this band normalized to 1 Hz. - */ - public double getStart() - { - return mStart; - } - - /** - * End frequency edge for this band normalized to 1 Hz. - */ - public double getEnd() - { - return mEnd; - } - - /** - * Desired amplitude response for this frequency band - * - * @return amplitude (0.0 - 1.0) - */ - public double getAmplitude() - { - return mAmplitude; - } - - /** - * Frequency band ripple in decibels - */ - public double getRippleDB() - { - return mRippleDB; - } - - /** - * Frequency band ripple as an amplitude value - */ - public double getRippleAmplitude() - { - return ( Math.pow( 10.0, ( mRippleDB / 20 ) ) - 1 ) - / ( Math.pow( 10.0, ( mRippleDB / 20 ) ) + 1 ); - } - - /** - * Calculates the weighting of this frequency band's ripple relative to the max ripple for a - * filter specification. - * - * @param maxRippleAmplitude for the filter specification - * - * @return weight of this frequency band - */ - public double getWeight( double maxRippleAmplitude ) - { - return 1.0 / ( getRippleAmplitude() / maxRippleAmplitude ); - } - - /** - * Double-sided bandwidth of this frequency band - */ - public double getBandWidth() - { - return mEnd - mStart; - } - } + private final static Logger mLog = LoggerFactory.getLogger(FIRFilterSpecification.class); + + private static final double PERFECT_RECONSTRUCTION_GAIN_AT_BAND_EDGE = -6.020599842071533; //0.5 magnitude + private static final DecimalFormat DECIMAL_FORMATTER = new DecimalFormat("0.00000"); + + private FIRLinearPhaseFilterType mRemezFilterType; + private int mOrder; + private int mGridDensity = 16; + private List mFrequencyBands = new ArrayList<>(); + + /** + * Constructs a Remez filter specification. Use one of the filter builders to construct a Remez + * filter specification. + * + * @param type of filter + * @param order of the filter + * @param gridDensity to use when calculating the coefficients + */ + protected FIRFilterSpecification(FIRLinearPhaseFilterType type, int order, int gridDensity) + { + mRemezFilterType = type; + mOrder = order; + mGridDensity = gridDensity; + } + + public String toString() + { + StringBuilder sb = new StringBuilder(); + sb.append("Type: "); + sb.append(mRemezFilterType.name()); + sb.append(" Order: "); + sb.append(mOrder); + sb.append(" Extrema: "); + sb.append(getExtremaCount()); + sb.append(" Grid Density: "); + sb.append(mGridDensity); + sb.append(" Grid Size: "); + sb.append(getGridSize()); + + for(FrequencyBand band : mFrequencyBands) + { + sb.append("\n"); + sb.append(band.toString()); + } + + return sb.toString(); + } + + public void addFrequencyBand(FrequencyBand band) + { + mFrequencyBands.add(band); + + updateGridSize(); + } + + public void addFrequencyBands(Collection bands) + { + mFrequencyBands.addAll(bands); + + updateGridSize(); + } + + /** + * Removes all frequency bands from this filter specification + */ + public void clearFrequencyBands() + { + mFrequencyBands.clear(); + } + + public List getFrequencyBands() + { + return mFrequencyBands; + } + + /** + * Returns the total bandwidth of the defined frequency bands in the specification. + * + * @return total bandwidth -- should be less than or equal to 0.5 + */ + public double getTotalBandwidth() + { + double bandwidth = 0.0; + + for(FrequencyBand band : mFrequencyBands) + { + bandwidth += band.getBandWidth(); + } + + return bandwidth; + } + + /** + * Creates a Low-Pass FIR filter specification builder that allows you to define the filter + * parameters and create a filter specification. + */ + public static LowPassBuilder lowPassBuilder() + { + return new LowPassBuilder(); + } + + /** + * Creates a High-Pass FIR filter specification builder that allows you to define the filter + * parameters and create a filter specification. + */ + public static HighPassBuilder highPassBuilder() + { + return new HighPassBuilder(); + } + + /** + * Creates a Band-Pass FIR filter specification builder that allows you to define the filter + * parameters and create a filter specification. + */ + public static BandPassBuilder bandPassBuilder() + { + return new BandPassBuilder(); + } + + /** + * Creates a Polyphase TunerChannelizer filter specification builder that allows you to define the filter parameters and + * create a filter specification + */ + public static ChannelizerBuilder channelizerBuilder() + { + return new ChannelizerBuilder(); + } + + public FIRLinearPhaseFilterType getFilterType() + { + return mRemezFilterType; + } + + public boolean isSymmetrical() + { + return mRemezFilterType == FIRLinearPhaseFilterType.TYPE_1_ODD_LENGTH_EVEN_ORDER_SYMMETRICAL || + mRemezFilterType == FIRLinearPhaseFilterType.TYPE_2_EVEN_LENGTH_ODD_ORDER_SYMMETRICAL; + } + + public boolean isAntiSymmetrical() + { + return mRemezFilterType == FIRLinearPhaseFilterType.TYPE_3_ODD_LENGTH_EVEN_ORDER_ANTI_SYMMETRICAL || + mRemezFilterType == FIRLinearPhaseFilterType.TYPE_4_EVEN_LENGTH_ODD_ORDER_ANTI_SYMMETRICAL; + } + + public int getOrder() + { + return mOrder; + } + + public boolean isEvenOrder() + { + return mOrder % 2 == 0; + } + + public boolean isOddOrder() + { + return mOrder % 2 == 1; + } + + public int getFilterLength() + { + return mOrder + 1; + } + + public int getBandCount() + { + return mFrequencyBands.size(); + } + + /** + * Grid density setting. Default value is 16. + */ + public int getGridDensity() + { + return mGridDensity; + } + + /** + * Sets the grid density setting. Default value is 16. + */ + public void setGridDensity(int density) + { + mGridDensity = density; + + updateGridSize(); + } + + /** + * Maximum number of alternations/extrema (L + 2 or M + 2) + */ + public int getExtremaCount() + { + return getHalfFilterOrder() + 2; + } + + /** + * Half of the filter order. + * + * This value is referred to as L or M, depending on the source. + */ + public int getHalfFilterOrder() + { + switch(mRemezFilterType) + { + case TYPE_1_ODD_LENGTH_EVEN_ORDER_SYMMETRICAL: + return (mOrder) / 2; + case TYPE_2_EVEN_LENGTH_ODD_ORDER_SYMMETRICAL: + return (mOrder - 1) / 2; + case TYPE_3_ODD_LENGTH_EVEN_ORDER_ANTI_SYMMETRICAL: + return (mOrder - 2) / 2; + case TYPE_4_EVEN_LENGTH_ODD_ORDER_ANTI_SYMMETRICAL: + return (mOrder - 1) / 2; + default: + return 0; + } + } + + /** + * Determines the max band ripple from across the frequency bands in this specification. + */ + public double getMaxBandAmplitude() + { + double maxRippleAmplitude = 0.0; + + for(FrequencyBand band : mFrequencyBands) + { + double bandRippleAmplitude = band.getRippleAmplitude(); + + if(bandRippleAmplitude > maxRippleAmplitude) + { + maxRippleAmplitude = bandRippleAmplitude; + } + } + + return maxRippleAmplitude; + } + + public int getNominalGridSize() + { + return (getExtremaCount() - 1) * getGridDensity() + 1; + } + + public int getGridSize() + { + int gridSize = 0; + + for(FrequencyBand band : mFrequencyBands) + { + gridSize += band.getGridSize(); + } + + return gridSize; + } + + private void updateGridSize() + { + if(!mFrequencyBands.isEmpty()) + { + int gridSize = getNominalGridSize(); + + double totalBandwidth = getTotalBandwidth(); + + for(FrequencyBand band : mFrequencyBands) + { + band.setGridSize(gridSize, totalBandwidth); + } + } + } + + /** + * Dense frequency spacing for extrema count * grid density over a frequency range of 0.0 - 1.0 + */ + public double getGridFrequencyInterval() + { + return getTotalBandwidth() / (double)((getGridSize() - mFrequencyBands.size())); + } + + /** + * Low-pass FIR filter specification builder + */ + public static class LowPassBuilder + { + private int mSampleRate; + private int mOrder; + private int mGridDensity = 16; + private int mPassBandEndFrequency; + private int mStopBandStartFrequency; + private double mPassBandRipple; + private double mStopBandRipple; + private double mPassBandAmplitude = 1.0; + private double mStopBandAmplitude = 0.0; + + public LowPassBuilder() + { + } + + public LowPassBuilder sampleRate(int sampleRateHz) + { + mSampleRate = sampleRateHz; + return this; + } + + /** + * Sets the filter order. If the order is not specified, it will be calculated from the + * other parameters. + */ + public LowPassBuilder order(int order) + { + mOrder = order; + return this; + } + + public LowPassBuilder gridDensity(int density) + { + mGridDensity = density; + return this; + } + + public LowPassBuilder passBandCutoff(int frequencyHz) + { + mPassBandEndFrequency = frequencyHz; + return this; + } + + public LowPassBuilder stopBandStart(int frequencyHz) + { + mStopBandStartFrequency = frequencyHz; + return this; + } + + public LowPassBuilder passBandRipple(double rippleDb) + { + mPassBandRipple = rippleDb; + return this; + } + + public LowPassBuilder stopBandRipple(double rippleDb) + { + mStopBandRipple = rippleDb; + return this; + } + + public LowPassBuilder passBandAmplitude(double amplitude) + { + mPassBandAmplitude = amplitude; + return this; + } + + public LowPassBuilder stopBandAmplitude(double amplitude) + { + mStopBandAmplitude = amplitude; + return this; + } + + public FIRFilterSpecification build() + { + if(mOrder < 6) + { + mOrder = estimateFilterOrder(mSampleRate, mPassBandEndFrequency, + mStopBandStartFrequency, mPassBandRipple, mStopBandRipple); + } + + FIRLinearPhaseFilterType type = (mOrder % 2 == 0 + ? FIRLinearPhaseFilterType.TYPE_1_ODD_LENGTH_EVEN_ORDER_SYMMETRICAL + : FIRLinearPhaseFilterType.TYPE_2_EVEN_LENGTH_ODD_ORDER_SYMMETRICAL); + + FIRFilterSpecification spec = new FIRFilterSpecification(type, mOrder, mGridDensity); + + FrequencyBand passBand = new FrequencyBand(mSampleRate, 0, mPassBandEndFrequency, + mPassBandAmplitude, mPassBandRipple); + spec.addFrequencyBand(passBand); + + FrequencyBand stopBand = new FrequencyBand(mSampleRate, mStopBandStartFrequency, + mSampleRate / 2, mStopBandAmplitude, mStopBandRipple); + spec.addFrequencyBand(stopBand); + + return spec; + } + } + + /** + * Remez high-pass FIR filter specification builder + */ + public static class HighPassBuilder + { + private int mSampleRate; + private int mOrder = -1; + private int mGridDensity = 16; + private int mPassBandStartFrequency; + private int mStopBandEndFrequency; + private double mPassBandRipple; + private double mStopBandRipple; + private double mPassBandAmplitude = 1.0; + private double mStopBandAmplitude = 0.0; + + public HighPassBuilder() + { + } + + public HighPassBuilder sampleRate(int sampleRateHz) + { + mSampleRate = sampleRateHz; + return this; + } + + /** + * Sets the filter order. If the order is not specified, it will be calculated from the + * other parameters. + */ + public HighPassBuilder order(int order) + { + mOrder = order; + return this; + } + + public HighPassBuilder gridDensity(int density) + { + mGridDensity = density; + return this; + } + + public HighPassBuilder passBandStart(int frequencyHz) + { + mPassBandStartFrequency = frequencyHz; + return this; + } + + public HighPassBuilder stopBandCutoff(int frequencyHz) + { + mStopBandEndFrequency = frequencyHz; + return this; + } + + public HighPassBuilder passBandRipple(double rippleDb) + { + mPassBandRipple = rippleDb; + return this; + } + + public HighPassBuilder stopBandRipple(double rippleDb) + { + mStopBandRipple = rippleDb; + return this; + } + + public HighPassBuilder passBandAmplitude(double amplitude) + { + mPassBandAmplitude = amplitude; + return this; + } + + public HighPassBuilder stopBandAmplitude(double amplitude) + { + mStopBandAmplitude = amplitude; + return this; + } + + public FIRFilterSpecification build() + { + if(mOrder < 6) + { + mOrder = estimateFilterOrder(mSampleRate, mStopBandEndFrequency, + mPassBandStartFrequency, mPassBandRipple, mStopBandRipple); + } + + // High pass is a Type 1 filter -- ensure order is even + if(mOrder % 2 == 1) + { + mOrder++; + } + + FIRFilterSpecification spec = new FIRFilterSpecification( + FIRLinearPhaseFilterType.TYPE_1_ODD_LENGTH_EVEN_ORDER_SYMMETRICAL, mOrder, + mGridDensity); + + FrequencyBand stopBand = new FrequencyBand(mSampleRate, 0, mStopBandEndFrequency, + mStopBandAmplitude, mStopBandRipple); + spec.addFrequencyBand(stopBand); + + FrequencyBand passBand = new FrequencyBand(mSampleRate, mPassBandStartFrequency, + mSampleRate / 2, mPassBandAmplitude, mPassBandRipple); + spec.addFrequencyBand(passBand); + + return spec; + } + } + + /** + * Band-pass or band-stop FIR filter specification builder + */ + public static class BandPassBuilder + { + private int mOrder; + private int mGridDensity = 16; + private int mSampleRate; + private int mStopFrequency1; + private int mPassFrequencyBegin; + private int mPassFrequencyEnd; + private int mStopFrequency2; + private double mStopRipple = 0.01; + private double mPassRipple = 0.01; + private double mStopAmplitude = 0.0; + private double mPassAmplitude = 1.0; + + public BandPassBuilder() + { + } + + /** + * Sets the filter order. If the order is not specified, it will be calculated from the + * other parameters. If you specify and odd-length order, it will be corrected (+1) to make + * it an even order number. + */ + public BandPassBuilder order(int order) + { + mOrder = order; + return this; + } + + /** + * Sets the frequency grid density. Default is 16. + */ + public BandPassBuilder gridDensity(int density) + { + mGridDensity = density; + return this; + } + + /** + * Sets the filter sample rate in hertz + */ + public BandPassBuilder sampleRate(int sampleRate) + { + mSampleRate = sampleRate; + return this; + } + + /** + * Specifies the ending frequency of the first stop band + */ + public BandPassBuilder stopFrequency1(int stopFrequency) + { + mStopFrequency1 = stopFrequency; + return this; + } + + /** + * Specifies the beginning frequency of the second stop band + */ + public BandPassBuilder stopFrequency2(int stopFrequency) + { + mStopFrequency2 = stopFrequency; + return this; + } + + /** + * Specifies the pass band beginning frequency + */ + public BandPassBuilder passFrequencyBegin(int passFrequency) + { + mPassFrequencyBegin = passFrequency; + return this; + } + + /** + * Specifies the pass band ending frequency + */ + public BandPassBuilder passFrequencyEnd(int passFrequency) + { + mPassFrequencyEnd = passFrequency; + return this; + } + + /** + * Specifies the ripple in dB for both stop bands + */ + public BandPassBuilder stopRipple(double rippleDb) + { + mStopRipple = rippleDb; + return this; + } + + /** + * Specifies the ripple in dB for the pass band + */ + public BandPassBuilder passRipple(double rippleDb) + { + mPassRipple = rippleDb; + return this; + } + + /** + * Specifies the amplitude of the stop bands. Default is 0.0 + */ + public BandPassBuilder stopAmplitude(double amplitude) + { + mStopAmplitude = amplitude; + return this; + } + + /** + * Specifies the amplitude of the pass band. Default is 1.0 + */ + public BandPassBuilder passAmplitude(double amplitude) + { + mPassAmplitude = amplitude; + return this; + } + + /** + * Validates the frequency bands to test for overlap and completeness. + */ + private void validateFrequencyBands() + { + if(mStopFrequency1 >= mPassFrequencyBegin) + { + throw new IllegalArgumentException("Stop band 1 ending frequency [" + + mStopFrequency1 + "] must be less than the pass band start frequency [" + + mPassFrequencyBegin + "]"); + } + + if(mPassFrequencyBegin >= mPassFrequencyEnd) + { + throw new IllegalArgumentException("Pass band begin frequency [" + + mPassFrequencyBegin + "] must be less than the pass band end frequency [" + + mPassFrequencyEnd + "]"); + } + + if(mPassFrequencyEnd >= mStopFrequency2) + { + throw new IllegalArgumentException("Pass band end frequency [" + mPassFrequencyEnd + + "] must be less than stop band 2 beginning frequency [" + mStopFrequency2 + + "]"); + } + + if(mStopFrequency2 >= (mSampleRate / 2)) + { + throw new IllegalArgumentException("Stop band 2 beginning frequency [" + + mStopFrequency2 + "] must be less than half of the sample rate [" + + (mSampleRate / 2) + "]"); + } + } + + public FIRFilterSpecification build() + { + validateFrequencyBands(); + + FIRLinearPhaseFilterType type = FIRLinearPhaseFilterType.TYPE_1_ODD_LENGTH_EVEN_ORDER_SYMMETRICAL; + + if(mOrder < 10) + { + mOrder = estimateBandPassOrder(mSampleRate, mPassFrequencyBegin, mPassFrequencyEnd, + mPassRipple, mStopRipple); + } + + // Ensure even order since we're designing a Type 1 filter + if(mOrder % 2 == 1) + { + mOrder++; + } + + FIRFilterSpecification spec = new FIRFilterSpecification(type, mOrder, mGridDensity); + + spec.addFrequencyBand(new FrequencyBand(mSampleRate, 0, mStopFrequency1, + mStopAmplitude, mStopRipple)); + spec.addFrequencyBand(new FrequencyBand(mSampleRate, mPassFrequencyBegin, + mPassFrequencyEnd, mPassAmplitude, mPassRipple)); + spec.addFrequencyBand(new FrequencyBand(mSampleRate, mStopFrequency2, + (int)(mSampleRate / 2), mStopAmplitude, mStopRipple)); + + return spec; + } + } + + /** + * Builder for creating polyphase channelizer low-pass prototype filter. + */ + public static class ChannelizerBuilder + { + private int mGridDensity = 16; + private int mSampleRate; + private int mChannelCount; + private int mChannelBandwidth; + private int mTapsPerChannel; + private double mStopRipple = 0.001; + private double mPassRipple = 0.01; + private double mAlpha = 0.2; + + public ChannelizerBuilder() + { + } + + /** + * Sets the filter sample rate in hertz + */ + public ChannelizerBuilder sampleRate(int sampleRate) + { + mSampleRate = sampleRate; + return this; + } + + /** + * Number of Channels + * @param channels - must be a multiple of 2 + * @return + */ + public ChannelizerBuilder channels(int channels) + { + Validate.isTrue(channels % 2 == 0, "Channel count must be a multiple of 2"); + mChannelCount = channels; + return this; + } + + /** + * Sets the channel bandwidth in hertz + */ + public ChannelizerBuilder channelBandwidth(int channelBandwidth) + { + mChannelBandwidth = channelBandwidth; + return this; + } + + /** + * Number of filter taps per channel. Note: total filter length will be channels * tapsPerChannel + */ + public ChannelizerBuilder tapsPerChannel(int tapsPerChannel) + { + mTapsPerChannel = tapsPerChannel; + return this; + } + + /** + * Sets the frequency grid density. Default is 16. + */ + public ChannelizerBuilder gridDensity(int density) + { + mGridDensity = density; + return this; + } + + /** + * Specifies the ripple in dB for both stop bands + */ + public ChannelizerBuilder stopRipple(double rippleDb) + { + mStopRipple = rippleDb; + return this; + } + + /** + * Specifies the ripple in dB for the pass band + */ + public ChannelizerBuilder passRipple(double rippleDb) + { + mPassRipple = rippleDb; + return this; + } + + /** + * Rolloff factor for the start band and stop band. + * @param alpha - value in range 0.0 - 1.0 + */ + public ChannelizerBuilder alpha(double alpha) + { + Validate.isTrue(0.0 <= alpha && alpha <= 1.0); + mAlpha = alpha; + + return this; + } + + public FIRFilterSpecification build() + { + FIRLinearPhaseFilterType type = FIRLinearPhaseFilterType.TYPE_2_EVEN_LENGTH_ODD_ORDER_SYMMETRICAL; + + int passFrequencyEnd = (int)(mChannelBandwidth * (1.0 - mAlpha)); + int stopFrequencyBegin = (int)(mChannelBandwidth * (1.0 + mAlpha)); + + int estimatedOrder = estimateFilterOrder(mSampleRate, passFrequencyEnd, stopFrequencyBegin, mPassRipple, + mStopRipple); + + // Ensure odd order since we're designing a Type 2 filter + if(estimatedOrder % 2 == 0) + { + estimatedOrder++; + } + + int order = mChannelCount * mTapsPerChannel - 1; + + mLog.debug("Order:" + order + " Sample Rate:" + mSampleRate + " Estimated Order:" + estimatedOrder + " Requested Order:" + order); + + FIRFilterSpecification spec = new FIRFilterSpecification(type, order, mGridDensity); + + double bandEdge = (double)mChannelBandwidth / (double)mSampleRate; + double passEnd = bandEdge * (1.0 - mAlpha); + double stopStart = bandEdge * (1.0 + mAlpha); + + mLog.debug("Pass End:" + passEnd + " Band Edge:" + bandEdge + " Stop Begin:" + stopStart); + mLog.debug("Pass End:" + (int)(mSampleRate * passEnd) + + " Band Edge:" + (int)(mSampleRate * bandEdge) + + " Stop Begin:" + (int)(mSampleRate * stopStart)); + + FrequencyBand passBand = new FrequencyBand(0.0, passEnd, 1.0, mPassRipple); + FrequencyBand edgeBand = new FrequencyBand(bandEdge, bandEdge, 0.5, mPassRipple); + edgeBand.setWeight(8.0 * edgeBand.getWeight(mPassRipple)); + FrequencyBand stopBand = new FrequencyBand(stopStart, 0.5, 0.0, mStopRipple); + + spec.addFrequencyBand(passBand); + spec.addFrequencyBand(edgeBand); + spec.addFrequencyBand(stopBand); + + mLog.debug(spec.toString()); + return spec; + } + } + + // /** + // * Converts the set of band amplitude values into a set of weights + // * normalized to the largest band amplitude value + // */ + // private static double[] getBandWeights( double... amplitudes ) + // { + // double maximum = 0.0; + // + // for ( double amplitude : amplitudes ) + // { + // if ( amplitude > maximum ) + // { + // maximum = amplitude; + // } + // } + // + // double[] weights = new double[ amplitudes.length ]; + // + // for ( int x = 0; x < amplitudes.length; x++ ) + // { + // weights[ x ] = maximum / amplitudes[ x ]; + // } + // + // return weights; + // } + + /** + * Calculates filter order from filter length + */ + private static int getFilterOrder(double length) + { + return (int)(Math.ceil(length) - 1); + } + + /** + * Estimates filter length for a two-band (high or low pass) FIR filter. + * + * Note: this estimator doesn't work well if the transition frequency is near 0 or sampleRate/2 + * + * From Herrmann et al (1973), Practical design rules for optimum finite impulse response + * filters. Bell System Technical J., 52, 769-99 + * + * @param sampleRate in hertz + * @param frequency1 in hertz + * @param frequency2 in hertz + * @param passBandRipple pass band ripple in dB + * @param stopBandRipple stop band ripple in dB + * @return estimated filter length + */ + public static int estimateFilterOrder(int sampleRate, int frequency1, int frequency2, + double passBandRipple, double stopBandRipple) + { + double df = Math.abs(frequency2 - frequency1) / (double)sampleRate; + + double ddp = Math + .log10(stopBandRipple <= passBandRipple ? passBandRipple : stopBandRipple); + double dds = Math + .log10(stopBandRipple <= passBandRipple ? stopBandRipple : passBandRipple); + + double a1 = 5.309e-3; + double a2 = 7.114e-2; + double a3 = -4.761e-1; + double a4 = -2.66e-3; + double a5 = -5.941e-1; + double a6 = -4.278e-1; + + double b1 = 11.01217; + double b2 = 0.5124401; + + double t1 = a1 * ddp * ddp; + double t2 = a2 * ddp; + double t3 = a4 * ddp * ddp; + double t4 = a5 * ddp; + + double dinf = ((t1 + t2 + a3) * dds) + (t3 + t4 + a6); + double ff = b1 + b2 * (ddp - dds); + double n = dinf / df - ff * df + 1.0; + + return (int)Math.ceil(n); + } + + /** + * Estimates band pass filter length; + * + * @param passBandStart frequency normalized to sample rate + * @param passBandEnd frequency normalized to sample rate + * @param passBandRippleDb + * @param stopBandRippleDb + */ + public static int estimateBandPassOrder(int sampleRate, int passBandStart, int passBandEnd, + double passBandRippleDb, double stopBandRippleDb) + { + double df = (double)Math.abs(passBandEnd - passBandStart) / (double)sampleRate; + double ddp = (double)Math.log10(passBandRippleDb); + double dds = (double)Math.log10(stopBandRippleDb); + + double a1 = 0.01201; + double a2 = 0.09664; + double a3 = -0.51325; + double a4 = 0.00203; + double a5 = -0.57054; + double a6 = -0.44314; + + double t1 = a1 * ddp * ddp; + double t2 = a2 * ddp; + double t3 = a4 * ddp * ddp; + double t4 = a5 * ddp; + + double cinf = dds * (t1 + t2 + a3) + t3 + t4 + a6; + double ginf = -14.6f * (double)Math.log10(passBandRippleDb / stopBandRippleDb) - 16.9; + double n = cinf / df + ginf * df + 1.0; + + return (int)Math.ceil(n); + } + + /** + * Frequency band + */ + public static class FrequencyBand + { + private int mGridSize; + private double mStart; + private double mEnd; + private double mAmplitude; + private double mRippleDB; + private Double mWeight; + + /** + * Constructs a frequency band where the start and end frequencies are normalized to a + * sample rate of 1. + * + * @param start edge of the frequency band (0.0 - 1.0) + * @param end edge of the frequency band (0.0 - 1.0) + * @param amplitude of the frequency band on the unity scale (0.0 - 1.0) + * @param ripple of the frequency band specified as db ripple + */ + public FrequencyBand(double start, double end, double amplitude, double ripple) + { + assert (0.0 <= mStart); + assert (mStart <= mEnd); + assert (mEnd <= 1.0); + assert (0.0 <= amplitude && amplitude <= 1.0); + assert (0.0 <= ripple); + + mStart = start; + mEnd = end; + mAmplitude = amplitude; + mRippleDB = ripple; + } + + /** + * Constructs a frequency band for the sample rate and start and end frequencies. + * + * @param sampleRate in hertz + * @param start edge of the frequency band in hertz + * @param end edge of the frequency band in hertz + * @param amplitude of the frequency band relative to unity (0.0 - 1.0) + * @param ripple of the frequency band specified as db ripple + */ + public FrequencyBand(int sampleRate, int start, int end, double amplitude, double ripple) + { + this((double)start / (double)sampleRate, (double)end / (double)sampleRate, + amplitude, ripple); + } + + public FrequencyBand(int sampleRate, int start, int end, double amplitude, double ripple, double weight) + { + this(sampleRate, start, end, amplitude, ripple); + mWeight = weight; + } + + /** + * Explicitly sets the weight of this frequency band. + */ + public void setWeight(double weight) + { + mWeight = weight; + } + + public int getGridSize() + { + return mGridSize; + } + + public void setGridSize(int totalGridSize, double totalBandwidth) + { + mGridSize = Math.max(1, (int)Math.ceil((double)totalGridSize * (getBandWidth() / totalBandwidth))); + } + + public String toString() + { + StringBuilder sb = new StringBuilder(); + + sb.append("Band Start: "); + sb.append(mStart); + sb.append(" End:"); + sb.append(mEnd); + sb.append(" Amplitude: "); + sb.append(mAmplitude); + sb.append(" Ripple dB: "); + sb.append(mRippleDB); + sb.append(" Ripple Amplitude: "); + sb.append(DECIMAL_FORMATTER.format(getRippleAmplitude())); + sb.append(" Grid Size: "); + sb.append(mGridSize); + sb.append(" Weight: "); + sb.append(getWeight(mRippleDB)); + + return sb.toString(); + } + + /** + * Start frequency edge for this band normalized to 1 Hz. + */ + public double getStart() + { + return mStart; + } + + /** + * End frequency edge for this band normalized to 1 Hz. + */ + public double getEnd() + { + return mEnd; + } + + /** + * Desired amplitude response for this frequency band + * + * @return amplitude (0.0 - 1.0) + */ + public double getAmplitude() + { + return mAmplitude; + } + + /** + * Frequency band ripple in decibels + */ + public double getRippleDB() + { + return mRippleDB; + } + + /** + * Frequency band ripple as an amplitude value + */ + public double getRippleAmplitude() + { + return (Math.pow(10.0, (mRippleDB / 20)) - 1) + / (Math.pow(10.0, (mRippleDB / 20)) + 1); + } + + /** + * Calculates the weighting of this frequency band's ripple relative to the max ripple for a + * filter specification. + * + * @param maxRippleAmplitude for the filter specification + * @return weight of this frequency band + */ + public double getWeight(double maxRippleAmplitude) + { + if(mWeight == null) + { + return 1.0 / (getRippleAmplitude() / maxRippleAmplitude); + } + + return mWeight; + } + + /** + * Double-sided bandwidth of this frequency band + */ + public double getBandWidth() + { + return mEnd - mStart; + } + } } diff --git a/src/dsp/filter/fir/remez/PolyphaseChannelizerDesigner.java b/src/main/java/io/github/dsheirer/dsp/filter/fir/remez/PolyphaseChannelizerDesigner.java similarity index 92% rename from src/dsp/filter/fir/remez/PolyphaseChannelizerDesigner.java rename to src/main/java/io/github/dsheirer/dsp/filter/fir/remez/PolyphaseChannelizerDesigner.java index 01d8a8ff..6930b127 100644 --- a/src/dsp/filter/fir/remez/PolyphaseChannelizerDesigner.java +++ b/src/main/java/io/github/dsheirer/dsp/filter/fir/remez/PolyphaseChannelizerDesigner.java @@ -16,9 +16,9 @@ * along with this program. If not, see * ******************************************************************************/ -package dsp.filter.fir.remez; +package io.github.dsheirer.dsp.filter.fir.remez; -import dsp.filter.fir.FIRFilterSpecification; +import io.github.dsheirer.dsp.filter.fir.FIRFilterSpecification; public class PolyphaseChannelizerDesigner extends FIRFilterSpecification { diff --git a/src/dsp/filter/fir/remez/PolyphaseChannelizerFilterFactory.java b/src/main/java/io/github/dsheirer/dsp/filter/fir/remez/PolyphaseChannelizerFilterFactory.java similarity index 97% rename from src/dsp/filter/fir/remez/PolyphaseChannelizerFilterFactory.java rename to src/main/java/io/github/dsheirer/dsp/filter/fir/remez/PolyphaseChannelizerFilterFactory.java index 6b0ea0ff..9cad5751 100644 --- a/src/dsp/filter/fir/remez/PolyphaseChannelizerFilterFactory.java +++ b/src/main/java/io/github/dsheirer/dsp/filter/fir/remez/PolyphaseChannelizerFilterFactory.java @@ -16,9 +16,9 @@ * along with this program. If not, see * ******************************************************************************/ -package dsp.filter.fir.remez; +package io.github.dsheirer.dsp.filter.fir.remez; -import dsp.filter.fir.FIRFilterSpecification; +import io.github.dsheirer.dsp.filter.fir.FIRFilterSpecification; import org.slf4j.Logger; import org.slf4j.LoggerFactory; diff --git a/src/main/java/io/github/dsheirer/dsp/filter/fir/remez/RemezFIRFilterDesigner.java b/src/main/java/io/github/dsheirer/dsp/filter/fir/remez/RemezFIRFilterDesigner.java index 9a6dca91..bbfbf209 100644 --- a/src/main/java/io/github/dsheirer/dsp/filter/fir/remez/RemezFIRFilterDesigner.java +++ b/src/main/java/io/github/dsheirer/dsp/filter/fir/remez/RemezFIRFilterDesigner.java @@ -211,15 +211,12 @@ public class RemezFIRFilterDesigner for( int k = 0; k < mExtremalIndices.size() - 1; k++ ) { - double cosineDelta = cosineOfFrequency - - mGrid.getCosineFrequencyGrid()[ mExtremalIndices.get( k ) ]; + double cosineDelta = cosineOfFrequency - mGrid.getCosineFrequencyGrid()[ mExtremalIndices.get( k ) ]; - //If this frequency is or is close to one of the polynomial points, use c[k] for that point + //If this frequency is close to one of the polynomial points, use the polynomial point for the response if( Math.abs( cosineDelta ) < 1.0e-7 ) { - numerator = mIdealFrequencyResponse[ k ]; - denominator = 1.0; - break; + return mIdealFrequencyResponse[ k ]; } else { diff --git a/src/dsp/filter/fir/remez/RemezFIRFilterDesigner2.java b/src/main/java/io/github/dsheirer/dsp/filter/fir/remez/RemezFIRFilterDesigner2.java similarity index 99% rename from src/dsp/filter/fir/remez/RemezFIRFilterDesigner2.java rename to src/main/java/io/github/dsheirer/dsp/filter/fir/remez/RemezFIRFilterDesigner2.java index 521b4f65..42c4c6d5 100644 --- a/src/dsp/filter/fir/remez/RemezFIRFilterDesigner2.java +++ b/src/main/java/io/github/dsheirer/dsp/filter/fir/remez/RemezFIRFilterDesigner2.java @@ -16,10 +16,10 @@ * along with this program. If not, see * ******************************************************************************/ -package dsp.filter.fir.remez; +package io.github.dsheirer.dsp.filter.fir.remez; -import dsp.filter.design.FilterDesignException; -import dsp.filter.fir.FIRFilterSpecification; +import io.github.dsheirer.dsp.filter.design.FilterDesignException; +import io.github.dsheirer.dsp.filter.fir.FIRFilterSpecification; import org.slf4j.Logger; import org.slf4j.LoggerFactory; diff --git a/src/dsp/filter/fir/remez/RemezFIRFilterDesignerWithLagrange.java b/src/main/java/io/github/dsheirer/dsp/filter/fir/remez/RemezFIRFilterDesignerWithLagrange.java similarity index 99% rename from src/dsp/filter/fir/remez/RemezFIRFilterDesignerWithLagrange.java rename to src/main/java/io/github/dsheirer/dsp/filter/fir/remez/RemezFIRFilterDesignerWithLagrange.java index d3fee4cf..07765625 100644 --- a/src/dsp/filter/fir/remez/RemezFIRFilterDesignerWithLagrange.java +++ b/src/main/java/io/github/dsheirer/dsp/filter/fir/remez/RemezFIRFilterDesignerWithLagrange.java @@ -16,10 +16,10 @@ * along with this program. If not, see * ******************************************************************************/ -package dsp.filter.fir.remez; +package io.github.dsheirer.dsp.filter.fir.remez; -import dsp.filter.design.FilterDesignException; -import dsp.filter.fir.FIRFilterSpecification; +import io.github.dsheirer.dsp.filter.design.FilterDesignException; +import io.github.dsheirer.dsp.filter.fir.FIRFilterSpecification; import org.apache.commons.math3.analysis.polynomials.PolynomialFunctionLagrangeForm; import org.slf4j.Logger; import org.slf4j.LoggerFactory; diff --git a/src/main/java/io/github/dsheirer/dsp/gain/AutomaticGainControl.java b/src/main/java/io/github/dsheirer/dsp/gain/AutomaticGainControl.java index d6873037..77e4d7f7 100644 --- a/src/main/java/io/github/dsheirer/dsp/gain/AutomaticGainControl.java +++ b/src/main/java/io/github/dsheirer/dsp/gain/AutomaticGainControl.java @@ -142,7 +142,7 @@ public class AutomaticGainControl public float process( float currentSample ) { - float delayedSample = mDelayBuffer.get( currentSample ); + float delayedSample = mDelayBuffer.putAndGet( currentSample ); double gain = MANUAL_AGC_GAIN; diff --git a/src/main/java/io/github/dsheirer/dsp/gain/ComplexFeedForwardGainControl.java b/src/main/java/io/github/dsheirer/dsp/gain/ComplexFeedForwardGainControl.java index c939b952..d1ca98a1 100644 --- a/src/main/java/io/github/dsheirer/dsp/gain/ComplexFeedForwardGainControl.java +++ b/src/main/java/io/github/dsheirer/dsp/gain/ComplexFeedForwardGainControl.java @@ -74,7 +74,7 @@ public class ComplexFeedForwardGainControl implements ComplexSampleListener } /* Replace oldest envelope value with current envelope value */ - float oldestEnvelope = mEnvelopeHistory.get( envelope ); + float oldestEnvelope = mEnvelopeHistory.putAndGet( envelope ); /* If the oldest envelope value was the max envelope value, then we * have to rediscover the max value from the envelope history */ diff --git a/src/dsp/mixer/LowPhaseNoiseOscillator.java b/src/main/java/io/github/dsheirer/dsp/mixer/LowPhaseNoiseOscillator.java similarity index 98% rename from src/dsp/mixer/LowPhaseNoiseOscillator.java rename to src/main/java/io/github/dsheirer/dsp/mixer/LowPhaseNoiseOscillator.java index 160d0e1e..bc33df62 100644 --- a/src/dsp/mixer/LowPhaseNoiseOscillator.java +++ b/src/main/java/io/github/dsheirer/dsp/mixer/LowPhaseNoiseOscillator.java @@ -16,13 +16,11 @@ * along with this program. If not, see * ******************************************************************************/ -package dsp.mixer; +package io.github.dsheirer.dsp.mixer; +import io.github.dsheirer.sample.complex.ComplexBuffer; import org.slf4j.Logger; import org.slf4j.LoggerFactory; -import sample.complex.ComplexBuffer; - -import java.util.Arrays; public class LowPhaseNoiseOscillator { diff --git a/src/main/java/io/github/dsheirer/dsp/mixer/Oscillator.java b/src/main/java/io/github/dsheirer/dsp/mixer/Oscillator.java index 50511208..3099a5bf 100644 --- a/src/main/java/io/github/dsheirer/dsp/mixer/Oscillator.java +++ b/src/main/java/io/github/dsheirer/dsp/mixer/Oscillator.java @@ -1,112 +1,152 @@ /******************************************************************************* - * SDR Trunk - * Copyright (C) 2014 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 + * 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 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 { - private final static Logger mLog = LoggerFactory.getLogger( Oscillator.class ); + private final static Logger mLog = LoggerFactory.getLogger(Oscillator.class); - private double mFrequency; - private double mSampleRate; + private boolean mEnabled = true; + private double mFrequency; + private double mSampleRate; - private Complex mAnglePerSample; - private Complex mCurrentAngle = new Complex( 0.0f, -1.0f ); + 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 - * - * @param frequency - positive or negative frequency in hertz - * @param sampleRate - in hertz - */ - public Oscillator( long frequency, int sampleRate ) - { - mSampleRate = (double)sampleRate; - mFrequency = (double)frequency; - - update(); - } - - /** - * Updates the internal values after a frequency or sample rate change - */ - private void update() - { - float anglePerSample = - (float)( 2.0d * Math.PI * mFrequency / mSampleRate ); + /** + * Oscillator 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) + { + mEnabled = frequency != 0; - mAnglePerSample = Complex.fromAngle( anglePerSample ); - } + mSampleRate = sampleRate; + mFrequency = (double)frequency; - /** - * Sets or changes the frequency of this oscillator - */ - public void setFrequency( long frequency ) - { - mFrequency = (double)frequency; - update(); - } + update(); + } - /** - * Sets or changes the sample rate of this oscillator - */ - public void setSampleRate( int sampleRate ) - { - mSampleRate = (double)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. + */ + public boolean enabled() + { + return mEnabled; + } - /** - * Steps the current angle by the angle per sample amount - */ - public void rotate() - { - mCurrentAngle.multiply( mAnglePerSample ); - mCurrentAngle.fastNormalize(); - } + /** + * Updates the internal values after a frequency or sample rate change + */ + private void update() + { + float anglePerSample = + (float)(2.0d * Math.PI * mFrequency / mSampleRate); - public float inphase() - { - return mCurrentAngle.inphase(); - } - - public float quadrature() - { - return mCurrentAngle.quadrature(); - } - - /** - * Get next complex sample - */ - public Complex getComplex() - { - return mCurrentAngle.copy(); - } + mAnglePerSample = Complex.fromAngle(anglePerSample); + } - /** - * Get the next float sample - */ - public float getFloat() - { - return mCurrentAngle.real(); - } + /** + * 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(); + } + + /** + * Steps the current angle by the angle per sample amount + */ + public void rotate() + { + mCurrentAngle.multiply(mAnglePerSample); + mCurrentAngle.fastNormalize(); + } + + public float inphase() + { + return mCurrentAngle.inphase(); + } + + public float quadrature() + { + return mCurrentAngle.quadrature(); + } + + /** + * Get next complex sample + */ + public Complex getComplex() + { + 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); + } } diff --git a/src/main/java/io/github/dsheirer/dsp/psk/LSMDemodulator.java b/src/main/java/io/github/dsheirer/dsp/psk/LSMDemodulator.java index 030ef0d8..8eac3fd7 100644 --- a/src/main/java/io/github/dsheirer/dsp/psk/LSMDemodulator.java +++ b/src/main/java/io/github/dsheirer/dsp/psk/LSMDemodulator.java @@ -41,7 +41,7 @@ import io.github.dsheirer.instrument.tap.stream.EyeDiagramDataTap; import io.github.dsheirer.sample.Listener; import io.github.dsheirer.sample.complex.Complex; import io.github.dsheirer.sample.complex.ComplexSampleListener; -import io.github.dsheirer.source.tuner.frequency.IFrequencyChangeListener; +import io.github.dsheirer.source.ISourceEventListener; import java.util.ArrayList; import java.util.Arrays; @@ -72,7 +72,7 @@ public class LSMDemodulator implements Instrumentable, ComplexSampleListener private CostasLoop mCostasLoop = new CostasLoop(); - private IFrequencyChangeListener mFrequencyChangeListener; + private ISourceEventListener mFrequencyChangeListener; private EyeDiagramDataTap mEyeDiagramDataTap; @@ -146,12 +146,12 @@ public class LSMDemodulator implements Instrumentable, ComplexSampleListener } } - public void addListener( IFrequencyChangeListener listener ) + public void addListener( ISourceEventListener listener ) { mFrequencyChangeListener = listener; } - public void removeListener( IFrequencyChangeListener listener ) + public void removeListener( ISourceEventListener listener ) { mFrequencyChangeListener = null; } @@ -201,8 +201,8 @@ public class LSMDemodulator implements Instrumentable, ComplexSampleListener // if( correctionNeeded && mFrequencyChangeListener != null ) // { // mLog.debug( "Issuing Frequency Correction: " + mFrequencyError ); -// mFrequencyChangeListener.frequencyChanged( -// new FrequencyChangeEvent( Attribute.FREQUENCY_ERROR, +// mFrequencyChangeListener.process( +// new SourceEvent( Attribute.FREQUENCY_ERROR, // mFrequencyError ) ); // } // diff --git a/src/main/java/io/github/dsheirer/gui/control/JFrequencyControl.java b/src/main/java/io/github/dsheirer/gui/control/JFrequencyControl.java index ee417149..4dfef7a2 100644 --- a/src/main/java/io/github/dsheirer/gui/control/JFrequencyControl.java +++ b/src/main/java/io/github/dsheirer/gui/control/JFrequencyControl.java @@ -1,6 +1,6 @@ /******************************************************************************* - * SDR Trunk - * Copyright (C) 2014 Dennis Sheirer + * 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 @@ -17,17 +17,25 @@ ******************************************************************************/ package io.github.dsheirer.gui.control; +import io.github.dsheirer.source.ISourceEventProcessor; import io.github.dsheirer.source.InvalidFrequencyException; +import io.github.dsheirer.source.SourceEvent; +import io.github.dsheirer.source.SourceEvent.Event; import io.github.dsheirer.source.SourceException; -import io.github.dsheirer.source.tuner.frequency.FrequencyChangeEvent; -import io.github.dsheirer.source.tuner.frequency.FrequencyChangeEvent.Event; -import io.github.dsheirer.source.tuner.frequency.IFrequencyChangeProcessor; import net.miginfocom.swing.MigLayout; import org.slf4j.Logger; import org.slf4j.LoggerFactory; -import javax.swing.*; -import java.awt.*; +import javax.swing.JLabel; +import javax.swing.JOptionPane; +import javax.swing.JPanel; +import javax.swing.JTextField; +import java.awt.Color; +import java.awt.Cursor; +import java.awt.Font; +import java.awt.Image; +import java.awt.Point; +import java.awt.Toolkit; import java.awt.event.KeyEvent; import java.awt.event.KeyListener; import java.awt.event.MouseEvent; @@ -40,13 +48,12 @@ import java.util.HashMap; import java.util.Iterator; import java.util.List; -public class JFrequencyControl extends JPanel implements IFrequencyChangeProcessor +public class JFrequencyControl extends JPanel implements ISourceEventProcessor { private static final long serialVersionUID = 1L; - private final static Logger mLog = - LoggerFactory.getLogger(JFrequencyControl.class); + private final static Logger mLog = LoggerFactory.getLogger(JFrequencyControl.class); - private List mProcessors = new ArrayList<>(); + private List mProcessors = new ArrayList<>(); private Color mHighlightColor = Color.YELLOW; @@ -54,7 +61,7 @@ public class JFrequencyControl extends JPanel implements IFrequencyChangeProcess private Cursor mBlankCursor; - private HashMap mDigits = new HashMap(); + private HashMap mDigits = new HashMap(); public JFrequencyControl() { @@ -73,7 +80,7 @@ public class JFrequencyControl extends JPanel implements IFrequencyChangeProcess Font font = new Font(Font.MONOSPACED, Font.BOLD, 30); - for (int x = 9; x >= 0; x--) + for(int x = 9; x >= 0; x--) { Digit digit = null; @@ -81,13 +88,13 @@ public class JFrequencyControl extends JPanel implements IFrequencyChangeProcess { digit = new Digit(x); } - catch (ParseException e) + catch(ParseException e) { mLog.error("JFrequencyControl - parse exception " - + "constructing a digit - " + e); + + "constructing a digit - " + e); } - if (digit != null) + if(digit != null) { mDigits.put(x, digit); @@ -95,7 +102,7 @@ public class JFrequencyControl extends JPanel implements IFrequencyChangeProcess digit.setFont(font); - if (x == 6) + if(x == 6) { JLabel period = new JLabel("."); @@ -116,7 +123,7 @@ public class JFrequencyControl extends JPanel implements IFrequencyChangeProcess Image cursorImage = Toolkit.getDefaultToolkit().createImage(imageByte); mBlankCursor = Toolkit.getDefaultToolkit() - .createCustomCursor(cursorImage, new Point(0, 0), "cursor"); + .createCustomCursor(cursorImage, new Point(0, 0), "cursor"); revalidate(); } @@ -126,7 +133,7 @@ public class JFrequencyControl extends JPanel implements IFrequencyChangeProcess { super.setEnabled(enabled); - for (Digit digit : mDigits.values()) + for(Digit digit : mDigits.values()) { digit.setEnabled(enabled); } @@ -137,9 +144,9 @@ public class JFrequencyControl extends JPanel implements IFrequencyChangeProcess * rebroadcast this event, just set the control to indicate the new frequency. */ @Override - public void frequencyChanged(FrequencyChangeEvent event) + public void process(SourceEvent event) { - if (event.getEvent() == Event.NOTIFICATION_FREQUENCY_CHANGE) + if(event.getEvent() == Event.NOTIFICATION_FREQUENCY_CHANGE) { setFrequency(event.getValue().longValue(), false); } @@ -153,7 +160,7 @@ public class JFrequencyControl extends JPanel implements IFrequencyChangeProcess { mFrequency = frequency; - for (Digit digit : mDigits.values()) + for(Digit digit : mDigits.values()) { digit.setFrequency(frequency, fireChangeEvent); } @@ -168,7 +175,7 @@ public class JFrequencyControl extends JPanel implements IFrequencyChangeProcess { long frequency = 0; - for (Digit digit : mDigits.values()) + for(Digit digit : mDigits.values()) { frequency += digit.getFrequency(); } @@ -176,27 +183,26 @@ public class JFrequencyControl extends JPanel implements IFrequencyChangeProcess mFrequency = frequency; } - private void fireFrequencyChanged() throws SourceException + private void fireSourceChanged() throws SourceException { updateFrequency(); - Iterator it = mProcessors.iterator(); + Iterator it = mProcessors.iterator(); - FrequencyChangeEvent event = new FrequencyChangeEvent( - Event.REQUEST_FREQUENCY_CHANGE, mFrequency); + SourceEvent event = SourceEvent.frequencyRequest(mFrequency); - while (it.hasNext()) + while(it.hasNext()) { - it.next().frequencyChanged(event); + it.next().process(event); } } - public void addListener(IFrequencyChangeProcessor processor) + public void addListener(ISourceEventProcessor processor) { mProcessors.add(processor); } - public void removeListener(IFrequencyChangeProcessor processor) + public void removeListener(ISourceEventProcessor processor) { mProcessors.remove(processor); } @@ -228,17 +234,17 @@ public class JFrequencyControl extends JPanel implements IFrequencyChangeProcess public void setFrequency(long frequency, boolean fireChangeEvent) { //Strip the digits higher than this one - long lower = frequency % (long) (Math.pow(10, mPower + 1)); + long lower = frequency % (long)(Math.pow(10, mPower + 1)); //Set the value to int value of dividing by 10 to this power - long value = (long) (lower / (long) (Math.pow(10, mPower))); + long value = (long)(lower / (long)(Math.pow(10, mPower))); set(value, fireChangeEvent); } public long getFrequency() { - return mValue * (long) Math.pow(10, mPower); + return mValue * (long)Math.pow(10, mPower); } public void increment() @@ -248,7 +254,7 @@ public class JFrequencyControl extends JPanel implements IFrequencyChangeProcess public void increment(boolean fireChangeEvent) { - if (isEnabled()) + if(isEnabled()) { set(mValue + 1, fireChangeEvent); } @@ -261,7 +267,7 @@ public class JFrequencyControl extends JPanel implements IFrequencyChangeProcess public void decrement(boolean fireChangeEvent) { - if (isEnabled()) + if(isEnabled()) { set(mValue - 1, fireChangeEvent); } @@ -287,13 +293,13 @@ public class JFrequencyControl extends JPanel implements IFrequencyChangeProcess mValue = amount; - while (mValue < 0) + while(mValue < 0) { mValue += 10; Digit nextHigherDigit = mDigits.get(mPower + 1); - if (nextHigherDigit != null) + if(nextHigherDigit != null) { nextHigherDigit.decrement(false); higherDigitDecremented = true; @@ -301,55 +307,55 @@ public class JFrequencyControl extends JPanel implements IFrequencyChangeProcess } - while (mValue > 9) + while(mValue > 9) { mValue -= 10; Digit nextHigherDigit = mDigits.get(mPower + 1); - if (nextHigherDigit != null) + if(nextHigherDigit != null) { nextHigherDigit.increment(false); higherDigitIncremented = true; } } - if (fireChangeEvent) + if(fireChangeEvent) { try { - fireFrequencyChanged(); + fireSourceChanged(); } - catch (SourceException se) + catch(SourceException se) { mValue = previous; - if (higherDigitIncremented) + if(higherDigitIncremented) { Digit nextHigherDigit = mDigits.get(mPower + 1); - if (nextHigherDigit != null) + if(nextHigherDigit != null) { nextHigherDigit.decrement(false); } } - if (higherDigitDecremented) + if(higherDigitDecremented) { Digit nextHigherDigit = mDigits.get(mPower + 1); - if (nextHigherDigit != null) + if(nextHigherDigit != null) { nextHigherDigit.increment(false); } } - if (se instanceof InvalidFrequencyException) + if(se instanceof InvalidFrequencyException) { - InvalidFrequencyException ife = (InvalidFrequencyException) se; + InvalidFrequencyException ife = (InvalidFrequencyException)se; JOptionPane.showMessageDialog(this, "Frequency [" + ife.getInvalidFrequency() + - "] exceeds the frequency limit [" + ife.getValidFrequency() + "] for this tuner."); + "] exceeds the frequency limit [" + ife.getValidFrequency() + "] for this tuner."); } } } @@ -361,15 +367,15 @@ public class JFrequencyControl extends JPanel implements IFrequencyChangeProcess } private class Listener implements KeyListener, - MouseListener, - MouseWheelListener + MouseListener, + MouseWheelListener { @Override public void keyReleased(KeyEvent e) { int key = e.getKeyCode(); - switch (key) + switch(key) { case KeyEvent.VK_0: case KeyEvent.VK_NUMPAD0: @@ -456,7 +462,7 @@ public class JFrequencyControl extends JPanel implements IFrequencyChangeProcess @Override public void mouseClicked(MouseEvent e) { - switch (e.getButton()) + switch(e.getButton()) { case MouseEvent.BUTTON1: increment(); diff --git a/src/main/java/io/github/dsheirer/instrument/tap/stream/BinaryTapViewPanel.java b/src/main/java/io/github/dsheirer/instrument/tap/stream/BinaryTapViewPanel.java index 3172dfe7..76f6bab6 100644 --- a/src/main/java/io/github/dsheirer/instrument/tap/stream/BinaryTapViewPanel.java +++ b/src/main/java/io/github/dsheirer/instrument/tap/stream/BinaryTapViewPanel.java @@ -21,7 +21,10 @@ import io.github.dsheirer.instrument.gui.SampleModel; import io.github.dsheirer.instrument.tap.Tap; import io.github.dsheirer.instrument.tap.TapViewPanel; -import java.awt.*; +import java.awt.Color; +import java.awt.FontMetrics; +import java.awt.Graphics; +import java.awt.Graphics2D; import java.awt.geom.Path2D; import java.awt.geom.Rectangle2D; import java.util.List; @@ -90,7 +93,7 @@ public class BinaryTapViewPanel extends TapViewPanel * Paints a polyline representing a boolean value with a 0 or 1 label in * the middle of it * - * @param graphics + * @param graphics object * @param index - index of the sample int the sample buffer * @param value - bit value, 0 or 1 */ diff --git a/src/main/java/io/github/dsheirer/module/Module.java b/src/main/java/io/github/dsheirer/module/Module.java index ce1253d6..afeb566a 100644 --- a/src/main/java/io/github/dsheirer/module/Module.java +++ b/src/main/java/io/github/dsheirer/module/Module.java @@ -18,8 +18,6 @@ package io.github.dsheirer.module; -import java.util.concurrent.ScheduledExecutorService; - public abstract class Module { /** @@ -59,9 +57,9 @@ public abstract class Module public abstract void reset(); /** - * Start processing. Provides an executor to use for scheduling tasks + * Start processing. */ - public abstract void start( ScheduledExecutorService executor ); + public abstract void start(); /** * Stop processing diff --git a/src/main/java/io/github/dsheirer/module/ProcessingChain.java b/src/main/java/io/github/dsheirer/module/ProcessingChain.java index 391d999f..7dddae2a 100644 --- a/src/main/java/io/github/dsheirer/module/ProcessingChain.java +++ b/src/main/java/io/github/dsheirer/module/ProcessingChain.java @@ -24,9 +24,6 @@ import io.github.dsheirer.audio.IAudioPacketProvider; import io.github.dsheirer.audio.squelch.ISquelchStateListener; import io.github.dsheirer.audio.squelch.ISquelchStateProvider; import io.github.dsheirer.audio.squelch.SquelchState; -import io.github.dsheirer.channel.heartbeat.Heartbeat; -import io.github.dsheirer.channel.heartbeat.IHeartbeatListener; -import io.github.dsheirer.channel.heartbeat.IHeartbeatProvider; import io.github.dsheirer.channel.metadata.AttributeChangeRequest; import io.github.dsheirer.channel.metadata.IAttributeChangeRequestListener; import io.github.dsheirer.channel.metadata.IAttributeChangeRequestProvider; @@ -60,12 +57,14 @@ import io.github.dsheirer.sample.real.IUnFilteredRealBufferListener; import io.github.dsheirer.sample.real.IUnFilteredRealBufferProvider; import io.github.dsheirer.sample.real.RealBuffer; import io.github.dsheirer.source.ComplexSource; +import io.github.dsheirer.source.ISourceEventListener; +import io.github.dsheirer.source.ISourceEventProvider; import io.github.dsheirer.source.RealSource; import io.github.dsheirer.source.Source; -import io.github.dsheirer.source.tuner.frequency.FrequencyChangeEvent; -import io.github.dsheirer.source.tuner.frequency.IFrequencyChangeListener; -import io.github.dsheirer.source.tuner.frequency.IFrequencyChangeProvider; -import io.github.dsheirer.util.ThreadPool; +import io.github.dsheirer.source.SourceEvent; +import io.github.dsheirer.source.heartbeat.Heartbeat; +import io.github.dsheirer.source.heartbeat.IHeartbeatListener; +import io.github.dsheirer.source.heartbeat.IHeartbeatProvider; import org.slf4j.Logger; import org.slf4j.LoggerFactory; @@ -101,8 +100,8 @@ public class ProcessingChain implements IChannelEventListener private Broadcaster mChannelEventBroadcaster = new Broadcaster<>(); private Broadcaster mComplexBufferBroadcaster = new Broadcaster<>(); private Broadcaster mDecoderStateEventBroadcaster = new Broadcaster<>(); - private Broadcaster mFrequencyChangeEventBroadcaster = new Broadcaster<>(); private Broadcaster mHeartbeatBroadcaster = new Broadcaster<>(); + private Broadcaster mSourceEventBroadcaster = new Broadcaster<>(); private Broadcaster mMessageBroadcaster = new Broadcaster<>(); private Broadcaster mFilteredRealBufferBroadcaster = new Broadcaster<>(); private Broadcaster mUnFilteredRealBufferBroadcaster = new Broadcaster<>(); @@ -190,11 +189,13 @@ public class ProcessingChain implements IChannelEventListener } /** - * Applies a source to this processing chain. Processing won't start until the start() method is invoked. + * Applies a sample source to this processing chain. Processing won't + * start until the start() method is invoked. * - * @param source - real or complex source - * @throws IllegalStateException if the processing chain is currently processing with another source. Invoke stop() - * before applying a new source. + * @param source - real or complex sample source + * @throws IllegalStateException if the processing chain is currently + * processing with another source. Invoke stop() before applying a new + * source. */ public void setSource(Source source) throws IllegalStateException { @@ -316,14 +317,13 @@ public class ProcessingChain implements IChannelEventListener mDecoderStateEventBroadcaster.addListener(((IDecoderStateEventListener) module).getDecoderStateListener()); } - if(module instanceof IFrequencyChangeListener) + if(module instanceof ISourceEventListener) { - Listener frequencyChangeEventListener = - ((IFrequencyChangeListener)module).getFrequencyChangeListener(); + Listener listener = ((ISourceEventListener)module).getSourceEventListener(); - if(frequencyChangeEventListener != null) + if(listener != null) { - mFrequencyChangeEventBroadcaster.addListener(frequencyChangeEventListener); + mSourceEventBroadcaster.addListener(listener); } } @@ -389,9 +389,9 @@ public class ProcessingChain implements IChannelEventListener mDecoderStateEventBroadcaster.removeListener(((IDecoderStateEventListener) module).getDecoderStateListener()); } - if(module instanceof IFrequencyChangeListener) + if(module instanceof ISourceEventListener) { - mFrequencyChangeEventBroadcaster.removeListener(((IFrequencyChangeListener) module).getFrequencyChangeListener()); + mSourceEventBroadcaster.removeListener(((ISourceEventListener) module).getSourceEventListener()); } if(module instanceof IHeartbeatListener) @@ -451,14 +451,9 @@ public class ProcessingChain implements IChannelEventListener ((IDecoderStateEventProvider) module).setDecoderStateListener(mDecoderStateEventBroadcaster); } - if(module instanceof IFrequencyChangeProvider) - { - ((IFrequencyChangeProvider) module).setFrequencyChangeListener(mFrequencyChangeEventBroadcaster); - } - if(module instanceof IHeartbeatProvider) { - ((IHeartbeatProvider)module).setHeartbeatListener(mHeartbeatBroadcaster); + ((IHeartbeatProvider)module).getHeartbeatManager().addHeartbeatListener(mHeartbeatBroadcaster); } if(module instanceof IMessageProvider) @@ -471,6 +466,16 @@ public class ProcessingChain implements IChannelEventListener ((IFilteredRealBufferProvider) module).setFilteredRealBufferListener(mFilteredRealBufferBroadcaster); } + if(module instanceof IMessageProvider) + { + ((IMessageProvider) module).setMessageListener(mMessageBroadcaster); + } + + if(module instanceof ISourceEventProvider) + { + ((ISourceEventProvider) module).setSourceEventListener(mSourceEventBroadcaster); + } + if(module instanceof ISquelchStateProvider) { ((ISquelchStateProvider) module).setSquelchStateListener(mSquelchStateBroadcaster); @@ -513,14 +518,9 @@ public class ProcessingChain implements IChannelEventListener ((IDecoderStateEventProvider) module).setDecoderStateListener(null); } - if(module instanceof IFrequencyChangeProvider) - { - ((IFrequencyChangeProvider) module).setFrequencyChangeListener(null); - } - if(module instanceof IHeartbeatProvider) { - ((IHeartbeatProvider)module).removeHeartbeatListener(); + ((IHeartbeatProvider)module).getHeartbeatManager().removeHeartbeatListener(mHeartbeatBroadcaster); } if(module instanceof IMessageProvider) @@ -533,6 +533,16 @@ public class ProcessingChain implements IChannelEventListener ((IFilteredRealBufferProvider) module).setFilteredRealBufferListener(null); } + if(module instanceof IMessageProvider) + { + ((IMessageProvider) module).setMessageListener(null); + } + + if(module instanceof ISourceEventProvider) + { + ((ISourceEventProvider) module).setSourceEventListener(null); + } + if(module instanceof ISquelchStateProvider) { ((ISquelchStateProvider) module).setSquelchStateListener(null); @@ -584,7 +594,7 @@ public class ProcessingChain implements IChannelEventListener { try { - module.start(ThreadPool.SCHEDULED); + module.start(); } catch(Exception e) { @@ -600,8 +610,7 @@ public class ProcessingChain implements IChannelEventListener } /** - * Stops processing if the chain is currently processing. Invocations on - * an already stopped chain have no effect. + * Stops processing if the chain is currently processing. Invocations on an already stopped chain have no effect. */ public void stop() { @@ -628,15 +637,14 @@ public class ProcessingChain implements IChannelEventListener ((RealSource) mSource).removeListener(mFilteredRealBufferBroadcaster); break; default: - throw new IllegalArgumentException("Unrecognized source " - + "sample type - cannot start processing chain"); + throw new IllegalArgumentException("Unrecognized source sample type - cannot start processing " + + "chain"); } /* Release the source */ mSource.dispose(); mSource = null; } - } } @@ -767,18 +775,18 @@ public class ProcessingChain implements IChannelEventListener * Adds the listener to receive frequency change events from the processing chain * @param listener to receive events */ - public void addFrequencyChangeListener(Listener listener) + public void addFrequencyChangeListener(Listener listener) { - mFrequencyChangeEventBroadcaster.addListener(listener); + mSourceEventBroadcaster.addListener(listener); } /** * Removes the listener from receiving frequency change events * @param listener to remove */ - public void removeFrequencyChangeListener(Listener listener) + public void removeFrequencyChangeListener(Listener listener) { - mFrequencyChangeEventBroadcaster.removeListener(listener); + mSourceEventBroadcaster.removeListener(listener); } /** diff --git a/src/main/java/io/github/dsheirer/module/decode/Decoder.java b/src/main/java/io/github/dsheirer/module/decode/Decoder.java index 9efeb0e7..dedac6df 100644 --- a/src/main/java/io/github/dsheirer/module/decode/Decoder.java +++ b/src/main/java/io/github/dsheirer/module/decode/Decoder.java @@ -22,8 +22,7 @@ import io.github.dsheirer.message.Message; import io.github.dsheirer.module.Module; import io.github.dsheirer.sample.Listener; -public abstract class Decoder extends Module - implements IMessageProvider, Listener +public abstract class Decoder extends Module implements IMessageProvider, Listener { /* This has to be a broadcaster in order for references to persist */ protected Listener mMessageListener; @@ -82,4 +81,19 @@ public abstract class Decoder extends Module { broadcast( message ); } + + @Override + public void reset() + { + } + + @Override + public void start() + { + } + + @Override + public void stop() + { + } } diff --git a/src/main/java/io/github/dsheirer/module/decode/DecoderFactory.java b/src/main/java/io/github/dsheirer/module/decode/DecoderFactory.java index 29dbcf5b..5092c317 100644 --- a/src/main/java/io/github/dsheirer/module/decode/DecoderFactory.java +++ b/src/main/java/io/github/dsheirer/module/decode/DecoderFactory.java @@ -66,6 +66,7 @@ import io.github.dsheirer.module.decode.mdc1200.MDCDecoderState; import io.github.dsheirer.module.decode.mdc1200.MDCMessageFilter; import io.github.dsheirer.module.decode.mpt1327.DecodeConfigMPT1327; import io.github.dsheirer.module.decode.mpt1327.MPT1327Decoder; +import io.github.dsheirer.module.decode.mpt1327.MPT1327Decoder.Sync; import io.github.dsheirer.module.decode.mpt1327.MPT1327DecoderEditor; import io.github.dsheirer.module.decode.mpt1327.MPT1327DecoderState; import io.github.dsheirer.module.decode.mpt1327.MPT1327MessageFilter; @@ -73,7 +74,7 @@ import io.github.dsheirer.module.decode.nbfm.DecodeConfigNBFM; import io.github.dsheirer.module.decode.nbfm.NBFMDecoder; import io.github.dsheirer.module.decode.nbfm.NBFMDecoderEditor; import io.github.dsheirer.module.decode.p25.DecodeConfigP25Phase1; -import io.github.dsheirer.module.decode.p25.P25Decoder; +import io.github.dsheirer.module.decode.p25.P25Decoder.Modulation; import io.github.dsheirer.module.decode.p25.P25DecoderEditor; import io.github.dsheirer.module.decode.p25.P25DecoderState; import io.github.dsheirer.module.decode.p25.P25_C4FMDecoder; @@ -145,7 +146,7 @@ public class DecoderFactory Metadata metadata) { - /* Get the optional alias list for the decode modules to use */ + /* Get the optional alias list for the decode modules to use */ AliasList aliasList = aliasModel.getAliasList(channel.getAliasListName()); List modules = getPrimaryModules(channelModel, channelMapModel, channelProcessingManager, aliasList, @@ -170,7 +171,7 @@ public class DecoderFactory ChannelType channelType = channel.getChannelType(); - /* Baseband low-pass filter pass and stop frequencies */ + /* Baseband low-pass filter pass and stop frequencies */ DecodeConfiguration decodeConfig = channel.getDecodeConfiguration(); int iqPass = decodeConfig.getDecoderType().getChannelBandwidth() / 2; @@ -212,7 +213,7 @@ public class DecoderFactory ChannelMap channelMap = channelMapModel.getChannelMap(mptConfig.getChannelMapName()); - MPT1327Decoder.Sync sync = mptConfig.getSync(); + Sync sync = mptConfig.getSync(); modules.add(new MPT1327Decoder(aliasList, sync)); @@ -221,7 +222,7 @@ public class DecoderFactory if(channelType == ChannelType.STANDARD) { - modules.add(new TrafficChannelManager(channelModel, decodeConfig, channel.getEventLogConfiguration(), + modules.add(new TrafficChannelManager(channelModel, decodeConfig, channel.getRecordConfiguration(), channel.getSystem(), channel.getSite(), (aliasList != null ? aliasList.getName() : null), mptConfig.getTrafficChannelPoolSize())); } @@ -240,7 +241,7 @@ public class DecoderFactory case P25_PHASE1: DecodeConfigP25Phase1 p25Config = (DecodeConfigP25Phase1) decodeConfig; - P25Decoder.Modulation modulation = p25Config.getModulation(); + Modulation modulation = p25Config.getModulation(); switch(modulation) @@ -249,12 +250,12 @@ public class DecoderFactory modules.add(new FMDemodulatorModule(P25_C4FM_IQ_FILTER)); modules.add(new DemodulatedAudioFilterModule(P25_C4FM_DEMOD_FILTER, 1.0f)); modules.add(new P25_C4FMDecoder(aliasList, decodeConfig.getAFCMaximumCorrection())); - modules.add(new P25DecoderState(aliasList, channelType, P25Decoder.Modulation.C4FM, + modules.add(new P25DecoderState(aliasList, channelType, Modulation.C4FM, p25Config.getIgnoreDataCalls())); break; case CQPSK: modules.add(new P25_LSMDecoder(aliasList)); - modules.add(new P25DecoderState(aliasList, channelType, P25Decoder.Modulation.CQPSK, p25Config.getIgnoreDataCalls())); + modules.add(new P25DecoderState(aliasList, channelType, Modulation.CQPSK, p25Config.getIgnoreDataCalls())); break; default: throw new IllegalArgumentException("Unrecognized P25 Phase 1 Modulation [" + modulation + "]"); @@ -262,7 +263,7 @@ public class DecoderFactory if(channelType == ChannelType.STANDARD) { - modules.add(new TrafficChannelManager(channelModel, decodeConfig, channel.getEventLogConfiguration(), + modules.add(new TrafficChannelManager(channelModel, decodeConfig, channel.getRecordConfiguration(), channel.getSystem(), channel.getSite(), (aliasList != null ? aliasList.getName() : null), p25Config.getTrafficChannelPoolSize())); } @@ -337,7 +338,7 @@ public class DecoderFactory } } - /* If we don't have any filters, add an ALL-PASS filter */ + /* If we don't have any filters, add an ALL-PASS filter */ if(filterSet.getFilters().isEmpty()) { filterSet.addFilter(new AllPassFilter()); @@ -510,4 +511,4 @@ public class DecoderFactory return null; } -} +} \ No newline at end of file diff --git a/src/main/java/io/github/dsheirer/module/decode/am/AMDecoder.java b/src/main/java/io/github/dsheirer/module/decode/am/AMDecoder.java index 31362eba..73f9a824 100644 --- a/src/main/java/io/github/dsheirer/module/decode/am/AMDecoder.java +++ b/src/main/java/io/github/dsheirer/module/decode/am/AMDecoder.java @@ -22,8 +22,6 @@ import io.github.dsheirer.module.decode.DecoderType; import io.github.dsheirer.module.decode.PrimaryDecoder; import io.github.dsheirer.module.decode.config.DecodeConfiguration; -import java.util.concurrent.ScheduledExecutorService; - public class AMDecoder extends PrimaryDecoder { public AMDecoder( DecodeConfiguration config ) @@ -42,10 +40,10 @@ public class AMDecoder extends PrimaryDecoder { } - @Override - public void start( ScheduledExecutorService executor ) - { - } + @Override + public void start() + { + } @Override public void stop() diff --git a/src/main/java/io/github/dsheirer/module/decode/fleetsync2/Fleetsync2Decoder.java b/src/main/java/io/github/dsheirer/module/decode/fleetsync2/Fleetsync2Decoder.java index 072d28f4..4a75ab70 100644 --- a/src/main/java/io/github/dsheirer/module/decode/fleetsync2/Fleetsync2Decoder.java +++ b/src/main/java/io/github/dsheirer/module/decode/fleetsync2/Fleetsync2Decoder.java @@ -38,7 +38,6 @@ import io.github.dsheirer.sample.real.RealBuffer; import java.util.ArrayList; import java.util.List; -import java.util.concurrent.ScheduledExecutorService; /** * Fleetsync II Decoder - 1200 baud 2FSK decoder that can process 48k sample rate @@ -173,19 +172,4 @@ public class Fleetsync2Decoder extends Decoder implements IFilteredRealBufferLis { return mDecimationFilter; } - - @Override - public void reset() - { - } - - @Override - public void start( ScheduledExecutorService executor ) - { - } - - @Override - public void stop() - { - } } diff --git a/src/main/java/io/github/dsheirer/module/decode/fleetsync2/Fleetsync2DecoderState.java b/src/main/java/io/github/dsheirer/module/decode/fleetsync2/Fleetsync2DecoderState.java index 0c49e560..e65848d3 100644 --- a/src/main/java/io/github/dsheirer/module/decode/fleetsync2/Fleetsync2DecoderState.java +++ b/src/main/java/io/github/dsheirer/module/decode/fleetsync2/Fleetsync2DecoderState.java @@ -34,7 +34,6 @@ import io.github.dsheirer.util.StringUtils; import java.util.Iterator; import java.util.TreeSet; -import java.util.concurrent.ScheduledExecutorService; public class Fleetsync2DecoderState extends DecoderState { @@ -63,27 +62,12 @@ public class Fleetsync2DecoderState extends DecoderState return DecoderType.FLEETSYNC2; } - @Override - public void start(ScheduledExecutorService executor) - { - } - - @Override - public void stop() - { - } - @Override public void dispose() { super.dispose(); } - @Override - public void init() - { - } - /** * Resets the overall decoder state and clears any accumulated event details */ @@ -96,6 +80,24 @@ public class Fleetsync2DecoderState extends DecoderState resetState(); } + @Override + public void start() + { + + } + + @Override + public void stop() + { + + } + + @Override + public void init() + { + + } + /** * Resets this decoder state */ diff --git a/src/main/java/io/github/dsheirer/module/decode/lj1200/LJ1200Decoder.java b/src/main/java/io/github/dsheirer/module/decode/lj1200/LJ1200Decoder.java index fb19a8d2..2556e207 100644 --- a/src/main/java/io/github/dsheirer/module/decode/lj1200/LJ1200Decoder.java +++ b/src/main/java/io/github/dsheirer/module/decode/lj1200/LJ1200Decoder.java @@ -41,7 +41,6 @@ import io.github.dsheirer.sample.real.RealSampleListener; import java.util.ArrayList; import java.util.List; -import java.util.concurrent.ScheduledExecutorService; /** * LJ1200 - 1200 baud 2FSK decoder @@ -191,19 +190,4 @@ public class LJ1200Decoder extends Decoder implements IFilteredRealBufferListene { return mDecimationFilter; } - - @Override - public void reset() - { - } - - @Override - public void start( ScheduledExecutorService executor ) - { - } - - @Override - public void stop() - { - } } diff --git a/src/main/java/io/github/dsheirer/module/decode/lj1200/LJ1200DecoderState.java b/src/main/java/io/github/dsheirer/module/decode/lj1200/LJ1200DecoderState.java index c4154dc5..e479c434 100644 --- a/src/main/java/io/github/dsheirer/module/decode/lj1200/LJ1200DecoderState.java +++ b/src/main/java/io/github/dsheirer/module/decode/lj1200/LJ1200DecoderState.java @@ -36,7 +36,6 @@ import java.util.Collections; import java.util.List; import java.util.Set; import java.util.TreeSet; -import java.util.concurrent.ScheduledExecutorService; public class LJ1200DecoderState extends DecoderState { @@ -60,16 +59,6 @@ public class LJ1200DecoderState extends DecoderState return DecoderType.LJ_1200; } - @Override - public void start(ScheduledExecutorService executor) - { - } - - @Override - public void stop() - { - } - @Override public void receive(Message message) { @@ -122,12 +111,6 @@ public class LJ1200DecoderState extends DecoderState return sb.toString(); } - @Override - public void init() - { - /* No initialization steps */ - } - @Override public void receiveDecoderStateEvent(DecoderStateEvent event) { @@ -148,4 +131,22 @@ public class LJ1200DecoderState extends DecoderState mAddresses.clear(); resetState(); } + + @Override + public void start() + { + + } + + @Override + public void stop() + { + + } + + @Override + public void init() + { + + } } diff --git a/src/main/java/io/github/dsheirer/module/decode/ltrnet/LTRNetDecoder.java b/src/main/java/io/github/dsheirer/module/decode/ltrnet/LTRNetDecoder.java index 08041b72..78d3db90 100644 --- a/src/main/java/io/github/dsheirer/module/decode/ltrnet/LTRNetDecoder.java +++ b/src/main/java/io/github/dsheirer/module/decode/ltrnet/LTRNetDecoder.java @@ -33,7 +33,6 @@ import io.github.dsheirer.sample.real.RealBuffer; import java.util.ArrayList; import java.util.List; -import java.util.concurrent.ScheduledExecutorService; public class LTRNetDecoder extends Decoder implements IUnFilteredRealBufferListener, Instrumentable @@ -119,14 +118,4 @@ public class LTRNetDecoder extends Decoder { mLTRMessageFramer.reset(); } - - @Override - public void start( ScheduledExecutorService executor ) - { - } - - @Override - public void stop() - { - } } diff --git a/src/main/java/io/github/dsheirer/module/decode/ltrnet/LTRNetDecoderState.java b/src/main/java/io/github/dsheirer/module/decode/ltrnet/LTRNetDecoderState.java index 48c3b3ef..7d8ce721 100644 --- a/src/main/java/io/github/dsheirer/module/decode/ltrnet/LTRNetDecoderState.java +++ b/src/main/java/io/github/dsheirer/module/decode/ltrnet/LTRNetDecoderState.java @@ -39,7 +39,6 @@ import java.text.DecimalFormat; import java.util.HashMap; import java.util.Iterator; import java.util.TreeSet; -import java.util.concurrent.ScheduledExecutorService; public class LTRNetDecoderState extends DecoderState { @@ -695,6 +694,24 @@ public class LTRNetDecoderState extends DecoderState resetState(); } + @Override + public void start() + { + + } + + @Override + public void stop() + { + + } + + @Override + public void init() + { + + } + /** * Resets the decoder state after a call or other decode event */ @@ -764,12 +781,6 @@ public class LTRNetDecoderState extends DecoderState } } - @Override - public void init() - { - /* No initialization steps required */ - } - @Override public void receiveDecoderStateEvent(DecoderStateEvent event) { @@ -785,14 +796,4 @@ public class LTRNetDecoderState extends DecoderState break; } } - - @Override - public void start(ScheduledExecutorService executor) - { - } - - @Override - public void stop() - { - } } diff --git a/src/main/java/io/github/dsheirer/module/decode/ltrstandard/LTRStandardDecoder.java b/src/main/java/io/github/dsheirer/module/decode/ltrstandard/LTRStandardDecoder.java index 0a96486b..8a6355a9 100644 --- a/src/main/java/io/github/dsheirer/module/decode/ltrstandard/LTRStandardDecoder.java +++ b/src/main/java/io/github/dsheirer/module/decode/ltrstandard/LTRStandardDecoder.java @@ -28,8 +28,6 @@ import io.github.dsheirer.sample.Listener; import io.github.dsheirer.sample.real.IUnFilteredRealBufferListener; import io.github.dsheirer.sample.real.RealBuffer; -import java.util.concurrent.ScheduledExecutorService; - public class LTRStandardDecoder extends Decoder implements IUnFilteredRealBufferListener { public static final int LTR_STANDARD_MESSAGE_LENGTH = 40; @@ -84,14 +82,4 @@ public class LTRStandardDecoder extends Decoder implements IUnFilteredRealBuffer { mLTRMessageFramer.reset(); } - - @Override - public void start( ScheduledExecutorService executor ) - { - } - - @Override - public void stop() - { - } } diff --git a/src/main/java/io/github/dsheirer/module/decode/ltrstandard/LTRStandardDecoderState.java b/src/main/java/io/github/dsheirer/module/decode/ltrstandard/LTRStandardDecoderState.java index 788c8d9b..32d7647c 100644 --- a/src/main/java/io/github/dsheirer/module/decode/ltrstandard/LTRStandardDecoderState.java +++ b/src/main/java/io/github/dsheirer/module/decode/ltrstandard/LTRStandardDecoderState.java @@ -46,7 +46,6 @@ import java.util.List; import java.util.Map; import java.util.Set; import java.util.TreeSet; -import java.util.concurrent.ScheduledExecutorService; public class LTRStandardDecoderState extends DecoderState { @@ -75,18 +74,6 @@ public class LTRStandardDecoderState extends DecoderState return DecoderType.LTR_STANDARD; } - @Override - public void start(ScheduledExecutorService executor) - { - // TODO Auto-generated method stub - - } - - @Override - public void stop() - { - } - @Override public void receive(Message message) { @@ -211,6 +198,24 @@ public class LTRStandardDecoderState extends DecoderState resetState(); } + @Override + public void start() + { + + } + + @Override + public void stop() + { + + } + + @Override + public void init() + { + + } + /** * Performs a temporal reset following a call or other decode event */ @@ -256,11 +261,6 @@ public class LTRStandardDecoderState extends DecoderState } } - @Override - public void init() - { - } - @Override public void receiveDecoderStateEvent(DecoderStateEvent event) { diff --git a/src/main/java/io/github/dsheirer/module/decode/mdc1200/MDCDecoder.java b/src/main/java/io/github/dsheirer/module/decode/mdc1200/MDCDecoder.java index beaf51da..1764c5db 100644 --- a/src/main/java/io/github/dsheirer/module/decode/mdc1200/MDCDecoder.java +++ b/src/main/java/io/github/dsheirer/module/decode/mdc1200/MDCDecoder.java @@ -40,7 +40,6 @@ import io.github.dsheirer.sample.real.RealBuffer; import java.util.ArrayList; import java.util.List; -import java.util.concurrent.ScheduledExecutorService; /** * MDC1200 Decoder - 1200 baud 2FSK decoder that can process 48k sample rate @@ -201,25 +200,4 @@ public class MDCDecoder extends Decoder implements IFilteredRealBufferListener, { return mDecimationFilter; } - - @Override - public void reset() - { - // TODO Auto-generated method stub - - } - - @Override - public void start( ScheduledExecutorService executor ) - { - // TODO Auto-generated method stub - - } - - @Override - public void stop() - { - // TODO Auto-generated method stub - - } } diff --git a/src/main/java/io/github/dsheirer/module/decode/mdc1200/MDCDecoderState.java b/src/main/java/io/github/dsheirer/module/decode/mdc1200/MDCDecoderState.java index 379b4037..8f8667fb 100644 --- a/src/main/java/io/github/dsheirer/module/decode/mdc1200/MDCDecoderState.java +++ b/src/main/java/io/github/dsheirer/module/decode/mdc1200/MDCDecoderState.java @@ -34,7 +34,6 @@ import io.github.dsheirer.util.StringUtils; import java.util.Iterator; import java.util.TreeSet; -import java.util.concurrent.ScheduledExecutorService; public class MDCDecoderState extends DecoderState { @@ -59,16 +58,6 @@ public class MDCDecoderState extends DecoderState return DecoderType.MDC1200; } - @Override - public void start(ScheduledExecutorService executor) - { - } - - @Override - public void stop() - { - } - @Override public void reset() { @@ -78,6 +67,24 @@ public class MDCDecoderState extends DecoderState resetState(); } + @Override + public void start() + { + + } + + @Override + public void stop() + { + + } + + @Override + public void init() + { + + } + private void resetState() { mFromAttribute.reset(); @@ -245,12 +252,6 @@ public class MDCDecoderState extends DecoderState return sb.toString(); } - @Override - public void init() - { - /* No initialization required */ - } - /** * Responds to reset events issued by the channel state */ diff --git a/src/main/java/io/github/dsheirer/module/decode/mpt1327/MPT1327Decoder.java b/src/main/java/io/github/dsheirer/module/decode/mpt1327/MPT1327Decoder.java index d78d3670..c9455d95 100644 --- a/src/main/java/io/github/dsheirer/module/decode/mpt1327/MPT1327Decoder.java +++ b/src/main/java/io/github/dsheirer/module/decode/mpt1327/MPT1327Decoder.java @@ -44,7 +44,6 @@ import org.slf4j.LoggerFactory; import java.util.ArrayList; import java.util.List; -import java.util.concurrent.ScheduledExecutorService; /** * MPT1327 Decoder - 1200 baud 2FSK decoder that can process 48k sample rate @@ -296,16 +295,4 @@ public class MPT1327Decoder extends Decoder implements IFilteredRealBufferListen mControlMessageFramer.reset(); mTrafficMessageFramer.reset(); } - - @Override - public void start(ScheduledExecutorService executor) - { - // TODO Auto-generated method stub - } - - @Override - public void stop() - { - // TODO Auto-generated method stub - } } diff --git a/src/main/java/io/github/dsheirer/module/decode/mpt1327/MPT1327DecoderState.java b/src/main/java/io/github/dsheirer/module/decode/mpt1327/MPT1327DecoderState.java index ca931381..d09cf40d 100644 --- a/src/main/java/io/github/dsheirer/module/decode/mpt1327/MPT1327DecoderState.java +++ b/src/main/java/io/github/dsheirer/module/decode/mpt1327/MPT1327DecoderState.java @@ -46,7 +46,6 @@ import java.util.HashMap; import java.util.Iterator; import java.util.List; import java.util.TreeSet; -import java.util.concurrent.ScheduledExecutorService; public class MPT1327DecoderState extends DecoderState { @@ -85,16 +84,6 @@ public class MPT1327DecoderState extends DecoderState return DecoderType.MPT1327; } - @Override - public void start(ScheduledExecutorService executor) - { - } - - @Override - public void stop() - { - } - public ChannelType getChannelType() { return mChannelType; @@ -300,6 +289,24 @@ public class MPT1327DecoderState extends DecoderState resetState(); } + @Override + public void start() + { + + } + + @Override + public void stop() + { + + } + + @Override + public void init() + { + + } + private void resetState() { mFromAttribute.reset(); @@ -499,12 +506,6 @@ public class MPT1327DecoderState extends DecoderState return sb.toString(); } - @Override - public void init() - { - /* No initialization required */ - } - @Override public void receiveDecoderStateEvent(DecoderStateEvent event) { diff --git a/src/main/java/io/github/dsheirer/module/decode/nbfm/NBFMDecoder.java b/src/main/java/io/github/dsheirer/module/decode/nbfm/NBFMDecoder.java index 3e0e326d..31752072 100644 --- a/src/main/java/io/github/dsheirer/module/decode/nbfm/NBFMDecoder.java +++ b/src/main/java/io/github/dsheirer/module/decode/nbfm/NBFMDecoder.java @@ -22,8 +22,6 @@ import io.github.dsheirer.module.decode.DecoderType; import io.github.dsheirer.module.decode.PrimaryDecoder; import io.github.dsheirer.module.decode.config.DecodeConfiguration; -import java.util.concurrent.ScheduledExecutorService; - public class NBFMDecoder extends PrimaryDecoder { public NBFMDecoder( DecodeConfiguration config ) @@ -43,7 +41,7 @@ public class NBFMDecoder extends PrimaryDecoder } @Override - public void start( ScheduledExecutorService executor ) + public void start() { } diff --git a/src/main/java/io/github/dsheirer/module/decode/p25/C4FMSymbolFilter.java b/src/main/java/io/github/dsheirer/module/decode/p25/C4FMSymbolFilter.java index 7279a511..bca6a3a2 100644 --- a/src/main/java/io/github/dsheirer/module/decode/p25/C4FMSymbolFilter.java +++ b/src/main/java/io/github/dsheirer/module/decode/p25/C4FMSymbolFilter.java @@ -10,499 +10,499 @@ import io.github.dsheirer.sample.Listener; import io.github.dsheirer.sample.real.RealBuffer; import io.github.dsheirer.sample.real.RealSampleListener; import io.github.dsheirer.sample.real.RealSampleProvider; -import io.github.dsheirer.source.tuner.frequency.FrequencyChangeEvent; -import io.github.dsheirer.source.tuner.frequency.FrequencyChangeEvent.Event; -import io.github.dsheirer.source.tuner.frequency.IFrequencyChangeListener; +import io.github.dsheirer.source.ISourceEventListener; +import io.github.dsheirer.source.SourceEvent; import java.util.ArrayList; import java.util.List; -public class C4FMSymbolFilter implements Listener, IFrequencyChangeListener, RealSampleProvider, Instrumentable +public class C4FMSymbolFilter implements Listener, ISourceEventListener, RealSampleProvider, Instrumentable { - private static final float TAPS[][] = - { - { 0.00000e+00f, 0.00000e+00f, 0.00000e+00f, 0.00000e+00f, 1.00000e+00f, 0.00000e+00f, 0.00000e+00f, 0.00000e+00f }, // 0/128 - { -1.54700e-04f, 8.53777e-04f, -2.76968e-03f, 7.89295e-03f, 9.98534e-01f, -5.41054e-03f, 1.24642e-03f, -1.98993e-04f }, // 1/128 - { -3.09412e-04f, 1.70888e-03f, -5.55134e-03f, 1.58840e-02f, 9.96891e-01f, -1.07209e-02f, 2.47942e-03f, -3.96391e-04f }, // 2/128 - { -4.64053e-04f, 2.56486e-03f, -8.34364e-03f, 2.39714e-02f, 9.95074e-01f, -1.59305e-02f, 3.69852e-03f, -5.92100e-04f }, // 3/128 - { -6.18544e-04f, 3.42130e-03f, -1.11453e-02f, 3.21531e-02f, 9.93082e-01f, -2.10389e-02f, 4.90322e-03f, -7.86031e-04f }, // 4/128 - { -7.72802e-04f, 4.27773e-03f, -1.39548e-02f, 4.04274e-02f, 9.90917e-01f, -2.60456e-02f, 6.09305e-03f, -9.78093e-04f }, // 5/128 - { -9.26747e-04f, 5.13372e-03f, -1.67710e-02f, 4.87921e-02f, 9.88580e-01f, -3.09503e-02f, 7.26755e-03f, -1.16820e-03f }, // 6/128 - { -1.08030e-03f, 5.98883e-03f, -1.95925e-02f, 5.72454e-02f, 9.86071e-01f, -3.57525e-02f, 8.42626e-03f, -1.35627e-03f }, // 7/128 - { -1.23337e-03f, 6.84261e-03f, -2.24178e-02f, 6.57852e-02f, 9.83392e-01f, -4.04519e-02f, 9.56876e-03f, -1.54221e-03f }, // 8/128 - { -1.38589e-03f, 7.69462e-03f, -2.52457e-02f, 7.44095e-02f, 9.80543e-01f, -4.50483e-02f, 1.06946e-02f, -1.72594e-03f }, // 9/128 - { -1.53777e-03f, 8.54441e-03f, -2.80746e-02f, 8.31162e-02f, 9.77526e-01f, -4.95412e-02f, 1.18034e-02f, -1.90738e-03f }, // 10/128 - { -1.68894e-03f, 9.39154e-03f, -3.09033e-02f, 9.19033e-02f, 9.74342e-01f, -5.39305e-02f, 1.28947e-02f, -2.08645e-03f }, // 11/128 - { -1.83931e-03f, 1.02356e-02f, -3.37303e-02f, 1.00769e-01f, 9.70992e-01f, -5.82159e-02f, 1.39681e-02f, -2.26307e-03f }, // 12/128 - { -1.98880e-03f, 1.10760e-02f, -3.65541e-02f, 1.09710e-01f, 9.67477e-01f, -6.23972e-02f, 1.50233e-02f, -2.43718e-03f }, // 13/128 - { -2.13733e-03f, 1.19125e-02f, -3.93735e-02f, 1.18725e-01f, 9.63798e-01f, -6.64743e-02f, 1.60599e-02f, -2.60868e-03f }, // 14/128 - { -2.28483e-03f, 1.27445e-02f, -4.21869e-02f, 1.27812e-01f, 9.59958e-01f, -7.04471e-02f, 1.70776e-02f, -2.77751e-03f }, // 15/128 - { -2.43121e-03f, 1.35716e-02f, -4.49929e-02f, 1.36968e-01f, 9.55956e-01f, -7.43154e-02f, 1.80759e-02f, -2.94361e-03f }, // 16/128 - { -2.57640e-03f, 1.43934e-02f, -4.77900e-02f, 1.46192e-01f, 9.51795e-01f, -7.80792e-02f, 1.90545e-02f, -3.10689e-03f }, // 17/128 - { -2.72032e-03f, 1.52095e-02f, -5.05770e-02f, 1.55480e-01f, 9.47477e-01f, -8.17385e-02f, 2.00132e-02f, -3.26730e-03f }, // 18/128 - { -2.86289e-03f, 1.60193e-02f, -5.33522e-02f, 1.64831e-01f, 9.43001e-01f, -8.52933e-02f, 2.09516e-02f, -3.42477e-03f }, // 19/128 - { -3.00403e-03f, 1.68225e-02f, -5.61142e-02f, 1.74242e-01f, 9.38371e-01f, -8.87435e-02f, 2.18695e-02f, -3.57923e-03f }, // 20/128 - { -3.14367e-03f, 1.76185e-02f, -5.88617e-02f, 1.83711e-01f, 9.33586e-01f, -9.20893e-02f, 2.27664e-02f, -3.73062e-03f }, // 21/128 - { -3.28174e-03f, 1.84071e-02f, -6.15931e-02f, 1.93236e-01f, 9.28650e-01f, -9.53307e-02f, 2.36423e-02f, -3.87888e-03f }, // 22/128 - { -3.41815e-03f, 1.91877e-02f, -6.43069e-02f, 2.02814e-01f, 9.23564e-01f, -9.84679e-02f, 2.44967e-02f, -4.02397e-03f }, // 23/128 - { -3.55283e-03f, 1.99599e-02f, -6.70018e-02f, 2.12443e-01f, 9.18329e-01f, -1.01501e-01f, 2.53295e-02f, -4.16581e-03f }, // 24/128 - { -3.68570e-03f, 2.07233e-02f, -6.96762e-02f, 2.22120e-01f, 9.12947e-01f, -1.04430e-01f, 2.61404e-02f, -4.30435e-03f }, // 25/128 - { -3.81671e-03f, 2.14774e-02f, -7.23286e-02f, 2.31843e-01f, 9.07420e-01f, -1.07256e-01f, 2.69293e-02f, -4.43955e-03f }, // 26/128 - { -3.94576e-03f, 2.22218e-02f, -7.49577e-02f, 2.41609e-01f, 9.01749e-01f, -1.09978e-01f, 2.76957e-02f, -4.57135e-03f }, // 27/128 - { -4.07279e-03f, 2.29562e-02f, -7.75620e-02f, 2.51417e-01f, 8.95936e-01f, -1.12597e-01f, 2.84397e-02f, -4.69970e-03f }, // 28/128 - { -4.19774e-03f, 2.36801e-02f, -8.01399e-02f, 2.61263e-01f, 8.89984e-01f, -1.15113e-01f, 2.91609e-02f, -4.82456e-03f }, // 29/128 - { -4.32052e-03f, 2.43930e-02f, -8.26900e-02f, 2.71144e-01f, 8.83893e-01f, -1.17526e-01f, 2.98593e-02f, -4.94589e-03f }, // 30/128 - { -4.44107e-03f, 2.50946e-02f, -8.52109e-02f, 2.81060e-01f, 8.77666e-01f, -1.19837e-01f, 3.05345e-02f, -5.06363e-03f }, // 31/128 - { -4.55932e-03f, 2.57844e-02f, -8.77011e-02f, 2.91006e-01f, 8.71305e-01f, -1.22047e-01f, 3.11866e-02f, -5.17776e-03f }, // 32/128 - { -4.67520e-03f, 2.64621e-02f, -9.01591e-02f, 3.00980e-01f, 8.64812e-01f, -1.24154e-01f, 3.18153e-02f, -5.28823e-03f }, // 33/128 - { -4.78866e-03f, 2.71272e-02f, -9.25834e-02f, 3.10980e-01f, 8.58189e-01f, -1.26161e-01f, 3.24205e-02f, -5.39500e-03f }, // 34/128 - { -4.89961e-03f, 2.77794e-02f, -9.49727e-02f, 3.21004e-01f, 8.51437e-01f, -1.28068e-01f, 3.30021e-02f, -5.49804e-03f }, // 35/128 - { -5.00800e-03f, 2.84182e-02f, -9.73254e-02f, 3.31048e-01f, 8.44559e-01f, -1.29874e-01f, 3.35600e-02f, -5.59731e-03f }, // 36/128 - { -5.11376e-03f, 2.90433e-02f, -9.96402e-02f, 3.41109e-01f, 8.37557e-01f, -1.31581e-01f, 3.40940e-02f, -5.69280e-03f }, // 37/128 - { -5.21683e-03f, 2.96543e-02f, -1.01915e-01f, 3.51186e-01f, 8.30432e-01f, -1.33189e-01f, 3.46042e-02f, -5.78446e-03f }, // 38/128 - { -5.31716e-03f, 3.02507e-02f, -1.04150e-01f, 3.61276e-01f, 8.23188e-01f, -1.34699e-01f, 3.50903e-02f, -5.87227e-03f }, // 39/128 - { -5.41467e-03f, 3.08323e-02f, -1.06342e-01f, 3.71376e-01f, 8.15826e-01f, -1.36111e-01f, 3.55525e-02f, -5.95620e-03f }, // 40/128 - { -5.50931e-03f, 3.13987e-02f, -1.08490e-01f, 3.81484e-01f, 8.08348e-01f, -1.37426e-01f, 3.59905e-02f, -6.03624e-03f }, // 41/128 - { -5.60103e-03f, 3.19495e-02f, -1.10593e-01f, 3.91596e-01f, 8.00757e-01f, -1.38644e-01f, 3.64044e-02f, -6.11236e-03f }, // 42/128 - { -5.68976e-03f, 3.24843e-02f, -1.12650e-01f, 4.01710e-01f, 7.93055e-01f, -1.39767e-01f, 3.67941e-02f, -6.18454e-03f }, // 43/128 - { -5.77544e-03f, 3.30027e-02f, -1.14659e-01f, 4.11823e-01f, 7.85244e-01f, -1.40794e-01f, 3.71596e-02f, -6.25277e-03f }, // 44/128 - { -5.85804e-03f, 3.35046e-02f, -1.16618e-01f, 4.21934e-01f, 7.77327e-01f, -1.41727e-01f, 3.75010e-02f, -6.31703e-03f }, // 45/128 - { -5.93749e-03f, 3.39894e-02f, -1.18526e-01f, 4.32038e-01f, 7.69305e-01f, -1.42566e-01f, 3.78182e-02f, -6.37730e-03f }, // 46/128 - { -6.01374e-03f, 3.44568e-02f, -1.20382e-01f, 4.42134e-01f, 7.61181e-01f, -1.43313e-01f, 3.81111e-02f, -6.43358e-03f }, // 47/128 - { -6.08674e-03f, 3.49066e-02f, -1.22185e-01f, 4.52218e-01f, 7.52958e-01f, -1.43968e-01f, 3.83800e-02f, -6.48585e-03f }, // 48/128 - { -6.15644e-03f, 3.53384e-02f, -1.23933e-01f, 4.62289e-01f, 7.44637e-01f, -1.44531e-01f, 3.86247e-02f, -6.53412e-03f }, // 49/128 - { -6.22280e-03f, 3.57519e-02f, -1.25624e-01f, 4.72342e-01f, 7.36222e-01f, -1.45004e-01f, 3.88454e-02f, -6.57836e-03f }, // 50/128 - { -6.28577e-03f, 3.61468e-02f, -1.27258e-01f, 4.82377e-01f, 7.27714e-01f, -1.45387e-01f, 3.90420e-02f, -6.61859e-03f }, // 51/128 - { -6.34530e-03f, 3.65227e-02f, -1.28832e-01f, 4.92389e-01f, 7.19116e-01f, -1.45682e-01f, 3.92147e-02f, -6.65479e-03f }, // 52/128 - { -6.40135e-03f, 3.68795e-02f, -1.30347e-01f, 5.02377e-01f, 7.10431e-01f, -1.45889e-01f, 3.93636e-02f, -6.68698e-03f }, // 53/128 - { -6.45388e-03f, 3.72167e-02f, -1.31800e-01f, 5.12337e-01f, 7.01661e-01f, -1.46009e-01f, 3.94886e-02f, -6.71514e-03f }, // 54/128 - { -6.50285e-03f, 3.75341e-02f, -1.33190e-01f, 5.22267e-01f, 6.92808e-01f, -1.46043e-01f, 3.95900e-02f, -6.73929e-03f }, // 55/128 - { -6.54823e-03f, 3.78315e-02f, -1.34515e-01f, 5.32164e-01f, 6.83875e-01f, -1.45993e-01f, 3.96678e-02f, -6.75943e-03f }, // 56/128 - { -6.58996e-03f, 3.81085e-02f, -1.35775e-01f, 5.42025e-01f, 6.74865e-01f, -1.45859e-01f, 3.97222e-02f, -6.77557e-03f }, // 57/128 - { -6.62802e-03f, 3.83650e-02f, -1.36969e-01f, 5.51849e-01f, 6.65779e-01f, -1.45641e-01f, 3.97532e-02f, -6.78771e-03f }, // 58/128 - { -6.66238e-03f, 3.86006e-02f, -1.38094e-01f, 5.61631e-01f, 6.56621e-01f, -1.45343e-01f, 3.97610e-02f, -6.79588e-03f }, // 59/128 - { -6.69300e-03f, 3.88151e-02f, -1.39150e-01f, 5.71370e-01f, 6.47394e-01f, -1.44963e-01f, 3.97458e-02f, -6.80007e-03f }, // 60/128 - { -6.71985e-03f, 3.90083e-02f, -1.40136e-01f, 5.81063e-01f, 6.38099e-01f, -1.44503e-01f, 3.97077e-02f, -6.80032e-03f }, // 61/128 - { -6.74291e-03f, 3.91800e-02f, -1.41050e-01f, 5.90706e-01f, 6.28739e-01f, -1.43965e-01f, 3.96469e-02f, -6.79662e-03f }, // 62/128 - { -6.76214e-03f, 3.93299e-02f, -1.41891e-01f, 6.00298e-01f, 6.19318e-01f, -1.43350e-01f, 3.95635e-02f, -6.78902e-03f }, // 63/128 - { -6.77751e-03f, 3.94578e-02f, -1.42658e-01f, 6.09836e-01f, 6.09836e-01f, -1.42658e-01f, 3.94578e-02f, -6.77751e-03f }, // 64/128 - { -6.78902e-03f, 3.95635e-02f, -1.43350e-01f, 6.19318e-01f, 6.00298e-01f, -1.41891e-01f, 3.93299e-02f, -6.76214e-03f }, // 65/128 - { -6.79662e-03f, 3.96469e-02f, -1.43965e-01f, 6.28739e-01f, 5.90706e-01f, -1.41050e-01f, 3.91800e-02f, -6.74291e-03f }, // 66/128 - { -6.80032e-03f, 3.97077e-02f, -1.44503e-01f, 6.38099e-01f, 5.81063e-01f, -1.40136e-01f, 3.90083e-02f, -6.71985e-03f }, // 67/128 - { -6.80007e-03f, 3.97458e-02f, -1.44963e-01f, 6.47394e-01f, 5.71370e-01f, -1.39150e-01f, 3.88151e-02f, -6.69300e-03f }, // 68/128 - { -6.79588e-03f, 3.97610e-02f, -1.45343e-01f, 6.56621e-01f, 5.61631e-01f, -1.38094e-01f, 3.86006e-02f, -6.66238e-03f }, // 69/128 - { -6.78771e-03f, 3.97532e-02f, -1.45641e-01f, 6.65779e-01f, 5.51849e-01f, -1.36969e-01f, 3.83650e-02f, -6.62802e-03f }, // 70/128 - { -6.77557e-03f, 3.97222e-02f, -1.45859e-01f, 6.74865e-01f, 5.42025e-01f, -1.35775e-01f, 3.81085e-02f, -6.58996e-03f }, // 71/128 - { -6.75943e-03f, 3.96678e-02f, -1.45993e-01f, 6.83875e-01f, 5.32164e-01f, -1.34515e-01f, 3.78315e-02f, -6.54823e-03f }, // 72/128 - { -6.73929e-03f, 3.95900e-02f, -1.46043e-01f, 6.92808e-01f, 5.22267e-01f, -1.33190e-01f, 3.75341e-02f, -6.50285e-03f }, // 73/128 - { -6.71514e-03f, 3.94886e-02f, -1.46009e-01f, 7.01661e-01f, 5.12337e-01f, -1.31800e-01f, 3.72167e-02f, -6.45388e-03f }, // 74/128 - { -6.68698e-03f, 3.93636e-02f, -1.45889e-01f, 7.10431e-01f, 5.02377e-01f, -1.30347e-01f, 3.68795e-02f, -6.40135e-03f }, // 75/128 - { -6.65479e-03f, 3.92147e-02f, -1.45682e-01f, 7.19116e-01f, 4.92389e-01f, -1.28832e-01f, 3.65227e-02f, -6.34530e-03f }, // 76/128 - { -6.61859e-03f, 3.90420e-02f, -1.45387e-01f, 7.27714e-01f, 4.82377e-01f, -1.27258e-01f, 3.61468e-02f, -6.28577e-03f }, // 77/128 - { -6.57836e-03f, 3.88454e-02f, -1.45004e-01f, 7.36222e-01f, 4.72342e-01f, -1.25624e-01f, 3.57519e-02f, -6.22280e-03f }, // 78/128 - { -6.53412e-03f, 3.86247e-02f, -1.44531e-01f, 7.44637e-01f, 4.62289e-01f, -1.23933e-01f, 3.53384e-02f, -6.15644e-03f }, // 79/128 - { -6.48585e-03f, 3.83800e-02f, -1.43968e-01f, 7.52958e-01f, 4.52218e-01f, -1.22185e-01f, 3.49066e-02f, -6.08674e-03f }, // 80/128 - { -6.43358e-03f, 3.81111e-02f, -1.43313e-01f, 7.61181e-01f, 4.42134e-01f, -1.20382e-01f, 3.44568e-02f, -6.01374e-03f }, // 81/128 - { -6.37730e-03f, 3.78182e-02f, -1.42566e-01f, 7.69305e-01f, 4.32038e-01f, -1.18526e-01f, 3.39894e-02f, -5.93749e-03f }, // 82/128 - { -6.31703e-03f, 3.75010e-02f, -1.41727e-01f, 7.77327e-01f, 4.21934e-01f, -1.16618e-01f, 3.35046e-02f, -5.85804e-03f }, // 83/128 - { -6.25277e-03f, 3.71596e-02f, -1.40794e-01f, 7.85244e-01f, 4.11823e-01f, -1.14659e-01f, 3.30027e-02f, -5.77544e-03f }, // 84/128 - { -6.18454e-03f, 3.67941e-02f, -1.39767e-01f, 7.93055e-01f, 4.01710e-01f, -1.12650e-01f, 3.24843e-02f, -5.68976e-03f }, // 85/128 - { -6.11236e-03f, 3.64044e-02f, -1.38644e-01f, 8.00757e-01f, 3.91596e-01f, -1.10593e-01f, 3.19495e-02f, -5.60103e-03f }, // 86/128 - { -6.03624e-03f, 3.59905e-02f, -1.37426e-01f, 8.08348e-01f, 3.81484e-01f, -1.08490e-01f, 3.13987e-02f, -5.50931e-03f }, // 87/128 - { -5.95620e-03f, 3.55525e-02f, -1.36111e-01f, 8.15826e-01f, 3.71376e-01f, -1.06342e-01f, 3.08323e-02f, -5.41467e-03f }, // 88/128 - { -5.87227e-03f, 3.50903e-02f, -1.34699e-01f, 8.23188e-01f, 3.61276e-01f, -1.04150e-01f, 3.02507e-02f, -5.31716e-03f }, // 89/128 - { -5.78446e-03f, 3.46042e-02f, -1.33189e-01f, 8.30432e-01f, 3.51186e-01f, -1.01915e-01f, 2.96543e-02f, -5.21683e-03f }, // 90/128 - { -5.69280e-03f, 3.40940e-02f, -1.31581e-01f, 8.37557e-01f, 3.41109e-01f, -9.96402e-02f, 2.90433e-02f, -5.11376e-03f }, // 91/128 - { -5.59731e-03f, 3.35600e-02f, -1.29874e-01f, 8.44559e-01f, 3.31048e-01f, -9.73254e-02f, 2.84182e-02f, -5.00800e-03f }, // 92/128 - { -5.49804e-03f, 3.30021e-02f, -1.28068e-01f, 8.51437e-01f, 3.21004e-01f, -9.49727e-02f, 2.77794e-02f, -4.89961e-03f }, // 93/128 - { -5.39500e-03f, 3.24205e-02f, -1.26161e-01f, 8.58189e-01f, 3.10980e-01f, -9.25834e-02f, 2.71272e-02f, -4.78866e-03f }, // 94/128 - { -5.28823e-03f, 3.18153e-02f, -1.24154e-01f, 8.64812e-01f, 3.00980e-01f, -9.01591e-02f, 2.64621e-02f, -4.67520e-03f }, // 95/128 - { -5.17776e-03f, 3.11866e-02f, -1.22047e-01f, 8.71305e-01f, 2.91006e-01f, -8.77011e-02f, 2.57844e-02f, -4.55932e-03f }, // 96/128 - { -5.06363e-03f, 3.05345e-02f, -1.19837e-01f, 8.77666e-01f, 2.81060e-01f, -8.52109e-02f, 2.50946e-02f, -4.44107e-03f }, // 97/128 - { -4.94589e-03f, 2.98593e-02f, -1.17526e-01f, 8.83893e-01f, 2.71144e-01f, -8.26900e-02f, 2.43930e-02f, -4.32052e-03f }, // 98/128 - { -4.82456e-03f, 2.91609e-02f, -1.15113e-01f, 8.89984e-01f, 2.61263e-01f, -8.01399e-02f, 2.36801e-02f, -4.19774e-03f }, // 99/128 - { -4.69970e-03f, 2.84397e-02f, -1.12597e-01f, 8.95936e-01f, 2.51417e-01f, -7.75620e-02f, 2.29562e-02f, -4.07279e-03f }, // 100/128 - { -4.57135e-03f, 2.76957e-02f, -1.09978e-01f, 9.01749e-01f, 2.41609e-01f, -7.49577e-02f, 2.22218e-02f, -3.94576e-03f }, // 101/128 - { -4.43955e-03f, 2.69293e-02f, -1.07256e-01f, 9.07420e-01f, 2.31843e-01f, -7.23286e-02f, 2.14774e-02f, -3.81671e-03f }, // 102/128 - { -4.30435e-03f, 2.61404e-02f, -1.04430e-01f, 9.12947e-01f, 2.22120e-01f, -6.96762e-02f, 2.07233e-02f, -3.68570e-03f }, // 103/128 - { -4.16581e-03f, 2.53295e-02f, -1.01501e-01f, 9.18329e-01f, 2.12443e-01f, -6.70018e-02f, 1.99599e-02f, -3.55283e-03f }, // 104/128 - { -4.02397e-03f, 2.44967e-02f, -9.84679e-02f, 9.23564e-01f, 2.02814e-01f, -6.43069e-02f, 1.91877e-02f, -3.41815e-03f }, // 105/128 - { -3.87888e-03f, 2.36423e-02f, -9.53307e-02f, 9.28650e-01f, 1.93236e-01f, -6.15931e-02f, 1.84071e-02f, -3.28174e-03f }, // 106/128 - { -3.73062e-03f, 2.27664e-02f, -9.20893e-02f, 9.33586e-01f, 1.83711e-01f, -5.88617e-02f, 1.76185e-02f, -3.14367e-03f }, // 107/128 - { -3.57923e-03f, 2.18695e-02f, -8.87435e-02f, 9.38371e-01f, 1.74242e-01f, -5.61142e-02f, 1.68225e-02f, -3.00403e-03f }, // 108/128 - { -3.42477e-03f, 2.09516e-02f, -8.52933e-02f, 9.43001e-01f, 1.64831e-01f, -5.33522e-02f, 1.60193e-02f, -2.86289e-03f }, // 109/128 - { -3.26730e-03f, 2.00132e-02f, -8.17385e-02f, 9.47477e-01f, 1.55480e-01f, -5.05770e-02f, 1.52095e-02f, -2.72032e-03f }, // 110/128 - { -3.10689e-03f, 1.90545e-02f, -7.80792e-02f, 9.51795e-01f, 1.46192e-01f, -4.77900e-02f, 1.43934e-02f, -2.57640e-03f }, // 111/128 - { -2.94361e-03f, 1.80759e-02f, -7.43154e-02f, 9.55956e-01f, 1.36968e-01f, -4.49929e-02f, 1.35716e-02f, -2.43121e-03f }, // 112/128 - { -2.77751e-03f, 1.70776e-02f, -7.04471e-02f, 9.59958e-01f, 1.27812e-01f, -4.21869e-02f, 1.27445e-02f, -2.28483e-03f }, // 113/128 - { -2.60868e-03f, 1.60599e-02f, -6.64743e-02f, 9.63798e-01f, 1.18725e-01f, -3.93735e-02f, 1.19125e-02f, -2.13733e-03f }, // 114/128 - { -2.43718e-03f, 1.50233e-02f, -6.23972e-02f, 9.67477e-01f, 1.09710e-01f, -3.65541e-02f, 1.10760e-02f, -1.98880e-03f }, // 115/128 - { -2.26307e-03f, 1.39681e-02f, -5.82159e-02f, 9.70992e-01f, 1.00769e-01f, -3.37303e-02f, 1.02356e-02f, -1.83931e-03f }, // 116/128 - { -2.08645e-03f, 1.28947e-02f, -5.39305e-02f, 9.74342e-01f, 9.19033e-02f, -3.09033e-02f, 9.39154e-03f, -1.68894e-03f }, // 117/128 - { -1.90738e-03f, 1.18034e-02f, -4.95412e-02f, 9.77526e-01f, 8.31162e-02f, -2.80746e-02f, 8.54441e-03f, -1.53777e-03f }, // 118/128 - { -1.72594e-03f, 1.06946e-02f, -4.50483e-02f, 9.80543e-01f, 7.44095e-02f, -2.52457e-02f, 7.69462e-03f, -1.38589e-03f }, // 119/128 - { -1.54221e-03f, 9.56876e-03f, -4.04519e-02f, 9.83392e-01f, 6.57852e-02f, -2.24178e-02f, 6.84261e-03f, -1.23337e-03f }, // 120/128 - { -1.35627e-03f, 8.42626e-03f, -3.57525e-02f, 9.86071e-01f, 5.72454e-02f, -1.95925e-02f, 5.98883e-03f, -1.08030e-03f }, // 121/128 - { -1.16820e-03f, 7.26755e-03f, -3.09503e-02f, 9.88580e-01f, 4.87921e-02f, -1.67710e-02f, 5.13372e-03f, -9.26747e-04f }, // 122/128 - { -9.78093e-04f, 6.09305e-03f, -2.60456e-02f, 9.90917e-01f, 4.04274e-02f, -1.39548e-02f, 4.27773e-03f, -7.72802e-04f }, // 123/128 - { -7.86031e-04f, 4.90322e-03f, -2.10389e-02f, 9.93082e-01f, 3.21531e-02f, -1.11453e-02f, 3.42130e-03f, -6.18544e-04f }, // 124/128 - { -5.92100e-04f, 3.69852e-03f, -1.59305e-02f, 9.95074e-01f, 2.39714e-02f, -8.34364e-03f, 2.56486e-03f, -4.64053e-04f }, // 125/128 - { -3.96391e-04f, 2.47942e-03f, -1.07209e-02f, 9.96891e-01f, 1.58840e-02f, -5.55134e-03f, 1.70888e-03f, -3.09412e-04f }, // 126/128 - { -1.98993e-04f, 1.24642e-03f, -5.41054e-03f, 9.98534e-01f, 7.89295e-03f, -2.76968e-03f, 8.53777e-04f, -1.54700e-04f }, // 127/128 - { 0.00000e+00f, 0.00000e+00f, 0.00000e+00f, 1.00000e+00f, 0.00000e+00f, 0.00000e+00f, 0.00000e+00f, 0.00000e+00f }, // 128/128 - }; - + private static final float TAPS[][] = + { + {0.00000e+00f, 0.00000e+00f, 0.00000e+00f, 0.00000e+00f, 1.00000e+00f, 0.00000e+00f, 0.00000e+00f, 0.00000e+00f}, // 0/128 + {-1.54700e-04f, 8.53777e-04f, -2.76968e-03f, 7.89295e-03f, 9.98534e-01f, -5.41054e-03f, 1.24642e-03f, -1.98993e-04f}, // 1/128 + {-3.09412e-04f, 1.70888e-03f, -5.55134e-03f, 1.58840e-02f, 9.96891e-01f, -1.07209e-02f, 2.47942e-03f, -3.96391e-04f}, // 2/128 + {-4.64053e-04f, 2.56486e-03f, -8.34364e-03f, 2.39714e-02f, 9.95074e-01f, -1.59305e-02f, 3.69852e-03f, -5.92100e-04f}, // 3/128 + {-6.18544e-04f, 3.42130e-03f, -1.11453e-02f, 3.21531e-02f, 9.93082e-01f, -2.10389e-02f, 4.90322e-03f, -7.86031e-04f}, // 4/128 + {-7.72802e-04f, 4.27773e-03f, -1.39548e-02f, 4.04274e-02f, 9.90917e-01f, -2.60456e-02f, 6.09305e-03f, -9.78093e-04f}, // 5/128 + {-9.26747e-04f, 5.13372e-03f, -1.67710e-02f, 4.87921e-02f, 9.88580e-01f, -3.09503e-02f, 7.26755e-03f, -1.16820e-03f}, // 6/128 + {-1.08030e-03f, 5.98883e-03f, -1.95925e-02f, 5.72454e-02f, 9.86071e-01f, -3.57525e-02f, 8.42626e-03f, -1.35627e-03f}, // 7/128 + {-1.23337e-03f, 6.84261e-03f, -2.24178e-02f, 6.57852e-02f, 9.83392e-01f, -4.04519e-02f, 9.56876e-03f, -1.54221e-03f}, // 8/128 + {-1.38589e-03f, 7.69462e-03f, -2.52457e-02f, 7.44095e-02f, 9.80543e-01f, -4.50483e-02f, 1.06946e-02f, -1.72594e-03f}, // 9/128 + {-1.53777e-03f, 8.54441e-03f, -2.80746e-02f, 8.31162e-02f, 9.77526e-01f, -4.95412e-02f, 1.18034e-02f, -1.90738e-03f}, // 10/128 + {-1.68894e-03f, 9.39154e-03f, -3.09033e-02f, 9.19033e-02f, 9.74342e-01f, -5.39305e-02f, 1.28947e-02f, -2.08645e-03f}, // 11/128 + {-1.83931e-03f, 1.02356e-02f, -3.37303e-02f, 1.00769e-01f, 9.70992e-01f, -5.82159e-02f, 1.39681e-02f, -2.26307e-03f}, // 12/128 + {-1.98880e-03f, 1.10760e-02f, -3.65541e-02f, 1.09710e-01f, 9.67477e-01f, -6.23972e-02f, 1.50233e-02f, -2.43718e-03f}, // 13/128 + {-2.13733e-03f, 1.19125e-02f, -3.93735e-02f, 1.18725e-01f, 9.63798e-01f, -6.64743e-02f, 1.60599e-02f, -2.60868e-03f}, // 14/128 + {-2.28483e-03f, 1.27445e-02f, -4.21869e-02f, 1.27812e-01f, 9.59958e-01f, -7.04471e-02f, 1.70776e-02f, -2.77751e-03f}, // 15/128 + {-2.43121e-03f, 1.35716e-02f, -4.49929e-02f, 1.36968e-01f, 9.55956e-01f, -7.43154e-02f, 1.80759e-02f, -2.94361e-03f}, // 16/128 + {-2.57640e-03f, 1.43934e-02f, -4.77900e-02f, 1.46192e-01f, 9.51795e-01f, -7.80792e-02f, 1.90545e-02f, -3.10689e-03f}, // 17/128 + {-2.72032e-03f, 1.52095e-02f, -5.05770e-02f, 1.55480e-01f, 9.47477e-01f, -8.17385e-02f, 2.00132e-02f, -3.26730e-03f}, // 18/128 + {-2.86289e-03f, 1.60193e-02f, -5.33522e-02f, 1.64831e-01f, 9.43001e-01f, -8.52933e-02f, 2.09516e-02f, -3.42477e-03f}, // 19/128 + {-3.00403e-03f, 1.68225e-02f, -5.61142e-02f, 1.74242e-01f, 9.38371e-01f, -8.87435e-02f, 2.18695e-02f, -3.57923e-03f}, // 20/128 + {-3.14367e-03f, 1.76185e-02f, -5.88617e-02f, 1.83711e-01f, 9.33586e-01f, -9.20893e-02f, 2.27664e-02f, -3.73062e-03f}, // 21/128 + {-3.28174e-03f, 1.84071e-02f, -6.15931e-02f, 1.93236e-01f, 9.28650e-01f, -9.53307e-02f, 2.36423e-02f, -3.87888e-03f}, // 22/128 + {-3.41815e-03f, 1.91877e-02f, -6.43069e-02f, 2.02814e-01f, 9.23564e-01f, -9.84679e-02f, 2.44967e-02f, -4.02397e-03f}, // 23/128 + {-3.55283e-03f, 1.99599e-02f, -6.70018e-02f, 2.12443e-01f, 9.18329e-01f, -1.01501e-01f, 2.53295e-02f, -4.16581e-03f}, // 24/128 + {-3.68570e-03f, 2.07233e-02f, -6.96762e-02f, 2.22120e-01f, 9.12947e-01f, -1.04430e-01f, 2.61404e-02f, -4.30435e-03f}, // 25/128 + {-3.81671e-03f, 2.14774e-02f, -7.23286e-02f, 2.31843e-01f, 9.07420e-01f, -1.07256e-01f, 2.69293e-02f, -4.43955e-03f}, // 26/128 + {-3.94576e-03f, 2.22218e-02f, -7.49577e-02f, 2.41609e-01f, 9.01749e-01f, -1.09978e-01f, 2.76957e-02f, -4.57135e-03f}, // 27/128 + {-4.07279e-03f, 2.29562e-02f, -7.75620e-02f, 2.51417e-01f, 8.95936e-01f, -1.12597e-01f, 2.84397e-02f, -4.69970e-03f}, // 28/128 + {-4.19774e-03f, 2.36801e-02f, -8.01399e-02f, 2.61263e-01f, 8.89984e-01f, -1.15113e-01f, 2.91609e-02f, -4.82456e-03f}, // 29/128 + {-4.32052e-03f, 2.43930e-02f, -8.26900e-02f, 2.71144e-01f, 8.83893e-01f, -1.17526e-01f, 2.98593e-02f, -4.94589e-03f}, // 30/128 + {-4.44107e-03f, 2.50946e-02f, -8.52109e-02f, 2.81060e-01f, 8.77666e-01f, -1.19837e-01f, 3.05345e-02f, -5.06363e-03f}, // 31/128 + {-4.55932e-03f, 2.57844e-02f, -8.77011e-02f, 2.91006e-01f, 8.71305e-01f, -1.22047e-01f, 3.11866e-02f, -5.17776e-03f}, // 32/128 + {-4.67520e-03f, 2.64621e-02f, -9.01591e-02f, 3.00980e-01f, 8.64812e-01f, -1.24154e-01f, 3.18153e-02f, -5.28823e-03f}, // 33/128 + {-4.78866e-03f, 2.71272e-02f, -9.25834e-02f, 3.10980e-01f, 8.58189e-01f, -1.26161e-01f, 3.24205e-02f, -5.39500e-03f}, // 34/128 + {-4.89961e-03f, 2.77794e-02f, -9.49727e-02f, 3.21004e-01f, 8.51437e-01f, -1.28068e-01f, 3.30021e-02f, -5.49804e-03f}, // 35/128 + {-5.00800e-03f, 2.84182e-02f, -9.73254e-02f, 3.31048e-01f, 8.44559e-01f, -1.29874e-01f, 3.35600e-02f, -5.59731e-03f}, // 36/128 + {-5.11376e-03f, 2.90433e-02f, -9.96402e-02f, 3.41109e-01f, 8.37557e-01f, -1.31581e-01f, 3.40940e-02f, -5.69280e-03f}, // 37/128 + {-5.21683e-03f, 2.96543e-02f, -1.01915e-01f, 3.51186e-01f, 8.30432e-01f, -1.33189e-01f, 3.46042e-02f, -5.78446e-03f}, // 38/128 + {-5.31716e-03f, 3.02507e-02f, -1.04150e-01f, 3.61276e-01f, 8.23188e-01f, -1.34699e-01f, 3.50903e-02f, -5.87227e-03f}, // 39/128 + {-5.41467e-03f, 3.08323e-02f, -1.06342e-01f, 3.71376e-01f, 8.15826e-01f, -1.36111e-01f, 3.55525e-02f, -5.95620e-03f}, // 40/128 + {-5.50931e-03f, 3.13987e-02f, -1.08490e-01f, 3.81484e-01f, 8.08348e-01f, -1.37426e-01f, 3.59905e-02f, -6.03624e-03f}, // 41/128 + {-5.60103e-03f, 3.19495e-02f, -1.10593e-01f, 3.91596e-01f, 8.00757e-01f, -1.38644e-01f, 3.64044e-02f, -6.11236e-03f}, // 42/128 + {-5.68976e-03f, 3.24843e-02f, -1.12650e-01f, 4.01710e-01f, 7.93055e-01f, -1.39767e-01f, 3.67941e-02f, -6.18454e-03f}, // 43/128 + {-5.77544e-03f, 3.30027e-02f, -1.14659e-01f, 4.11823e-01f, 7.85244e-01f, -1.40794e-01f, 3.71596e-02f, -6.25277e-03f}, // 44/128 + {-5.85804e-03f, 3.35046e-02f, -1.16618e-01f, 4.21934e-01f, 7.77327e-01f, -1.41727e-01f, 3.75010e-02f, -6.31703e-03f}, // 45/128 + {-5.93749e-03f, 3.39894e-02f, -1.18526e-01f, 4.32038e-01f, 7.69305e-01f, -1.42566e-01f, 3.78182e-02f, -6.37730e-03f}, // 46/128 + {-6.01374e-03f, 3.44568e-02f, -1.20382e-01f, 4.42134e-01f, 7.61181e-01f, -1.43313e-01f, 3.81111e-02f, -6.43358e-03f}, // 47/128 + {-6.08674e-03f, 3.49066e-02f, -1.22185e-01f, 4.52218e-01f, 7.52958e-01f, -1.43968e-01f, 3.83800e-02f, -6.48585e-03f}, // 48/128 + {-6.15644e-03f, 3.53384e-02f, -1.23933e-01f, 4.62289e-01f, 7.44637e-01f, -1.44531e-01f, 3.86247e-02f, -6.53412e-03f}, // 49/128 + {-6.22280e-03f, 3.57519e-02f, -1.25624e-01f, 4.72342e-01f, 7.36222e-01f, -1.45004e-01f, 3.88454e-02f, -6.57836e-03f}, // 50/128 + {-6.28577e-03f, 3.61468e-02f, -1.27258e-01f, 4.82377e-01f, 7.27714e-01f, -1.45387e-01f, 3.90420e-02f, -6.61859e-03f}, // 51/128 + {-6.34530e-03f, 3.65227e-02f, -1.28832e-01f, 4.92389e-01f, 7.19116e-01f, -1.45682e-01f, 3.92147e-02f, -6.65479e-03f}, // 52/128 + {-6.40135e-03f, 3.68795e-02f, -1.30347e-01f, 5.02377e-01f, 7.10431e-01f, -1.45889e-01f, 3.93636e-02f, -6.68698e-03f}, // 53/128 + {-6.45388e-03f, 3.72167e-02f, -1.31800e-01f, 5.12337e-01f, 7.01661e-01f, -1.46009e-01f, 3.94886e-02f, -6.71514e-03f}, // 54/128 + {-6.50285e-03f, 3.75341e-02f, -1.33190e-01f, 5.22267e-01f, 6.92808e-01f, -1.46043e-01f, 3.95900e-02f, -6.73929e-03f}, // 55/128 + {-6.54823e-03f, 3.78315e-02f, -1.34515e-01f, 5.32164e-01f, 6.83875e-01f, -1.45993e-01f, 3.96678e-02f, -6.75943e-03f}, // 56/128 + {-6.58996e-03f, 3.81085e-02f, -1.35775e-01f, 5.42025e-01f, 6.74865e-01f, -1.45859e-01f, 3.97222e-02f, -6.77557e-03f}, // 57/128 + {-6.62802e-03f, 3.83650e-02f, -1.36969e-01f, 5.51849e-01f, 6.65779e-01f, -1.45641e-01f, 3.97532e-02f, -6.78771e-03f}, // 58/128 + {-6.66238e-03f, 3.86006e-02f, -1.38094e-01f, 5.61631e-01f, 6.56621e-01f, -1.45343e-01f, 3.97610e-02f, -6.79588e-03f}, // 59/128 + {-6.69300e-03f, 3.88151e-02f, -1.39150e-01f, 5.71370e-01f, 6.47394e-01f, -1.44963e-01f, 3.97458e-02f, -6.80007e-03f}, // 60/128 + {-6.71985e-03f, 3.90083e-02f, -1.40136e-01f, 5.81063e-01f, 6.38099e-01f, -1.44503e-01f, 3.97077e-02f, -6.80032e-03f}, // 61/128 + {-6.74291e-03f, 3.91800e-02f, -1.41050e-01f, 5.90706e-01f, 6.28739e-01f, -1.43965e-01f, 3.96469e-02f, -6.79662e-03f}, // 62/128 + {-6.76214e-03f, 3.93299e-02f, -1.41891e-01f, 6.00298e-01f, 6.19318e-01f, -1.43350e-01f, 3.95635e-02f, -6.78902e-03f}, // 63/128 + {-6.77751e-03f, 3.94578e-02f, -1.42658e-01f, 6.09836e-01f, 6.09836e-01f, -1.42658e-01f, 3.94578e-02f, -6.77751e-03f}, // 64/128 + {-6.78902e-03f, 3.95635e-02f, -1.43350e-01f, 6.19318e-01f, 6.00298e-01f, -1.41891e-01f, 3.93299e-02f, -6.76214e-03f}, // 65/128 + {-6.79662e-03f, 3.96469e-02f, -1.43965e-01f, 6.28739e-01f, 5.90706e-01f, -1.41050e-01f, 3.91800e-02f, -6.74291e-03f}, // 66/128 + {-6.80032e-03f, 3.97077e-02f, -1.44503e-01f, 6.38099e-01f, 5.81063e-01f, -1.40136e-01f, 3.90083e-02f, -6.71985e-03f}, // 67/128 + {-6.80007e-03f, 3.97458e-02f, -1.44963e-01f, 6.47394e-01f, 5.71370e-01f, -1.39150e-01f, 3.88151e-02f, -6.69300e-03f}, // 68/128 + {-6.79588e-03f, 3.97610e-02f, -1.45343e-01f, 6.56621e-01f, 5.61631e-01f, -1.38094e-01f, 3.86006e-02f, -6.66238e-03f}, // 69/128 + {-6.78771e-03f, 3.97532e-02f, -1.45641e-01f, 6.65779e-01f, 5.51849e-01f, -1.36969e-01f, 3.83650e-02f, -6.62802e-03f}, // 70/128 + {-6.77557e-03f, 3.97222e-02f, -1.45859e-01f, 6.74865e-01f, 5.42025e-01f, -1.35775e-01f, 3.81085e-02f, -6.58996e-03f}, // 71/128 + {-6.75943e-03f, 3.96678e-02f, -1.45993e-01f, 6.83875e-01f, 5.32164e-01f, -1.34515e-01f, 3.78315e-02f, -6.54823e-03f}, // 72/128 + {-6.73929e-03f, 3.95900e-02f, -1.46043e-01f, 6.92808e-01f, 5.22267e-01f, -1.33190e-01f, 3.75341e-02f, -6.50285e-03f}, // 73/128 + {-6.71514e-03f, 3.94886e-02f, -1.46009e-01f, 7.01661e-01f, 5.12337e-01f, -1.31800e-01f, 3.72167e-02f, -6.45388e-03f}, // 74/128 + {-6.68698e-03f, 3.93636e-02f, -1.45889e-01f, 7.10431e-01f, 5.02377e-01f, -1.30347e-01f, 3.68795e-02f, -6.40135e-03f}, // 75/128 + {-6.65479e-03f, 3.92147e-02f, -1.45682e-01f, 7.19116e-01f, 4.92389e-01f, -1.28832e-01f, 3.65227e-02f, -6.34530e-03f}, // 76/128 + {-6.61859e-03f, 3.90420e-02f, -1.45387e-01f, 7.27714e-01f, 4.82377e-01f, -1.27258e-01f, 3.61468e-02f, -6.28577e-03f}, // 77/128 + {-6.57836e-03f, 3.88454e-02f, -1.45004e-01f, 7.36222e-01f, 4.72342e-01f, -1.25624e-01f, 3.57519e-02f, -6.22280e-03f}, // 78/128 + {-6.53412e-03f, 3.86247e-02f, -1.44531e-01f, 7.44637e-01f, 4.62289e-01f, -1.23933e-01f, 3.53384e-02f, -6.15644e-03f}, // 79/128 + {-6.48585e-03f, 3.83800e-02f, -1.43968e-01f, 7.52958e-01f, 4.52218e-01f, -1.22185e-01f, 3.49066e-02f, -6.08674e-03f}, // 80/128 + {-6.43358e-03f, 3.81111e-02f, -1.43313e-01f, 7.61181e-01f, 4.42134e-01f, -1.20382e-01f, 3.44568e-02f, -6.01374e-03f}, // 81/128 + {-6.37730e-03f, 3.78182e-02f, -1.42566e-01f, 7.69305e-01f, 4.32038e-01f, -1.18526e-01f, 3.39894e-02f, -5.93749e-03f}, // 82/128 + {-6.31703e-03f, 3.75010e-02f, -1.41727e-01f, 7.77327e-01f, 4.21934e-01f, -1.16618e-01f, 3.35046e-02f, -5.85804e-03f}, // 83/128 + {-6.25277e-03f, 3.71596e-02f, -1.40794e-01f, 7.85244e-01f, 4.11823e-01f, -1.14659e-01f, 3.30027e-02f, -5.77544e-03f}, // 84/128 + {-6.18454e-03f, 3.67941e-02f, -1.39767e-01f, 7.93055e-01f, 4.01710e-01f, -1.12650e-01f, 3.24843e-02f, -5.68976e-03f}, // 85/128 + {-6.11236e-03f, 3.64044e-02f, -1.38644e-01f, 8.00757e-01f, 3.91596e-01f, -1.10593e-01f, 3.19495e-02f, -5.60103e-03f}, // 86/128 + {-6.03624e-03f, 3.59905e-02f, -1.37426e-01f, 8.08348e-01f, 3.81484e-01f, -1.08490e-01f, 3.13987e-02f, -5.50931e-03f}, // 87/128 + {-5.95620e-03f, 3.55525e-02f, -1.36111e-01f, 8.15826e-01f, 3.71376e-01f, -1.06342e-01f, 3.08323e-02f, -5.41467e-03f}, // 88/128 + {-5.87227e-03f, 3.50903e-02f, -1.34699e-01f, 8.23188e-01f, 3.61276e-01f, -1.04150e-01f, 3.02507e-02f, -5.31716e-03f}, // 89/128 + {-5.78446e-03f, 3.46042e-02f, -1.33189e-01f, 8.30432e-01f, 3.51186e-01f, -1.01915e-01f, 2.96543e-02f, -5.21683e-03f}, // 90/128 + {-5.69280e-03f, 3.40940e-02f, -1.31581e-01f, 8.37557e-01f, 3.41109e-01f, -9.96402e-02f, 2.90433e-02f, -5.11376e-03f}, // 91/128 + {-5.59731e-03f, 3.35600e-02f, -1.29874e-01f, 8.44559e-01f, 3.31048e-01f, -9.73254e-02f, 2.84182e-02f, -5.00800e-03f}, // 92/128 + {-5.49804e-03f, 3.30021e-02f, -1.28068e-01f, 8.51437e-01f, 3.21004e-01f, -9.49727e-02f, 2.77794e-02f, -4.89961e-03f}, // 93/128 + {-5.39500e-03f, 3.24205e-02f, -1.26161e-01f, 8.58189e-01f, 3.10980e-01f, -9.25834e-02f, 2.71272e-02f, -4.78866e-03f}, // 94/128 + {-5.28823e-03f, 3.18153e-02f, -1.24154e-01f, 8.64812e-01f, 3.00980e-01f, -9.01591e-02f, 2.64621e-02f, -4.67520e-03f}, // 95/128 + {-5.17776e-03f, 3.11866e-02f, -1.22047e-01f, 8.71305e-01f, 2.91006e-01f, -8.77011e-02f, 2.57844e-02f, -4.55932e-03f}, // 96/128 + {-5.06363e-03f, 3.05345e-02f, -1.19837e-01f, 8.77666e-01f, 2.81060e-01f, -8.52109e-02f, 2.50946e-02f, -4.44107e-03f}, // 97/128 + {-4.94589e-03f, 2.98593e-02f, -1.17526e-01f, 8.83893e-01f, 2.71144e-01f, -8.26900e-02f, 2.43930e-02f, -4.32052e-03f}, // 98/128 + {-4.82456e-03f, 2.91609e-02f, -1.15113e-01f, 8.89984e-01f, 2.61263e-01f, -8.01399e-02f, 2.36801e-02f, -4.19774e-03f}, // 99/128 + {-4.69970e-03f, 2.84397e-02f, -1.12597e-01f, 8.95936e-01f, 2.51417e-01f, -7.75620e-02f, 2.29562e-02f, -4.07279e-03f}, // 100/128 + {-4.57135e-03f, 2.76957e-02f, -1.09978e-01f, 9.01749e-01f, 2.41609e-01f, -7.49577e-02f, 2.22218e-02f, -3.94576e-03f}, // 101/128 + {-4.43955e-03f, 2.69293e-02f, -1.07256e-01f, 9.07420e-01f, 2.31843e-01f, -7.23286e-02f, 2.14774e-02f, -3.81671e-03f}, // 102/128 + {-4.30435e-03f, 2.61404e-02f, -1.04430e-01f, 9.12947e-01f, 2.22120e-01f, -6.96762e-02f, 2.07233e-02f, -3.68570e-03f}, // 103/128 + {-4.16581e-03f, 2.53295e-02f, -1.01501e-01f, 9.18329e-01f, 2.12443e-01f, -6.70018e-02f, 1.99599e-02f, -3.55283e-03f}, // 104/128 + {-4.02397e-03f, 2.44967e-02f, -9.84679e-02f, 9.23564e-01f, 2.02814e-01f, -6.43069e-02f, 1.91877e-02f, -3.41815e-03f}, // 105/128 + {-3.87888e-03f, 2.36423e-02f, -9.53307e-02f, 9.28650e-01f, 1.93236e-01f, -6.15931e-02f, 1.84071e-02f, -3.28174e-03f}, // 106/128 + {-3.73062e-03f, 2.27664e-02f, -9.20893e-02f, 9.33586e-01f, 1.83711e-01f, -5.88617e-02f, 1.76185e-02f, -3.14367e-03f}, // 107/128 + {-3.57923e-03f, 2.18695e-02f, -8.87435e-02f, 9.38371e-01f, 1.74242e-01f, -5.61142e-02f, 1.68225e-02f, -3.00403e-03f}, // 108/128 + {-3.42477e-03f, 2.09516e-02f, -8.52933e-02f, 9.43001e-01f, 1.64831e-01f, -5.33522e-02f, 1.60193e-02f, -2.86289e-03f}, // 109/128 + {-3.26730e-03f, 2.00132e-02f, -8.17385e-02f, 9.47477e-01f, 1.55480e-01f, -5.05770e-02f, 1.52095e-02f, -2.72032e-03f}, // 110/128 + {-3.10689e-03f, 1.90545e-02f, -7.80792e-02f, 9.51795e-01f, 1.46192e-01f, -4.77900e-02f, 1.43934e-02f, -2.57640e-03f}, // 111/128 + {-2.94361e-03f, 1.80759e-02f, -7.43154e-02f, 9.55956e-01f, 1.36968e-01f, -4.49929e-02f, 1.35716e-02f, -2.43121e-03f}, // 112/128 + {-2.77751e-03f, 1.70776e-02f, -7.04471e-02f, 9.59958e-01f, 1.27812e-01f, -4.21869e-02f, 1.27445e-02f, -2.28483e-03f}, // 113/128 + {-2.60868e-03f, 1.60599e-02f, -6.64743e-02f, 9.63798e-01f, 1.18725e-01f, -3.93735e-02f, 1.19125e-02f, -2.13733e-03f}, // 114/128 + {-2.43718e-03f, 1.50233e-02f, -6.23972e-02f, 9.67477e-01f, 1.09710e-01f, -3.65541e-02f, 1.10760e-02f, -1.98880e-03f}, // 115/128 + {-2.26307e-03f, 1.39681e-02f, -5.82159e-02f, 9.70992e-01f, 1.00769e-01f, -3.37303e-02f, 1.02356e-02f, -1.83931e-03f}, // 116/128 + {-2.08645e-03f, 1.28947e-02f, -5.39305e-02f, 9.74342e-01f, 9.19033e-02f, -3.09033e-02f, 9.39154e-03f, -1.68894e-03f}, // 117/128 + {-1.90738e-03f, 1.18034e-02f, -4.95412e-02f, 9.77526e-01f, 8.31162e-02f, -2.80746e-02f, 8.54441e-03f, -1.53777e-03f}, // 118/128 + {-1.72594e-03f, 1.06946e-02f, -4.50483e-02f, 9.80543e-01f, 7.44095e-02f, -2.52457e-02f, 7.69462e-03f, -1.38589e-03f}, // 119/128 + {-1.54221e-03f, 9.56876e-03f, -4.04519e-02f, 9.83392e-01f, 6.57852e-02f, -2.24178e-02f, 6.84261e-03f, -1.23337e-03f}, // 120/128 + {-1.35627e-03f, 8.42626e-03f, -3.57525e-02f, 9.86071e-01f, 5.72454e-02f, -1.95925e-02f, 5.98883e-03f, -1.08030e-03f}, // 121/128 + {-1.16820e-03f, 7.26755e-03f, -3.09503e-02f, 9.88580e-01f, 4.87921e-02f, -1.67710e-02f, 5.13372e-03f, -9.26747e-04f}, // 122/128 + {-9.78093e-04f, 6.09305e-03f, -2.60456e-02f, 9.90917e-01f, 4.04274e-02f, -1.39548e-02f, 4.27773e-03f, -7.72802e-04f}, // 123/128 + {-7.86031e-04f, 4.90322e-03f, -2.10389e-02f, 9.93082e-01f, 3.21531e-02f, -1.11453e-02f, 3.42130e-03f, -6.18544e-04f}, // 124/128 + {-5.92100e-04f, 3.69852e-03f, -1.59305e-02f, 9.95074e-01f, 2.39714e-02f, -8.34364e-03f, 2.56486e-03f, -4.64053e-04f}, // 125/128 + {-3.96391e-04f, 2.47942e-03f, -1.07209e-02f, 9.96891e-01f, 1.58840e-02f, -5.55134e-03f, 1.70888e-03f, -3.09412e-04f}, // 126/128 + {-1.98993e-04f, 1.24642e-03f, -5.41054e-03f, 9.98534e-01f, 7.89295e-03f, -2.76968e-03f, 8.53777e-04f, -1.54700e-04f}, // 127/128 + {0.00000e+00f, 0.00000e+00f, 0.00000e+00f, 1.00000e+00f, 0.00000e+00f, 0.00000e+00f, 0.00000e+00f, 0.00000e+00f}, // 128/128 + }; + /* Instrumentation Taps */ - private static final String INSTRUMENT_SYMBOL_SPREAD = "Tap Point: Symbol Spread (Goal=2.0)"; - private FloatTap mSymbolSpreadTap; + private static final String INSTRUMENT_SYMBOL_SPREAD = "Tap Point: Symbol Spread (Goal=2.0)"; + private FloatTap mSymbolSpreadTap; private List mAvailableTaps; - - private static final int NUMBER_FILTER_TAPS = 8; - private static final int NUMBER_FILTER_STEPS = 128; - - private static final int SAMPLE_RATE = 48000; - private static final int SYMBOL_RATE = 4800; - - /* Tracking loop gain constant */ - private static final double K_SYMBOL_SPREAD = 0.0100; - /* Constraints on symbol spreading */ - private static final float SYMBOL_SPREAD_MAX = 2.4f; // upper range limit: +20% - private static final float SYMBOL_SPREAD_MIN = 1.6f; // lower range limit: -20% + private static final int NUMBER_FILTER_TAPS = 8; + private static final int NUMBER_FILTER_STEPS = 128; - /* Symbol clock tracking loop gain */ - private static final double K_SYMBOL_TIMING = 0.025; - - /* Coarse and fine frequency tracking constants */ - private static final double K_COARSE_FREQUENCY = 0.00125; - private static final double K_FINE_FREQUENCY = 0.125; - - /* Frequency correction broadcast threshold */ + private static final int SAMPLE_RATE = 48000; + private static final int SYMBOL_RATE = 4800; + + /* Tracking loop gain constant */ + private static final double K_SYMBOL_SPREAD = 0.0100; + + /* Constraints on symbol spreading */ + private static final float SYMBOL_SPREAD_MAX = 2.4f; // upper range limit: +20% + private static final float SYMBOL_SPREAD_MIN = 1.6f; // lower range limit: -20% + + /* Symbol clock tracking loop gain */ + private static final double K_SYMBOL_TIMING = 0.025; + + /* Coarse and fine frequency tracking constants */ + private static final double K_COARSE_FREQUENCY = 0.00125; + private static final double K_FINE_FREQUENCY = 0.125; + + /* Frequency correction broadcast threshold */ // private static final double COARSE_FREQUENCY_DEADBAND = 1.66; - private static final double COARSE_FREQUENCY_THRESHOLD = 1.20; - - /* 2.0 symbol spread gives -3, -1, 1, 3 */ - private float mSymbolSpread = 2.0f; - private float mSymbolClock = 0.0f; - private float mSymbolTime = (float)SYMBOL_RATE / (float)SAMPLE_RATE; - - private float mFineFrequencyCorrection = 0.0f; - private float mCoarseFrequencyCorrection = 0.0f; + private static final double COARSE_FREQUENCY_THRESHOLD = 1.20; - private float mHistory[] = new float[ NUMBER_FILTER_TAPS ]; - private int mHistoryLast = 0; - - private RealSampleListener mListener; - - private DirectGainControl mGainController = new DirectGainControl( 15.0f, 0.1f, 35.0f, 0.3f ); + /* 2.0 symbol spread gives -3, -1, 1, 3 */ + private float mSymbolSpread = 2.0f; + private float mSymbolClock = 0.0f; + private float mSymbolTime = (float)SYMBOL_RATE / (float)SAMPLE_RATE; - private int mFrequencyAdjustmentRequested = 0; - private int mFrequencyCorrection = 0; - private int mFrequencyCorrectionMaximum = 3000; - private boolean mResetFrequencyTracker = false; - private FrequencyCorrectionProcessor mFrequencyCorrectionProcessor; - private Listener mFrequencyChangeListener; - - /** - * C4FM Symbol Filter - * - * Sample Gain values - the gain of incoming sample values will critically - * impact the performance of this filter. Gain should be adjusted to - * optimize the value of mSymbolSpread toward a value of 2.0. If the value - * is less than 2.0, then increase gain. If the value is over 2.0, then - * decrease gain. - * - * When preceded by the RealAutomaticGainFilter class, an optimal gain - * setting for this filter is 15.4 and that yields a symbol spread that - * is centered on 2.0, ranging 1.92 to 2.08. - */ - public C4FMSymbolFilter( int frequencyCorrectionMaximum ) - { - mFrequencyCorrectionMaximum = frequencyCorrectionMaximum; - } - - public void dispose() - { - mGainController = null; + private float mFineFrequencyCorrection = 0.0f; + private float mCoarseFrequencyCorrection = 0.0f; + + private float mHistory[] = new float[NUMBER_FILTER_TAPS]; + private int mHistoryLast = 0; + + private RealSampleListener mListener; + + private DirectGainControl mGainController = new DirectGainControl(15.0f, 0.1f, 35.0f, 0.3f); + + private int mFrequencyAdjustmentRequested = 0; + private int mFrequencyCorrection = 0; + private int mFrequencyCorrectionMaximum = 3000; + private boolean mResetFrequencyTracker = false; + private FrequencyCorrectionProcessor mFrequencyCorrectionProcessor; + private Listener mFrequencyChangeListener; + + /** + * C4FM Symbol Filter + * + * Sample Gain values - the gain of incoming sample values will critically + * impact the performance of this filter. Gain should be adjusted to + * optimize the value of mSymbolSpread toward a value of 2.0. If the value + * is less than 2.0, then increase gain. If the value is over 2.0, then + * decrease gain. + * + * When preceded by the RealAutomaticGainFilter class, an optimal gain + * setting for this filter is 15.4 and that yields a symbol spread that + * is centered on 2.0, ranging 1.92 to 2.08. + */ + public C4FMSymbolFilter(int frequencyCorrectionMaximum) + { + mFrequencyCorrectionMaximum = frequencyCorrectionMaximum; + } + + public void dispose() + { + mGainController = null; // mFrequencyCorrectionControl = null; - mListener = null; - } - - @Override - public void receive( RealBuffer buffer ) - { - for( float sample: buffer.getSamples() ) - { - receive( sample ); - } + mListener = null; + } + + @Override + public void receive(RealBuffer buffer) + { + for(float sample : buffer.getSamples()) + { + receive(sample); + } /* If a frequency correction was requested during the processing of this - * buffer, we'll apply the change and it will be reflected in the next + * buffer, we'll apply the change and it will be reflected in the next * arriving buffer. Reset the lock on frequency correction and reset * the internal frequency correction tracker */ - if( mFrequencyAdjustmentRequested != 0 ) - { - int correction = mFrequencyCorrection + mFrequencyAdjustmentRequested; - - if( correction > mFrequencyCorrectionMaximum ) - { - correction = mFrequencyCorrectionMaximum; - } - else if( correction < -mFrequencyCorrectionMaximum ) - { - correction = -mFrequencyCorrectionMaximum; - } + if(mFrequencyAdjustmentRequested != 0) + { + int correction = mFrequencyCorrection + mFrequencyAdjustmentRequested; - broadcast( new FrequencyChangeEvent(Event.REQUEST_CHANNEL_FREQUENCY_CORRECTION_CHANGE, correction ) ); + if(correction > mFrequencyCorrectionMaximum) + { + correction = mFrequencyCorrectionMaximum; + } + else if(correction < -mFrequencyCorrectionMaximum) + { + correction = -mFrequencyCorrectionMaximum; + } - mFrequencyAdjustmentRequested = 0; - } - } + broadcast(SourceEvent.channelFrequencyCorrectionRequest(correction)); - public void receive( float sample ) + mFrequencyAdjustmentRequested = 0; + } + } + + public void receive(float sample) { - sample = mGainController.correct( sample ); - - if( mResetFrequencyTracker ) - { - mCoarseFrequencyCorrection = 0.0f; - mFineFrequencyCorrection = 0.0f; - - broadcast( new FrequencyChangeEvent( - Event.REQUEST_CHANNEL_FREQUENCY_CORRECTION_CHANGE, 0 )); - - mResetFrequencyTracker = false; - } - - mSymbolClock += mSymbolTime; - - mHistory[ mHistoryLast++ ] = sample; - - mHistoryLast %= NUMBER_FILTER_TAPS; - - if( mSymbolClock > 1.0f ) - { - mSymbolClock -= 1.0f; - - int imu = (int)Math.floor( 0.5 + - ( (float)NUMBER_FILTER_STEPS * ( mSymbolClock / mSymbolTime ) ) ); - - if( imu >= NUMBER_FILTER_STEPS ) - { - imu = NUMBER_FILTER_STEPS - 1; - } + sample = mGainController.correct(sample); - int imu_p1 = imu + 1; + if(mResetFrequencyTracker) + { + mCoarseFrequencyCorrection = 0.0f; + mFineFrequencyCorrection = 0.0f; - int j = mHistoryLast; - - double interp = 0.0; - double interp_p1 = 0.0; - - for( int i = 0; i < NUMBER_FILTER_TAPS; i++ ) - { - interp += TAPS[ imu][ i ] * mHistory[ j ]; - interp_p1 += TAPS[ imu_p1 ][ i ] * mHistory[ j ]; - - j = ( j + 1 ) % NUMBER_FILTER_TAPS; - } + broadcast(SourceEvent.channelFrequencyCorrectionRequest(0l)); + + mResetFrequencyTracker = false; + } + + mSymbolClock += mSymbolTime; + + mHistory[mHistoryLast++] = sample; + + mHistoryLast %= NUMBER_FILTER_TAPS; + + if(mSymbolClock > 1.0f) + { + mSymbolClock -= 1.0f; + + int imu = (int)Math.floor(0.5 + + ((float)NUMBER_FILTER_STEPS * (mSymbolClock / mSymbolTime))); + + if(imu >= NUMBER_FILTER_STEPS) + { + imu = NUMBER_FILTER_STEPS - 1; + } + + int imu_p1 = imu + 1; + + int j = mHistoryLast; + + double interp = 0.0; + double interp_p1 = 0.0; + + for(int i = 0; i < NUMBER_FILTER_TAPS; i++) + { + interp += TAPS[imu][i] * mHistory[j]; + interp_p1 += TAPS[imu_p1][i] * mHistory[j]; + + j = (j + 1) % NUMBER_FILTER_TAPS; + } /* Output symbol will be interpolated value corrected for symbol * spread and frequency offset */ - interp -= mFineFrequencyCorrection; - interp_p1 -= mFineFrequencyCorrection; + interp -= mFineFrequencyCorrection; + interp_p1 -= mFineFrequencyCorrection; /* Correct output for symbol deviation (spread) */ - float output = (float)( 2.0 * interp / mSymbolSpread ); + float output = (float)(2.0 * interp / mSymbolSpread); /* Detect received symbol error: basically use a hard decision and * subtract off expected position nominal symbol level which will be * +/- 0.5 * symbol spread and +/- 1.5 symbol spread. Remember that * nominal symbol spread will be 2.0 */ - double symbolError; - - if( interp < -mSymbolSpread ) - { + double symbolError; + + if(interp < -mSymbolSpread) + { /* symbol is -3: Expected at -1.5 * symbol spread */ - symbolError = interp + ( 1.5 * mSymbolSpread ); - mSymbolSpread -= ( symbolError * 0.5 * K_SYMBOL_SPREAD ); - } - else if( interp < 0.0 ) - { + symbolError = interp + (1.5 * mSymbolSpread); + mSymbolSpread -= (symbolError * 0.5 * K_SYMBOL_SPREAD); + } + else if(interp < 0.0) + { /* symbol is -1: Expected at -0.5 * symbol_spread */ - symbolError = interp + (0.5 * mSymbolSpread ); - mSymbolSpread -= ( symbolError * K_SYMBOL_SPREAD ); - } - else if( interp < mSymbolSpread ) - { + symbolError = interp + (0.5 * mSymbolSpread); + mSymbolSpread -= (symbolError * K_SYMBOL_SPREAD); + } + else if(interp < mSymbolSpread) + { /* symbol is +1: Expected at +0.5 * symbol_spread */ - symbolError = interp - ( 0.5 * mSymbolSpread ); - mSymbolSpread += ( symbolError * K_SYMBOL_SPREAD ); - } - else - { + symbolError = interp - (0.5 * mSymbolSpread); + mSymbolSpread += (symbolError * K_SYMBOL_SPREAD); + } + else + { /* symbol is +3: Expected at +1.5 * symbol_spread */ - symbolError = interp - ( 1.5 * mSymbolSpread ); - mSymbolSpread += ( symbolError * 0.5 * K_SYMBOL_SPREAD ); - } + symbolError = interp - (1.5 * mSymbolSpread); + mSymbolSpread += (symbolError * 0.5 * K_SYMBOL_SPREAD); + } /* Symbol clock tracking loop adjustment */ - if( interp_p1 < interp ) - { - mSymbolClock += symbolError * K_SYMBOL_TIMING; - } - else - { - mSymbolClock -= symbolError * K_SYMBOL_TIMING; - } - - if( mSymbolSpread < SYMBOL_SPREAD_MIN ) - { - mGainController.increase(); - - mSymbolSpread = SYMBOL_SPREAD_MIN; - } - else if( mSymbolSpread > SYMBOL_SPREAD_MAX ) - { - mGainController.decrease(); + if(interp_p1 < interp) + { + mSymbolClock += symbolError * K_SYMBOL_TIMING; + } + else + { + mSymbolClock -= symbolError * K_SYMBOL_TIMING; + } - mSymbolSpread = SYMBOL_SPREAD_MAX; - } + if(mSymbolSpread < SYMBOL_SPREAD_MIN) + { + mGainController.increase(); - mCoarseFrequencyCorrection += ( ( mFineFrequencyCorrection - - mCoarseFrequencyCorrection ) * K_COARSE_FREQUENCY ); - - mFineFrequencyCorrection += ( symbolError * K_FINE_FREQUENCY ); + mSymbolSpread = SYMBOL_SPREAD_MIN; + } + else if(mSymbolSpread > SYMBOL_SPREAD_MAX) + { + mGainController.decrease(); + + mSymbolSpread = SYMBOL_SPREAD_MAX; + } + + mCoarseFrequencyCorrection += ((mFineFrequencyCorrection - + mCoarseFrequencyCorrection) * K_COARSE_FREQUENCY); + + mFineFrequencyCorrection += (symbolError * K_FINE_FREQUENCY); /* Queue a frequency adjustment (once per buffer) as needed */ - if( Math.abs( mCoarseFrequencyCorrection ) > COARSE_FREQUENCY_THRESHOLD ) - { - mFrequencyAdjustmentRequested = - 500 * ( mCoarseFrequencyCorrection > 0 ? 1 : -1 ); - } - - if( mSymbolSpreadTap != null ) - { - mSymbolSpreadTap.receive( mSymbolSpread ); - } + if(Math.abs(mCoarseFrequencyCorrection) > COARSE_FREQUENCY_THRESHOLD) + { + mFrequencyAdjustmentRequested = + 500 * (mCoarseFrequencyCorrection > 0 ? 1 : -1); + } + + if(mSymbolSpreadTap != null) + { + mSymbolSpreadTap.receive(mSymbolSpread); + } /* dispatch the interpolated value to the listener */ - if( mListener != null ) - { - mListener.receive( output ); - } - } + if(mListener != null) + { + mListener.receive(output); + } + } } - @Override - public void setListener( RealSampleListener listener ) + @Override + public void setListener(RealSampleListener listener) { - mListener = listener; + mListener = listener; } - @Override - public void removeListener( RealSampleListener listener ) + @Override + public void removeListener(RealSampleListener listener) { - mListener = null; + mListener = null; } - - @Override + + @Override public List getTapGroups() { - if( mAvailableTaps == null ) - { - mAvailableTaps = new ArrayList<>(); + if(mAvailableTaps == null) + { + mAvailableTaps = new ArrayList<>(); /* * Since the target is 2.0, we subtract 2.0 from the value so that * a good value is plotted right on the zero center line */ - TapGroup group = new TapGroup( "C4FM Symbol Filter" ); - - group.add( - new AdditiveFloatTap( INSTRUMENT_SYMBOL_SPREAD, 0, 0.1f, -2.0f ) ); - - mAvailableTaps.add( group ); - } - - return mAvailableTaps; + TapGroup group = new TapGroup("C4FM Symbol Filter"); + + group.add( + new AdditiveFloatTap(INSTRUMENT_SYMBOL_SPREAD, 0, 0.1f, -2.0f)); + + mAvailableTaps.add(group); + } + + return mAvailableTaps; } - @Override - public void registerTap( Tap tap ) + @Override + public void registerTap(Tap tap) { - if( tap.getName().contentEquals( INSTRUMENT_SYMBOL_SPREAD ) ) - { - mSymbolSpreadTap = (FloatTap)tap; - } + if(tap.getName().contentEquals(INSTRUMENT_SYMBOL_SPREAD)) + { + mSymbolSpreadTap = (FloatTap)tap; + } } - @Override - public void unregisterTap( Tap tap ) + @Override + public void unregisterTap(Tap tap) { - if( tap.getName().contentEquals( INSTRUMENT_SYMBOL_SPREAD ) ) - { - mSymbolSpreadTap = null; - } + if(tap.getName().contentEquals(INSTRUMENT_SYMBOL_SPREAD)) + { + mSymbolSpreadTap = null; + } } /** * Broadcasts a frequency change event to the registered listener + * * @param event */ - public void broadcast( FrequencyChangeEvent event ) - { - if( mFrequencyChangeListener != null ) - { - mFrequencyChangeListener.receive( event ); - } - } + public void broadcast(SourceEvent event) + { + if(mFrequencyChangeListener != null) + { + mFrequencyChangeListener.receive(event); + } + } /** * Sets the listener for frequency change events. + * * @param listener */ - public void setFrequencyChangeListener( Listener listener ) - { - mFrequencyChangeListener = listener; - } - - @Override - public Listener getFrequencyChangeListener() - { - if( mFrequencyCorrectionProcessor == null ) - { - mFrequencyCorrectionProcessor = new FrequencyCorrectionProcessor(); - } - - return mFrequencyCorrectionProcessor; - } + public void setFrequencyChangeListener(Listener listener) + { + mFrequencyChangeListener = listener; + } - /** - * Receives notifications that the channel frequency correction has been - * applied and updates internal tracking value. - */ - public class FrequencyCorrectionProcessor implements Listener - { - @Override - public void receive( FrequencyChangeEvent event ) - { - switch( event.getEvent() ) - { - case NOTIFICATION_CHANNEL_FREQUENCY_CORRECTION_CHANGE: - mFrequencyCorrection = event.getValue().intValue(); + @Override + public Listener getSourceEventListener() + { + if(mFrequencyCorrectionProcessor == null) + { + mFrequencyCorrectionProcessor = new FrequencyCorrectionProcessor(); + } - //Reset internal frequency tracking - mCoarseFrequencyCorrection = 0.0f; - mFineFrequencyCorrection = 0.0f; - break; - case NOTIFICATION_FREQUENCY_CORRECTION_CHANGE: - case NOTIFICATION_SAMPLE_RATE_CHANGE: - mResetFrequencyTracker = true; - break; - default: - break; - } - } - } + return mFrequencyCorrectionProcessor; + } + + /** + * Receives notifications that the channel frequency correction has been + * applied and updates internal tracking value. + */ + public class FrequencyCorrectionProcessor implements Listener + { + @Override + public void receive(SourceEvent event) + { + switch(event.getEvent()) + { + case NOTIFICATION_CHANNEL_FREQUENCY_CORRECTION_CHANGE: + mFrequencyCorrection = event.getValue().intValue(); + + //Reset internal frequency tracking + mCoarseFrequencyCorrection = 0.0f; + mFineFrequencyCorrection = 0.0f; + break; + case NOTIFICATION_FREQUENCY_CORRECTION_CHANGE: + case NOTIFICATION_SAMPLE_RATE_CHANGE: + mResetFrequencyTracker = true; + break; + default: + break; + } + } + } } diff --git a/src/main/java/io/github/dsheirer/module/decode/p25/P25Decoder.java b/src/main/java/io/github/dsheirer/module/decode/p25/P25Decoder.java index 38e3fdfc..2f08eb4f 100644 --- a/src/main/java/io/github/dsheirer/module/decode/p25/P25Decoder.java +++ b/src/main/java/io/github/dsheirer/module/decode/p25/P25Decoder.java @@ -4,10 +4,10 @@ import io.github.dsheirer.alias.AliasList; import io.github.dsheirer.instrument.Instrumentable; import io.github.dsheirer.module.decode.Decoder; import io.github.dsheirer.module.decode.DecoderType; -import io.github.dsheirer.source.tuner.frequency.IFrequencyChangeListener; -import io.github.dsheirer.source.tuner.frequency.IFrequencyChangeProvider; +import io.github.dsheirer.source.ISourceEventListener; +import io.github.dsheirer.source.ISourceEventProvider; -public abstract class P25Decoder extends Decoder implements IFrequencyChangeListener, IFrequencyChangeProvider, Instrumentable +public abstract class P25Decoder extends Decoder implements ISourceEventListener, ISourceEventProvider, Instrumentable { private P25MessageProcessor mMessageProcessor; diff --git a/src/main/java/io/github/dsheirer/module/decode/p25/P25DecoderState.java b/src/main/java/io/github/dsheirer/module/decode/p25/P25DecoderState.java index f73c4b05..d2331b60 100644 --- a/src/main/java/io/github/dsheirer/module/decode/p25/P25DecoderState.java +++ b/src/main/java/io/github/dsheirer/module/decode/p25/P25DecoderState.java @@ -142,7 +142,6 @@ import java.util.List; import java.util.Map; import java.util.Set; import java.util.TreeSet; -import java.util.concurrent.ScheduledExecutorService; public class P25DecoderState extends DecoderState { @@ -223,11 +222,6 @@ public class P25DecoderState extends DecoderState return DecoderType.P25_PHASE1; } - @Override - public void stop() - { - } - /** * Performs a full reset to prepare this object for reuse on a new channel */ @@ -241,6 +235,12 @@ public class P25DecoderState extends DecoderState mSystem = null; } + @Override + public void init() + { + + } + /** * Resets any temporal state details */ @@ -259,14 +259,6 @@ public class P25DecoderState extends DecoderState mCurrentCallEvent = null; } - /** - * Performs any initialization operations to prepare for use - */ - @Override - public void init() - { - } - /** * Indicates if there a call event exists for the specified channel and * from and to talkgroup identifiers. @@ -3625,7 +3617,7 @@ public class P25DecoderState extends DecoderState } @Override - public void start(ScheduledExecutorService executor) + public void start() { //Change the default (45-second) traffic channel timeout to 1 second if(mChannelType == ChannelType.TRAFFIC) @@ -3633,4 +3625,10 @@ public class P25DecoderState extends DecoderState broadcast(new ChangeChannelTimeoutEvent(this, ChannelType.TRAFFIC, 1000)); } } + + @Override + public void stop() + { + + } } diff --git a/src/main/java/io/github/dsheirer/module/decode/p25/P25_C4FMDecoder.java b/src/main/java/io/github/dsheirer/module/decode/p25/P25_C4FMDecoder.java index 13512400..c4d714e2 100644 --- a/src/main/java/io/github/dsheirer/module/decode/p25/P25_C4FMDecoder.java +++ b/src/main/java/io/github/dsheirer/module/decode/p25/P25_C4FMDecoder.java @@ -26,13 +26,12 @@ import io.github.dsheirer.instrument.tap.stream.FloatTap; import io.github.dsheirer.sample.Listener; import io.github.dsheirer.sample.real.IFilteredRealBufferListener; import io.github.dsheirer.sample.real.RealBuffer; -import io.github.dsheirer.source.tuner.frequency.FrequencyChangeEvent; +import io.github.dsheirer.source.SourceEvent; import org.slf4j.Logger; import org.slf4j.LoggerFactory; import java.util.ArrayList; import java.util.List; -import java.util.concurrent.ScheduledExecutorService; public class P25_C4FMDecoder extends P25Decoder implements IFilteredRealBufferListener { @@ -60,7 +59,7 @@ public class P25_C4FMDecoder extends P25Decoder implements IFilteredRealBufferLi * infrastructure and subscriber identities that will be included in each * decoded message */ - public P25_C4FMDecoder(AliasList aliasList, int frequencyCorrectionMaximum ) + public P25_C4FMDecoder( AliasList aliasList, int frequencyCorrectionMaximum ) { super( aliasList ); @@ -193,28 +192,7 @@ public class P25_C4FMDecoder extends P25Decoder implements IFilteredRealBufferLi } @Override - public void reset() - { - // TODO Auto-generated method stub - - } - - @Override - public void start( ScheduledExecutorService executor ) - { - // TODO Auto-generated method stub - - } - - @Override - public void stop() - { - // TODO Auto-generated method stub - - } - - @Override - public void setFrequencyChangeListener( Listener listener ) + public void setSourceEventListener(Listener listener ) { if( mSymbolFilter != null ) { @@ -223,7 +201,7 @@ public class P25_C4FMDecoder extends P25Decoder implements IFilteredRealBufferLi } @Override - public void removeFrequencyChangeListener() + public void removeSourceEventListener() { if( mSymbolFilter != null ) { @@ -232,11 +210,11 @@ public class P25_C4FMDecoder extends P25Decoder implements IFilteredRealBufferLi } @Override - public Listener getFrequencyChangeListener() + public Listener getSourceEventListener() { if( mSymbolFilter != null ) { - return mSymbolFilter.getFrequencyChangeListener(); + return mSymbolFilter.getSourceEventListener(); } return null; diff --git a/src/main/java/io/github/dsheirer/module/decode/p25/P25_LSMDecoder.java b/src/main/java/io/github/dsheirer/module/decode/p25/P25_LSMDecoder.java index 806b20e6..9ec1ec00 100644 --- a/src/main/java/io/github/dsheirer/module/decode/p25/P25_LSMDecoder.java +++ b/src/main/java/io/github/dsheirer/module/decode/p25/P25_LSMDecoder.java @@ -34,13 +34,12 @@ import io.github.dsheirer.sample.Listener; import io.github.dsheirer.sample.complex.ComplexBuffer; import io.github.dsheirer.sample.complex.ComplexBufferToStreamConverter; import io.github.dsheirer.sample.complex.IComplexBufferListener; -import io.github.dsheirer.source.tuner.frequency.FrequencyChangeEvent; +import io.github.dsheirer.source.SourceEvent; import org.slf4j.Logger; import org.slf4j.LoggerFactory; import java.util.ArrayList; import java.util.List; -import java.util.concurrent.ScheduledExecutorService; public class P25_LSMDecoder extends P25Decoder implements IComplexBufferListener { @@ -67,7 +66,7 @@ public class P25_LSMDecoder extends P25Decoder implements IComplexBufferListener super( aliasList ); mBasebandFilter = new ComplexFIRFilter_CB_CB( FilterFactory.getLowPass( - 48000, 7250, 8000, 60, WindowType.HANNING, true ), 1.0f ); + 48000, 7250, 8000, 60, WindowType.HANN, true ), 1.0f ); mBasebandFilter.setListener( mStreamConverter ); @@ -106,29 +105,6 @@ public class P25_LSMDecoder extends P25Decoder implements IComplexBufferListener mMessageFramer = null; } - @Override - public void setFrequencyChangeListener(Listener listener) - { - } - - @Override - public void removeFrequencyChangeListener() - { - } - - @Override - public Listener getFrequencyChangeListener() - { - return new Listener() - { - @Override - public void receive(FrequencyChangeEvent frequencyChangeEvent) - { - //Ignored - } - }; - } - @Override public Listener getComplexBufferListener() { @@ -237,23 +213,20 @@ public class P25_LSMDecoder extends P25Decoder implements IComplexBufferListener } @Override - public void reset() + public Listener getSourceEventListener() { - // TODO Auto-generated method stub - + return null; } @Override - public void start( ScheduledExecutorService executor ) + public void setSourceEventListener(Listener listener) { - // TODO Auto-generated method stub - + } @Override - public void stop() + public void removeSourceEventListener() { - // TODO Auto-generated method stub - + } } diff --git a/src/main/java/io/github/dsheirer/module/decode/p25/audio/P25AudioModule.java b/src/main/java/io/github/dsheirer/module/decode/p25/audio/P25AudioModule.java index f94f4b9b..2c34b25d 100644 --- a/src/main/java/io/github/dsheirer/module/decode/p25/audio/P25AudioModule.java +++ b/src/main/java/io/github/dsheirer/module/decode/p25/audio/P25AudioModule.java @@ -20,8 +20,6 @@ import jmbe.iface.AudioConverter; import org.slf4j.Logger; import org.slf4j.LoggerFactory; -import java.util.concurrent.ScheduledExecutorService; - public class P25AudioModule extends Module implements Listener, IAudioPacketProvider, IMessageListener, ISquelchStateListener { @@ -71,7 +69,7 @@ public class P25AudioModule extends Module implements Listener, IAudioP } @Override - public void start(ScheduledExecutorService executor) + public void start() { } diff --git a/src/main/java/io/github/dsheirer/module/decode/passport/PassportDecoder.java b/src/main/java/io/github/dsheirer/module/decode/passport/PassportDecoder.java index 72ba45a2..5311eb1d 100644 --- a/src/main/java/io/github/dsheirer/module/decode/passport/PassportDecoder.java +++ b/src/main/java/io/github/dsheirer/module/decode/passport/PassportDecoder.java @@ -33,7 +33,6 @@ import io.github.dsheirer.sample.real.RealBuffer; import java.util.ArrayList; import java.util.List; -import java.util.concurrent.ScheduledExecutorService; public class PassportDecoder extends Decoder implements IUnFilteredRealBufferListener, Instrumentable @@ -108,14 +107,4 @@ public class PassportDecoder extends Decoder { mPassportMessageFramer.reset(); } - - @Override - public void start( ScheduledExecutorService executor ) - { - } - - @Override - public void stop() - { - } } diff --git a/src/main/java/io/github/dsheirer/module/decode/passport/PassportDecoderState.java b/src/main/java/io/github/dsheirer/module/decode/passport/PassportDecoderState.java index 1174224e..805278ec 100644 --- a/src/main/java/io/github/dsheirer/module/decode/passport/PassportDecoderState.java +++ b/src/main/java/io/github/dsheirer/module/decode/passport/PassportDecoderState.java @@ -40,7 +40,6 @@ import java.util.HashMap; import java.util.HashSet; import java.util.Iterator; import java.util.TreeSet; -import java.util.concurrent.ScheduledExecutorService; public class PassportDecoderState extends DecoderState { @@ -76,21 +75,6 @@ public class PassportDecoderState extends DecoderState return DecoderType.PASSPORT; } - @Override - public void start(ScheduledExecutorService executor) - { - } - - @Override - public void init() - { - } - - @Override - public void stop() - { - } - private void logTalkgroup(String talkgroup) { if(mTalkgroupsFirstHeard.contains(talkgroup)) @@ -519,6 +503,24 @@ public class PassportDecoderState extends DecoderState resetState(); } + @Override + public void start() + { + + } + + @Override + public void stop() + { + + } + + @Override + public void init() + { + + } + private void resetState() { mToTalkgroupAttribute.reset(); diff --git a/src/main/java/io/github/dsheirer/module/decode/tait/Tait1200Decoder.java b/src/main/java/io/github/dsheirer/module/decode/tait/Tait1200Decoder.java index 9ccae831..18174720 100644 --- a/src/main/java/io/github/dsheirer/module/decode/tait/Tait1200Decoder.java +++ b/src/main/java/io/github/dsheirer/module/decode/tait/Tait1200Decoder.java @@ -40,7 +40,6 @@ import io.github.dsheirer.sample.real.RealBuffer; import java.util.ArrayList; import java.util.List; -import java.util.concurrent.ScheduledExecutorService; /** * TAIT 1200 - 1200 baud 2FSK decoder @@ -189,25 +188,4 @@ public class Tait1200Decoder extends Decoder implements IFilteredRealBufferListe { return DecoderType.TAIT_1200; } - - @Override - public void reset() - { - // TODO Auto-generated method stub - - } - - @Override - public void start( ScheduledExecutorService executor ) - { - // TODO Auto-generated method stub - - } - - @Override - public void stop() - { - // TODO Auto-generated method stub - - } } diff --git a/src/main/java/io/github/dsheirer/module/decode/tait/Tait1200DecoderState.java b/src/main/java/io/github/dsheirer/module/decode/tait/Tait1200DecoderState.java index 44367d23..e82526bc 100644 --- a/src/main/java/io/github/dsheirer/module/decode/tait/Tait1200DecoderState.java +++ b/src/main/java/io/github/dsheirer/module/decode/tait/Tait1200DecoderState.java @@ -38,7 +38,6 @@ import java.util.ArrayList; import java.util.Collections; import java.util.List; import java.util.TreeSet; -import java.util.concurrent.ScheduledExecutorService; public class Tait1200DecoderState extends DecoderState { @@ -65,16 +64,6 @@ public class Tait1200DecoderState extends DecoderState return DecoderType.TAIT_1200; } - @Override - public void start(ScheduledExecutorService executor) - { - } - - @Override - public void stop() - { - } - @Override public void reset() { @@ -83,6 +72,24 @@ public class Tait1200DecoderState extends DecoderState resetState(); } + @Override + public void start() + { + + } + + @Override + public void stop() + { + + } + + @Override + public void init() + { + + } + private void resetState() { mFromAttribute.reset(); @@ -176,12 +183,6 @@ public class Tait1200DecoderState extends DecoderState return sb.toString(); } - @Override - public void init() - { - /* No initialization required */ - } - /** * Responds to reset events issued by the channel state */ diff --git a/src/main/java/io/github/dsheirer/module/demodulate/am/AMDemodulatorModule.java b/src/main/java/io/github/dsheirer/module/demodulate/am/AMDemodulatorModule.java index b52cd0eb..0cbfdf7a 100644 --- a/src/main/java/io/github/dsheirer/module/demodulate/am/AMDemodulatorModule.java +++ b/src/main/java/io/github/dsheirer/module/demodulate/am/AMDemodulatorModule.java @@ -30,95 +30,81 @@ import io.github.dsheirer.sample.complex.IComplexBufferListener; import io.github.dsheirer.sample.real.IFilteredRealBufferProvider; import io.github.dsheirer.sample.real.RealBuffer; -import java.util.concurrent.ScheduledExecutorService; - public class AMDemodulatorModule extends Module - implements IComplexBufferListener, IFilteredRealBufferProvider + implements IComplexBufferListener, IFilteredRealBufferProvider { - private static final int SAMPLE_RATE = 48000; - - private ComplexFIRFilter_CB_CB mIQFilter; - private AMDemodulator_CB mDemodulator; - private RealFIRFilter_RB_RB mLowPassFilter; + private static final int SAMPLE_RATE = 48000; + + private ComplexFIRFilter_CB_CB mIQFilter; + private AMDemodulator_CB mDemodulator; + private RealFIRFilter_RB_RB mLowPassFilter; private AutomaticGainControl_RB mAGC; - /** - * FM Demodulator with integrated DC removal and automatic frequency - * correction control. - * - * @param pass - pass frequency for IQ filtering prior to demodulation. This - * frequency should be half of the signal bandwidth since the filter will - * be applied against each of the inphase and quadrature signals and the - * combined pass bandwidth will be twice this value. - * - * @param stop - stop frequency for IQ filtering prior to demodulation. - * - * @param maxFrequencyCorrection - defines the maximum frequency +/- that - * the correction controller can adjust from the center tuned frequency. - * Set this value to 0 for no frequency correction control. - */ - public AMDemodulatorModule() - { - mIQFilter = new ComplexFIRFilter_CB_CB( FilterFactory.getLowPass( - SAMPLE_RATE, 5000, 73, WindowType.HAMMING ), 1.0f ); - - mDemodulator = new AMDemodulator_CB( 500.0f ); - mIQFilter.setListener( mDemodulator ); + /** + * AM Demodulator Module + */ + public AMDemodulatorModule() + { + mIQFilter = new ComplexFIRFilter_CB_CB(FilterFactory.getLowPass( + SAMPLE_RATE, 5000, 73, WindowType.HAMMING), 1.0f); - mLowPassFilter = new RealFIRFilter_RB_RB( - FilterFactory.getLowPass( 48000, 3000, 31, WindowType.COSINE ), 1.0f ); - mDemodulator.setListener( mLowPassFilter ); - - mAGC = new AutomaticGainControl_RB(); - mLowPassFilter.setListener( mAGC ); - } + mDemodulator = new AMDemodulator_CB(500.0f); + mIQFilter.setListener(mDemodulator); - @Override - public Listener getComplexBufferListener() - { - return mIQFilter; - } + mLowPassFilter = new RealFIRFilter_RB_RB( + FilterFactory.getLowPass(48000, 3000, 31, WindowType.COSINE), 1.0f); + mDemodulator.setListener(mLowPassFilter); - @Override - public void dispose() - { - mIQFilter.dispose(); - mIQFilter = null; - - mDemodulator.dispose(); - mDemodulator = null; - - mLowPassFilter.dispose(); - mLowPassFilter = null; - - mAGC.dispose(); - mAGC = null; - } + mAGC = new AutomaticGainControl_RB(); + mLowPassFilter.setListener(mAGC); + } - @Override - public void reset() - { - } + @Override + public Listener getComplexBufferListener() + { + return mIQFilter; + } - @Override - public void setFilteredRealBufferListener( Listener listener ) - { - mAGC.setListener( listener ); - } + @Override + public void dispose() + { + mIQFilter.dispose(); + mIQFilter = null; - @Override - public void removeFilteredRealBufferListener() - { - mAGC.removeListener(); - } + mDemodulator.dispose(); + mDemodulator = null; - @Override - public void start( ScheduledExecutorService executor ) - { - } + mLowPassFilter.dispose(); + mLowPassFilter = null; - @Override - public void stop() - { - } + mAGC.dispose(); + mAGC = null; + } + + @Override + public void reset() + { + } + + @Override + public void setFilteredRealBufferListener(Listener listener) + { + mAGC.setListener(listener); + } + + @Override + public void removeFilteredRealBufferListener() + { + mAGC.removeListener(); + } + + @Override + public void start() + { + } + + @Override + public void stop() + { + } } diff --git a/src/main/java/io/github/dsheirer/module/demodulate/audio/DemodulatedAudioFilterModule.java b/src/main/java/io/github/dsheirer/module/demodulate/audio/DemodulatedAudioFilterModule.java index b583605a..5560b9dd 100644 --- a/src/main/java/io/github/dsheirer/module/demodulate/audio/DemodulatedAudioFilterModule.java +++ b/src/main/java/io/github/dsheirer/module/demodulate/audio/DemodulatedAudioFilterModule.java @@ -30,8 +30,6 @@ import io.github.dsheirer.sample.real.RealBuffer; import org.slf4j.Logger; import org.slf4j.LoggerFactory; -import java.util.concurrent.ScheduledExecutorService; - public class DemodulatedAudioFilterModule extends Module implements IUnFilteredRealBufferListener, IFilteredRealBufferProvider { private final static Logger mLog = LoggerFactory.getLogger(DemodulatedAudioFilterModule.class); @@ -53,7 +51,7 @@ public class DemodulatedAudioFilterModule extends Module implements IUnFilteredR //TODO: change this to a band pass filter mBandPassFilter = new RealFIRFilter_RB_RB(FilterFactory.getLowPass(48000, pass, stop, 60, - WindowType.HANNING, true), 1.0f); + WindowType.HANN, true), 1.0f); mDCFilter.setListener(mBandPassFilter); } @@ -97,7 +95,7 @@ public class DemodulatedAudioFilterModule extends Module implements IUnFilteredR } @Override - public void start(ScheduledExecutorService executor) + public void start() { } diff --git a/src/main/java/io/github/dsheirer/module/demodulate/fm/FMDemodulatorModule.java b/src/main/java/io/github/dsheirer/module/demodulate/fm/FMDemodulatorModule.java index e94bffd4..7a06160a 100644 --- a/src/main/java/io/github/dsheirer/module/demodulate/fm/FMDemodulatorModule.java +++ b/src/main/java/io/github/dsheirer/module/demodulate/fm/FMDemodulatorModule.java @@ -30,8 +30,6 @@ import io.github.dsheirer.sample.real.RealBuffer; import org.slf4j.Logger; import org.slf4j.LoggerFactory; -import java.util.concurrent.ScheduledExecutorService; - public class FMDemodulatorModule extends Module implements IComplexBufferListener, IUnFilteredRealBufferProvider { private final static Logger mLog = LoggerFactory.getLogger(FMDemodulatorModule.class); @@ -104,7 +102,7 @@ public class FMDemodulatorModule extends Module implements IComplexBufferListene } @Override - public void start(ScheduledExecutorService executor) + public void start() { } diff --git a/src/main/java/io/github/dsheirer/module/log/EventLogger.java b/src/main/java/io/github/dsheirer/module/log/EventLogger.java index b4099cde..2785d365 100644 --- a/src/main/java/io/github/dsheirer/module/log/EventLogger.java +++ b/src/main/java/io/github/dsheirer/module/log/EventLogger.java @@ -30,7 +30,6 @@ import java.io.IOException; import java.io.OutputStreamWriter; import java.io.Writer; import java.nio.file.Path; -import java.util.concurrent.ScheduledExecutorService; public abstract class EventLogger extends Module { @@ -65,7 +64,7 @@ public abstract class EventLogger extends Module public abstract String getHeader(); @Override - public void start(ScheduledExecutorService executor) + public void start() { if(mLogFile == null) { @@ -80,6 +79,8 @@ public abstract class EventLogger extends Module mLogFileName = sb.toString(); + mLog.info("Creating log file:" + mLogFileName); + mLogFile = new OutputStreamWriter(new FileOutputStream(mLogFileName)); write(getHeader()); @@ -123,17 +124,14 @@ public abstract class EventLogger extends Module protected void write(String eventLogEntry) { - if(mLogFile != null) + try { - try - { - mLogFile.write(eventLogEntry + "\n"); - mLogFile.flush(); - } - catch(IOException e) - { - mLog.error("Error writing entry to event log file", e); - } + mLogFile.write(eventLogEntry + "\n"); + mLogFile.flush(); + } + catch(IOException e) + { + mLog.error("Error writing entry to event log file", e); } } } diff --git a/src/main/java/io/github/dsheirer/record/AudioRecorder.java b/src/main/java/io/github/dsheirer/record/AudioRecorder.java index 656b275c..b53eead8 100644 --- a/src/main/java/io/github/dsheirer/record/AudioRecorder.java +++ b/src/main/java/io/github/dsheirer/record/AudioRecorder.java @@ -23,6 +23,7 @@ import io.github.dsheirer.audio.IAudioPacketListener; import io.github.dsheirer.channel.metadata.Metadata; import io.github.dsheirer.module.Module; import io.github.dsheirer.sample.Listener; +import io.github.dsheirer.util.ThreadPool; import org.slf4j.Logger; import org.slf4j.LoggerFactory; @@ -33,7 +34,6 @@ import java.nio.file.Path; import java.util.ArrayList; import java.util.List; import java.util.concurrent.LinkedBlockingQueue; -import java.util.concurrent.ScheduledExecutorService; import java.util.concurrent.ScheduledFuture; import java.util.concurrent.TimeUnit; import java.util.concurrent.atomic.AtomicBoolean; @@ -193,10 +193,8 @@ public abstract class AudioRecorder extends Module implements Listener { recorder = new AudioPacketWaveRecorder(filePrefix, audioPacket.getMetadata()); - recorder.start(ThreadPool.SCHEDULED); + recorder.start(); recorder.receive(audioPacket); mRecorders.put(identifier, recorder); diff --git a/src/main/java/io/github/dsheirer/record/wave/AudioPacketWaveRecorder.java b/src/main/java/io/github/dsheirer/record/wave/AudioPacketWaveRecorder.java index ab3d806a..aa0fdad8 100644 --- a/src/main/java/io/github/dsheirer/record/wave/AudioPacketWaveRecorder.java +++ b/src/main/java/io/github/dsheirer/record/wave/AudioPacketWaveRecorder.java @@ -22,6 +22,7 @@ import io.github.dsheirer.channel.metadata.Metadata; import io.github.dsheirer.module.Module; import io.github.dsheirer.sample.ConversionUtils; import io.github.dsheirer.sample.Listener; +import io.github.dsheirer.util.ThreadPool; import io.github.dsheirer.util.TimeStamp; import org.slf4j.Logger; import org.slf4j.LoggerFactory; @@ -31,7 +32,6 @@ import java.io.IOException; import java.nio.file.Path; import java.nio.file.Paths; import java.util.concurrent.LinkedBlockingQueue; -import java.util.concurrent.ScheduledExecutorService; import java.util.concurrent.ScheduledFuture; import java.util.concurrent.TimeUnit; import java.util.concurrent.atomic.AtomicBoolean; @@ -89,7 +89,7 @@ public class AudioPacketWaveRecorder extends Module implements Listener { - private final static Logger mLog = - LoggerFactory.getLogger( ComplexBufferWaveRecorder.class ); + private final static Logger mLog = LoggerFactory.getLogger( ComplexBufferWaveRecorder.class ); private WaveWriter mWriter; private String mFilePrefix; @@ -68,19 +67,19 @@ public class ComplexBufferWaveRecorder extends Module mFilePrefix = filePrefix; } - public Path getFile() - { - return mFile; - } - - public void start( ScheduledExecutorService executor ) - { - if( mRunning.compareAndSet( false, true ) ) - { - if( mBufferProcessor == null ) - { - mBufferProcessor = new BufferProcessor(); - } + public Path getFile() + { + return mFile; + } + + public void start() + { + if(mRunning.compareAndSet(false, true)) + { + if(mBufferProcessor == null) + { + mBufferProcessor = new BufferProcessor(); + } try { @@ -95,7 +94,7 @@ public class ComplexBufferWaveRecorder extends Module mWriter = new WaveWriter( mAudioFormat, mFile ); /* Schedule the processor to run every 500 milliseconds */ - mProcessorHandle = executor.scheduleAtFixedRate( + mProcessorHandle = ThreadPool.SCHEDULED.scheduleAtFixedRate( mBufferProcessor, 0, 500, TimeUnit.MILLISECONDS ); } catch( IOException io ) diff --git a/src/main/java/io/github/dsheirer/record/wave/RealBufferWaveRecorder.java b/src/main/java/io/github/dsheirer/record/wave/RealBufferWaveRecorder.java index 78ee73a1..28c56db5 100644 --- a/src/main/java/io/github/dsheirer/record/wave/RealBufferWaveRecorder.java +++ b/src/main/java/io/github/dsheirer/record/wave/RealBufferWaveRecorder.java @@ -23,6 +23,7 @@ import io.github.dsheirer.sample.ConversionUtils; import io.github.dsheirer.sample.Listener; import io.github.dsheirer.sample.real.IFilteredRealBufferListener; import io.github.dsheirer.sample.real.RealBuffer; +import io.github.dsheirer.util.ThreadPool; import io.github.dsheirer.util.TimeStamp; import org.slf4j.Logger; import org.slf4j.LoggerFactory; @@ -32,7 +33,6 @@ import java.io.IOException; import java.nio.file.Path; import java.nio.file.Paths; import java.util.concurrent.LinkedBlockingQueue; -import java.util.concurrent.ScheduledExecutorService; import java.util.concurrent.ScheduledFuture; import java.util.concurrent.TimeUnit; import java.util.concurrent.atomic.AtomicBoolean; @@ -40,8 +40,7 @@ import java.util.concurrent.atomic.AtomicBoolean; /** * WAVE audio recorder module for recording real sample buffers to a wave file */ -public class RealBufferWaveRecorder extends Module - implements IFilteredRealBufferListener, Listener +public class RealBufferWaveRecorder extends Module implements IFilteredRealBufferListener, Listener { private final static Logger mLog = LoggerFactory.getLogger(RealBufferWaveRecorder.class); @@ -89,7 +88,7 @@ public class RealBufferWaveRecorder extends Module return mFile; } - public void start(ScheduledExecutorService executor) + public void start() { if(mRunning.compareAndSet(false, true)) { @@ -111,7 +110,7 @@ public class RealBufferWaveRecorder extends Module mWriter = new WaveWriter(mAudioFormat, mFile); /* Schedule the processor to run every 500 milliseconds */ - mProcessorHandle = executor.scheduleAtFixedRate(mBufferProcessor, 0, 500, TimeUnit.MILLISECONDS); + mProcessorHandle = ThreadPool.SCHEDULED.scheduleAtFixedRate(mBufferProcessor, 0, 500, TimeUnit.MILLISECONDS); } catch(IOException io) { diff --git a/src/main/java/io/github/dsheirer/sample/Broadcaster.java b/src/main/java/io/github/dsheirer/sample/Broadcaster.java index 92496aad..52df7104 100644 --- a/src/main/java/io/github/dsheirer/sample/Broadcaster.java +++ b/src/main/java/io/github/dsheirer/sample/Broadcaster.java @@ -25,62 +25,100 @@ import java.util.concurrent.CopyOnWriteArrayList; */ public class Broadcaster implements Listener { - private List> mListeners = new CopyOnWriteArrayList<>(); + private List> mListeners = new CopyOnWriteArrayList<>(); - @Override - public void receive( T t ) + /** + * Implements the Listener interface to receive an element and broadcast that element to all registered + * listeners. + * + * @param t element to broadcast + */ + @Override + public void receive(T t) { - broadcast( t ); + broadcast(t); } - - /** - * Clear listeners to prepare for garbage collection - */ - public void dispose() - { - mListeners.clear(); - } - - public boolean hasListeners() - { - return !mListeners.isEmpty(); - } - - public int getListenerCount() - { - return mListeners.size(); - } - - public List> getListeners() - { - return mListeners; - } - - public void addListener( Listener listener ) - { - if(listener == null) - { - throw new IllegalArgumentException("Listener cannot be null"); - } - mListeners.add( listener ); - } - - public void removeListener( Listener listener ) - { - mListeners.remove( listener ); - } - - public void clear() - { - mListeners.clear(); - } - - public void broadcast( T t ) + /** + * Clear listeners to prepare for garbage collection + */ + public void dispose() { - for( Listener listener: mListeners ) - { - listener.receive( t ); - } + mListeners.clear(); + } + + /** + * Indicates if this broadcaster has any listeners registered + */ + public boolean hasListeners() + { + return !mListeners.isEmpty(); + } + + /** + * Indicates if the listener is currently registered with this broadcaster + */ + public boolean hasListener(Listener listener) + { + return listener != null && mListeners.contains(listener); + } + + /** + * The count of listeners currently registered with this broadcaster + */ + public int getListenerCount() + { + return mListeners.size(); + } + + /** + * The list of listeners currently registered with this broadcaster + */ + public List> getListeners() + { + return mListeners; + } + + /** + * Registers the listener to receive elements from this broadcaster + * + * @param listener + */ + public void addListener(Listener listener) + { + if(listener != null && !mListeners.contains(listener)) + { + mListeners.add(listener); + } + } + + /** + * Deregisters the listener from receiving elements from this broadcaster + */ + public void removeListener(Listener listener) + { + if(listener != null && mListeners.contains(listener)) + { + mListeners.remove(listener); + } + } + + /** + * Deregisters all listeners from this broadcaster + */ + public void clear() + { + mListeners.clear(); + } + + /** + * Broadcasts the element to all registered listeners + */ + public void broadcast(T t) + { + for(Listener listener : mListeners) + { + listener.receive(t); + } } } diff --git a/src/main/java/io/github/dsheirer/sample/OverflowableTransferQueue.java b/src/main/java/io/github/dsheirer/sample/OverflowableTransferQueue.java index 70d20728..6eb49d6d 100644 --- a/src/main/java/io/github/dsheirer/sample/OverflowableTransferQueue.java +++ b/src/main/java/io/github/dsheirer/sample/OverflowableTransferQueue.java @@ -19,6 +19,7 @@ package io.github.dsheirer.sample; import io.github.dsheirer.sample.real.IOverflowListener; +import io.github.dsheirer.source.Source; import org.slf4j.Logger; import org.slf4j.LoggerFactory; @@ -33,6 +34,7 @@ public class OverflowableTransferQueue public enum State {NORMAL, OVERFLOW}; private IOverflowListener mOverflowListener; + private Source mSourceOverflowListener; private LinkedTransferQueue mQueue = new LinkedTransferQueue(); private AtomicInteger mCounter = new AtomicInteger(); @@ -73,6 +75,14 @@ public class OverflowableTransferQueue } } + /** + * Removes and returns a single element from the head of the queue or null if the queue is empty + */ + public E poll() + { + return mQueue.poll(); + } + /** * Retrieves elements from the queue into the collection up to the maximum number of elements specified */ @@ -91,13 +101,21 @@ public class OverflowableTransferQueue } /** - * Sets a listener to receive overflow state change events + * Sets a listener to receive overflow state change events. */ public void setOverflowListener(IOverflowListener listener) { mOverflowListener = listener; } + /** + * Sets the source to receive overflow state change events (in addition to an IOverflow listener) + */ + public void setSourceOverflowListener(Source source) + { + mSourceOverflowListener = source; + } + /** * Toggles the overflow state and broadcast state change to listener @@ -110,6 +128,11 @@ public class OverflowableTransferQueue { mOverflowListener.sourceOverflow(overflow); } + + if(mSourceOverflowListener != null) + { + mSourceOverflowListener.broadcastOverflowState(overflow); + } } } @@ -125,4 +148,4 @@ public class OverflowableTransferQueue mOverflow.set(false); } } -} +} \ No newline at end of file diff --git a/src/main/java/io/github/dsheirer/sample/SampleType.java b/src/main/java/io/github/dsheirer/sample/SampleType.java index f796ee4e..f3a497f9 100644 --- a/src/main/java/io/github/dsheirer/sample/SampleType.java +++ b/src/main/java/io/github/dsheirer/sample/SampleType.java @@ -2,7 +2,6 @@ package io.github.dsheirer.sample; public enum SampleType { - COMPLEX, //Baseband - REAL, //Demodulated - COMPLEX_AND_REAL; //Baseband and Demodulated + COMPLEX, // I/Q + REAL; // Real } diff --git a/src/sample/complex/IComplexBufferProvider.java b/src/main/java/io/github/dsheirer/sample/complex/IComplexBufferProvider.java similarity index 93% rename from src/sample/complex/IComplexBufferProvider.java rename to src/main/java/io/github/dsheirer/sample/complex/IComplexBufferProvider.java index 480cede0..af6f93a3 100644 --- a/src/sample/complex/IComplexBufferProvider.java +++ b/src/main/java/io/github/dsheirer/sample/complex/IComplexBufferProvider.java @@ -16,9 +16,9 @@ * along with this program. If not, see * ******************************************************************************/ -package sample.complex; +package io.github.dsheirer.sample.complex; -import sample.Listener; +import io.github.dsheirer.sample.Listener; public interface IComplexBufferProvider { diff --git a/src/main/java/io/github/dsheirer/source/ComplexSource.java b/src/main/java/io/github/dsheirer/source/ComplexSource.java index 30add060..e23e3305 100644 --- a/src/main/java/io/github/dsheirer/source/ComplexSource.java +++ b/src/main/java/io/github/dsheirer/source/ComplexSource.java @@ -6,8 +6,9 @@ import io.github.dsheirer.sample.complex.ComplexBuffer; public abstract class ComplexSource extends Source implements Provider { - public ComplexSource() - { - super( SampleType.COMPLEX ); - } + @Override + public SampleType getSampleType() + { + return SampleType.COMPLEX; + } } diff --git a/src/source/ISourceEventListener.java b/src/main/java/io/github/dsheirer/source/ISourceEventListener.java similarity index 94% rename from src/source/ISourceEventListener.java rename to src/main/java/io/github/dsheirer/source/ISourceEventListener.java index feb65ddb..6f3a2cfc 100644 --- a/src/source/ISourceEventListener.java +++ b/src/main/java/io/github/dsheirer/source/ISourceEventListener.java @@ -16,9 +16,9 @@ * along with this program. If not, see * ******************************************************************************/ -package source; +package io.github.dsheirer.source; -import sample.Listener; +import io.github.dsheirer.sample.Listener; /** diff --git a/src/source/ISourceEventProcessor.java b/src/main/java/io/github/dsheirer/source/ISourceEventProcessor.java similarity index 96% rename from src/source/ISourceEventProcessor.java rename to src/main/java/io/github/dsheirer/source/ISourceEventProcessor.java index 45f74e20..8f85e0fd 100644 --- a/src/source/ISourceEventProcessor.java +++ b/src/main/java/io/github/dsheirer/source/ISourceEventProcessor.java @@ -16,7 +16,7 @@ * along with this program. If not, see * ******************************************************************************/ -package source; +package io.github.dsheirer.source; public interface ISourceEventProcessor { diff --git a/src/source/ISourceEventProvider.java b/src/main/java/io/github/dsheirer/source/ISourceEventProvider.java similarity index 93% rename from src/source/ISourceEventProvider.java rename to src/main/java/io/github/dsheirer/source/ISourceEventProvider.java index 6e6a1bed..ff247fb5 100644 --- a/src/source/ISourceEventProvider.java +++ b/src/main/java/io/github/dsheirer/source/ISourceEventProvider.java @@ -16,9 +16,9 @@ * along with this program. If not, see * ******************************************************************************/ -package source; +package io.github.dsheirer.source; -import sample.Listener; +import io.github.dsheirer.sample.Listener; /** * Provider of Source Change Events diff --git a/src/main/java/io/github/dsheirer/source/InvalidFrequencyException.java b/src/main/java/io/github/dsheirer/source/InvalidFrequencyException.java index 54bd8666..bc30a29a 100644 --- a/src/main/java/io/github/dsheirer/source/InvalidFrequencyException.java +++ b/src/main/java/io/github/dsheirer/source/InvalidFrequencyException.java @@ -54,4 +54,11 @@ public class InvalidFrequencyException extends SourceException { return mValidFrequency; } + + @Override + public String getMessage() + { + return super.getMessage() + " Invalid Frequency: " + getInvalidFrequency() + + " - current valid frequency:" + getValidFrequency(); + } } diff --git a/src/main/java/io/github/dsheirer/source/RealSource.java b/src/main/java/io/github/dsheirer/source/RealSource.java index 3f73e7c1..1dfc15f9 100644 --- a/src/main/java/io/github/dsheirer/source/RealSource.java +++ b/src/main/java/io/github/dsheirer/source/RealSource.java @@ -7,8 +7,9 @@ import io.github.dsheirer.sample.real.RealBuffer; public abstract class RealSource extends Source implements Provider { - public RealSource() - { - super( SampleType.REAL ); - } + @Override + public SampleType getSampleType() + { + return SampleType.REAL; + } } diff --git a/src/main/java/io/github/dsheirer/source/Source.java b/src/main/java/io/github/dsheirer/source/Source.java index 23916db2..404a2cec 100644 --- a/src/main/java/io/github/dsheirer/source/Source.java +++ b/src/main/java/io/github/dsheirer/source/Source.java @@ -17,42 +17,53 @@ ******************************************************************************/ package io.github.dsheirer.source; -import io.github.dsheirer.channel.heartbeat.IHeartbeatProvider; import io.github.dsheirer.module.Module; import io.github.dsheirer.sample.SampleType; import io.github.dsheirer.sample.real.IOverflowListener; -import io.github.dsheirer.source.tuner.frequency.IFrequencyChangeListener; -import io.github.dsheirer.source.tuner.frequency.IFrequencyChangeProvider; +import io.github.dsheirer.source.heartbeat.HeartbeatManager; +import io.github.dsheirer.source.heartbeat.IHeartbeatProvider; /** * Abstract class to define the minimum functionality of a sample data provider. */ -public abstract class Source extends Module implements IFrequencyChangeListener, IFrequencyChangeProvider, - IHeartbeatProvider +public abstract class Source extends Module implements ISourceEventListener, ISourceEventProvider, IHeartbeatProvider { - protected SampleType mSampleType; + private HeartbeatManager mHeartbeatManager = new HeartbeatManager(); + + /** + * Heartbeat manager for this source + */ + public HeartbeatManager getHeartbeatManager() + { + return mHeartbeatManager; + } + + protected IOverflowListener mOverflowListener; - public Source( SampleType sampleType ) - { - mSampleType = sampleType; - } - - public abstract int getSampleRate() throws SourceException; - - public abstract long getFrequency() throws SourceException; - + /** + * Indicates the type of samples provided by this source: real or complex + */ + public abstract SampleType getSampleType(); + + /** + * Sample rate provided by this source + * + * @throws SourceException if there is an issue determining the sample rate for this source + */ + public abstract double getSampleRate(); + + /** + * Center frequency for this source in Hertz + * + * @throws SourceException if there is an issue in determining the center frequency for this source + */ + public abstract long getFrequency(); + + /** + * Process any cleanup actions to prepare for garbage collection of this source + */ public abstract void dispose(); - - public SampleType getSampleType() - { - return mSampleType; - } - - public void setSampleType( SampleType sampleType ) - { - mSampleType = sampleType; - } /** * Registers the listener to receive overflow state changes. Use null argument to clear the listener @@ -65,9 +76,9 @@ public abstract class Source extends Module implements IFrequencyChangeListener, /** * Broadcasts an overflow state * - * @param overflow true if overlow, false if normal + * @param overflow true if overflow, false if normal */ - protected void broadcastOverflowState(boolean overflow) + public void broadcastOverflowState(boolean overflow) { if(mOverflowListener != null) { diff --git a/src/source/SourceEvent.java b/src/main/java/io/github/dsheirer/source/SourceEvent.java similarity index 99% rename from src/source/SourceEvent.java rename to src/main/java/io/github/dsheirer/source/SourceEvent.java index bade0e58..8f9b2857 100644 --- a/src/source/SourceEvent.java +++ b/src/main/java/io/github/dsheirer/source/SourceEvent.java @@ -16,7 +16,7 @@ * along with this program. If not, see * ******************************************************************************/ -package source; +package io.github.dsheirer.source; public class SourceEvent { diff --git a/src/source/SourceEventListenerToProcessorAdapter.java b/src/main/java/io/github/dsheirer/source/SourceEventListenerToProcessorAdapter.java similarity index 91% rename from src/source/SourceEventListenerToProcessorAdapter.java rename to src/main/java/io/github/dsheirer/source/SourceEventListenerToProcessorAdapter.java index 85f50138..958b3a4f 100644 --- a/src/source/SourceEventListenerToProcessorAdapter.java +++ b/src/main/java/io/github/dsheirer/source/SourceEventListenerToProcessorAdapter.java @@ -16,16 +16,15 @@ * along with this program. If not, see * ******************************************************************************/ -package source; +package io.github.dsheirer.source; +import io.github.dsheirer.sample.Listener; import org.slf4j.Logger; import org.slf4j.LoggerFactory; -import sample.Listener; -import source.tuner.channel.TunerChannelSource; public class SourceEventListenerToProcessorAdapter implements Listener { - private final static Logger mLog = LoggerFactory.getLogger(TunerChannelSource.class); + private final static Logger mLog = LoggerFactory.getLogger(SourceEventListenerToProcessorAdapter.class); private ISourceEventProcessor mSourceEventProcessor; diff --git a/src/main/java/io/github/dsheirer/source/config/SourceConfigRecording.java b/src/main/java/io/github/dsheirer/source/config/SourceConfigRecording.java index aa8717b5..3d6d8356 100644 --- a/src/main/java/io/github/dsheirer/source/config/SourceConfigRecording.java +++ b/src/main/java/io/github/dsheirer/source/config/SourceConfigRecording.java @@ -1,26 +1,23 @@ /******************************************************************************* - * SDR Trunk - * Copyright (C) 2014 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 + * sdr-trunk + * 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 io.github.dsheirer.source.config; import com.fasterxml.jackson.dataformat.xml.annotation.JacksonXmlProperty; import io.github.dsheirer.source.SourceType; -import io.github.dsheirer.source.tuner.TunerChannel; -import io.github.dsheirer.source.tuner.TunerChannel.Type; +import io.github.dsheirer.source.tuner.channel.TunerChannel; public class SourceConfigRecording extends SourceConfiguration { @@ -59,11 +56,11 @@ public class SourceConfigRecording extends SourceConfiguration { return getRecordingAlias(); } - - public TunerChannel getTunerChannel() - { - /* Create a tuner channel with a default bandwidth of 15 kHz */ - return new TunerChannel( Type.LOCKED, mFrequency, 15000 ); - } - + + public TunerChannel getTunerChannel() + { + /* Create a tuner channel with a default bandwidth of 12.5 kHz */ + return new TunerChannel(mFrequency, 12500); + } + } diff --git a/src/main/java/io/github/dsheirer/source/config/SourceConfigTuner.java b/src/main/java/io/github/dsheirer/source/config/SourceConfigTuner.java index a542e8c6..9e9db98e 100644 --- a/src/main/java/io/github/dsheirer/source/config/SourceConfigTuner.java +++ b/src/main/java/io/github/dsheirer/source/config/SourceConfigTuner.java @@ -21,15 +21,14 @@ import com.fasterxml.jackson.annotation.JsonIgnore; import com.fasterxml.jackson.annotation.JsonSubTypes; import com.fasterxml.jackson.dataformat.xml.annotation.JacksonXmlProperty; import io.github.dsheirer.source.SourceType; -import io.github.dsheirer.source.tuner.TunerChannel; -import io.github.dsheirer.source.tuner.TunerChannel.Type; +import io.github.dsheirer.source.tuner.channel.TunerChannel; import java.text.DecimalFormat; @JsonSubTypes.Type(value = SourceConfigTuner.class, name = "sourceConfigTuner") public class SourceConfigTuner extends SourceConfiguration { - private static DecimalFormat sFORMAT = new DecimalFormat("0.00000"); + private static DecimalFormat FREQUENCY_FORMAT = new DecimalFormat("0.00000"); private long mFrequency = 0; private int mBandwidth = 12500; @@ -62,12 +61,12 @@ public class SourceConfigTuner extends SourceConfiguration @Override public String getDescription() { - return sFORMAT.format((double)mFrequency / 1000000.0d) + " MHz"; + return FREQUENCY_FORMAT.format((double)mFrequency / 1000000.0d) + " MHz"; } @JsonIgnore public TunerChannel getTunerChannel() { - return new TunerChannel(Type.LOCKED, mFrequency, mBandwidth); + return new TunerChannel(mFrequency, mBandwidth); } } diff --git a/src/source/heartbeat/Heartbeat.java b/src/main/java/io/github/dsheirer/source/heartbeat/Heartbeat.java similarity index 96% rename from src/source/heartbeat/Heartbeat.java rename to src/main/java/io/github/dsheirer/source/heartbeat/Heartbeat.java index 993d51ee..2eef023f 100644 --- a/src/source/heartbeat/Heartbeat.java +++ b/src/main/java/io/github/dsheirer/source/heartbeat/Heartbeat.java @@ -16,7 +16,7 @@ * along with this program. If not, see * ******************************************************************************/ -package source.heartbeat; +package io.github.dsheirer.source.heartbeat; public class Heartbeat { diff --git a/src/source/heartbeat/HeartbeatManager.java b/src/main/java/io/github/dsheirer/source/heartbeat/HeartbeatManager.java similarity index 92% rename from src/source/heartbeat/HeartbeatManager.java rename to src/main/java/io/github/dsheirer/source/heartbeat/HeartbeatManager.java index f570171b..98ebfbd4 100644 --- a/src/source/heartbeat/HeartbeatManager.java +++ b/src/main/java/io/github/dsheirer/source/heartbeat/HeartbeatManager.java @@ -16,10 +16,10 @@ * along with this program. If not, see * ******************************************************************************/ -package source.heartbeat; +package io.github.dsheirer.source.heartbeat; -import sample.Broadcaster; -import sample.Listener; +import io.github.dsheirer.sample.Broadcaster; +import io.github.dsheirer.sample.Listener; public class HeartbeatManager { diff --git a/src/main/java/io/github/dsheirer/channel/heartbeat/IHeartbeatListener.java b/src/main/java/io/github/dsheirer/source/heartbeat/IHeartbeatListener.java similarity index 95% rename from src/main/java/io/github/dsheirer/channel/heartbeat/IHeartbeatListener.java rename to src/main/java/io/github/dsheirer/source/heartbeat/IHeartbeatListener.java index f3bcb319..149fd527 100644 --- a/src/main/java/io/github/dsheirer/channel/heartbeat/IHeartbeatListener.java +++ b/src/main/java/io/github/dsheirer/source/heartbeat/IHeartbeatListener.java @@ -16,7 +16,7 @@ * along with this program. If not, see * ******************************************************************************/ -package io.github.dsheirer.channel.heartbeat; +package io.github.dsheirer.source.heartbeat; import io.github.dsheirer.sample.Listener; diff --git a/src/source/heartbeat/IHeartbeatProvider.java b/src/main/java/io/github/dsheirer/source/heartbeat/IHeartbeatProvider.java similarity index 95% rename from src/source/heartbeat/IHeartbeatProvider.java rename to src/main/java/io/github/dsheirer/source/heartbeat/IHeartbeatProvider.java index 9941b716..076b50a2 100644 --- a/src/source/heartbeat/IHeartbeatProvider.java +++ b/src/main/java/io/github/dsheirer/source/heartbeat/IHeartbeatProvider.java @@ -16,7 +16,7 @@ * along with this program. If not, see * ******************************************************************************/ -package source.heartbeat; +package io.github.dsheirer.source.heartbeat; public interface IHeartbeatProvider { diff --git a/src/main/java/io/github/dsheirer/source/mixer/RealMixerSource.java b/src/main/java/io/github/dsheirer/source/mixer/RealMixerSource.java index 4135f790..6d6ef037 100644 --- a/src/main/java/io/github/dsheirer/source/mixer/RealMixerSource.java +++ b/src/main/java/io/github/dsheirer/source/mixer/RealMixerSource.java @@ -17,14 +17,11 @@ ******************************************************************************/ package io.github.dsheirer.source.mixer; -import io.github.dsheirer.channel.heartbeat.Heartbeat; -import io.github.dsheirer.dsp.gain.AutomaticGainControl; import io.github.dsheirer.sample.Listener; import io.github.dsheirer.sample.adapter.ISampleAdapter; import io.github.dsheirer.sample.real.RealBuffer; import io.github.dsheirer.source.RealSource; -import io.github.dsheirer.source.SourceException; -import io.github.dsheirer.source.tuner.frequency.FrequencyChangeEvent; +import io.github.dsheirer.source.SourceEvent; import org.slf4j.Logger; import org.slf4j.LoggerFactory; @@ -34,7 +31,6 @@ import javax.sound.sampled.LineUnavailableException; import javax.sound.sampled.TargetDataLine; import java.util.Iterator; import java.util.concurrent.CopyOnWriteArrayList; -import java.util.concurrent.ScheduledExecutorService; import java.util.concurrent.atomic.AtomicBoolean; public class RealMixerSource extends RealSource @@ -49,9 +45,6 @@ public class RealMixerSource extends RealSource private AudioFormat mAudioFormat; private int mBytesPerFrame = 0; private ISampleAdapter mSampleAdapter; - private AutomaticGainControl mAGC = new AutomaticGainControl(); - private Listener mHeartbeatListener; - private static final Heartbeat HEARTBEAT = new Heartbeat(); private CopyOnWriteArrayList> mSampleListeners = new CopyOnWriteArrayList>(); @@ -76,44 +69,24 @@ public class RealMixerSource extends RealSource mSampleAdapter = sampleAdapter; } - @Override - public void setFrequencyChangeListener(Listener listener) - { - //Not implemented - } + @Override + public void setSourceEventListener(Listener listener) + { + //Not implemented + } - @Override - public void removeFrequencyChangeListener() - { - //Not implemented - } + @Override + public void removeSourceEventListener() + { + //Not implemented + } - @Override - public Listener getFrequencyChangeListener() - { - //Not implemented - return null; - } - - /** - * Registers the listener to receive heartbeats from this source. - * - * @param listener to receive heartbeats - */ - @Override - public void setHeartbeatListener(Listener listener) - { - mHeartbeatListener = listener; - } - - /** - * Removes the currently registered heartbeat listener - */ - @Override - public void removeHeartbeatListener() - { - mHeartbeatListener = null; - } + @Override + public Listener getSourceEventListener() + { + //Not implemented + return null; + } @Override public void reset() @@ -121,7 +94,7 @@ public class RealMixerSource extends RealSource } @Override - public void start(ScheduledExecutorService executor) + public void start() { } @@ -176,11 +149,11 @@ public class RealMixerSource extends RealSource } } - public int getSampleRate() + public double getSampleRate() { if(mTargetDataLine != null) { - return (int)mTargetDataLine.getFormat().getSampleRate(); + return mTargetDataLine.getFormat().getSampleRate(); } else { @@ -191,7 +164,7 @@ public class RealMixerSource extends RealSource /** * Returns the frequency of this source. Default is 0. */ - public long getFrequency() throws SourceException + public long getFrequency() { return mFrequency; } @@ -220,11 +193,7 @@ public class RealMixerSource extends RealSource { if(mRunning.compareAndSet(false, true)) { - //Send a heartbeat every time we run - if(mHeartbeatListener != null) - { - mHeartbeatListener.receive(HEARTBEAT); - } + getHeartbeatManager().broadcast(); if(mTargetDataLine == null) { diff --git a/src/main/java/io/github/dsheirer/source/recording/Recording.java b/src/main/java/io/github/dsheirer/source/recording/Recording.java index 2d8c74ff..1fb2495f 100644 --- a/src/main/java/io/github/dsheirer/source/recording/Recording.java +++ b/src/main/java/io/github/dsheirer/source/recording/Recording.java @@ -1,26 +1,37 @@ +/******************************************************************************* + * sdr-trunk + * 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 io.github.dsheirer.source.recording; import io.github.dsheirer.settings.SettingsManager; +import io.github.dsheirer.source.ISourceEventProcessor; +import io.github.dsheirer.source.SourceEvent; import io.github.dsheirer.source.SourceException; import io.github.dsheirer.source.tuner.ITunerChannelProvider; -import io.github.dsheirer.source.tuner.TunerChannel; -import io.github.dsheirer.source.tuner.TunerChannelSource; -import io.github.dsheirer.source.tuner.frequency.FrequencyChangeEvent; -import io.github.dsheirer.source.tuner.frequency.FrequencyChangeEvent.Event; -import io.github.dsheirer.source.tuner.frequency.IFrequencyChangeProcessor; +import io.github.dsheirer.source.tuner.channel.TunerChannel; +import io.github.dsheirer.source.tuner.channel.TunerChannelSource; import java.io.File; import java.util.ArrayList; -import java.util.concurrent.RejectedExecutionException; +import java.util.List; -public class Recording implements Comparable, - IFrequencyChangeProcessor, - ITunerChannelProvider +public class Recording implements Comparable, ISourceEventProcessor, ITunerChannelProvider { private RecordingConfiguration mConfiguration; - private ArrayList mTunerChannels = - new ArrayList(); + private List mTunerChannels = new ArrayList(); @SuppressWarnings( "unused" ) private long mCenterFrequency; @@ -65,8 +76,7 @@ public class Recording implements Comparable, } @Override - public TunerChannelSource getChannel( TunerChannel channel ) - throws RejectedExecutionException, SourceException + public TunerChannelSource getChannel( TunerChannel channel ) { // TODO Auto-generated method stub return null; @@ -89,9 +99,9 @@ public class Recording implements Comparable, } @Override - public void frequencyChanged( FrequencyChangeEvent event ) throws SourceException + public void process(SourceEvent event ) throws SourceException { - if( event.getEvent() == Event.NOTIFICATION_FREQUENCY_CHANGE ) + if( event.getEvent() == SourceEvent.Event.NOTIFICATION_FREQUENCY_CHANGE ) { long frequency = event.getValue().longValue(); @@ -103,7 +113,7 @@ public class Recording implements Comparable, for( TunerChannelSource channel: mTunerChannels ) { - channel.frequencyChanged( event ); + channel.process( event ); } } } diff --git a/src/main/java/io/github/dsheirer/source/recording/RecordingSourceManager.java b/src/main/java/io/github/dsheirer/source/recording/RecordingSourceManager.java index 810b17e0..e9f280ed 100644 --- a/src/main/java/io/github/dsheirer/source/recording/RecordingSourceManager.java +++ b/src/main/java/io/github/dsheirer/source/recording/RecordingSourceManager.java @@ -22,8 +22,8 @@ import io.github.dsheirer.source.Source; import io.github.dsheirer.source.SourceException; import io.github.dsheirer.source.config.SourceConfigRecording; import io.github.dsheirer.source.config.SourceConfiguration; -import io.github.dsheirer.source.tuner.TunerChannel; -import io.github.dsheirer.source.tuner.TunerChannelSource; +import io.github.dsheirer.source.tuner.channel.TunerChannel; +import io.github.dsheirer.source.tuner.channel.TunerChannelSource; import org.slf4j.Logger; import org.slf4j.LoggerFactory; @@ -33,8 +33,7 @@ import java.util.List; public class RecordingSourceManager { - private final static Logger mLog = - LoggerFactory.getLogger( RecordingSourceManager.class ); + private final static Logger mLog = LoggerFactory.getLogger( RecordingSourceManager.class ); private ArrayList mRecordings = new ArrayList(); @@ -50,12 +49,12 @@ public class RecordingSourceManager * Iterates current recordings to get a tuner channel source for the frequency * specified in the channel config's source config object */ - public Source getSource(SourceConfiguration config, int bandwidth ) + public Source getSource( SourceConfiguration config, int bandwidth ) throws SourceException { TunerChannelSource retVal = null; - if( config instanceof SourceConfigRecording) + if( config instanceof SourceConfigRecording ) { SourceConfigRecording configRecording = (SourceConfigRecording)config; diff --git a/src/main/java/io/github/dsheirer/source/tuner/ITunerChannelProvider.java b/src/main/java/io/github/dsheirer/source/tuner/ITunerChannelProvider.java index 92cb95fc..550931c9 100644 --- a/src/main/java/io/github/dsheirer/source/tuner/ITunerChannelProvider.java +++ b/src/main/java/io/github/dsheirer/source/tuner/ITunerChannelProvider.java @@ -18,6 +18,8 @@ package io.github.dsheirer.source.tuner; import io.github.dsheirer.source.SourceException; +import io.github.dsheirer.source.tuner.channel.TunerChannel; +import io.github.dsheirer.source.tuner.channel.TunerChannelSource; import java.util.concurrent.RejectedExecutionException; @@ -25,18 +27,14 @@ public interface ITunerChannelProvider { /** * Returns a tuner frequency channel source, tuned to the correct frequency - * - * @param frequency - desired frequency - * + * * @return - source for 48k sample rate */ - public abstract TunerChannelSource getChannel( TunerChannel channel ) + public abstract TunerChannelSource getChannel(TunerChannel channel ) throws RejectedExecutionException, SourceException; /** * Releases the tuned channel resources - * - * @param channel - previously obtained tuner channel */ public abstract void releaseChannel( TunerChannelSource source ); diff --git a/src/main/java/io/github/dsheirer/source/tuner/MixerTuner.java b/src/main/java/io/github/dsheirer/source/tuner/MixerTuner.java deleted file mode 100644 index 586ef6e3..00000000 --- a/src/main/java/io/github/dsheirer/source/tuner/MixerTuner.java +++ /dev/null @@ -1,129 +0,0 @@ -/******************************************************************************* - * SDR Trunk - * Copyright (C) 2014 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 io.github.dsheirer.source.tuner; - -import io.github.dsheirer.sample.Listener; -import io.github.dsheirer.sample.adapter.ISampleAdapter; -import io.github.dsheirer.sample.complex.ComplexBuffer; -import io.github.dsheirer.sample.complex.ComplexSampleListListener; -import io.github.dsheirer.source.mixer.ComplexMixer; -import org.slf4j.Logger; -import org.slf4j.LoggerFactory; - -import javax.sound.sampled.TargetDataLine; -import java.util.ArrayList; - -public abstract class MixerTuner extends Tuner implements Listener -{ - private final static Logger mLog = - LoggerFactory.getLogger( MixerTuner.class ); - - protected MixerTunerType mMixerTunerType; - protected ComplexMixer mComplexMixer; - protected ArrayList mListeners = - new ArrayList(); - - /** - * Wrapper class to add basic Tuner functionality to the ComplexMixer. - * Subclasses should couple this functionality with a tuner controller class - * to support tuning. - */ - public MixerTuner( String name, - TunerController tunerController, - MixerTunerType mixerTunerType, - TargetDataLine targetDataLine, - ISampleAdapter sampleAdapter ) - { - super( name, tunerController ); - - mMixerTunerType = mixerTunerType; - - mComplexMixer = new ComplexMixer( targetDataLine, - mMixerTunerType.getAudioFormat(), - name, - sampleAdapter, - (Listener)this ); - } - - public MixerTuner( String name, - TunerController tunerController, - MixerTunerDataLine mixerTuner, - ISampleAdapter sampleAdapter ) - { - this( name, - tunerController, - mixerTuner.getMixerTunerType(), - mixerTuner.getTargetDataLine(), - sampleAdapter ); - } - - public void dispose() - { - mComplexMixer.stop(); - - super.dispose(); - } - - public MixerTunerType getMixerTunerType() - { - return mMixerTunerType; - } - - public TargetDataLine getTargetDataLine() - { - return mComplexMixer.getTargetDataLine(); - } - - /** - * Dispatches the received complex buffers from the mComplexMixer to all - * listeners registered on the parent Tuner class - */ - @Override - public void receive( ComplexBuffer buffer ) - { - super.broadcast( buffer ); - } - - /** - * Overrides the parent Tuner class listener management method to allow - * starting the buffer processing thread - */ - @Override - public void addListener( Listener listener ) - { - super.addListener( listener ); - - /* We can call start multiple times -- it ignores additional requests */ - mComplexMixer.start(); - } - - /** - * Overrides the parent Tuner class listener management method to allow - * stopping the buffer processing thread when there are no more listeners - */ - @Override - public void removeListener( Listener listener ) - { - super.removeListener( listener ); - - if( mSampleListeners.isEmpty() ) - { - mComplexMixer.stop(); - } - } -} diff --git a/src/main/java/io/github/dsheirer/source/tuner/Tuner.java b/src/main/java/io/github/dsheirer/source/tuner/Tuner.java index 8b74a58d..70e02d71 100644 --- a/src/main/java/io/github/dsheirer/source/tuner/Tuner.java +++ b/src/main/java/io/github/dsheirer/source/tuner/Tuner.java @@ -17,196 +17,145 @@ ******************************************************************************/ package io.github.dsheirer.source.tuner; +import io.github.dsheirer.sample.Broadcaster; import io.github.dsheirer.sample.Listener; -import io.github.dsheirer.sample.complex.ComplexBuffer; -import io.github.dsheirer.source.SourceException; -import io.github.dsheirer.source.tuner.frequency.FrequencyChangeEvent; -import io.github.dsheirer.source.tuner.frequency.IFrequencyChangeProcessor; +import io.github.dsheirer.source.ISourceEventProcessor; +import io.github.dsheirer.source.SourceEvent; +import io.github.dsheirer.source.tuner.TunerEvent.Event; +import io.github.dsheirer.source.tuner.manager.AbstractSourceManager; +import io.github.dsheirer.source.tuner.manager.TunerSourceManager; import org.slf4j.Logger; import org.slf4j.LoggerFactory; -import java.util.ArrayList; -import java.util.List; -import java.util.concurrent.CopyOnWriteArrayList; -import java.util.concurrent.RejectedExecutionException; - /** * Tuner - provides tuner channel sources, representing a channel frequency */ -public abstract class Tuner implements ITunerChannelProvider +public abstract class Tuner { - private final static Logger mLog = LoggerFactory.getLogger( Tuner.class ); - - private String mName; - private TunerController mTunerController; - /** - * Sample Listeners - these will typically be the DFT processor for spectral - * display, or it will be one or more tuner channel sources - */ - protected List> mSampleListeners = - new CopyOnWriteArrayList<>(); - - protected List> mTunerChangeListeners = - new ArrayList<>(); - - public Tuner( String name, TunerController tunerController ) - { - mName = name; - mTunerController = tunerController; + private final static Logger mLog = LoggerFactory.getLogger(Tuner.class); - //Rebroadcast frequency and sample rate change events as tuner events - mTunerController.addListener( new IFrequencyChangeProcessor() - { - @Override - public void frequencyChanged( FrequencyChangeEvent event ) - { - switch( event.getEvent() ) - { - case NOTIFICATION_FREQUENCY_CHANGE: - broadcast( new TunerEvent( Tuner.this, - TunerEvent.Event.FREQUENCY )); - break; - case NOTIFICATION_SAMPLE_RATE_CHANGE: - broadcast( new TunerEvent( Tuner.this, - TunerEvent.Event.SAMPLE_RATE )); - break; - default: - break; - } - } - } ); - } - - public TunerController getTunerController() - { - return mTunerController; - } - - public String toString() - { - return mName; - } - - public void dispose() - { - mSampleListeners.clear(); - } - - public void setName( String name ) - { - mName = name; - } - - /** - * Unique identifier for this tuner, used to lookup a tuner configuration - * from the settings manager. - * - * @return - unique identifier like a serial number, or a usb bus location - * or ip address and port. Return some form of unique identification that - * allows this tuner to be identified from among several of the same types - * of tuners. - */ - public abstract String getUniqueID(); + protected Broadcaster mTunerEventBroadcaster = new Broadcaster<>(); + private TunerController mTunerController; + private String mName; + private AbstractSourceManager mTunerSourceManager; - /** - * @return - tuner class enum entry - */ - public abstract TunerClass getTunerClass(); - - /** - * @return - tuner type enum entry - */ - public abstract TunerType getTunerType(); - - /** - * Name of this tuner object - * @return - string name of this tuner object - */ - public String getName() - { - return mName; - } - - /** - * Sample size in bits - */ - public abstract double getSampleSize(); - - /** - * Returns a tuner frequency channel source, tuned to the correct frequency - * - * Note: ensure the concrete implementation registers the returned - * tuner channel source as a listener on the tuner, to receive frequency - * change events. - * - * @param frequency - desired frequency - * - * @return - source for 48k sample rate - */ - public abstract TunerChannelSource getChannel( TunerChannel channel ) - throws RejectedExecutionException, SourceException; - - /** - * Releases the tuned channel resources - * - * Note: ensure the concrete implementation unregisters the tuner channel - * source from the tuner as a frequency change listener - * - * @param channel - previously obtained tuner channel - */ - public abstract void releaseChannel( TunerChannelSource source ); - - /** - * Registers the listener to receive complex float sample arrays - */ - public void addListener( Listener listener ) + /** + * Abstract tuner class. + * @param name of the tuner + * @param tunerController for the tuner + */ + public Tuner(String name, TunerController tunerController) { - mSampleListeners.add( listener ); - } + mName = name; + mTunerController = tunerController; - /** - * Removes the registered listener - */ - public void removeListener( Listener listener ) - { - mSampleListeners.remove( listener ); + mTunerSourceManager = new TunerSourceManager(mTunerController); + + //Rebroadcast frequency and sample rate change events as tuner events + mTunerController.addListener(new ISourceEventProcessor() + { + @Override + public void process(SourceEvent event) + { + switch(event.getEvent()) + { + case NOTIFICATION_FREQUENCY_CHANGE: + broadcast(new TunerEvent(Tuner.this, Event.FREQUENCY)); + break; + case NOTIFICATION_SAMPLE_RATE_CHANGE: + broadcast(new TunerEvent(Tuner.this, Event.SAMPLE_RATE)); + break; + default: + break; + } + } + }); } /** - * Broadcasts the samples to all registered listeners + * Source Manager. Provides access to registering for complex buffer samples and source event notifications. */ - public void broadcast( ComplexBuffer sampleBuffer ) + public AbstractSourceManager getSourceManager() { - for( Listener listener: mSampleListeners ) - { - listener.receive( sampleBuffer ); - } + return mTunerSourceManager; } - /** - * Registers the listener - */ - public void addTunerChangeListener( Listener listener ) + public TunerController getTunerController() { - mTunerChangeListeners.add( listener ); + return mTunerController; } - /** - * Removes the registered listener - */ - public void removeTunerChangeListener( Listener listener ) + public String toString() { - mTunerChangeListeners.remove( listener ); + return mName; + } + + public void dispose() + { + } + + public void setName(String name) + { + mName = name; + } + + /** + * Unique identifier for this tuner, used to lookup a tuner configuration + * from the settings manager. + * + * @return - unique identifier like a serial number, or a usb bus location + * or ip address and port. Return some form of unique identification that + * allows this tuner to be identified from among several of the same types + * of tuners. + */ + public abstract String getUniqueID(); + + /** + * @return - tuner class enum entry + */ + public abstract TunerClass getTunerClass(); + + /** + * @return - tuner type enum entry + */ + public abstract TunerType getTunerType(); + + /** + * Name of this tuner object + * + * @return - string name of this tuner object + */ + public String getName() + { + return mName; + } + + /** + * Sample size in bits + */ + public abstract double getSampleSize(); + + /** + * Registers the listener + */ + public void addTunerChangeListener(Listener listener) + { + mTunerEventBroadcaster.addListener(listener); + } + + /** + * Removes the registered listener + */ + public void removeTunerChangeListener(Listener listener) + { + mTunerEventBroadcaster.removeListener(listener); } /** * Broadcasts the tuner change event */ - public void broadcast( TunerEvent event ) + public void broadcast(TunerEvent tunerEvent) { - for( Listener listener: mTunerChangeListeners ) - { - listener.receive( event ); - } + mTunerEventBroadcaster.broadcast(tunerEvent); } -} +} \ No newline at end of file diff --git a/src/main/java/io/github/dsheirer/source/tuner/TunerChannel.java b/src/main/java/io/github/dsheirer/source/tuner/TunerChannel.java deleted file mode 100644 index cb17482a..00000000 --- a/src/main/java/io/github/dsheirer/source/tuner/TunerChannel.java +++ /dev/null @@ -1,122 +0,0 @@ -/******************************************************************************* - * SDR Trunk - * Copyright (C) 2014,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 - ******************************************************************************/ -package io.github.dsheirer.source.tuner; - -public class TunerChannel implements Comparable -{ - private Type mType; - private long mFrequency; - private int mBandwidth; - - public TunerChannel( Type type, long frequency, int bandwidth ) - { - mType = type; - mFrequency = frequency; - mBandwidth = bandwidth; - } - - public Type getType() - { - return mType; - } - - public void setType( Type type ) - { - mType = type; - } - - public long getFrequency() - { - return mFrequency; - } - - public void setFrequency( long frequency ) - { - mFrequency = frequency; - } - - public int getBandwidth() - { - return mBandwidth; - } - - public void setBandwidth( int bandwidth ) - { - mBandwidth = bandwidth; - } - - public long getMinFrequency() - { - return mFrequency - ( mBandwidth / 2 ); - } - - public long getMaxFrequency() - { - return mFrequency + ( mBandwidth / 2 ); - } - - /** - * Indicates if any part of this tuner channel overlaps the specified range - */ - public boolean overlaps( long minimum, long maximum ) - { - long channelMin = getMinFrequency(); - long channelMax = getMaxFrequency(); - - return ( ( minimum <= channelMin && channelMin <= maximum ) || - ( minimum <= channelMax && channelMax <= maximum ) || - ( minimum <= channelMin && channelMax <= maximum ) || - ( channelMin <= minimum && maximum <= channelMax ) ); - } - - public String toString() - { - StringBuilder sb = new StringBuilder(); - - sb.append( "Channel: " ); - sb.append( getMinFrequency() ); - sb.append( "-" ); - sb.append( getMaxFrequency() ); - - return sb.toString(); - } - - @Override - public int compareTo( TunerChannel otherLock ) - { - if( this.equals( otherLock ) ) - { - return 0; - } - else if( mFrequency < otherLock.getFrequency() ) - { - return -1; - } - else - { - return 1; - } - } - - public enum Type - { - LOCKED, //Use for decoding channels, so that they cannot be tuned out of range - TRAFFIC, //Like locked, but for temporary allocated traffic channels - TUNABLE; //Tunable -- can be tuned out of range - } -} diff --git a/src/main/java/io/github/dsheirer/source/tuner/TunerChannelProvider.java b/src/main/java/io/github/dsheirer/source/tuner/TunerChannelProvider.java deleted file mode 100644 index e1de1a12..00000000 --- a/src/main/java/io/github/dsheirer/source/tuner/TunerChannelProvider.java +++ /dev/null @@ -1,27 +0,0 @@ -package io.github.dsheirer.source.tuner; - -import io.github.dsheirer.source.SourceException; - -import java.util.concurrent.RejectedExecutionException; - -public interface TunerChannelProvider -{ - /** - * Returns a tuner frequency channel source, tuned to the correct frequency - * - * @param frequency - desired frequency - * - * @return - source for 48k sample rate - */ - public abstract TunerChannelSource getChannel( TunerChannel channel ) - throws RejectedExecutionException, SourceException; - - /** - * Releases the tuned channel resources - * - * @param channel - previously obtained tuner channel - */ - public abstract void releaseChannel( TunerChannelSource source ); - - -} diff --git a/src/main/java/io/github/dsheirer/source/tuner/TunerChannelSource.java b/src/main/java/io/github/dsheirer/source/tuner/TunerChannelSource.java deleted file mode 100644 index c577789e..00000000 --- a/src/main/java/io/github/dsheirer/source/tuner/TunerChannelSource.java +++ /dev/null @@ -1,572 +0,0 @@ -/******************************************************************************* - * 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 io.github.dsheirer.source.tuner; - -import io.github.dsheirer.channel.heartbeat.Heartbeat; -import io.github.dsheirer.dsp.filter.FilterFactory; -import io.github.dsheirer.dsp.filter.Window.WindowType; -import io.github.dsheirer.dsp.filter.cic.ComplexPrimeCICDecimate; -import io.github.dsheirer.dsp.mixer.Oscillator; -import io.github.dsheirer.sample.Buffer; -import io.github.dsheirer.sample.Listener; -import io.github.dsheirer.sample.OverflowableTransferQueue; -import io.github.dsheirer.sample.complex.Complex; -import io.github.dsheirer.sample.complex.ComplexBuffer; -import io.github.dsheirer.sample.real.IOverflowListener; -import io.github.dsheirer.source.ComplexSource; -import io.github.dsheirer.source.SourceException; -import io.github.dsheirer.source.tuner.frequency.FrequencyChangeEvent; -import io.github.dsheirer.source.tuner.frequency.FrequencyChangeEvent.Event; -import io.github.dsheirer.source.tuner.frequency.IFrequencyChangeListener; -import io.github.dsheirer.source.tuner.frequency.IFrequencyChangeProcessor; -import io.github.dsheirer.source.tuner.frequency.IFrequencyChangeProvider; -import org.slf4j.Logger; -import org.slf4j.LoggerFactory; - -import java.util.ArrayList; -import java.util.List; -import java.util.concurrent.RejectedExecutionException; -import java.util.concurrent.ScheduledExecutorService; -import java.util.concurrent.ScheduledFuture; -import java.util.concurrent.TimeUnit; -import java.util.concurrent.atomic.AtomicBoolean; - -public class TunerChannelSource extends ComplexSource implements IFrequencyChangeProcessor, Listener -{ - private final static Logger mLog = LoggerFactory.getLogger(TunerChannelSource.class); - - //Maximum number of filled buffers for the blocking queue - private static final int BUFFER_MAX_CAPACITY = 300; - - //Threshold for resetting buffer overflow condition - private static final int BUFFER_OVERFLOW_RESET_THRESHOLD = 100; - - private static int CHANNEL_RATE = 48000; - private static int CHANNEL_PASS_FREQUENCY = 12000; - private static final Heartbeat HEARTBEAT = new Heartbeat(); - - private OverflowableTransferQueue mBuffer; - - private Tuner mTuner; - private TunerChannel mTunerChannel; - private Oscillator mMixer; - private ComplexPrimeCICDecimate mDecimationFilter; - private Listener mListener; - private Listener mHeartbeatListener; - private IFrequencyChangeProcessor mFrequencyChangeProcessor; - private DownstreamProcessor mDownstreamFrequencyEventProcessor = new DownstreamProcessor(); - private ScheduledFuture mTaskHandle; - - private long mTunerFrequency = 0; - private int mTunerSampleRate; - private int mChannelFrequencyCorrection = 0; - - private DecimationProcessor mDecimationProcessor = new DecimationProcessor(); - - private AtomicBoolean mRunning = new AtomicBoolean(); - private boolean mExpended = false; - - /** - * Provides a Digital Drop Channel (DDC) to decimate the IQ output from a - * tuner down to a 48 kHz IQ channel rate. - * - * Note: this class can only be used once (started and stopped) and a new - * tuner channel source must be requested from the tuner once this object - * has been stopped. This is because channels are managed dynamically and - * center tuned frequency may have changed since this source was obtained - * and thus the tuner might no longer be able to source this channel once it - * has been stopped. - * - * @param tuner to obtain wideband IQ samples from - * @param tunerChannel specifying the center frequency for the DDC - * @throws RejectedExecutionException if the thread pool manager cannot - * accept the decimation processing task - * @throws SourceException if the tuner has an issue providing IQ samples - */ - public TunerChannelSource(Tuner tuner, TunerChannel tunerChannel) throws RejectedExecutionException, SourceException - { - mTuner = tuner; - mTunerChannel = tunerChannel; - mTuner.getTunerController().addListener((IFrequencyChangeProcessor) this); - mTunerFrequency = mTuner.getTunerController().getFrequency(); - - mBuffer = new OverflowableTransferQueue<>(BUFFER_MAX_CAPACITY, BUFFER_OVERFLOW_RESET_THRESHOLD); - - /* Setup the frequency translator to the current source frequency */ - long frequencyOffset = mTunerFrequency - mTunerChannel.getFrequency(); - - mMixer = new Oscillator(frequencyOffset, mTuner.getTunerController().getSampleRate()); - - /* Fire a sample rate change event to setup the decimation chain */ - frequencyChanged(new FrequencyChangeEvent(Event.NOTIFICATION_SAMPLE_RATE_CHANGE, - mTuner.getTunerController().getSampleRate())); - } - - /** - * Overrides the default source overflow listener management to delegate responsibility to the overflow buffer - */ - @Override - public void setOverflowListener(IOverflowListener listener) - { - mBuffer.setOverflowListener(listener); - } - - public void start(ScheduledExecutorService executor) - { - if(mExpended) - { - throw new IllegalStateException("Attempt to re-start an expended tuner channel source. TunerChannelSource" + - " objects can only be used once. "); - } - - if(mRunning.compareAndSet(false, true)) - { - //Broadcast current frequency and sample rate settings so all downstream components are aware - mDownstreamFrequencyEventProcessor.broadcastCurrentFrequency(); - mDownstreamFrequencyEventProcessor.broadcastCurrentSampleRate(); - - //Schedule the decimation task to run every 9 ms (111 iterations/second), an odd periodicity relative - //to the inbound periodicity of 20 ms, to attempt to avoid thread queue contention - mTaskHandle = executor.scheduleAtFixedRate(mDecimationProcessor, 0, 9, TimeUnit.MILLISECONDS); - - /* Finally, register to receive samples from the tuner */ - mTuner.addListener((Listener) this); - } - else - { - mLog.warn("Attempt to start() an already running tuner channel source was ignored"); - } - } - - @Override - public void reset() - { - } - - @Override - public void stop() - { - if(mRunning.compareAndSet(true, false)) - { - mTuner.releaseChannel(this); - mDecimationProcessor.shutdown(); - - if(mTaskHandle != null) - { - mTaskHandle.cancel(true); - mTaskHandle = null; - } - - mBuffer.clear(); - - mExpended = true; - } - else - { - mLog.warn("Attempt to stop() an already stopped tuner channel source was ignored"); - } - } - - - @Override - public void dispose() - { - if(!mRunning.get()) - { - /* Tell the tuner to release/unregister our resources */ - mTuner.getTunerController().removeListener(this); - } - } - - /** - * Changes the frequency correction value and broadcasts the change to the registered downstream listener. - * @param correction current frequency correction value. - */ - private void setFrequencyCorrection(int correction) - { - mChannelFrequencyCorrection = correction; - - updateMixerFrequencyOffset(); - - mDownstreamFrequencyEventProcessor.broadcast( - new FrequencyChangeEvent( Event.NOTIFICATION_CHANNEL_FREQUENCY_CORRECTION_CHANGE, mChannelFrequencyCorrection)); - } - - public Tuner getTuner() - { - return mTuner; - } - - public TunerChannel getTunerChannel() - { - return mTunerChannel; - } - - @Override - public void receive(ComplexBuffer buffer) - { - if(mRunning.get()) - { - mBuffer.offer(buffer); - } - } - - public void setFrequencyChangeListener(IFrequencyChangeProcessor processor) - { - mFrequencyChangeProcessor = processor; - } - - @Override - public void setListener(Listener listener) - { - /* Save a pointer to the listener so that if we have to change the - * decimation filter, we can re-add the listener */ - mListener = listener; - - mDecimationFilter.setListener(listener); - } - - @Override - public void removeListener(Listener listener) - { - mDecimationFilter.removeListener(); - } - - /** - * Handler for frequency change events received from the tuner and channel - * frequency correction events received from the channel consumer/listener - */ - @Override - public void frequencyChanged(FrequencyChangeEvent event) throws SourceException - { - // Echo the event to the registered event listener - if(mFrequencyChangeProcessor != null) - { - mFrequencyChangeProcessor.frequencyChanged(event); - } - - switch(event.getEvent()) - { - case NOTIFICATION_FREQUENCY_CHANGE: - mTunerFrequency = event.getValue().longValue(); - updateMixerFrequencyOffset(); - - //Reset frequency correction so that downstream components can recalculate the value - setFrequencyCorrection(0); - break; - case NOTIFICATION_SAMPLE_RATE_CHANGE: - int sampleRate = event.getValue().intValue(); - setSampleRate(sampleRate); - break; - default: - break; - } - } - - /** - * Updates the sample rate to the requested value and notifies any downstream components of the change - * @param sampleRate to set - */ - private void setSampleRate(int sampleRate) - { - if(mTunerSampleRate != sampleRate) - { - mMixer.setSampleRate(sampleRate); - - /* Get new decimation filter */ - mDecimationFilter = FilterFactory.getDecimationFilter(sampleRate, CHANNEL_RATE, 1, - CHANNEL_PASS_FREQUENCY, 60, WindowType.HAMMING); - - /* re-add the original output listener */ - mDecimationFilter.setListener(mListener); - - mTunerSampleRate = sampleRate; - - mDownstreamFrequencyEventProcessor.broadcastCurrentSampleRate(); - } - } - - /** - * Calculates the local mixer frequency offset from the tuned frequency, - * channel's requested frequency, and channel frequency correction. - */ - private void updateMixerFrequencyOffset() - { - long offset = mTunerFrequency - mTunerChannel.getFrequency() - mChannelFrequencyCorrection; - mMixer.setFrequency(offset); - } - - public int getSampleRate() throws SourceException - { - return CHANNEL_RATE; - } - - public long getFrequency() throws SourceException - { - return mTunerChannel.getFrequency(); - } - - /** - * Implements IFrequencyChangeProvider to enable this source to broadcast frequency change events to downstream - * listeners. - * - * @param listener to receive downstream events - */ - @Override - public void setFrequencyChangeListener(Listener listener) - { - mDownstreamFrequencyEventProcessor.setFrequencyChangeListener(listener); - } - - /** - * Implements IFrequencyChangeProvider to remove the frequency change listener from receiving down-stream frequency - * change events. - */ - @Override - public void removeFrequencyChangeListener() - { - mDownstreamFrequencyEventProcessor.removeFrequencyChangeListener(); - } - - /** - * Implements IFrequencyChangeListener to receive frequency change events containing requests from downstream - * listeners to change frequency values. - * @return listener - */ - @Override - public Listener getFrequencyChangeListener() - { - return mDownstreamFrequencyEventProcessor.getFrequencyChangeListener(); - } - - /** - * Registers the listener to receive heartbeats from this source. - * @param listener to receive heartbeats - */ - @Override - public void setHeartbeatListener(Listener listener) - { - mHeartbeatListener = listener; - } - - /** - * Removes the currently registered heartbeat listener - */ - @Override - public void removeHeartbeatListener() - { - mHeartbeatListener = null; - } - - /** - * Managers frequency change requests and notifications from/to any downstream component. Downstream - * components are those that receive samples from this tuner channel source. These downstream components will be - * notified of any frequency or sample rate change events and will also be able to request frequency correction - * updates. - */ - public class DownstreamProcessor implements IFrequencyChangeListener, IFrequencyChangeProvider, - Listener - { - //Listener to receive downstream events - private Listener mListener; - - /** - * Broadcasts the frequency change event to the downstream frequency change listener - * @param event to broadcast - */ - public void broadcast(FrequencyChangeEvent event) - { - if(mListener != null) - { - mListener.receive(event); - } - } - - /** - * Broadcasts the current frequency of this tuner channel source to the downstream listener - */ - public void broadcastCurrentFrequency() - { - try - { - long frequency = getFrequency(); - broadcast(new FrequencyChangeEvent(Event.NOTIFICATION_FREQUENCY_CHANGE, frequency)); - } - catch(SourceException se) - { - mLog.error("Error obtaining frequency from tuner to broadcast downstream"); - } - } - - /** - * Broadcasts the current decimated sample rate of this tuner channel source - */ - public void broadcastCurrentSampleRate() - { - try - { - //Note: downstream sample rate is currently a fixed value -- it will change in the future - broadcast(new FrequencyChangeEvent(Event.NOTIFICATION_SAMPLE_RATE_CHANGE, getSampleRate())); - } - catch(SourceException se) - { - mLog.error("Error obtaining sample rate from tuner to broadcast downstream"); - } - } - - /** - * Sets the downstream listener to receive frequency change events from this tuner channel source - * @param listener to receive events - */ - @Override - public void setFrequencyChangeListener(Listener listener) - { - mListener = listener; - } - - /** - * Removes the downstream listener from receiving frequency change events. - */ - @Override - public void removeFrequencyChangeListener() - { - mListener = null; - } - - /** - * Listener for receiving frequency change events from downstream components - */ - @Override - public Listener getFrequencyChangeListener() - { - return this; - } - - /** - * Processes frequency change events from downstream components. - * @param event to process - */ - @Override - public void receive(FrequencyChangeEvent event) - { - switch(event.getEvent()) - { - //Frequency correction requests are the only change requests supported from downstream components - case REQUEST_CHANNEL_FREQUENCY_CORRECTION_CHANGE: - setFrequencyCorrection(event.getValue().intValue()); - break; - } - } - } - - /** - * Decimates an inbound buffer of I/Q samples from the source down to the - * standard 48000 channel sample rate - */ - public class DecimationProcessor implements Runnable - { - private boolean mProcessing = true; - private List mSampleBuffers = new ArrayList(); - - public void shutdown() - { - mProcessing = false; - } - - @Override - public void run() - { - /* General exception handler so that any errors won't kill the - * decimation thread and cause the input buffers to fill up and - * run the program out of memory */ - try - { - if(mProcessing) - { - //Send a heartbeat every time this runs to allow downstream components to perform periodic - //state monitoring functions on this thread - if(mHeartbeatListener != null) - { - mHeartbeatListener.receive(HEARTBEAT); - } - - mBuffer.drainTo(mSampleBuffers, 20); - - for(Buffer buffer : mSampleBuffers) - { - /* Check to see if we've been shutdown */ - if(!mProcessing) - { - mBuffer.clear(); - return; - } - else - { - float[] samples = buffer.getSamples(); - - /* We make a copy of the buffer so that we don't affect - * anyone else that is using the same buffer, like other - * channels or the spectral display */ - float[] translated = new float[samples.length]; - - /* Perform frequency translation */ - for(int x = 0; x < samples.length; x += 2) - { - mMixer.rotate(); - - translated[x] = Complex.multiplyInphase( - samples[x], samples[x + 1], mMixer.inphase(), mMixer.quadrature()); - - translated[x + 1] = Complex.multiplyQuadrature( - samples[x], samples[x + 1], mMixer.inphase(), mMixer.quadrature()); - } - - if(mProcessing) - { - final ComplexPrimeCICDecimate filter = mDecimationFilter; - filter.receive(new ComplexBuffer(translated)); - } - } - } - - mSampleBuffers.clear(); - } - } - catch(Exception e) - { - /* Only log the stack trace if we're still processing */ - if(mProcessing) - { - mLog.error("Error encountered during decimation process", e); - } - } - catch(Throwable throwable) - { - mLog.error("Code error encountered during decimation process - channel thread will probably die", throwable); - } - - /* Check to see if we've been shutdown */ - if(!mProcessing) - { - mBuffer.clear(); - mSampleBuffers.clear(); - } - } - } -} diff --git a/src/main/java/io/github/dsheirer/source/tuner/TunerClass.java b/src/main/java/io/github/dsheirer/source/tuner/TunerClass.java index c255fc4b..2086d137 100644 --- a/src/main/java/io/github/dsheirer/source/tuner/TunerClass.java +++ b/src/main/java/io/github/dsheirer/source/tuner/TunerClass.java @@ -51,7 +51,8 @@ public enum TunerClass TERRATEC_NOXON_REV2( TunerType.ELONICS_E4000, "0CCD", "00E0", "Terratec", "NOXON R2" ), TERRATEC_T_STICK_PLUS( TunerType.ELONICS_E4000, "0CCD", "00D7", "Terratec", "T Stick Plus" ), TWINTECH_UT40( TunerType.FITIPOWER_FC0013, "1B80", "D3A4", "Twintech", "UT-40" ), - ZAAPA_ZTMINDVBZP( TunerType.FITIPOWER_FC0012, "1B80", "D398", "Zaapa", "ZT-MINDVBZP" ), + ZAAPA_ZTMINDVBZP( TunerType.FITIPOWER_FC0012, "1B80", "D398", "Zaapa", "ZT-MINDVBZP" ), + TEST_TUNER(TunerType.TEST, "0", "0", "ABC Tuners Inc.", "Model XYZ"), UNKNOWN( TunerType.UNKNOWN, "0", "0", "Unknown Manufacturer", "Unknown Device" ); private TunerType mTunerType; diff --git a/src/main/java/io/github/dsheirer/source/tuner/TunerController.java b/src/main/java/io/github/dsheirer/source/tuner/TunerController.java index 116459ce..0076862f 100644 --- a/src/main/java/io/github/dsheirer/source/tuner/TunerController.java +++ b/src/main/java/io/github/dsheirer/source/tuner/TunerController.java @@ -17,13 +17,21 @@ ******************************************************************************/ package io.github.dsheirer.source.tuner; +import io.github.dsheirer.sample.Broadcaster; +import io.github.dsheirer.sample.Listener; +import io.github.dsheirer.sample.complex.ComplexBuffer; +import io.github.dsheirer.sample.complex.IComplexBufferProvider; +import io.github.dsheirer.source.ISourceEventListener; +import io.github.dsheirer.source.ISourceEventProcessor; +import io.github.dsheirer.source.SourceEvent; +import io.github.dsheirer.source.SourceEventListenerToProcessorAdapter; import io.github.dsheirer.source.SourceException; +import io.github.dsheirer.source.tuner.channel.CICTunerChannelSource; +import io.github.dsheirer.source.tuner.channel.TunerChannel; +import io.github.dsheirer.source.tuner.channel.TunerChannelSource; import io.github.dsheirer.source.tuner.configuration.TunerConfiguration; -import io.github.dsheirer.source.tuner.frequency.FrequencyChangeEvent; -import io.github.dsheirer.source.tuner.frequency.FrequencyChangeEvent.Event; import io.github.dsheirer.source.tuner.frequency.FrequencyController; import io.github.dsheirer.source.tuner.frequency.FrequencyController.Tunable; -import io.github.dsheirer.source.tuner.frequency.IFrequencyChangeProcessor; import org.slf4j.Logger; import org.slf4j.LoggerFactory; @@ -31,15 +39,18 @@ import java.util.SortedSet; import java.util.concurrent.ConcurrentSkipListSet; import java.util.concurrent.RejectedExecutionException; -public abstract class TunerController implements Tunable, IFrequencyChangeProcessor +public abstract class TunerController implements Tunable, ISourceEventProcessor, ISourceEventListener, + IComplexBufferProvider, Listener { private final static Logger mLog = LoggerFactory.getLogger(TunerController.class); + protected Broadcaster mSampleBroadcaster = new Broadcaster<>(); /* List of currently tuned channels being served to demod channels */ private SortedSet mTunedChannels = new ConcurrentSkipListSet<>(); protected FrequencyController mFrequencyController; private int mMiddleUnusable; private double mUsableBandwidthPercentage; + private Listener mSourceEventListener; /** * Abstract tuner controller class. The tuner controller manages frequency bandwidth and currently tuned channels @@ -57,6 +68,16 @@ public abstract class TunerController implements Tunable, IFrequencyChangeProces mFrequencyController = new FrequencyController(this, minimumFrequency, maximumFrequency, 0.0d); mMiddleUnusable = middleUnusable; mUsableBandwidthPercentage = usableBandwidth; + mSourceEventListener = new SourceEventListenerToProcessorAdapter(this); + } + + /** + * Implements the ISourceEventListener interface to receive requests from sample consumers + */ + @Override + public Listener getSourceEventListener() + { + return mSourceEventListener; } /** @@ -68,11 +89,28 @@ public abstract class TunerController implements Tunable, IFrequencyChangeProces * Responds to requests to set the frequency */ @Override - public void frequencyChanged(FrequencyChangeEvent event) throws SourceException + public void process(SourceEvent sourceEvent ) throws SourceException { - if(event.getEvent() == Event.REQUEST_FREQUENCY_CHANGE) + switch(sourceEvent.getEvent()) { - setFrequency(event.getValue().longValue()); + case REQUEST_FREQUENCY_CHANGE: + setFrequency( sourceEvent.getValue().longValue() ); + break; + case REQUEST_START_SAMPLE_STREAM: + if(sourceEvent.getSource() instanceof Listener) + { + addComplexBufferListener((Listener)sourceEvent.getSource()); + } + break; + case REQUEST_STOP_SAMPLE_STREAM: + if(sourceEvent.getSource() instanceof Listener) + { + removeComplexBufferListener((Listener)sourceEvent.getSource()); + } + break; + default: + mLog.error("Ignoring unrecognized source event: " + sourceEvent.getEvent().name() + " from [" + + sourceEvent.getSource().getClass() + "]" ); } } @@ -102,9 +140,15 @@ public abstract class TunerController implements Tunable, IFrequencyChangeProces return mFrequencyController.getFrequency(); } - public int getSampleRate() + @Override + public boolean canTune(long frequency) { - return mFrequencyController.getBandwidth(); + return mFrequencyController.canTune(frequency); + } + + public double getSampleRate() + { + return mFrequencyController.getSampleRate(); } public double getFrequencyCorrection() @@ -210,7 +254,7 @@ public abstract class TunerController implements Tunable, IFrequencyChangeProces updateLOFrequency(); } - source = new TunerChannelSource(tuner, channel); + source = new CICTunerChannelSource(tuner, channel); } catch(SourceException se) { @@ -351,11 +395,13 @@ public abstract class TunerController implements Tunable, IFrequencyChangeProces } /** - * Sets the listener to be notified any time that the tuner changes frequency or bandwidth/sample rate. + * Sets the listener to be notified any time that the tuner changes frequency + * or bandwidth/sample rate. * - * Note: this is normally used by the Tuner. Any additional listeners can be registered on the tuner. + * Note: this is normally used by the Tuner. Any additional listeners can + * be registered on the tuner. */ - public void addListener(IFrequencyChangeProcessor processor) + public void addListener( ISourceEventProcessor processor ) { mFrequencyController.addListener(processor); } @@ -363,8 +409,52 @@ public abstract class TunerController implements Tunable, IFrequencyChangeProces /** * Removes the frequency change listener */ - public void removeListener(IFrequencyChangeProcessor processor) + public void removeListener( ISourceEventProcessor processor ) { mFrequencyController.removeFrequencyChangeProcessor(processor); } -} + + /** + * Adds the listener to receive complex buffer samples + */ + @Override + public void addComplexBufferListener(Listener listener) + { + mSampleBroadcaster.addListener(listener); + } + + /** + * Removes the listener from receiving complex buffer samples + */ + @Override + public void removeComplexBufferListener(Listener listener) + { + mSampleBroadcaster.removeListener(listener); + } + + /** + * Indicates if there are any complex buffer listeners registered on this controller + */ + @Override + public boolean hasComplexBufferListeners() + { + return mSampleBroadcaster.hasListeners(); + } + + /** + * Broadcasts the buffer to any registered listeners + */ + protected void broadcast(ComplexBuffer complexBuffer) + { + mSampleBroadcaster.broadcast(complexBuffer); + } + + /** + * Implements the Listener interface to receive and distribute complex buffers from subclass implementations + */ + @Override + public void receive(ComplexBuffer complexBuffer) + { + broadcast(complexBuffer); + } +} \ No newline at end of file diff --git a/src/main/java/io/github/dsheirer/source/tuner/TunerEditor.java b/src/main/java/io/github/dsheirer/source/tuner/TunerEditor.java index 4d30bd8a..e9c6a3f3 100644 --- a/src/main/java/io/github/dsheirer/source/tuner/TunerEditor.java +++ b/src/main/java/io/github/dsheirer/source/tuner/TunerEditor.java @@ -1,19 +1,17 @@ /******************************************************************************* - * SDR Trunk - * Copyright (C) 2014-2016 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 + * sdr-trunk + * 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 io.github.dsheirer.source.tuner; @@ -29,11 +27,18 @@ import net.miginfocom.swing.MigLayout; import org.slf4j.Logger; import org.slf4j.LoggerFactory; -import javax.swing.*; +import javax.swing.JButton; +import javax.swing.JLabel; +import javax.swing.JOptionPane; +import javax.swing.JPanel; +import javax.swing.JScrollPane; +import javax.swing.JTable; +import javax.swing.RowFilter; +import javax.swing.ScrollPaneConstants; import javax.swing.event.ListSelectionEvent; import javax.swing.event.ListSelectionListener; import javax.swing.table.TableRowSorter; -import java.awt.*; +import java.awt.Dimension; import java.awt.event.ActionEvent; import java.awt.event.ActionListener; @@ -54,13 +59,13 @@ public class TunerEditor extends Editor private JScrollPane mEditorScroller; private Editor mEditor = new EmptyEditor<>( "a tuner" ); private JideSplitPane mEditorSplitPane = new JideSplitPane(); - + public TunerEditor( TunerConfigurationModel tunerConfigurationModel ) { mTunerConfigurationModel = tunerConfigurationModel; init(); } - + public void init() { setLayout( new MigLayout( "insets 0 0 0 0", "[grow,fill]", "[grow,fill]" ) ); @@ -69,7 +74,7 @@ public class TunerEditor extends Editor listPanel.setLayout( new MigLayout( "fill,wrap 3", "[grow,fill]", "[][][][grow,fill][]" ) ); listPanel.add( mSelectedTunerType, "span" ); - + mFrequencyControl.setEnabled( false ); listPanel.add( mFrequencyControl, "span" ); @@ -85,7 +90,7 @@ public class TunerEditor extends Editor if( !event.getValueIsAdjusting() ) { int row = mTunerConfigurationTable.getSelectedRow(); - + if( row >= 0 ) { int modelRow = mTunerConfigurationTable.convertRowIndexToModel( row ); @@ -100,8 +105,8 @@ public class TunerEditor extends Editor } ); JScrollPane tableScroller = new JScrollPane( mTunerConfigurationTable ); - tableScroller.setHorizontalScrollBarPolicy( - ScrollPaneConstants.HORIZONTAL_SCROLLBAR_NEVER ); + tableScroller.setHorizontalScrollBarPolicy( + ScrollPaneConstants.HORIZONTAL_SCROLLBAR_NEVER ); listPanel.add( new JLabel( "Tuner Configurations" ), "span" ); listPanel.add( tableScroller, "span" ); @@ -113,14 +118,14 @@ public class TunerEditor extends Editor public void actionPerformed( ActionEvent e ) { Tuner tuner = getItem(); - + String name = mTunerConfigurationModel .getDistinctName( tuner.getTunerType(), tuner.getUniqueID() ); - + TunerConfiguration config = TunerConfigurationFactory - .getTunerConfiguration( tuner.getTunerType(), - tuner.getUniqueID(), name ); - + .getTunerConfiguration( tuner.getTunerType(), + tuner.getUniqueID(), name ); + mTunerConfigurationModel.addTunerConfiguration( config ); } } ); @@ -133,21 +138,21 @@ public class TunerEditor extends Editor @Override public void actionPerformed( ActionEvent e ) { - int choice = JOptionPane.showConfirmDialog( TunerEditor.this, - "Do you want to delete this tuner configuration?", - "Delete Tuner Configuration?", JOptionPane.YES_NO_OPTION ); - + int choice = JOptionPane.showConfirmDialog( TunerEditor.this, + "Do you want to delete this tuner configuration?", + "Delete Tuner Configuration?", JOptionPane.YES_NO_OPTION ); + if( choice == JOptionPane.YES_OPTION ) { TunerConfiguration selected = getSelectedTunerConfiguration(); - + mTunerConfigurationModel.removeTunerConfiguration( selected ); mTunerConfigurationTable.setRowSelectionInterval( 0, 0 ); } } } ); listPanel.add( mDeleteConfigurationButton ); - + mAssignConfigurationButton.setEnabled( false ); mAssignConfigurationButton.setToolTipText( "Assigns to the currently selected tuner configuration to the selected tuner" ); mAssignConfigurationButton.addActionListener( new ActionListener() @@ -156,18 +161,18 @@ public class TunerEditor extends Editor public void actionPerformed( ActionEvent e ) { TunerConfiguration selected = getSelectedTunerConfiguration(); - + mTunerConfigurationModel.assignTunerConfiguration( selected ); setTunerConfiguration( getSelectedTunerConfiguration() ); } } ); listPanel.add( mAssignConfigurationButton ); - + //Set preferred size on both scrollers to same width, so they split the space listPanel.setPreferredSize( new Dimension( 100, 80 ) ); JScrollPane listScroller = new JScrollPane( listPanel ); listScroller.setPreferredSize( new Dimension( 100, 80 ) ); - + mEditorScroller = new JScrollPane( mEditor ); mEditorScroller.setPreferredSize( new Dimension( 100, 80 ) ); @@ -177,15 +182,15 @@ public class TunerEditor extends Editor add( mEditorSplitPane ); } - + private TunerConfiguration getSelectedTunerConfiguration() { int tableRow = mTunerConfigurationTable.getSelectedRow(); - + if( tableRow >= 0 ) { int modelRow = mTunerConfigurationTable.convertRowIndexToModel( tableRow ); - + if( modelRow >= 0 ) { return mTunerConfigurationModel.getTunerConfiguration( modelRow ); @@ -194,7 +199,7 @@ public class TunerEditor extends Editor return null; } - + @Override public void save() { @@ -211,9 +216,9 @@ public class TunerEditor extends Editor previous.getTunerController().removeListener( mFrequencyControl ); mFrequencyControl.removeListener( previous.getTunerController() ); } - + super.setItem( tuner ); - + if( hasItem() ) { mSelectedTunerType.setText( tuner.getName() ); @@ -222,21 +227,21 @@ public class TunerEditor extends Editor mNewConfigurationButton.setEnabled( true ); //Change to an editor for this config's tuner type - mEditor = TunerConfigurationFactory.getEditor( getItem(), mTunerConfigurationModel ); - - mRowSorter.setRowFilter( new ConfigurationRowFilter( tuner.getTunerType(), - tuner.getUniqueID() ) ); + mEditor = TunerConfigurationFactory.getEditor( getItem(), mTunerConfigurationModel ); + + mRowSorter.setRowFilter( new ConfigurationRowFilter( tuner.getTunerType(), + tuner.getUniqueID() ) ); TunerConfiguration assigned = mTunerConfigurationModel .getTunerConfiguration( tuner.getTunerType(), tuner.getUniqueID() ); int modelIndex = mTunerConfigurationModel.getRowIndex( assigned ); - + if( modelIndex >= 0 ) { int viewIndex = mTunerConfigurationTable - .convertRowIndexToView( modelIndex ); - + .convertRowIndexToView( modelIndex ); + if( viewIndex >= 0 ) { mTunerConfigurationTable @@ -245,14 +250,14 @@ public class TunerEditor extends Editor } //Link frequency control to the tuner - mFrequencyControl.addListener( tuner.getTunerController() ); - - //Link tuner to frequency control - tuner.getTunerController().addListener( mFrequencyControl ); + mFrequencyControl.addListener( tuner.getTunerController() ); - //Set the displayed frequency without adjusting the tuner's frequency - mFrequencyControl.setFrequency( - tuner.getTunerController().getFrequency(), false ); + //Link tuner to frequency control + tuner.getTunerController().addListener( mFrequencyControl ); + + //Set the displayed frequency without adjusting the tuner's frequency + mFrequencyControl.setFrequency( + tuner.getTunerController().getFrequency(), false ); } else { @@ -263,15 +268,15 @@ public class TunerEditor extends Editor mRowSorter.setRowFilter( null ); mEditor = new EmptyEditor(); } - - //Swap out the editor + + //Swap out the editor int split = mEditorSplitPane.getDividerLocation( 0 ); mEditorSplitPane.remove( mEditorScroller ); mEditorScroller = new JScrollPane( mEditor ); mEditorSplitPane.add( mEditorScroller ); mEditorSplitPane.setDividerLocation( 0, split ); } - + private void setTunerConfiguration( TunerConfiguration config ) { if( config != null ) @@ -293,7 +298,7 @@ public class TunerEditor extends Editor { mAssignConfigurationButton.setEnabled( false ); } - + if( mDeleteConfigurationButton.isEnabled() ) { mDeleteConfigurationButton.setEnabled( false ); @@ -306,39 +311,39 @@ public class TunerEditor extends Editor { mDeleteConfigurationButton.setEnabled( false ); } - + if( mAssignConfigurationButton.isEnabled() ) { mAssignConfigurationButton.setEnabled( false ); } } - + mEditor.setItem( config ); } - + public class ConfigurationRowFilter extends RowFilter { private TunerType mTunerType; private String mUniqueID; - + public ConfigurationRowFilter( TunerType type, String uniqueID ) { mTunerType = type; mUniqueID = uniqueID; } - + @Override - public boolean include( javax.swing.RowFilter.Entry entry ) + public boolean include( javax.swing.RowFilter.Entry entry ) { TunerConfiguration config = entry.getModel() - .getTunerConfiguration( entry.getIdentifier() ); + .getTunerConfiguration( entry.getIdentifier() ); - return config != null && - config.getTunerType() == mTunerType && - config.getUniqueID() != null && - config.getUniqueID().contentEquals( mUniqueID ); + return config != null && + config.getTunerType() == mTunerType && + config.getUniqueID() != null && + config.getUniqueID().contentEquals( mUniqueID ); } } - -} + +} \ No newline at end of file diff --git a/src/main/java/io/github/dsheirer/source/tuner/TunerManager.java b/src/main/java/io/github/dsheirer/source/tuner/TunerManager.java index 5af7215e..74ee26b0 100644 --- a/src/main/java/io/github/dsheirer/source/tuner/TunerManager.java +++ b/src/main/java/io/github/dsheirer/source/tuner/TunerManager.java @@ -246,25 +246,21 @@ public class TunerManager + "supported"); } - private TunerInitStatus initFuncubeProTuner(Device device, - DeviceDescriptor descriptor) + private TunerInitStatus initFuncubeProTuner(Device device, DeviceDescriptor descriptor) { String reason = "NOT LOADED"; - MixerTunerDataLine dataline = getMixerTunerDataLine( - TunerClass.FUNCUBE_DONGLE_PRO.getTunerType()); + MixerTunerDataLine dataline = getMixerTunerDataLine(TunerClass.FUNCUBE_DONGLE_PRO.getTunerType()); if(dataline != null) { - FCD1TunerController controller = - new FCD1TunerController(device, descriptor); + FCD1TunerController controller = new FCD1TunerController(dataline, device, descriptor); try { controller.init(); - FCDTuner tuner = - new FCDTuner(dataline, controller); + FCDTuner tuner = new FCDTuner(controller); return new TunerInitStatus(tuner, "LOADED"); } @@ -284,25 +280,21 @@ public class TunerManager + "loaded - " + reason); } - private TunerInitStatus initFuncubeProPlusTuner(Device device, - DeviceDescriptor descriptor) + private TunerInitStatus initFuncubeProPlusTuner(Device device, DeviceDescriptor descriptor) { String reason = "NOT LOADED"; - MixerTunerDataLine dataline = getMixerTunerDataLine( - TunerClass.FUNCUBE_DONGLE_PRO_PLUS.getTunerType()); + MixerTunerDataLine dataline = getMixerTunerDataLine(TunerClass.FUNCUBE_DONGLE_PRO_PLUS.getTunerType()); if(dataline != null) { - FCD2TunerController controller = - new FCD2TunerController(device, descriptor); + FCD2TunerController controller = new FCD2TunerController(dataline, device, descriptor); try { controller.init(); - FCDTuner tuner = - new FCDTuner(dataline, controller); + FCDTuner tuner = new FCDTuner(controller); return new TunerInitStatus(tuner, "LOADED"); } @@ -323,8 +315,7 @@ public class TunerManager + "loaded - " + reason); } - private TunerInitStatus initHackRFTuner(Device device, - DeviceDescriptor descriptor) + private TunerInitStatus initHackRFTuner(Device device, DeviceDescriptor descriptor) { try { diff --git a/src/main/java/io/github/dsheirer/source/tuner/TunerModel.java b/src/main/java/io/github/dsheirer/source/tuner/TunerModel.java index 3b7aa297..a0735a24 100644 --- a/src/main/java/io/github/dsheirer/source/tuner/TunerModel.java +++ b/src/main/java/io/github/dsheirer/source/tuner/TunerModel.java @@ -5,6 +5,8 @@ import io.github.dsheirer.source.Source; import io.github.dsheirer.source.SourceException; import io.github.dsheirer.source.config.SourceConfigTuner; import io.github.dsheirer.source.tuner.TunerEvent.Event; +import io.github.dsheirer.source.tuner.channel.TunerChannel; +import io.github.dsheirer.source.tuner.channel.TunerChannelSource; import io.github.dsheirer.source.tuner.configuration.TunerConfiguration; import io.github.dsheirer.source.tuner.configuration.TunerConfigurationModel; import org.slf4j.Logger; @@ -15,282 +17,275 @@ import java.text.DecimalFormat; import java.util.ArrayList; import java.util.Iterator; import java.util.List; -import java.util.concurrent.RejectedExecutionException; public class TunerModel extends AbstractTableModel implements Listener { - private static final long serialVersionUID = 1L; - - private final static Logger mLog = LoggerFactory.getLogger( TunerModel.class ); + private static final long serialVersionUID = 1L; - public static final int TUNER_TYPE = 0; - public static final int TUNER_ID = 1; - public static final int SAMPLE_RATE = 2; - public static final int FREQUENCY = 3; - public static final int CHANNEL_COUNT = 4; - public static final int SPECTRAL_DISPLAY_MAIN = 5; - public static final int SPECTRAL_DISPLAY_NEW = 6; - - private static final String MHZ = " MHz"; - private static final String[] COLUMNS = - { "Tuner", "ID", "Sample Rate", "Frequency", "Channels", "Spectral", "Display" }; - - private List mTuners = new ArrayList<>(); - private List> mTunerEventListeners = new ArrayList<>(); - private DecimalFormat mFrequencyFormat = new DecimalFormat( "0.00000" ); - private DecimalFormat mSampleRateFormat = new DecimalFormat( "0.000" ); - - private TunerConfigurationModel mTunerConfigurationModel; + private final static Logger mLog = LoggerFactory.getLogger(TunerModel.class); - public TunerModel( TunerConfigurationModel tunerConfigurationModel ) - { - mTunerConfigurationModel = tunerConfigurationModel; - } - - public TunerConfigurationModel getTunerConfigurationModel() - { - return mTunerConfigurationModel; - } + public static final int TUNER_TYPE = 0; + public static final int TUNER_ID = 1; + public static final int SAMPLE_RATE = 2; + public static final int FREQUENCY = 3; + public static final int CHANNEL_COUNT = 4; + public static final int SPECTRAL_DISPLAY_MAIN = 5; + public static final int SPECTRAL_DISPLAY_NEW = 6; - /** - * List of Tuners currently in the model - */ - public List getTuners() - { - return mTuners; - } - - public Tuner getTuner( int index ) - { - if( index < mTuners.size() ) - { - return mTuners.get( index ); - } - - return null; - } + private static final String MHZ = " MHz"; + private static final String[] COLUMNS = + {"Tuner", "ID", "Sample Rate", "Frequency", "Channels", "Spectral", "Display"}; - public void addTuners( List Tuners ) - { - for( Tuner tuner: Tuners ) - { - addTuner( tuner ); - } - } + private List mTuners = new ArrayList<>(); + private List> mTunerEventListeners = new ArrayList<>(); + private DecimalFormat mFrequencyFormat = new DecimalFormat("0.00000"); + private DecimalFormat mSampleRateFormat = new DecimalFormat("0.000"); - /** - * Adds the Tuner to this model - */ - public void addTuner( Tuner tuner ) - { - if( !mTuners.contains( tuner ) ) - { - //Get the tuner configuration and apply it to the tuner - this - //call should always produce a tuner configuration - TunerConfiguration config = mTunerConfigurationModel - .getTunerConfiguration( tuner.getTunerType(), tuner.getUniqueID() ); - - try - { - tuner.getTunerController().apply( config ); - - mTuners.add( tuner ); - - int index = mTuners.indexOf( tuner ); - - fireTableRowsInserted( index, index ); - - tuner.addTunerChangeListener( this ); - } - catch( SourceException se ) - { - mLog.error( "Couldn't apply tuner configuration to tuner - [" - + tuner.getTunerType().name() + "] with id [" - + tuner.getUniqueID() + "] - tuner will not be included" ); - } - - } - } + private TunerConfigurationModel mTunerConfigurationModel; - /** - * Removes the Tuner from this model - */ - public void removeTuner( Tuner tuner ) - { - if( mTuners.contains( tuner ) ) - { - tuner.removeTunerChangeListener( this ); - - int index = mTuners.indexOf( tuner ); - - mTuners.remove( tuner ); - - fireTableRowsDeleted( index, index ); - } - } - - public void addListener( Listener listener ) - { - mTunerEventListeners.add( listener ); - } + public TunerModel(TunerConfigurationModel tunerConfigurationModel) + { + mTunerConfigurationModel = tunerConfigurationModel; + } - public void removeListener( Listener listener ) - { - mTunerEventListeners.remove( listener ); - } + public TunerConfigurationModel getTunerConfigurationModel() + { + return mTunerConfigurationModel; + } - public void broadcast( TunerEvent event ) - { - for( Listener listener: mTunerEventListeners ) - { - listener.receive( event ); - } - } + /** + * List of Tuners currently in the model + */ + public List getTuners() + { + return mTuners; + } - /** - * Requests to display the first tuner in this model. Invoke this method - * after all listeners have registered and tuners have been added to this - * model, in order to inform the primary display to use the first tuner. - */ - public void requestFirstTunerDisplay() - { - if( mTuners.size() > 0 ) - { - broadcast( new TunerEvent( mTuners.get( 0 ), - Event.REQUEST_MAIN_SPECTRAL_DISPLAY ) ); - } - } - - @Override - public void receive( TunerEvent event ) - { - if( event.getTuner() != null ) - { - int index = mTuners.indexOf( event.getTuner() ); - - if( index >= 0 ) - { - switch( event.getEvent() ) - { - case CHANNEL_COUNT: - fireTableCellUpdated( index, CHANNEL_COUNT ); - break; - case FREQUENCY: - fireTableCellUpdated( index, FREQUENCY ); - mTunerConfigurationModel.tunerFrequencyChanged( event.getTuner() ); - break; - case SAMPLE_RATE: - fireTableCellUpdated( index, SAMPLE_RATE ); - break; - case REQUEST_MAIN_SPECTRAL_DISPLAY: - fireTableCellUpdated( index, SPECTRAL_DISPLAY_MAIN ); - default: - break; - } - } - } - - broadcast( event ); - } + public Tuner getTuner(int index) + { + if(index < mTuners.size()) + { + return mTuners.get(index); + } - @Override - public int getRowCount() - { - return mTuners.size(); - } + return null; + } - @Override - public int getColumnCount() - { - return COLUMNS.length; - } + public void addTuners(List Tuners) + { + for(Tuner tuner : Tuners) + { + addTuner(tuner); + } + } - @Override - public Object getValueAt( int rowIndex, int columnIndex ) - { - if( rowIndex < mTuners.size() ) - { - Tuner tuner = mTuners.get( rowIndex ); + /** + * Adds the Tuner to this model + */ + public void addTuner(Tuner tuner) + { + if(!mTuners.contains(tuner)) + { + //Get the tuner configuration and apply it to the tuner - this + //call should always produce a tuner configuration + TunerConfiguration config = mTunerConfigurationModel + .getTunerConfiguration(tuner.getTunerType(), tuner.getUniqueID()); - switch( columnIndex ) - { - case TUNER_TYPE: - return tuner.getTunerType().getLabel(); - case TUNER_ID: - return tuner.getUniqueID(); - case SAMPLE_RATE: - int sampleRate = tuner.getTunerController().getSampleRate(); + try + { + tuner.getTunerController().apply(config); - return mSampleRateFormat.format( sampleRate / 1E6D ) + MHZ; - case FREQUENCY: - try - { - long frequency = tuner.getTunerController().getFrequency(); - - return mFrequencyFormat.format( frequency / 1E6D ) + MHZ; - } - catch( Exception e ) - { - return 0; - } - case CHANNEL_COUNT: - return tuner.getTunerController().getChannelCount(); - case SPECTRAL_DISPLAY_MAIN: - return "Main"; - case SPECTRAL_DISPLAY_NEW: - return "New"; - default: - break; - } - } + mTuners.add(tuner); + + int index = mTuners.indexOf(tuner); + + fireTableRowsInserted(index, index); + + tuner.addTunerChangeListener(this); + } + catch(SourceException se) + { + mLog.error("Couldn't apply tuner configuration to tuner - [" + + tuner.getTunerType().name() + "] with id [" + + tuner.getUniqueID() + "] - tuner will not be included"); + } + + } + } + + /** + * Removes the Tuner from this model + */ + public void removeTuner(Tuner tuner) + { + if(mTuners.contains(tuner)) + { + tuner.removeTunerChangeListener(this); + + int index = mTuners.indexOf(tuner); + + mTuners.remove(tuner); + + fireTableRowsDeleted(index, index); + } + } + + public void addListener(Listener listener) + { + mTunerEventListeners.add(listener); + } + + public void removeListener(Listener listener) + { + mTunerEventListeners.remove(listener); + } + + public void broadcast(TunerEvent event) + { + for(Listener listener : mTunerEventListeners) + { + listener.receive(event); + } + } + + /** + * Requests to display the first tuner in this model. Invoke this method + * after all listeners have registered and tuners have been added to this + * model, in order to inform the primary display to use the first tuner. + */ + public void requestFirstTunerDisplay() + { + if(mTuners.size() > 0) + { + broadcast(new TunerEvent(mTuners.get(0), + Event.REQUEST_MAIN_SPECTRAL_DISPLAY)); + } + } + + @Override + public void receive(TunerEvent event) + { + if(event.getTuner() != null) + { + int index = mTuners.indexOf(event.getTuner()); + + if(index >= 0) + { + switch(event.getEvent()) + { + case CHANNEL_COUNT: + fireTableCellUpdated(index, CHANNEL_COUNT); + break; + case FREQUENCY: + fireTableCellUpdated(index, FREQUENCY); + mTunerConfigurationModel.tunerFrequencyChanged(event.getTuner()); + break; + case SAMPLE_RATE: + fireTableCellUpdated(index, SAMPLE_RATE); + break; + case REQUEST_MAIN_SPECTRAL_DISPLAY: + fireTableCellUpdated(index, SPECTRAL_DISPLAY_MAIN); + default: + break; + } + } + } + + broadcast(event); + } + + @Override + public int getRowCount() + { + return mTuners.size(); + } + + @Override + public int getColumnCount() + { + return COLUMNS.length; + } + + @Override + public Object getValueAt(int rowIndex, int columnIndex) + { + if(rowIndex < mTuners.size()) + { + Tuner tuner = mTuners.get(rowIndex); + + switch(columnIndex) + { + case TUNER_TYPE: + return tuner.getTunerType().getLabel(); + case TUNER_ID: + return tuner.getUniqueID(); + case SAMPLE_RATE: + double sampleRate = tuner.getTunerController().getSampleRate(); + + return mSampleRateFormat.format(sampleRate / 1E6D) + MHZ; + case FREQUENCY: + try + { + long frequency = tuner.getTunerController().getFrequency(); + + return mFrequencyFormat.format(frequency / 1E6D) + MHZ; + } + catch(Exception e) + { + return 0; + } + case CHANNEL_COUNT: + return tuner.getTunerController().getChannelCount(); + case SPECTRAL_DISPLAY_MAIN: + return "Main"; + case SPECTRAL_DISPLAY_NEW: + return "New"; + default: + break; + } + } + + return null; + } + + @Override + public String getColumnName(int columnIndex) + { + return COLUMNS[columnIndex]; + } - return null; - } - - @Override - public String getColumnName( int columnIndex ) - { - return COLUMNS[ columnIndex ]; - } - /** * Iterates current tuners to get a tuner channel source for the frequency * specified in the channel config's source config object. - * + * * Returns null if no tuner can source the channel */ - public Source getSource(SourceConfigTuner config, int bandwidth ) + public Source getSource(SourceConfigTuner config, int bandwidth) { - TunerChannelSource retVal = null; - - TunerChannel tunerChannel = config.getTunerChannel(); - - tunerChannel.setBandwidth( bandwidth ); - - Iterator it = mTuners.iterator(); - - Tuner tuner; - - while( it.hasNext() && retVal == null ) - { - tuner = it.next(); - - try + TunerChannelSource retVal = null; + + TunerChannel tunerChannel = config.getTunerChannel(); + + tunerChannel.setBandwidth(bandwidth); + + Iterator it = mTuners.iterator(); + + Tuner tuner; + + while(it.hasNext() && retVal == null) + { + tuner = it.next(); + + try { - retVal = tuner.getChannel( tunerChannel ); + retVal = tuner.getSourceManager().getSource(tunerChannel); } - catch ( RejectedExecutionException ree ) - { - mLog.error( "couldn't provide tuner channel source", ree ); - } - catch ( SourceException e ) + catch(Exception e) { - mLog.error( "error obtaining channel from tuner [" + - tuner.getName() + "]", e ); + mLog.error("Error obtaining channel source from tuner [" + tuner.getName() + "]", e); } - } - - return retVal; + } + + return retVal; } - -} +} \ No newline at end of file diff --git a/src/main/java/io/github/dsheirer/source/tuner/TunerSourceEditor.java b/src/main/java/io/github/dsheirer/source/tuner/TunerSourceEditor.java index ccb907c7..be958fdc 100644 --- a/src/main/java/io/github/dsheirer/source/tuner/TunerSourceEditor.java +++ b/src/main/java/io/github/dsheirer/source/tuner/TunerSourceEditor.java @@ -20,16 +20,16 @@ package io.github.dsheirer.source.tuner; import io.github.dsheirer.controller.channel.Channel; import io.github.dsheirer.gui.control.JFrequencyControl; import io.github.dsheirer.gui.editor.Editor; +import io.github.dsheirer.source.ISourceEventProcessor; +import io.github.dsheirer.source.SourceEvent; import io.github.dsheirer.source.config.SourceConfigTuner; import io.github.dsheirer.source.config.SourceConfiguration; -import io.github.dsheirer.source.tuner.frequency.FrequencyChangeEvent; -import io.github.dsheirer.source.tuner.frequency.IFrequencyChangeProcessor; import net.miginfocom.swing.MigLayout; public class TunerSourceEditor extends Editor { - private static final long serialVersionUID = 1L; - private JFrequencyControl mFrequencyControl; + private static final long serialVersionUID = 1L; + private JFrequencyControl mFrequencyControl; public TunerSourceEditor() { @@ -41,10 +41,10 @@ public class TunerSourceEditor extends Editor setLayout( new MigLayout( "insets 0 0 0 0", "[left]", "" ) ); mFrequencyControl = new JFrequencyControl(); mFrequencyControl.setEnabled( false ); - mFrequencyControl.addListener( new IFrequencyChangeProcessor() + mFrequencyControl.addListener( new ISourceEventProcessor() { @Override - public void frequencyChanged( FrequencyChangeEvent event ) + public void process(SourceEvent event ) { setModified( true ); } @@ -63,16 +63,16 @@ public class TunerSourceEditor extends Editor setModified( false ); } - + @Override public void setItem( Channel item ) { super.setItem( item ); - + if( hasItem() ) { SourceConfiguration config = getItem().getSourceConfiguration(); - + mFrequencyControl.setEnabled( true ); if( config instanceof SourceConfigTuner ) @@ -92,4 +92,4 @@ public class TunerSourceEditor extends Editor setModified( false ); } } -} +} \ No newline at end of file diff --git a/src/main/java/io/github/dsheirer/source/tuner/TunerType.java b/src/main/java/io/github/dsheirer/source/tuner/TunerType.java index 93718fdb..6b18969d 100644 --- a/src/main/java/io/github/dsheirer/source/tuner/TunerType.java +++ b/src/main/java/io/github/dsheirer/source/tuner/TunerType.java @@ -32,6 +32,7 @@ public enum TunerType RAFAELMICRO_R820T( "R820T" ), RAFAELMICRO_R828D( "R828D" ), RTL2832_VARIOUS( "Generic" ), + TEST("Test"), UNKNOWN( "Unknown" ); private String mLabel; diff --git a/src/main/java/io/github/dsheirer/source/tuner/airspy/AirspyTuner.java b/src/main/java/io/github/dsheirer/source/tuner/airspy/AirspyTuner.java index ba1f5ad1..e22ab6df 100644 --- a/src/main/java/io/github/dsheirer/source/tuner/airspy/AirspyTuner.java +++ b/src/main/java/io/github/dsheirer/source/tuner/airspy/AirspyTuner.java @@ -17,21 +17,13 @@ ******************************************************************************/ package io.github.dsheirer.source.tuner.airspy; -import io.github.dsheirer.sample.Listener; -import io.github.dsheirer.sample.complex.ComplexBuffer; -import io.github.dsheirer.source.SourceException; import io.github.dsheirer.source.tuner.Tuner; -import io.github.dsheirer.source.tuner.TunerChannel; -import io.github.dsheirer.source.tuner.TunerChannelSource; import io.github.dsheirer.source.tuner.TunerClass; -import io.github.dsheirer.source.tuner.TunerEvent; import io.github.dsheirer.source.tuner.TunerType; import io.github.dsheirer.source.tuner.airspy.AirspyTunerController.BoardID; import org.slf4j.Logger; import org.slf4j.LoggerFactory; -import java.util.concurrent.RejectedExecutionException; - public class AirspyTuner extends Tuner { private final static Logger mLog = LoggerFactory.getLogger( AirspyTuner.class ); @@ -40,15 +32,15 @@ public class AirspyTuner extends Tuner { super( "Airspy " + controller.getDeviceInfo().getSerialNumber(), controller ); } - + public AirspyTunerController getController() { return (AirspyTunerController)getTunerController(); } @Override - public String getUniqueID() - { + public String getUniqueID() + { try { return getController().getDeviceInfo().getSerialNumber(); @@ -57,9 +49,9 @@ public class AirspyTuner extends Tuner { mLog.error( "error getting serial number", e ); } - + return BoardID.AIRSPY.getLabel(); - } + } @Override public TunerClass getTunerClass() @@ -78,50 +70,4 @@ public class AirspyTuner extends Tuner { return 13.0; } - - @Override - public TunerChannelSource getChannel( TunerChannel channel ) - throws RejectedExecutionException, SourceException - { - //TODO: this channel has a decimated sample rate of: - // 10.0 MSps = 10,000,000 / 208 = 48076.923 - // 5.0 MSps = 5,000,000 / 104 = 48076.923 - // 2.5 MSps = 2,500,000 / 52 = 48076.923 - //Consider implementing a fractional resampler to get a correct 48 kHz - //output sample rate - - TunerChannelSource source = getController().getChannel( this, channel ); - - if( source != null ) - { - broadcast( new TunerEvent( this, TunerEvent.Event.CHANNEL_COUNT ) ); - } - - return source; - } - - @Override - public void releaseChannel( TunerChannelSource source ) - { - /* Unregister for receiving samples */ - removeListener( (Listener)source ); - - /* Tell the controller to release the channel and cleanup */ - if( source != null ) - { - getController().releaseChannel( source ); - } - } - - @Override - public void addListener( Listener listener ) - { - getController().addListener( listener ); - } - - @Override - public void removeListener( Listener listener ) - { - getController().removeListener( listener ); - } -} +} \ No newline at end of file diff --git a/src/main/java/io/github/dsheirer/source/tuner/airspy/AirspyTunerController.java b/src/main/java/io/github/dsheirer/source/tuner/airspy/AirspyTunerController.java index be1163d6..8ea4ad65 100644 --- a/src/main/java/io/github/dsheirer/source/tuner/airspy/AirspyTunerController.java +++ b/src/main/java/io/github/dsheirer/source/tuner/airspy/AirspyTunerController.java @@ -1,11 +1,9 @@ package io.github.dsheirer.source.tuner.airspy; -import io.github.dsheirer.sample.Listener; -import io.github.dsheirer.sample.complex.ComplexBuffer; import io.github.dsheirer.source.SourceException; -import io.github.dsheirer.source.tuner.TunerController; import io.github.dsheirer.source.tuner.configuration.TunerConfiguration; import io.github.dsheirer.source.tuner.usb.USBTransferProcessor; +import io.github.dsheirer.source.tuner.usb.USBTunerController; import org.apache.commons.io.EndianUtils; import org.slf4j.Logger; import org.slf4j.LoggerFactory; @@ -62,7 +60,7 @@ import java.util.List; * POSSIBILITY OF SUCH DAMAGE. */ -public class AirspyTunerController extends TunerController +public class AirspyTunerController extends USBTunerController { public static final Gain LINEARITY_GAIN_DEFAULT = Gain.LINEARITY_14; public static final Gain SENSITIVITY_GAIN_DEFAULT = Gain.SENSITIVITY_10; @@ -183,6 +181,14 @@ public class AirspyTunerController extends TunerController USB_TRANSFER_BUFFER_SIZE); } + + @Override + protected USBTransferProcessor getUSBTransferProcessor() + { + return mUSBTransferProcessor; + } + + /** * Claims the USB interface. If another application currently has * the interface claimed, the USB_FORCE_CLAIM_INTERFACE setting @@ -368,7 +374,7 @@ public class AirspyTunerController extends TunerController } @Override - public int getCurrentSampleRate() throws SourceException + public double getCurrentSampleRate() { return mSampleRate; } @@ -456,7 +462,7 @@ public class AirspyTunerController extends TunerController throw new UsbException("Couldnt set sample packing enabled: " + enabled); } - /* If we didn't throw an exception above, then update the sample adapter + /* If we didn't throw an exception above, then update the sample adapter * to process samples accordingly */ mSampleAdapter.setSamplePacking(enabled); } @@ -671,7 +677,7 @@ public class AirspyTunerController extends TunerController mDeviceInfo = new AirspyDeviceInformation(); } - /* Board ID */ + /* Board ID */ try { int boardID = readByte(Command.BOARD_ID_READ, 0, 0, true); @@ -683,7 +689,7 @@ public class AirspyTunerController extends TunerController mLog.error("Error reading airspy board ID", e); } - /* Version String */ + /* Version String */ try { //NOTE: libairspy is internally reading 127 bytes, however airspy_info @@ -696,8 +702,8 @@ public class AirspyTunerController extends TunerController { mLog.error("Error reading airspy version string", e); } - - /* Part ID and Serial Number */ + + /* Part ID and Serial Number */ try { //Read 6 x 32-bit integers = 24 bytes @@ -1183,34 +1189,4 @@ public class AirspyTunerController extends TunerController return mValue; } } - - /** - * Adds the IQ buffer listener and automatically starts buffer transfer processing, if not already started. - */ - public void addListener(Listener listener) - { - if(mUSBTransferProcessor != null) - { - mUSBTransferProcessor.addListener(listener); - } - else - { - mLog.error("Couldn't add IQ buffer listener to Airspy tuner - processor is null"); - } - } - - /** - * Removes the IQ buffer listener and stops buffer transfer processing if there are no more listeners. - */ - public void removeListener(Listener listener) - { - if(mUSBTransferProcessor != null) - { - mUSBTransferProcessor.removeListener(listener); - } - else - { - mLog.error("Couldn't remove IQ buffer listener from Airspy tuner - processor is null"); - } - } -} +} \ No newline at end of file diff --git a/src/main/java/io/github/dsheirer/source/tuner/airspy/AirspyTunerEditor.java b/src/main/java/io/github/dsheirer/source/tuner/airspy/AirspyTunerEditor.java index 922f98f0..2d47f9c2 100644 --- a/src/main/java/io/github/dsheirer/source/tuner/airspy/AirspyTunerEditor.java +++ b/src/main/java/io/github/dsheirer/source/tuner/airspy/AirspyTunerEditor.java @@ -29,7 +29,19 @@ import org.slf4j.Logger; import org.slf4j.LoggerFactory; import org.usb4java.LibUsbException; -import javax.swing.*; +import javax.swing.DefaultComboBoxModel; +import javax.swing.JButton; +import javax.swing.JCheckBox; +import javax.swing.JComboBox; +import javax.swing.JLabel; +import javax.swing.JOptionPane; +import javax.swing.JSeparator; +import javax.swing.JSlider; +import javax.swing.JSpinner; +import javax.swing.JTextField; +import javax.swing.SpinnerModel; +import javax.swing.SpinnerNumberModel; +import javax.swing.SwingConstants; import javax.swing.event.ChangeEvent; import javax.swing.event.ChangeListener; import javax.usb.UsbException; @@ -42,43 +54,43 @@ import java.util.List; public class AirspyTunerEditor extends TunerConfigurationEditor { - private static final long serialVersionUID = 1L; + private static final long serialVersionUID = 1L; - private final static Logger mLog = - LoggerFactory.getLogger( AirspyTunerEditor.class ); + private final static Logger mLog = + LoggerFactory.getLogger( AirspyTunerEditor.class ); - private JTextField mConfigurationName; - private JButton mTunerInfo; + private JTextField mConfigurationName; + private JButton mTunerInfo; - private JComboBox mSampleRateCombo; - private JSpinner mFrequencyCorrection; + private JComboBox mSampleRateCombo; + private JSpinner mFrequencyCorrection; - private JComboBox mGainModeCombo; + private JComboBox mGainModeCombo; - private JSlider mMasterGain; - private JLabel mMasterGainValueLabel; + private JSlider mMasterGain; + private JLabel mMasterGainValueLabel; - private JSlider mIFGain; - private JLabel mIFGainValueLabel; + private JSlider mIFGain; + private JLabel mIFGainValueLabel; - private JSlider mLNAGain; - private JLabel mLNAGainValueLabel; + private JSlider mLNAGain; + private JLabel mLNAGainValueLabel; - private JSlider mMixerGain; - private JLabel mMixerGainValueLabel; + private JSlider mMixerGain; + private JLabel mMixerGainValueLabel; - private JCheckBox mLNAAGC; - private JCheckBox mMixerAGC; + private JCheckBox mLNAAGC; + private JCheckBox mMixerAGC; - private AirspyTunerController mController; - private boolean mLoading; + private AirspyTunerController mController; + private boolean mLoading; - public AirspyTunerEditor( TunerConfigurationModel tunerConfigurationModel, - AirspyTuner tuner ) + public AirspyTunerEditor( TunerConfigurationModel tunerConfigurationModel, + AirspyTuner tuner ) { super( tunerConfigurationModel ); mController = tuner.getController(); - + init(); } @@ -88,33 +100,33 @@ public class AirspyTunerEditor extends TunerConfigurationEditor { return (AirspyTunerConfiguration)getItem(); } - + return null; } - private void init() - { - setLayout( new MigLayout( "fill,wrap 4", "[right][grow,fill][right][grow,fill]", - "[][][][][][grow]" ) ); - + private void init() + { + setLayout( new MigLayout( "fill,wrap 4", "[right][grow,fill][right][grow,fill]", + "[][][][][][grow]" ) ); + add( new JLabel( "Airspy Tuner Configuration" ), "span,align center" ); - + mConfigurationName = new JTextField(); mConfigurationName.setEnabled( false ); - mConfigurationName.addFocusListener( new FocusListener() + mConfigurationName.addFocusListener( new FocusListener() { @Override - public void focusLost( FocusEvent e ) - { + public void focusLost( FocusEvent e ) + { save(); - } + } @Override - public void focusGained( FocusEvent e ) {} + public void focusGained( FocusEvent e ) {} } ); - + add( new JLabel( "Name:" ) ); add( mConfigurationName, "span 2" ); - + mTunerInfo = new JButton( "Tuner Info" ); mTunerInfo.setEnabled( false ); mTunerInfo.addActionListener( new ActionListener() @@ -122,22 +134,22 @@ public class AirspyTunerEditor extends TunerConfigurationEditor @Override public void actionPerformed( ActionEvent e ) { - JOptionPane.showMessageDialog( AirspyTunerEditor.this, getTunerInfo(), - "Tuner Info", JOptionPane.INFORMATION_MESSAGE ); + JOptionPane.showMessageDialog( AirspyTunerEditor.this, getTunerInfo(), + "Tuner Info", JOptionPane.INFORMATION_MESSAGE ); } } ); add( mTunerInfo ); - + /** * Sample Rate */ add( new JLabel( "Sample Rate:" ) ); - + List rates = mController.getSampleRates(); - - mSampleRateCombo = new JComboBox( - new DefaultComboBoxModel( rates.toArray( - new AirspySampleRate[ rates.size() ] ) ) ); + + mSampleRateCombo = new JComboBox( + new DefaultComboBoxModel( rates.toArray( + new AirspySampleRate[ rates.size() ] ) ) ); mSampleRateCombo.setEnabled( false ); mSampleRateCombo.addActionListener( new ActionListener() { @@ -150,62 +162,62 @@ public class AirspyTunerEditor extends TunerConfigurationEditor { mController.setSampleRate( rate ); save(); - } + } catch ( LibUsbException | UsbException | SourceException e1 ) { - JOptionPane.showMessageDialog( AirspyTunerEditor.this, + JOptionPane.showMessageDialog( AirspyTunerEditor.this, "Couldn't set sample rate to " + rate.getLabel() ); - + mLog.error( "Error setting airspy sample rate", e1 ); - } + } } } ); - + add( mSampleRateCombo ); - /** - * Frequency Correction - */ - SpinnerModel model = - new SpinnerNumberModel( 0.0, //initial value - -1000.0, //min - 1000.0, //max - 0.1 ); //step + /** + * Frequency Correction + */ + SpinnerModel model = + new SpinnerNumberModel( 0.0, //initial value + -1000.0, //min + 1000.0, //max + 0.1 ); //step - mFrequencyCorrection = new JSpinner( model ); - mFrequencyCorrection.setEnabled( false ); + mFrequencyCorrection = new JSpinner( model ); + mFrequencyCorrection.setEnabled( false ); - JSpinner.NumberEditor editor = - (JSpinner.NumberEditor)mFrequencyCorrection.getEditor(); - - DecimalFormat format = editor.getFormat(); - format.setMinimumFractionDigits( 1 ); - editor.getTextField().setHorizontalAlignment( SwingConstants.CENTER ); + JSpinner.NumberEditor editor = + (JSpinner.NumberEditor)mFrequencyCorrection.getEditor(); - mFrequencyCorrection.addChangeListener( new ChangeListener() - { + DecimalFormat format = editor.getFormat(); + format.setMinimumFractionDigits( 1 ); + editor.getTextField().setHorizontalAlignment( SwingConstants.CENTER ); + + mFrequencyCorrection.addChangeListener( new ChangeListener() + { @Override - public void stateChanged( ChangeEvent e ) - { + public void stateChanged( ChangeEvent e ) + { final double value = ((SpinnerNumberModel)mFrequencyCorrection - .getModel()).getNumber().doubleValue(); - - try + .getModel()).getNumber().doubleValue(); + + try { mController.setFrequencyCorrection( value ); - } - catch ( SourceException e1 ) + } + catch ( SourceException e1 ) { mLog.error( "Error setting frequency correction value", e1 ); } - - save(); - } - } ); - - add( new JLabel( "PPM:" ) ); - add( mFrequencyCorrection ); - + + save(); + } + } ); + + add( new JLabel( "PPM:" ) ); + add( mFrequencyCorrection ); + add( new JSeparator(), "span,growx,push" ); /** @@ -222,11 +234,11 @@ public class AirspyTunerEditor extends TunerConfigurationEditor GainMode mode = (GainMode)mGainModeCombo.getSelectedItem(); updateGainComponents( mode ); - + if( hasItem() ) { AirspyTunerConfiguration airspy = getConfiguration(); - + Gain gain = airspy.getGain(); if( mode == GainMode.CUSTOM ) @@ -247,25 +259,25 @@ public class AirspyTunerEditor extends TunerConfigurationEditor mLNAGain.setValue( gain.getLNA() ); } } - + save(); } } ); - + add( mGainModeCombo, "wrap" ); /** * Gain */ add( new JLabel( "Master:" ) ); - - mMasterGain = new JSlider( JSlider.HORIZONTAL, - AirspyTunerController.GAIN_MIN, - AirspyTunerController.GAIN_MAX, - AirspyTunerController.GAIN_MIN ); + + mMasterGain = new JSlider( JSlider.HORIZONTAL, + AirspyTunerController.GAIN_MIN, + AirspyTunerController.GAIN_MAX, + AirspyTunerController.GAIN_MIN ); mMasterGain.setMajorTickSpacing( 1 ); mMasterGain.setPaintTicks( true ); - + mMasterGain.addChangeListener( new ChangeListener() { @Override @@ -280,31 +292,31 @@ public class AirspyTunerEditor extends TunerConfigurationEditor mController.setGain( gain ); save(); mMasterGainValueLabel.setText( String.valueOf( gain.getValue() ) ); - } + } catch ( Exception e ) { mLog.error( "Couldn't set airspy gain to:" + gain.name(), e ); - JOptionPane.showMessageDialog( mMasterGain, "Couldn't set gain value to " + - gain.getValue() ); + JOptionPane.showMessageDialog( mMasterGain, "Couldn't set gain value to " + + gain.getValue() ); } } } ); add( mMasterGain, "span 2" ); - + mMasterGainValueLabel = new JLabel( "0" ); add( mMasterGainValueLabel ); - + add( new JLabel( "IF:" ) ); - - mIFGain = new JSlider( JSlider.HORIZONTAL, - AirspyTunerController.IF_GAIN_MIN, - AirspyTunerController.IF_GAIN_MAX, - AirspyTunerController.IF_GAIN_MIN ); - + + mIFGain = new JSlider( JSlider.HORIZONTAL, + AirspyTunerController.IF_GAIN_MIN, + AirspyTunerController.IF_GAIN_MAX, + AirspyTunerController.IF_GAIN_MIN ); + mIFGain.setMajorTickSpacing( 1 ); mIFGain.setPaintTicks( true ); - + mIFGain.addChangeListener( new ChangeListener() { @Override @@ -317,34 +329,34 @@ public class AirspyTunerEditor extends TunerConfigurationEditor mController.setIFGain( gain ); save(); mIFGainValueLabel.setText( String.valueOf( gain ) ); - } + } catch ( Exception e ) { mLog.error( "Couldn't set airspy IF gain to:" + gain, e ); - + JOptionPane.showMessageDialog( mIFGain, "Couldn't set IF gain value to " + gain ); } } } ); add( mIFGain, "span 2" ); - + mIFGainValueLabel = new JLabel( "0" ); add( mIFGainValueLabel ); - + /** - * Mixer Gain + * Mixer Gain */ add( new JLabel( "Mixer:" ) ); - - mMixerGain = new JSlider( JSlider.HORIZONTAL, - AirspyTunerController.MIXER_GAIN_MIN, - AirspyTunerController.MIXER_GAIN_MAX, - AirspyTunerController.MIXER_GAIN_MIN ); - + + mMixerGain = new JSlider( JSlider.HORIZONTAL, + AirspyTunerController.MIXER_GAIN_MIN, + AirspyTunerController.MIXER_GAIN_MAX, + AirspyTunerController.MIXER_GAIN_MIN ); + mMixerGain.setMajorTickSpacing( 1 ); mMixerGain.setPaintTicks( true ); - + mMixerGain.addChangeListener( new ChangeListener() { @Override @@ -357,7 +369,7 @@ public class AirspyTunerEditor extends TunerConfigurationEditor mController.setMixerGain( gain ); save(); mMixerGainValueLabel.setText( String.valueOf( gain ) ); - } + } catch ( Exception e ) { mLog.error( "Couldn't set airspy Mixer gain to:" + gain, e ); @@ -370,17 +382,17 @@ public class AirspyTunerEditor extends TunerConfigurationEditor mMixerGainValueLabel = new JLabel( "0" ); add( mMixerGainValueLabel ); - + add( new JLabel( "LNA:" ) ); - - mLNAGain = new JSlider( JSlider.HORIZONTAL, - AirspyTunerController.LNA_GAIN_MIN, - AirspyTunerController.LNA_GAIN_MAX, - AirspyTunerController.LNA_GAIN_MIN ); - + + mLNAGain = new JSlider( JSlider.HORIZONTAL, + AirspyTunerController.LNA_GAIN_MIN, + AirspyTunerController.LNA_GAIN_MAX, + AirspyTunerController.LNA_GAIN_MIN ); + mLNAGain.setMajorTickSpacing( 1 ); mLNAGain.setPaintTicks( true ); - + mLNAGain.addChangeListener( new ChangeListener() { @Override @@ -393,7 +405,7 @@ public class AirspyTunerEditor extends TunerConfigurationEditor mController.setLNAGain( gain ); save(); mLNAGainValueLabel.setText( String.valueOf( gain ) ); - } + } catch ( Exception e ) { mLog.error( "Couldn't set airspy LNA gain to:" + gain, e ); @@ -406,7 +418,7 @@ public class AirspyTunerEditor extends TunerConfigurationEditor mLNAGainValueLabel = new JLabel( "0" ); add( mLNAGainValueLabel ); - + mMixerAGC = new JCheckBox( "Mixer AGC" ); mMixerAGC.addActionListener( new ActionListener() { @@ -417,16 +429,16 @@ public class AirspyTunerEditor extends TunerConfigurationEditor { mController.setMixerAGC( mMixerAGC.isSelected() ); mMixerGain.setEnabled( !mMixerAGC.isSelected() ); - + save(); - } + } catch ( Exception e1 ) { mLog.error( "Error setting Mixer AGC Enabled" ); } } } ); - + add( mMixerAGC, "span 2,center" ); mLNAAGC = new JCheckBox( "LNA AGC" ); @@ -441,26 +453,26 @@ public class AirspyTunerEditor extends TunerConfigurationEditor mLNAGain.setEnabled( !mLNAAGC.isSelected() ); save(); - } + } catch ( Exception e1 ) { mLog.error( "Error setting LNA AGC Enabled" ); } - + } } ); - + add( mLNAAGC, "span 2,center" ); - } - - /** - * Updates the enabled state of each of the gain controls according to the - * specified gain mode. The master gain control is enabled for linearity - * and sensitivity and the individual gain controls are disabled, and - * vice-versa for custom mode. - */ - private void updateGainComponents( GainMode mode ) - { + } + + /** + * Updates the enabled state of each of the gain controls according to the + * specified gain mode. The master gain control is enabled for linearity + * and sensitivity and the individual gain controls are disabled, and + * vice-versa for custom mode. + */ + private void updateGainComponents( GainMode mode ) + { switch( mode ) { case LINEARITY: @@ -471,20 +483,20 @@ public class AirspyTunerEditor extends TunerConfigurationEditor } if( mIFGain.isEnabled() ) { - mIFGain.setEnabled( false ); + mIFGain.setEnabled( false ); mIFGainValueLabel.setEnabled( false ); } if( mLNAGain.isEnabled() ) { mLNAAGC.setEnabled( false ); - mLNAGainValueLabel.setEnabled( false ); - mLNAGain.setEnabled( false ); + mLNAGainValueLabel.setEnabled( false ); + mLNAGain.setEnabled( false ); } if( mMixerGain.isEnabled() ) { - mMixerGainValueLabel.setEnabled( false ); - mMixerAGC.setEnabled( false ); - mMixerGain.setEnabled( false ); + mMixerGainValueLabel.setEnabled( false ); + mMixerAGC.setEnabled( false ); + mMixerGain.setEnabled( false ); } break; case SENSITIVITY: @@ -495,20 +507,20 @@ public class AirspyTunerEditor extends TunerConfigurationEditor } if( mIFGain.isEnabled() ) { - mIFGain.setEnabled( false ); + mIFGain.setEnabled( false ); mIFGainValueLabel.setEnabled( false ); } if( mLNAGain.isEnabled() ) { mLNAAGC.setEnabled( false ); - mLNAGainValueLabel.setEnabled( false ); - mLNAGain.setEnabled( false ); + mLNAGainValueLabel.setEnabled( false ); + mLNAGain.setEnabled( false ); } if( mMixerGain.isEnabled() ) { - mMixerGainValueLabel.setEnabled( false ); - mMixerAGC.setEnabled( false ); - mMixerGain.setEnabled( false ); + mMixerGainValueLabel.setEnabled( false ); + mMixerAGC.setEnabled( false ); + mMixerGain.setEnabled( false ); } break; case CUSTOM: @@ -519,36 +531,36 @@ public class AirspyTunerEditor extends TunerConfigurationEditor } if( !mIFGain.isEnabled() ) { - mIFGain.setEnabled( true ); + mIFGain.setEnabled( true ); mIFGainValueLabel.setEnabled( true ); } if( !mLNAGain.isEnabled() ) { mLNAAGC.setEnabled( true ); - mLNAGainValueLabel.setEnabled( true ); - mLNAGain.setEnabled( true ); + mLNAGainValueLabel.setEnabled( true ); + mLNAGain.setEnabled( true ); } if( !mMixerGain.isEnabled() ) { - mMixerGainValueLabel.setEnabled( true ); - mMixerAGC.setEnabled( true ); - mMixerGain.setEnabled( true ); + mMixerGainValueLabel.setEnabled( true ); + mMixerAGC.setEnabled( true ); + mMixerGain.setEnabled( true ); } default: break; } - } + } @Override public void save() { - if( hasItem() && !mLoading ) - { - AirspyTunerConfiguration config = getConfiguration(); - config.setName( mConfigurationName.getText() ); - config.setSampleRate( ((AirspySampleRate)mSampleRateCombo.getSelectedItem()).getRate() ); + if( hasItem() && !mLoading ) + { + AirspyTunerConfiguration config = getConfiguration(); + config.setName( mConfigurationName.getText() ); + config.setSampleRate( ((AirspySampleRate)mSampleRateCombo.getSelectedItem()).getRate() ); - double value = ((SpinnerNumberModel)mFrequencyCorrection.getModel()).getNumber().doubleValue(); + double value = ((SpinnerNumberModel)mFrequencyCorrection.getModel()).getNumber().doubleValue(); config.setFrequencyCorrection( value ); Gain gain = Gain.getGain( (GainMode)mGainModeCombo.getSelectedItem(), mMasterGain.getValue() ); @@ -559,15 +571,15 @@ public class AirspyTunerEditor extends TunerConfigurationEditor config.setLNAGain( mLNAGain.getValue() ); config.setMixerAGC( mMixerAGC.isSelected() ); config.setLNAAGC( mLNAAGC.isSelected() ); - + getTunerConfigurationModel().broadcast( new TunerConfigurationEvent( config, TunerConfigurationEvent.Event.CHANGE ) ); - } + } } - + private AirspySampleRate fromValue( int value ) { List rates = mController.getSampleRates(); - + for( AirspySampleRate rate: rates ) { if( rate.getRate() == value ) @@ -580,10 +592,10 @@ public class AirspyTunerEditor extends TunerConfigurationEditor { return rates.get( 0 ); } - + return null; } - + /** * Sets all controls to the argument enabled state */ @@ -593,52 +605,52 @@ public class AirspyTunerEditor extends TunerConfigurationEditor { mConfigurationName.setEnabled( enabled ); } - + if( mTunerInfo.isEnabled() != enabled ) { mTunerInfo.setEnabled( enabled ); } - + if( mSampleRateCombo.isEnabled() != enabled ) { mSampleRateCombo.setEnabled( enabled ); } - + if( mFrequencyCorrection.isEnabled() != enabled ) { mFrequencyCorrection.setEnabled( enabled ); } - + if( mGainModeCombo.isEnabled() != enabled ) { mGainModeCombo.setEnabled( enabled ); } - + if( mMasterGain.isEnabled() != enabled ) { mMasterGain.setEnabled( enabled ); } - + if( mIFGain.isEnabled() != enabled ) { mIFGain.setEnabled( enabled ); } - + if( mMixerGain.isEnabled() != enabled ) { mMixerGain.setEnabled( enabled ); } - + if( mMixerAGC.isEnabled() != enabled ) { mMixerAGC.setEnabled( enabled ); } - + if( mLNAGain.isEnabled() != enabled ) { mLNAGain.setEnabled( enabled ); } - + if( mLNAAGC.isEnabled() != enabled ) { mLNAAGC.setEnabled( enabled ); @@ -646,43 +658,43 @@ public class AirspyTunerEditor extends TunerConfigurationEditor } /** - * Applies the tuner configuration to the editor. If the configuration is assigned to the + * Applies the tuner configuration to the editor. If the configuration is assigned to the * currently selected tuner, then the settings are applied. */ @Override public void setItem( TunerConfiguration config ) { super.setItem( config ); - + mLoading = true; - + if( hasItem() && getItem().isAssigned() ) { AirspyTunerConfiguration airspy = getConfiguration(); - + setControlsEnabled( true ); - + mConfigurationName.setText( airspy.getName() ); AirspySampleRate rate = fromValue( airspy.getSampleRate() ); - + if( rate != null ) { mSampleRateCombo.setSelectedItem( rate ); } - + mFrequencyCorrection.setValue( airspy.getFrequencyCorrection() ); GainMode mode = Gain.getGainMode( airspy.getGain() ); //This will set the master, IF, Mixer, LNA and AGC controls mGainModeCombo.setSelectedItem( mode ); - + if( mode == GainMode.CUSTOM ) { mMasterGain.setValue( airspy.getGain().getValue() ); mIFGain.setValue( airspy.getIFGain() ); - + if( airspy.isMixerAGC() ) { mMixerAGC.setSelected( airspy.isMixerAGC() ); @@ -707,7 +719,7 @@ public class AirspyTunerEditor extends TunerConfigurationEditor { mMixerAGC.setSelected( false ); mLNAAGC.setSelected( false ); - + Gain gain = airspy.getGain(); mMasterGain.setValue( gain.getValue() ); @@ -721,16 +733,16 @@ public class AirspyTunerEditor extends TunerConfigurationEditor setControlsEnabled( false ); mConfigurationName.setText( hasItem() ? getItem().getName() : "" ); } - + mLoading = false; } - + private String getTunerInfo() { StringBuilder sb = new StringBuilder(); AirspyDeviceInformation info = mController.getDeviceInfo(); - + sb.append( "

Airspy Tuner

" ); sb.append( "Serial: " ); sb.append( info.getSerialNumber() ); @@ -740,15 +752,15 @@ public class AirspyTunerEditor extends TunerConfigurationEditor String[] firmware = info.getVersion().split( " " ); sb.append( firmware.length > 1 ? firmware[ 0 ] : info.getVersion() ); sb.append( "
" ); - + sb.append( "Part: " ); sb.append( info.getPartNumber() ); sb.append( "
" ); - + sb.append( "Board ID: " ); sb.append( info.getBoardID().getLabel() ); sb.append( "
" ); return sb.toString(); } -} +} \ No newline at end of file diff --git a/src/source/tuner/channel/cic/CICTunerChannelSource.java b/src/main/java/io/github/dsheirer/source/tuner/channel/CICTunerChannelSource.java similarity index 83% rename from src/source/tuner/channel/cic/CICTunerChannelSource.java rename to src/main/java/io/github/dsheirer/source/tuner/channel/CICTunerChannelSource.java index a5086ae2..0e3625bd 100644 --- a/src/source/tuner/channel/cic/CICTunerChannelSource.java +++ b/src/main/java/io/github/dsheirer/source/tuner/channel/CICTunerChannelSource.java @@ -1,43 +1,38 @@ /******************************************************************************* - * sdrtrunk + * sdr-trunk * 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 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 + * You should have received a copy of the GNU General Public License along with this program. + * If not, see * ******************************************************************************/ -package source.tuner.channel.cic; +package io.github.dsheirer.source.tuner.channel; -import dsp.filter.FilterFactory; -import dsp.filter.Window; -import dsp.filter.cic.ComplexPrimeCICDecimate; -import dsp.mixer.Oscillator; +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.Oscillator; +import io.github.dsheirer.sample.Buffer; +import io.github.dsheirer.sample.Listener; +import io.github.dsheirer.sample.OverflowableTransferQueue; +import io.github.dsheirer.sample.complex.Complex; +import io.github.dsheirer.sample.complex.ComplexBuffer; +import io.github.dsheirer.source.SourceEvent; +import io.github.dsheirer.source.tuner.Tuner; import org.slf4j.Logger; import org.slf4j.LoggerFactory; -import sample.Buffer; -import sample.Listener; -import sample.OverflowableTransferQueue; -import sample.complex.Complex; -import sample.complex.ComplexBuffer; -import source.SourceEvent; -import source.tuner.Tuner; -import source.tuner.channel.TunerChannel; -import source.tuner.channel.TunerChannelSource; import java.util.ArrayList; import java.util.List; -public class CICTunerChannelSource extends TunerChannelSource implements Listener +public class CICTunerChannelSource extends TunerChannelSource { private final static Logger mLog = LoggerFactory.getLogger(CICTunerChannelSource.class); @@ -79,13 +74,12 @@ public class CICTunerChannelSource extends TunerChannelSource implements Listene mBuffer.setSourceOverflowListener(this); //Setup the frequency translator to the current source frequency - mTunerFrequency = tuner.getTunerController().getFrequencyController().getFrequency(); + mTunerFrequency = tuner.getTunerController().getFrequency(); long frequencyOffset = mTunerFrequency - getTunerChannel().getFrequency(); - mFrequencyCorrectionMixer = new Oscillator(frequencyOffset, - tuner.getTunerController().getFrequencyController().getSampleRate()); + mFrequencyCorrectionMixer = new Oscillator(frequencyOffset, tuner.getTunerController().getSampleRate()); //Setup the decimation filter chain - setSampleRate(tuner.getTunerController().getFrequencyController().getSampleRate()); + setSampleRate(tuner.getTunerController().getSampleRate()); } /** @@ -138,8 +132,8 @@ public class CICTunerChannelSource extends TunerChannelSource implements Listene @Override public void setListener(Listener listener) { - /* Save a pointer to the listener so that if we have to change the - * decimation filter, we can re-add the listener */ + /* Save a pointer to the listener so that if we have to change the + * decimation filter, we can re-add the listener */ mListener = listener; mDecimationFilter.setListener(listener); @@ -225,4 +219,4 @@ public class CICTunerChannelSource extends TunerChannelSource implements Listene mSampleBuffers.clear(); } -} +} \ No newline at end of file diff --git a/src/source/tuner/channel/TunerChannel.java b/src/main/java/io/github/dsheirer/source/tuner/channel/TunerChannel.java similarity index 98% rename from src/source/tuner/channel/TunerChannel.java rename to src/main/java/io/github/dsheirer/source/tuner/channel/TunerChannel.java index 0fa851a0..1c2e5424 100644 --- a/src/source/tuner/channel/TunerChannel.java +++ b/src/main/java/io/github/dsheirer/source/tuner/channel/TunerChannel.java @@ -16,7 +16,7 @@ * along with this program. If not, see * ******************************************************************************/ -package source.tuner.channel; +package io.github.dsheirer.source.tuner.channel; public class TunerChannel implements Comparable { diff --git a/src/main/java/io/github/dsheirer/source/tuner/channel/TunerChannelProvider.java b/src/main/java/io/github/dsheirer/source/tuner/channel/TunerChannelProvider.java new file mode 100644 index 00000000..bc655d34 --- /dev/null +++ b/src/main/java/io/github/dsheirer/source/tuner/channel/TunerChannelProvider.java @@ -0,0 +1,35 @@ +/******************************************************************************* + * sdr-trunk + * 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 io.github.dsheirer.source.tuner.channel; + +import io.github.dsheirer.source.SourceException; + +public interface TunerChannelProvider +{ + /** + * Returns a tuner frequency channel source, tuned to the correct frequency + * + * @return - source for 48k sample rate + */ + public TunerChannelSource getChannel( TunerChannel channel ) throws SourceException; + + /** + * Releases the tuned channel resources + */ + public void releaseChannel( TunerChannelSource source ); + + +} diff --git a/src/source/tuner/channel/TunerChannelSource.java b/src/main/java/io/github/dsheirer/source/tuner/channel/TunerChannelSource.java similarity index 92% rename from src/source/tuner/channel/TunerChannelSource.java rename to src/main/java/io/github/dsheirer/source/tuner/channel/TunerChannelSource.java index fef2a0eb..96386b45 100644 --- a/src/source/tuner/channel/TunerChannelSource.java +++ b/src/main/java/io/github/dsheirer/source/tuner/channel/TunerChannelSource.java @@ -16,18 +16,18 @@ * along with this program. If not, see * ******************************************************************************/ -package source.tuner.channel; +package io.github.dsheirer.source.tuner.channel; +import io.github.dsheirer.sample.Listener; +import io.github.dsheirer.sample.complex.ComplexBuffer; +import io.github.dsheirer.source.ComplexSource; +import io.github.dsheirer.source.ISourceEventProcessor; +import io.github.dsheirer.source.SourceEvent; +import io.github.dsheirer.source.SourceEventListenerToProcessorAdapter; +import io.github.dsheirer.source.SourceException; +import io.github.dsheirer.util.ThreadPool; import org.slf4j.Logger; import org.slf4j.LoggerFactory; -import sample.Listener; -import sample.complex.ComplexBuffer; -import source.ComplexSource; -import source.ISourceEventProcessor; -import source.SourceEvent; -import source.SourceEventListenerToProcessorAdapter; -import source.SourceException; -import util.ThreadPool; import java.util.concurrent.ScheduledFuture; import java.util.concurrent.TimeUnit; @@ -41,7 +41,6 @@ public abstract class TunerChannelSource extends ComplexSource implements ISourc private Listener mProducerSourceEventListener; private Listener mConsumerSourceEventListener; private BufferProcessor mBufferProcessor = new BufferProcessor(); - private boolean mExpended = false; /** * Tuner Channel Source is a Digital Drop Channel (DDC) abstract class that defines the minimum functionality @@ -117,12 +116,6 @@ public abstract class TunerChannelSource extends ComplexSource implements ISourc */ public void start() { - if(mExpended) - { - throw new IllegalStateException("Cannot start an expended tuner channel source. You must request a new" + - " tuner channel source each time. Tuner channel sources are one-use only"); - } - //Broadcast current frequency and sample rate so consumer can configure correctly broadcastConsumerSourceEvent(SourceEvent.frequencyChange(getFrequency())); broadcastConsumerSourceEvent(SourceEvent.sampleRateChange(getSampleRate())); @@ -141,8 +134,6 @@ public abstract class TunerChannelSource extends ComplexSource implements ISourc */ public void stop() { - mExpended = true; - if(mProducerSourceEventListener != null) { mProducerSourceEventListener.receive(SourceEvent.stopSampleStream(this)); @@ -155,11 +146,7 @@ public abstract class TunerChannelSource extends ComplexSource implements ISourc @Override public void reset() { - if(mExpended) - { - throw new IllegalStateException("Cannot reset an expended tuner channel source. You must request a new" + - " tuner channel source each time. Tuner channel sources are one-use only"); - } + //Reset is not valid for a tuner channel source - ignored } /** diff --git a/src/main/java/io/github/dsheirer/source/tuner/configuration/TunerConfigurationModel.java b/src/main/java/io/github/dsheirer/source/tuner/configuration/TunerConfigurationModel.java index d29626c1..d12cd470 100644 --- a/src/main/java/io/github/dsheirer/source/tuner/configuration/TunerConfigurationModel.java +++ b/src/main/java/io/github/dsheirer/source/tuner/configuration/TunerConfigurationModel.java @@ -1,19 +1,17 @@ /******************************************************************************* - * SDR Trunk - * Copyright (C) 2014-2016 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 + * sdr-trunk + * 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 io.github.dsheirer.source.tuner.configuration; @@ -31,8 +29,7 @@ import java.util.List; public class TunerConfigurationModel extends AbstractTableModel { private static final long serialVersionUID = 1L; - private final static Logger mLog = - LoggerFactory.getLogger( TunerConfigurationModel.class ); + private final static Logger mLog = LoggerFactory.getLogger( TunerConfigurationModel.class ); public static final int TUNER_TYPE = 0; public static final int CONFIGURATION_NAME = 1; diff --git a/src/main/java/io/github/dsheirer/source/tuner/ettus/B100Tuner.java b/src/main/java/io/github/dsheirer/source/tuner/ettus/B100Tuner.java deleted file mode 100644 index 591b81b2..00000000 --- a/src/main/java/io/github/dsheirer/source/tuner/ettus/B100Tuner.java +++ /dev/null @@ -1,83 +0,0 @@ -/******************************************************************************* - * SDR Trunk - * Copyright (C) 2014 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 io.github.dsheirer.source.tuner.ettus; - -import io.github.dsheirer.source.SourceException; -import io.github.dsheirer.source.tuner.Tuner; -import io.github.dsheirer.source.tuner.TunerChannel; -import io.github.dsheirer.source.tuner.TunerChannelSource; -import io.github.dsheirer.source.tuner.TunerClass; -import io.github.dsheirer.source.tuner.TunerType; -import io.github.dsheirer.source.tuner.usb.USBTunerDevice; - -import java.util.concurrent.RejectedExecutionException; - -public class B100Tuner extends Tuner -{ - public B100Tuner( USBTunerDevice device ) - { - //TODO: this should extend a USBTuner class, so that we can push the - //complex sample listener and the frequency change listener up to that class - - super( "Ettus B100 with WBX Tuner", null ); - } - - public void dispose() - { - - } - - @Override - public TunerClass getTunerClass() - { - return TunerClass.ETTUS_USRP_B100; - } - - @Override - public TunerType getTunerType() - { - return TunerType.ETTUS_WBX; - } - - @Override - public double getSampleSize() - { - return 16.0; - } - - @Override - public TunerChannelSource getChannel( TunerChannel channel ) - throws RejectedExecutionException, SourceException - { - //TODO: pass this call to the tuner controller - return null; - } - - @Override - public void releaseChannel( TunerChannelSource source ) - { - //TODO: pass this call to the tuner controller - } - - @Override - public String getUniqueID() - { - // TODO Auto-generated method stub - return null; - } -} diff --git a/src/main/java/io/github/dsheirer/source/tuner/ettus/B100TunerEditorPanel.java b/src/main/java/io/github/dsheirer/source/tuner/ettus/B100TunerEditorPanel.java deleted file mode 100644 index be2238f0..00000000 --- a/src/main/java/io/github/dsheirer/source/tuner/ettus/B100TunerEditorPanel.java +++ /dev/null @@ -1,42 +0,0 @@ -/******************************************************************************* - * SDR Trunk - * Copyright (C) 2014 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 io.github.dsheirer.source.tuner.ettus; - -import io.github.dsheirer.settings.SettingsManager; -import net.miginfocom.swing.MigLayout; - -import javax.swing.*; - -public class B100TunerEditorPanel extends JPanel -{ - private static final long serialVersionUID = 1L; - - public B100TunerEditorPanel( B100Tuner tuner, SettingsManager settingsManager ) - { - init(); - } - - private void init() - { - setLayout( new MigLayout( "", "[grow,fill]", "[grow,fill]" ) ); - - setBorder( BorderFactory.createTitledBorder( "Ettus B100 Tuner" ) ); - - add( new JLabel( "Note: SDRTrunk can only recognize the B100 at this time. It cannot use this tuner") ); - } -} diff --git a/src/main/java/io/github/dsheirer/source/tuner/fcd/FCDTuner.java b/src/main/java/io/github/dsheirer/source/tuner/fcd/FCDTuner.java index 7425a6d2..954e72c0 100644 --- a/src/main/java/io/github/dsheirer/source/tuner/fcd/FCDTuner.java +++ b/src/main/java/io/github/dsheirer/source/tuner/fcd/FCDTuner.java @@ -1,51 +1,33 @@ /******************************************************************************* - * SDR Trunk - * Copyright (C) 2014-2016 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 + * sdr-trunk + * 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 io.github.dsheirer.source.tuner.fcd; -import io.github.dsheirer.sample.Listener; -import io.github.dsheirer.sample.adapter.ShortAdapter; -import io.github.dsheirer.sample.complex.ComplexBuffer; -import io.github.dsheirer.source.SourceException; -import io.github.dsheirer.source.tuner.MixerTuner; -import io.github.dsheirer.source.tuner.MixerTunerDataLine; -import io.github.dsheirer.source.tuner.TunerChannel; -import io.github.dsheirer.source.tuner.TunerChannelSource; +import io.github.dsheirer.source.tuner.Tuner; import io.github.dsheirer.source.tuner.TunerClass; -import io.github.dsheirer.source.tuner.TunerEvent; import io.github.dsheirer.source.tuner.TunerType; -import java.util.concurrent.RejectedExecutionException; - -public class FCDTuner extends MixerTuner +public class FCDTuner extends Tuner { - public FCDTuner( MixerTunerDataLine mixerTDL, - FCDTunerController controller ) + public FCDTuner(FCDTunerController controller) { - super( controller.getConfiguration().toString(), - controller, - mixerTDL, - new ShortAdapter() ); + super( controller.getConfiguration().toString(), controller); } public void dispose() { - //TODO: release the mixer tuner data line as well - getController().dispose(); } @@ -77,36 +59,4 @@ public class FCDTuner extends MixerTuner { return 16.0; } - - @Override - public TunerChannelSource getChannel( TunerChannel tunerChannel ) - throws RejectedExecutionException, SourceException - { - TunerChannelSource source = getController().getChannel( this, tunerChannel ); - - if( source != null ) - { - broadcast( new TunerEvent( this, TunerEvent.Event.CHANNEL_COUNT ) ); - } - - return source; - } - - /** - * Releases the tuned channel so that the tuner controller can tune to - * other frequencies as needed. - */ - @Override - public void releaseChannel( TunerChannelSource source ) - { - if( source != null ) - { - /* Unregister for receiving samples */ - removeListener( (Listener)source ); - - /* Tell the controller to release the channel and cleanup */ - /* This will release the channel as a frequency change listener */ - getController().releaseChannel( source ); - } - } } diff --git a/src/main/java/io/github/dsheirer/source/tuner/fcd/FCDTunerController.java b/src/main/java/io/github/dsheirer/source/tuner/fcd/FCDTunerController.java index 842335b6..db6cf1bd 100644 --- a/src/main/java/io/github/dsheirer/source/tuner/fcd/FCDTunerController.java +++ b/src/main/java/io/github/dsheirer/source/tuner/fcd/FCDTunerController.java @@ -18,12 +18,16 @@ ******************************************************************************/ package io.github.dsheirer.source.tuner.fcd; +import io.github.dsheirer.sample.Listener; +import io.github.dsheirer.sample.adapter.ShortAdapter; +import io.github.dsheirer.sample.complex.ComplexBuffer; import io.github.dsheirer.source.SourceException; +import io.github.dsheirer.source.mixer.ComplexMixer; +import io.github.dsheirer.source.tuner.MixerTunerDataLine; import io.github.dsheirer.source.tuner.TunerClass; import io.github.dsheirer.source.tuner.TunerController; import io.github.dsheirer.source.tuner.TunerType; import io.github.dsheirer.source.tuner.configuration.TunerConfiguration; -import io.github.dsheirer.source.tuner.fcd.proV1.FCD1TunerController; import org.slf4j.Logger; import org.slf4j.LoggerFactory; import org.usb4java.Device; @@ -32,6 +36,7 @@ import org.usb4java.DeviceHandle; import org.usb4java.LibUsb; import org.usb4java.LibUsbException; +import javax.sound.sampled.AudioFormat; import javax.usb.UsbClaimException; import javax.usb.UsbException; import java.nio.ByteBuffer; @@ -54,6 +59,8 @@ public abstract class FCDTunerController extends TunerController private DeviceHandle mDeviceHandle; private FCDConfiguration mConfiguration = new FCDConfiguration(); + protected ComplexMixer mComplexMixer; + /** * Generic FCD tuner controller - contains functionality common across both @@ -64,11 +71,8 @@ public abstract class FCDTunerController extends TunerController * @param minTunableFrequency * @param maxTunableFrequency */ - public FCDTunerController(Device device, - DeviceDescriptor descriptor, - int sampleRate, - int minTunableFrequency, - int maxTunableFrequency) + public FCDTunerController(MixerTunerDataLine mixerTDL, Device device, DeviceDescriptor descriptor, String tunerName, + int minTunableFrequency, int maxTunableFrequency, AudioFormat audioFormat) { super(minTunableFrequency, maxTunableFrequency, DC_SPIKE_AVOID_BUFFER, USABLE_BANDWIDTH_PERCENT); mDevice = device; @@ -76,11 +80,45 @@ public abstract class FCDTunerController extends TunerController try { - mFrequencyController.setSampleRate(sampleRate); + mFrequencyController.setSampleRate((int)audioFormat.getSampleRate()); } catch(SourceException se) { - mLog.error("Error setting sample rate to [" + sampleRate + "]", se); + mLog.error("Error setting sample rate to [" + audioFormat.getSampleRate() + "]", se); + } + + mComplexMixer = new ComplexMixer( mixerTDL.getTargetDataLine(), audioFormat, tunerName, new ShortAdapter(), + mSampleBroadcaster); + } + + /** + * Overrides the super class functionality to auto-start the complex mixer and provide samples to listeners + */ + @Override + public void addComplexBufferListener(Listener listener) + { + boolean hasExistingListeners = hasComplexBufferListeners(); + + super.addComplexBufferListener(listener); + + if(!hasExistingListeners) + { + mComplexMixer.start(); + } + } + + /** + * Overrides the super class functionality to auto-stop the complex mixer and stop sample stream when there are no + * more registered listeners + */ + @Override + public void removeComplexBufferListener(Listener listener) + { + super.removeComplexBufferListener(listener); + + if(!hasComplexBufferListeners()) + { + mComplexMixer.stop(); } } @@ -135,7 +173,7 @@ public abstract class FCDTunerController extends TunerController /** * Sample rate of the tuner */ - public abstract int getCurrentSampleRate() throws SourceException; + public abstract double getCurrentSampleRate(); /** * Tuner class @@ -402,7 +440,7 @@ public abstract class FCDTunerController extends TunerController { ByteBuffer buffer = ByteBuffer.allocateDirect(64); - /* The FCD expects little-endian formatted values */ + /* The FCD expects little-endian formatted values */ buffer.order(ByteOrder.LITTLE_ENDIAN); buffer.put(0, command.getCommand()); @@ -538,14 +576,14 @@ public abstract class FCDTunerController extends TunerController return retVal; } - public FCD1TunerController.Block getBandBlocking() + public Block getBandBlocking() { - FCD1TunerController.Block retVal = FCD1TunerController.Block.UNKNOWN; + Block retVal = Block.UNKNOWN; switch(mMode) { case APPLICATION: - retVal = FCD1TunerController.Block.getBlock(mConfig.substring(23, 33).trim()); + retVal = Block.getBlock(mConfig.substring(23, 33).trim()); break; case BOOTLOADER: case UNKNOWN: @@ -615,4 +653,39 @@ public abstract class FCDTunerController extends TunerController return retVal; } } + + public enum Block + { + CELLULAR_BAND_BLOCKED("Blocked"), + NO_BAND_BLOCK("Unblocked"), + UNKNOWN("Unknown"); + + private String mLabel; + + private Block(String label) + { + mLabel = label; + } + + public String getLabel() + { + return mLabel; + } + + public static Block getBlock(String block) + { + Block retVal = UNKNOWN; + + if(block.equalsIgnoreCase("No blk")) + { + retVal = NO_BAND_BLOCK; + } + else if(block.equalsIgnoreCase("Cell blk")) + { + retVal = CELLULAR_BAND_BLOCKED; + } + + return retVal; + } + } } diff --git a/src/main/java/io/github/dsheirer/source/tuner/fcd/proV1/FCD1TunerController.java b/src/main/java/io/github/dsheirer/source/tuner/fcd/proV1/FCD1TunerController.java index 946c8d43..3049014a 100644 --- a/src/main/java/io/github/dsheirer/source/tuner/fcd/proV1/FCD1TunerController.java +++ b/src/main/java/io/github/dsheirer/source/tuner/fcd/proV1/FCD1TunerController.java @@ -18,6 +18,7 @@ package io.github.dsheirer.source.tuner.fcd.proV1; import io.github.dsheirer.source.SourceException; +import io.github.dsheirer.source.tuner.MixerTunerDataLine; import io.github.dsheirer.source.tuner.MixerTunerType; import io.github.dsheirer.source.tuner.TunerClass; import io.github.dsheirer.source.tuner.TunerType; @@ -36,682 +37,681 @@ import java.nio.ByteOrder; public class FCD1TunerController extends FCDTunerController { - private final static Logger mLog = - LoggerFactory.getLogger( FCD1TunerController.class ); + private final static Logger mLog = LoggerFactory.getLogger(FCD1TunerController.class); - public static final int sMINIMUM_TUNABLE_FREQUENCY = 64000000; - public static final int sMAXIMUM_TUNABLE_FREQUENCY = 1700000000; - public static final int sSAMPLE_RATE = 96000; - - private double mDCCorrectionInPhase = 0.0; - private double mDCCorrectionQuadrature = 0.0; - private double mPhaseCorrection = 0.0; - private double mGainCorrection = 0.0; - private LNAGain mLNAGain; - private LNAEnhance mLNAEnhance; - private MixerGain mMixerGain; - private FCD1TunerConfiguration mTunerConfiguration; - private FCD1TunerEditor mEditor; + public static final int MINIMUM_TUNABLE_FREQUENCY = 64000000; + public static final int MAXIMUM_TUNABLE_FREQUENCY = 1700000000; + public static final int SAMPLE_RATE = 96000; - public FCD1TunerController( Device device, DeviceDescriptor descriptor ) - { - super( device, - descriptor, - (int)MixerTunerType.FUNCUBE_DONGLE_PRO.getAudioFormat().getSampleRate(), - sMINIMUM_TUNABLE_FREQUENCY, - sMAXIMUM_TUNABLE_FREQUENCY ); - } - - public void init() throws SourceException - { - super.init(); - - mFrequencyController.setSampleRate( sSAMPLE_RATE ); - - try - { - setFCDMode( Mode.APPLICATION ); - getPhaseAndGainCorrection(); - getDCIQCorrection(); - getLNAGainSetting(); - getLNAEnhanceSetting(); - getMixerGainSetting(); - - send( FCDCommand.APP_SET_MIXER_GAIN, 1l ); - } - catch( Exception e ) - { - e.printStackTrace(); - - throw new SourceException( "FCDTunerController error " + - "during construction", e ); - } - } - - public int getCurrentSampleRate() - { - return sSAMPLE_RATE; - } + private double mDCCorrectionInPhase = 0.0; + private double mDCCorrectionQuadrature = 0.0; + private double mPhaseCorrection = 0.0; + private double mGainCorrection = 0.0; + private LNAGain mLNAGain; + private LNAEnhance mLNAEnhance; + private MixerGain mMixerGain; + private FCD1TunerConfiguration mTunerConfiguration; + private FCD1TunerEditor mEditor; - @Override + public FCD1TunerController(MixerTunerDataLine mixerTDL, Device device, DeviceDescriptor descriptor) + { + super(mixerTDL, device, descriptor, MixerTunerType.FUNCUBE_DONGLE_PRO.getDisplayString(), MINIMUM_TUNABLE_FREQUENCY, + MAXIMUM_TUNABLE_FREQUENCY, MixerTunerType.FUNCUBE_DONGLE_PRO.getAudioFormat()); + } + + public void init() throws SourceException + { + super.init(); + + mFrequencyController.setSampleRate(SAMPLE_RATE); + + try + { + setFCDMode(Mode.APPLICATION); + getPhaseAndGainCorrection(); + getDCIQCorrection(); + getLNAGainSetting(); + getLNAEnhanceSetting(); + getMixerGainSetting(); + + send(FCDCommand.APP_SET_MIXER_GAIN, 1l); + } + catch(Exception e) + { + e.printStackTrace(); + + throw new SourceException("FCDTunerController error " + + "during construction", e); + } + } + + public double getCurrentSampleRate() + { + return SAMPLE_RATE; + } + + @Override public TunerClass getTunerClass() { - return TunerClass.FUNCUBE_DONGLE_PRO; + return TunerClass.FUNCUBE_DONGLE_PRO; } - @Override + @Override public TunerType getTunerType() { - return TunerType.FUNCUBE_DONGLE_PRO; + return TunerType.FUNCUBE_DONGLE_PRO; } - /** - * Get the tuner configuration - * - * @return current config or null if one hasn't been applied yet - */ - public FCD1TunerConfiguration getTunerConfiguration() - { - return mTunerConfiguration; - } - - @Override - public void apply( TunerConfiguration tunerConfig ) - throws SourceException + /** + * Get the tuner configuration + * + * @return current config or null if one hasn't been applied yet + */ + public FCD1TunerConfiguration getTunerConfiguration() { - FCD1TunerConfiguration config = (FCD1TunerConfiguration)tunerConfig; - - try - { - setFrequencyCorrection( config.getFrequencyCorrection() ); - setLNAGain( config.getLNAGain() ); - setLNAEnhance( config.getLNAEnhance() ); - setMixerGain( config.getMixerGain() ); - setDCCorrectionInPhase( config.getInphaseDCCorrection() ); - setDCCorrectionQuadrature( config.getQuadratureDCCorrection() ); - setGainCorrection( config.getGainCorrection() ); - setPhaseCorrection( config.getPhaseCorrection() ); - - //If we get to here without exception, store this config - mTunerConfiguration = config; - } - catch( Exception e) - { - //Re-cast any usb exceptions as source exceptions - throw new SourceException( "FCD 1 Controller encountered an " - + "exception while applying tuner config - " + - e.getLocalizedMessage() ); - } - - try - { - setFrequency( config.getFrequency() ); - } - catch( SourceException se ) - { - //Do nothing, we couldn't set the frequency - } - } + return mTunerConfiguration; + } - /** - * Gets the current LNA Gain value from the controller and stores it - */ - private void getLNAGainSetting() throws UsbClaimException, UsbException - { - ByteBuffer buffer = send( FCDCommand.APP_GET_LNA_GAIN ); - - buffer.order( ByteOrder.LITTLE_ENDIAN ); - - int gain = buffer.getInt( 2 ); + @Override + public void apply(TunerConfiguration tunerConfig) + throws SourceException + { + FCD1TunerConfiguration config = (FCD1TunerConfiguration)tunerConfig; - mLNAGain = LNAGain.valueOf( gain ); - } - - /** - * @return - current lna gain setting - */ - public LNAGain getLNAGain() - { - return mLNAGain; - } + try + { + setFrequencyCorrection(config.getFrequencyCorrection()); + setLNAGain(config.getLNAGain()); + setLNAEnhance(config.getLNAEnhance()); + setMixerGain(config.getMixerGain()); + setDCCorrectionInPhase(config.getInphaseDCCorrection()); + setDCCorrectionQuadrature(config.getQuadratureDCCorrection()); + setGainCorrection(config.getGainCorrection()); + setPhaseCorrection(config.getPhaseCorrection()); - /** - * Sets lna gain for the controller - * @param gain - * @throws UsbException - * @throws UsbClaimException - */ - public void setLNAGain( LNAGain gain ) - throws UsbException, UsbClaimException - { - send( FCDCommand.APP_SET_LNA_GAIN, gain.getSetting() ); - } - - /** - * @return - current phase correction setting - */ - public double getPhaseCorrection() - { - return mPhaseCorrection; - } + //If we get to here without exception, store this config + mTunerConfiguration = config; + } + catch(Exception e) + { + //Re-cast any usb exceptions as source exceptions + throw new SourceException("FCD 1 Controller encountered an " + + "exception while applying tuner config - " + + e.getLocalizedMessage()); + } - /** - * Sets the phase correction - * @param value - new phase correction - * @throws IllegalArgumentException for value outside limits (0.0 <> 1.0) - * @throws UsbClaimException - if cannot claim the FCD HID controller - * @throws UsbException - if there was a usb error - */ - public void setPhaseCorrection( double value ) - throws UsbClaimException, UsbException - { - if( value < -1.0 || value > 1.0 ) - { - throw new IllegalArgumentException( "Phase correction value [" + - value + "] outside limits ( -1.0 <> 1.0 )" ); - } - - mPhaseCorrection = value; - setPhaseAndGainCorrection(); - } + try + { + setFrequency(config.getFrequency()); + } + catch(SourceException se) + { + //Do nothing, we couldn't set the frequency + } + } - /** - * @return current gain correction setting - */ - public double getGainCorrection() - { - return mGainCorrection; - } + /** + * Gets the current LNA Gain value from the controller and stores it + */ + private void getLNAGainSetting() throws UsbClaimException, UsbException + { + ByteBuffer buffer = send(FCDCommand.APP_GET_LNA_GAIN); - /** - * - * @param value - gain correction value - * @throws IllegalArgumentException for value outside limits (0.0 <> 1.0) - * @throws UsbClaimException - if cannot claim the FCD HID controller - * @throws UsbException - if there was a usb error - */ - public void setGainCorrection( double value ) - throws UsbClaimException, UsbException - { - if( value < -1.0 || value > 1.0 ) - { - throw new IllegalArgumentException( "Gain correction value [" + - value + "] outside limits ( -1.0 <> 1.0 )" ); - } - - mGainCorrection = value; - setPhaseAndGainCorrection(); - } - - /** - * Sends the phase and gain correction values to FCD. - * - * The interface expects an argument with the SIGNED phase value in the first - * 16 bits and the UNSIGNED gain value in the second 16 bits, both in - * big-endian format. Since we're sending the argument in java little endian - * format, we place the phase in the high-order bits and the gain in the - * low-order bits, so that the send(command) function will reorder the bytes - * into big endian format, and place the arguments correctly. - * - * Gain correction values range -1.0 to 0.0 to 1.0 and applied to the dongle - * as values 0 to 65534 - * - * Phase correction values range -1.0 to 0.0 to 1.0 and applied to the - * dongle as signed values -32768 to 32767 - */ - private void setPhaseAndGainCorrection() throws UsbClaimException, - UsbException - { - //UNSIGNED short gain value - masked into a long to avoid sign-extension - long gain = (long)( ( 1.0 + mGainCorrection ) * Short.MAX_VALUE ); - - //Left shift gain to place value in upper 16 bits - long longGain = Long.rotateLeft( ( 0xFFFF & gain ), 16 ); + buffer.order(ByteOrder.LITTLE_ENDIAN); - //SIGNED short phase value - short phase = (short)( mPhaseCorrection * Short.MAX_VALUE ); + int gain = buffer.getInt(2); - //Merge the results - long correction = longGain | phase; + mLNAGain = LNAGain.valueOf(gain); + } - send( FCDCommand.APP_SET_IQ_CORRECTION, correction ); - } + /** + * @return - current lna gain setting + */ + public LNAGain getLNAGain() + { + return mLNAGain; + } - /** - * Retrieves the stored phase and gain correction values from the FCD. - * - * Note: testing shows that when the dongle is unplugged, any stored phase - * and gain correction values are reset. Reading the dongle after a fresh - * plugin shows the values reset. - * - * @throws UsbClaimException - * @throws UsbException - */ - private void getPhaseAndGainCorrection() throws UsbClaimException, - UsbException - { - ByteBuffer buffer = send( FCDCommand.APP_GET_IQ_CORRECTION ); - - buffer.order( ByteOrder.LITTLE_ENDIAN ); - - int correction = buffer.getInt( 2 ); - - /** - * Gain: mask the upper 16 phase bits and right shift to get the - * unsigned short value and then divide by 32768 to get the double - * value. We place the unsigned short value in an int to avoid sign - * issues. - */ - int gain = (int)( Long.rotateRight( correction & 0xFFFF0000, 16 ) ); + /** + * Sets lna gain for the controller + * + * @param gain + * @throws UsbException + * @throws UsbClaimException + */ + public void setLNAGain(LNAGain gain) + throws UsbException, UsbClaimException + { + send(FCDCommand.APP_SET_LNA_GAIN, gain.getSetting()); + } - mGainCorrection = ( (double)gain / (double)Short.MAX_VALUE ) - 1.0; + /** + * @return - current phase correction setting + */ + public double getPhaseCorrection() + { + return mPhaseCorrection; + } - /** - * Phase: mask the lower 16 bits and divide by 32768 to get the double - * value - */ - short phase = (short)( correction & 0x0000FFFF ); - - mPhaseCorrection = ( (double)phase / (double)Short.MAX_VALUE ); - } - - /** - * @return current DC correction for the I (inphase) component - */ - public double getDCCorrectionInPhase() - { - return mDCCorrectionInPhase; - } + /** + * Sets the phase correction + * + * @param value - new phase correction + * @throws IllegalArgumentException for value outside limits (0.0 <> 1.0) + * @throws UsbClaimException - if cannot claim the FCD HID controller + * @throws UsbException - if there was a usb error + */ + public void setPhaseCorrection(double value) + throws UsbClaimException, UsbException + { + if(value < -1.0 || value > 1.0) + { + throw new IllegalArgumentException("Phase correction value [" + + value + "] outside limits ( -1.0 <> 1.0 )"); + } - /** - * Sets DC correction for the I (inphase) component and sends the new value - * to the FCD controller - * @param value - new DC correction value for the inphase component - * @throws IllegalArgumentException for values outside limits (-1.0 <> 1.0) - * @throws UsbClaimException - if cannot claim the FCD HID controller - * @throws UsbException - if there was a usb error - */ - public void setDCCorrectionInPhase( double value ) - throws UsbClaimException, UsbException - { - if( value < -1.0 || value > 1.0 ) - { - throw new IllegalArgumentException( "DC inphase correction value " - + "[" + value + "] outside limits ( 0.0 <> 1.0 )" ); - } - - mDCCorrectionInPhase = value; - setDCIQCorrection(); - } + mPhaseCorrection = value; + setPhaseAndGainCorrection(); + } - /** - * @return current DC correction for the Q (quadrature) component - */ - public double getDCCorrectionQuadrature() - { - return mDCCorrectionQuadrature; - } + /** + * @return current gain correction setting + */ + public double getGainCorrection() + { + return mGainCorrection; + } - /** - * Sets the DC correction for the Q (quadrature) component and send the - * new value to the FCD controller - * @param value - new DC correction value for the quadrature component - * @throws IllegalArgumentException for values outside limits (-1.0 <> 1.0) - * @throws UsbClaimException - if cannot claim the FCD HID controller - * @throws UsbException - if there was a usb error - */ - public void setDCCorrectionQuadrature( double value ) - throws UsbClaimException, UsbException - { - if( value < -1.0 || value > 1.0 ) - { - throw new IllegalArgumentException( "DC quadrature correction " - + "value [" + value + "] outside limits ( 0.0 <> 1.0 )" ); - } - - mDCCorrectionQuadrature = value; - setDCIQCorrection(); - } + /** + * @param value - gain correction value + * @throws IllegalArgumentException for value outside limits (0.0 <> 1.0) + * @throws UsbClaimException - if cannot claim the FCD HID controller + * @throws UsbException - if there was a usb error + */ + public void setGainCorrection(double value) + throws UsbClaimException, UsbException + { + if(value < -1.0 || value > 1.0) + { + throw new IllegalArgumentException("Gain correction value [" + + value + "] outside limits ( -1.0 <> 1.0 )"); + } - /** - * Sends the I/Q DC offset correction values to FCD. - * - * The interface expects an argument with the signed inphase value in the - * first 16 bits and the signed quadrature value in the second 16 bits, - * both in big-endian format. - * - * Since we're sending the argument in java little endian format, we place - * the quadrature in the high-order bits and the inphase in the low-order - * bits, so that the send(command) function will reorder the bytes into big - * endian format, and place the arguments correctly. - * - * Both Inphase and Quadrature correction values range -1.0 to 0.0 to 1.0 - * and are applied to the dongle as signed short values -32768 to 0 to 32767 - */ - private void setDCIQCorrection() throws UsbClaimException, UsbException - { - //I & Q DC offset correction values are signed short values - short inphase = (short)( mDCCorrectionInPhase * Short.MAX_VALUE ); - short quadrature = (short)( mDCCorrectionQuadrature * Short.MAX_VALUE ); + mGainCorrection = value; + setPhaseAndGainCorrection(); + } + + /** + * Sends the phase and gain correction values to FCD. + * + * The interface expects an argument with the SIGNED phase value in the first + * 16 bits and the UNSIGNED gain value in the second 16 bits, both in + * big-endian format. Since we're sending the argument in java little endian + * format, we place the phase in the high-order bits and the gain in the + * low-order bits, so that the send(command) function will reorder the bytes + * into big endian format, and place the arguments correctly. + * + * Gain correction values range -1.0 to 0.0 to 1.0 and applied to the dongle + * as values 0 to 65534 + * + * Phase correction values range -1.0 to 0.0 to 1.0 and applied to the + * dongle as signed values -32768 to 32767 + */ + private void setPhaseAndGainCorrection() throws UsbClaimException, + UsbException + { + //UNSIGNED short gain value - masked into a long to avoid sign-extension + long gain = (long)((1.0 + mGainCorrection) * Short.MAX_VALUE); + + //Left shift gain to place value in upper 16 bits + long longGain = Long.rotateLeft((0xFFFF & gain), 16); + + //SIGNED short phase value + short phase = (short)(mPhaseCorrection * Short.MAX_VALUE); + + //Merge the results + long correction = longGain | phase; + + send(FCDCommand.APP_SET_IQ_CORRECTION, correction); + } + + /** + * Retrieves the stored phase and gain correction values from the FCD. + * + * Note: testing shows that when the dongle is unplugged, any stored phase + * and gain correction values are reset. Reading the dongle after a fresh + * plugin shows the values reset. + * + * @throws UsbClaimException + * @throws UsbException + */ + private void getPhaseAndGainCorrection() throws UsbClaimException, + UsbException + { + ByteBuffer buffer = send(FCDCommand.APP_GET_IQ_CORRECTION); + + buffer.order(ByteOrder.LITTLE_ENDIAN); + + int correction = buffer.getInt(2); + + /** + * Gain: mask the upper 16 phase bits and right shift to get the + * unsigned short value and then divide by 32768 to get the double + * value. We place the unsigned short value in an int to avoid sign + * issues. + */ + int gain = (int)(Long.rotateRight(correction & 0xFFFF0000, 16)); + + mGainCorrection = ((double)gain / (double)Short.MAX_VALUE) - 1.0; + + /** + * Phase: mask the lower 16 bits and divide by 32768 to get the double + * value + */ + short phase = (short)(correction & 0x0000FFFF); + + mPhaseCorrection = ((double)phase / (double)Short.MAX_VALUE); + } + + /** + * @return current DC correction for the I (inphase) component + */ + public double getDCCorrectionInPhase() + { + return mDCCorrectionInPhase; + } + + /** + * Sets DC correction for the I (inphase) component and sends the new value + * to the FCD controller + * + * @param value - new DC correction value for the inphase component + * @throws IllegalArgumentException for values outside limits (-1.0 <> 1.0) + * @throws UsbClaimException - if cannot claim the FCD HID controller + * @throws UsbException - if there was a usb error + */ + public void setDCCorrectionInPhase(double value) + throws UsbClaimException, UsbException + { + if(value < -1.0 || value > 1.0) + { + throw new IllegalArgumentException("DC inphase correction value " + + "[" + value + "] outside limits ( 0.0 <> 1.0 )"); + } + + mDCCorrectionInPhase = value; + setDCIQCorrection(); + } + + /** + * @return current DC correction for the Q (quadrature) component + */ + public double getDCCorrectionQuadrature() + { + return mDCCorrectionQuadrature; + } + + /** + * Sets the DC correction for the Q (quadrature) component and send the + * new value to the FCD controller + * + * @param value - new DC correction value for the quadrature component + * @throws IllegalArgumentException for values outside limits (-1.0 <> 1.0) + * @throws UsbClaimException - if cannot claim the FCD HID controller + * @throws UsbException - if there was a usb error + */ + public void setDCCorrectionQuadrature(double value) + throws UsbClaimException, UsbException + { + if(value < -1.0 || value > 1.0) + { + throw new IllegalArgumentException("DC quadrature correction " + + "value [" + value + "] outside limits ( 0.0 <> 1.0 )"); + } + + mDCCorrectionQuadrature = value; + setDCIQCorrection(); + } + + /** + * Sends the I/Q DC offset correction values to FCD. + * + * The interface expects an argument with the signed inphase value in the + * first 16 bits and the signed quadrature value in the second 16 bits, + * both in big-endian format. + * + * Since we're sending the argument in java little endian format, we place + * the quadrature in the high-order bits and the inphase in the low-order + * bits, so that the send(command) function will reorder the bytes into big + * endian format, and place the arguments correctly. + * + * Both Inphase and Quadrature correction values range -1.0 to 0.0 to 1.0 + * and are applied to the dongle as signed short values -32768 to 0 to 32767 + */ + private void setDCIQCorrection() throws UsbClaimException, UsbException + { + //I & Q DC offset correction values are signed short values + short inphase = (short)(mDCCorrectionInPhase * Short.MAX_VALUE); + short quadrature = (short)(mDCCorrectionQuadrature * Short.MAX_VALUE); + + //Mask the shorts into longs to preserve the sign bit and prepare + //for merging to a single 32-bit value + long maskedInphase = inphase & 0x0000FFFF; + long maskedQuadrature = quadrature & 0x0000FFFF; + + //Left shift quadrature to place value in upper 16 bits + long shiftedQuadrature = Long.rotateLeft(maskedQuadrature, 16); + + //Merge the results + long correction = shiftedQuadrature | maskedInphase; - //Mask the shorts into longs to preserve the sign bit and prepare - //for merging to a single 32-bit value - long maskedInphase = inphase & 0x0000FFFF; - long maskedQuadrature = quadrature & 0x0000FFFF; - - //Left shift quadrature to place value in upper 16 bits - long shiftedQuadrature = Long.rotateLeft( maskedQuadrature, 16 ); - - //Merge the results - long correction = shiftedQuadrature | maskedInphase; - // Log.info( "FCD1: setting iq correction," + // " inphase:" + inphase + -// " masked inphase:" + maskedInphase + -// " quad:" + quadrature + -// " masked quad:" + maskedQuadrature + -// " shifted quadrature:" + shiftedQuadrature + +// " masked inphase:" + maskedInphase + +// " quad:" + quadrature + +// " masked quad:" + maskedQuadrature + +// " shifted quadrature:" + shiftedQuadrature + // " correction:" + correction ); - send( FCDCommand.APP_SET_DC_CORRECTION, correction ); - } - - private void getDCIQCorrection() throws UsbClaimException, - UsbException - { - ByteBuffer buffer = send( FCDCommand.APP_GET_DC_CORRECTION ); - - buffer.order( ByteOrder.LITTLE_ENDIAN ); - - int correction = buffer.getInt( 2 ); + send(FCDCommand.APP_SET_DC_CORRECTION, correction); + } - /** - * Quadrature: mask the upper 16 bits and divide by 32768 to get the - * stored quadrature value. Cast the 16-bit value to a short to get - * the signed short value - */ - long shiftedQuadrature = correction & 0xFFFF0000; - long quadrature = Long.rotateRight( shiftedQuadrature, 16 ); + private void getDCIQCorrection() throws UsbClaimException, + UsbException + { + ByteBuffer buffer = send(FCDCommand.APP_GET_DC_CORRECTION); - mDCCorrectionQuadrature = ( (short)quadrature / (double)Short.MAX_VALUE ); + buffer.order(ByteOrder.LITTLE_ENDIAN); + + int correction = buffer.getInt(2); + + /** + * Quadrature: mask the upper 16 bits and divide by 32768 to get the + * stored quadrature value. Cast the 16-bit value to a short to get + * the signed short value + */ + long shiftedQuadrature = correction & 0xFFFF0000; + long quadrature = Long.rotateRight(shiftedQuadrature, 16); + + mDCCorrectionQuadrature = ((short)quadrature / (double)Short.MAX_VALUE); + + /** + * InPhase: mask the lower 16 bits to get the short + * value and then divide by 32768 to get the stored inphase value. + * Cast the 16-bit value to a short to get the signed short value + */ + long inphase = correction & 0x0000FFFF; + mDCCorrectionInPhase = ((short)inphase / (double)Short.MAX_VALUE); - /** - * InPhase: mask the lower 16 bits to get the short - * value and then divide by 32768 to get the stored inphase value. - * Cast the 16-bit value to a short to get the signed short value - */ - long inphase = correction & 0x0000FFFF; - mDCCorrectionInPhase = ( (short)inphase / (double)Short.MAX_VALUE ); - // Log.info( "FCD1:" + -// " correction:" + correction + -// " Q:" + quadrature + -// " I:" + inphase + -// " QCorr:" + mDCCorrectionQuadrature + +// " correction:" + correction + +// " Q:" + quadrature + +// " I:" + inphase + +// " QCorr:" + mDCCorrectionQuadrature + // " ICorr:" + mDCCorrectionInPhase ); - } - - private void getLNAEnhanceSetting() throws UsbClaimException, UsbException - { - ByteBuffer buffer = send( FCDCommand.APP_GET_LNA_ENHANCE ); - - buffer.order( ByteOrder.LITTLE_ENDIAN ); - - int enhance = buffer.getInt( 2 ); - - mLNAEnhance = LNAEnhance.valueOf( enhance ); - } - - public LNAEnhance getLNAEnhance() - { - return mLNAEnhance; - } - - public void setLNAEnhance( LNAEnhance enhance ) - throws UsbClaimException, UsbException - { - send( FCDCommand.APP_SET_LNA_ENHANCE, enhance.getSetting() ); - } - - private void getMixerGainSetting() throws UsbClaimException, UsbException - { - ByteBuffer buffer = send( FCDCommand.APP_GET_MIXER_GAIN ); - - buffer.order( ByteOrder.LITTLE_ENDIAN ); - - int gain = buffer.getInt( 2 ); - - mMixerGain = MixerGain.valueOf( gain ); - } - - public MixerGain getMixerGain() - { - return mMixerGain; - } - - public void setMixerGain( MixerGain gain ) throws UsbClaimException, UsbException - { - send( FCDCommand.APP_SET_MIXER_GAIN, gain.getSetting() ); - } - - public enum Block - { - CELLULAR_BAND_BLOCKED( "Blocked" ), - NO_BAND_BLOCK( "Unblocked" ), - UNKNOWN( "Unknown" ); - - private String mLabel; - - private Block( String label ) - { - mLabel = label; - } - - public String getLabel() - { - return mLabel; - } - - public static Block getBlock( String block ) - { - Block retVal = UNKNOWN; - - if( block.equalsIgnoreCase( "No blk" ) ) - { - retVal = NO_BAND_BLOCK; - } - else if( block.equalsIgnoreCase( "Cell blk" ) ) - { - retVal = CELLULAR_BAND_BLOCKED; - } - - return retVal; - } - } - - /** - * LNA Gain values suppported by the FCD Pro 1.0 - */ - public enum LNAGain - { - LNA_GAIN_MINUS_5_0( 0, "-5.0 dB" ), - LNA_GAIN_MINUS_2_5( 1, "-2.5 dB" ), - LNA_GAIN_PLUS_0_0( 4, "0.0 dB" ), - LNA_GAIN_PLUS_2_5( 5, "2.5 dB" ), - LNA_GAIN_PLUS_5_0( 6, "5.0 dB" ), - LNA_GAIN_PLUS_7_5( 7, "7.5 dB" ), - LNA_GAIN_PLUS_10_0( 8, "10.0 dB" ), - LNA_GAIN_PLUS_12_5( 9, "12.5 dB" ), - LNA_GAIN_PLUS_15_0( 10, "15.0 dB" ), - LNA_GAIN_PLUS_17_5( 11, "17.5 dB" ), - LNA_GAIN_PLUS_20_0( 12, "20.0 dB" ), - LNA_GAIN_PLUS_25_0( 13, "25.0 dB" ), - LNA_GAIN_PLUS_30_0( 14, "30.0 dB" ); - - private int mSetting; - private String mLabel; - - private LNAGain( int setting, String label ) - { - mSetting = setting; - mLabel = label; - } - - public static LNAGain valueOf( int setting ) - { - switch( setting ) - { - case 0: - return LNA_GAIN_MINUS_5_0; - case 1: - return LNA_GAIN_MINUS_2_5; - case 4: - return LNA_GAIN_PLUS_0_0; - case 5: - return LNA_GAIN_PLUS_2_5; - case 6: - return LNA_GAIN_PLUS_5_0; - case 7: - return LNA_GAIN_PLUS_7_5; - case 8: - return LNA_GAIN_PLUS_10_0; - case 9: - return LNA_GAIN_PLUS_12_5; - case 10: - return LNA_GAIN_PLUS_15_0; - case 11: - return LNA_GAIN_PLUS_17_5; - case 12: - return LNA_GAIN_PLUS_20_0; - case 13: - return LNA_GAIN_PLUS_25_0; - case 14: - return LNA_GAIN_PLUS_30_0; - default: - throw new IllegalArgumentException( "FCD 1.0 Tuner " - + "Controller - unrecognized LNA gain setting " - + "value [" + setting + "]" ); - } - } - - public int getSetting() - { - return mSetting; - } - - public String getLabel() - { - return mLabel; - } - - public String toString() - { - return getLabel(); - } } - /** - * LNA Enhance values suppported by the FCD Pro 1.0 - */ - public enum LNAEnhance + private void getLNAEnhanceSetting() throws UsbClaimException, UsbException { - LNA_ENHANCE_OFF( 0, "Off" ), - LNA_ENHANCE_0( 1, "0" ), - LNA_ENHANCE_1( 3, "1" ), - LNA_ENHANCE_2( 5, "2" ), - LNA_ENHANCE_3( 7, "3" ); - - private int mSetting; - private String mLabel; - - private LNAEnhance( int setting, String label ) - { - mSetting = setting; - mLabel = label; - } - - public static LNAEnhance valueOf( int setting ) - { - switch( setting ) - { - case 0: - return LNA_ENHANCE_OFF; - case 1: - return LNA_ENHANCE_0; - case 3: - return LNA_ENHANCE_1; - case 5: - return LNA_ENHANCE_2; - case 7: - return LNA_ENHANCE_3; - default: - throw new IllegalArgumentException( "FCD 1.0 Tuner " - + "Controller - unrecognized LNA enhance setting " - + "value [" + setting + "]" ); - } - } - - public int getSetting() - { - return mSetting; - } - - public String getLabel() - { - return mLabel; - } - - public String toString() - { - return getLabel(); - } + ByteBuffer buffer = send(FCDCommand.APP_GET_LNA_ENHANCE); + + buffer.order(ByteOrder.LITTLE_ENDIAN); + + int enhance = buffer.getInt(2); + + mLNAEnhance = LNAEnhance.valueOf(enhance); } - /** - * Mixer Gain values suppported by the FCD Pro 1.0 - */ - public enum MixerGain + public LNAEnhance getLNAEnhance() { - MIXER_GAIN_PLUS_4_0( 0, "4.0 dB" ), - MIXER_GAIN_PLUS_12_0( 1, "12.0 dB" ); - - private int mSetting; - private String mLabel; - - private MixerGain( int setting, String label ) - { - mSetting = setting; - mLabel = label; - } - - public static MixerGain valueOf( int setting ) - { - switch( setting ) - { - case 0: - return MIXER_GAIN_PLUS_4_0; - case 1: - return MIXER_GAIN_PLUS_12_0; - default: - throw new IllegalArgumentException( "FCD 1.0 Tuner " - + "Controller - unrecognized mixer gain setting " - + "value [" + setting + "]" ); - } - } - - public int getSetting() - { - return mSetting; - } - - public String getLabel() - { - return mLabel; - } - - public String toString() - { - return getLabel(); - } + return mLNAEnhance; } -} + + public void setLNAEnhance(LNAEnhance enhance) + throws UsbClaimException, UsbException + { + send(FCDCommand.APP_SET_LNA_ENHANCE, enhance.getSetting()); + } + + private void getMixerGainSetting() throws UsbClaimException, UsbException + { + ByteBuffer buffer = send(FCDCommand.APP_GET_MIXER_GAIN); + + buffer.order(ByteOrder.LITTLE_ENDIAN); + + int gain = buffer.getInt(2); + + mMixerGain = MixerGain.valueOf(gain); + } + + public MixerGain getMixerGain() + { + return mMixerGain; + } + + public void setMixerGain(MixerGain gain) throws UsbClaimException, UsbException + { + send(FCDCommand.APP_SET_MIXER_GAIN, gain.getSetting()); + } + + public enum Block + { + CELLULAR_BAND_BLOCKED("Blocked"), + NO_BAND_BLOCK("Unblocked"), + UNKNOWN("Unknown"); + + private String mLabel; + + private Block(String label) + { + mLabel = label; + } + + public String getLabel() + { + return mLabel; + } + + public static Block getBlock(String block) + { + Block retVal = UNKNOWN; + + if(block.equalsIgnoreCase("No blk")) + { + retVal = NO_BAND_BLOCK; + } + else if(block.equalsIgnoreCase("Cell blk")) + { + retVal = CELLULAR_BAND_BLOCKED; + } + + return retVal; + } + } + + /** + * LNA Gain values suppported by the FCD Pro 1.0 + */ + public enum LNAGain + { + LNA_GAIN_MINUS_5_0(0, "-5.0 dB"), + LNA_GAIN_MINUS_2_5(1, "-2.5 dB"), + LNA_GAIN_PLUS_0_0(4, "0.0 dB"), + LNA_GAIN_PLUS_2_5(5, "2.5 dB"), + LNA_GAIN_PLUS_5_0(6, "5.0 dB"), + LNA_GAIN_PLUS_7_5(7, "7.5 dB"), + LNA_GAIN_PLUS_10_0(8, "10.0 dB"), + LNA_GAIN_PLUS_12_5(9, "12.5 dB"), + LNA_GAIN_PLUS_15_0(10, "15.0 dB"), + LNA_GAIN_PLUS_17_5(11, "17.5 dB"), + LNA_GAIN_PLUS_20_0(12, "20.0 dB"), + LNA_GAIN_PLUS_25_0(13, "25.0 dB"), + LNA_GAIN_PLUS_30_0(14, "30.0 dB"); + + private int mSetting; + private String mLabel; + + private LNAGain(int setting, String label) + { + mSetting = setting; + mLabel = label; + } + + public static LNAGain valueOf(int setting) + { + switch(setting) + { + case 0: + return LNA_GAIN_MINUS_5_0; + case 1: + return LNA_GAIN_MINUS_2_5; + case 4: + return LNA_GAIN_PLUS_0_0; + case 5: + return LNA_GAIN_PLUS_2_5; + case 6: + return LNA_GAIN_PLUS_5_0; + case 7: + return LNA_GAIN_PLUS_7_5; + case 8: + return LNA_GAIN_PLUS_10_0; + case 9: + return LNA_GAIN_PLUS_12_5; + case 10: + return LNA_GAIN_PLUS_15_0; + case 11: + return LNA_GAIN_PLUS_17_5; + case 12: + return LNA_GAIN_PLUS_20_0; + case 13: + return LNA_GAIN_PLUS_25_0; + case 14: + return LNA_GAIN_PLUS_30_0; + default: + throw new IllegalArgumentException("FCD 1.0 Tuner " + + "Controller - unrecognized LNA gain setting " + + "value [" + setting + "]"); + } + } + + public int getSetting() + { + return mSetting; + } + + public String getLabel() + { + return mLabel; + } + + public String toString() + { + return getLabel(); + } + } + + /** + * LNA Enhance values suppported by the FCD Pro 1.0 + */ + public enum LNAEnhance + { + LNA_ENHANCE_OFF(0, "Off"), + LNA_ENHANCE_0(1, "0"), + LNA_ENHANCE_1(3, "1"), + LNA_ENHANCE_2(5, "2"), + LNA_ENHANCE_3(7, "3"); + + private int mSetting; + private String mLabel; + + private LNAEnhance(int setting, String label) + { + mSetting = setting; + mLabel = label; + } + + public static LNAEnhance valueOf(int setting) + { + switch(setting) + { + case 0: + return LNA_ENHANCE_OFF; + case 1: + return LNA_ENHANCE_0; + case 3: + return LNA_ENHANCE_1; + case 5: + return LNA_ENHANCE_2; + case 7: + return LNA_ENHANCE_3; + default: + throw new IllegalArgumentException("FCD 1.0 Tuner " + + "Controller - unrecognized LNA enhance setting " + + "value [" + setting + "]"); + } + } + + public int getSetting() + { + return mSetting; + } + + public String getLabel() + { + return mLabel; + } + + public String toString() + { + return getLabel(); + } + } + + /** + * Mixer Gain values suppported by the FCD Pro 1.0 + */ + public enum MixerGain + { + MIXER_GAIN_PLUS_4_0(0, "4.0 dB"), + MIXER_GAIN_PLUS_12_0(1, "12.0 dB"); + + private int mSetting; + private String mLabel; + + private MixerGain(int setting, String label) + { + mSetting = setting; + mLabel = label; + } + + public static MixerGain valueOf(int setting) + { + switch(setting) + { + case 0: + return MIXER_GAIN_PLUS_4_0; + case 1: + return MIXER_GAIN_PLUS_12_0; + default: + throw new IllegalArgumentException("FCD 1.0 Tuner " + + "Controller - unrecognized mixer gain setting " + + "value [" + setting + "]"); + } + } + + public int getSetting() + { + return mSetting; + } + + public String getLabel() + { + return mLabel; + } + + public String toString() + { + return getLabel(); + } + } +} \ No newline at end of file diff --git a/src/main/java/io/github/dsheirer/source/tuner/fcd/proV1/FCD1TunerEditor.java b/src/main/java/io/github/dsheirer/source/tuner/fcd/proV1/FCD1TunerEditor.java index 1c785f29..eb8ac835 100644 --- a/src/main/java/io/github/dsheirer/source/tuner/fcd/proV1/FCD1TunerEditor.java +++ b/src/main/java/io/github/dsheirer/source/tuner/fcd/proV1/FCD1TunerEditor.java @@ -21,13 +21,26 @@ import io.github.dsheirer.source.SourceException; import io.github.dsheirer.source.tuner.configuration.TunerConfiguration; import io.github.dsheirer.source.tuner.configuration.TunerConfigurationEditor; import io.github.dsheirer.source.tuner.configuration.TunerConfigurationEvent; +import io.github.dsheirer.source.tuner.configuration.TunerConfigurationEvent.Event; import io.github.dsheirer.source.tuner.configuration.TunerConfigurationModel; import io.github.dsheirer.source.tuner.fcd.FCDTuner; +import io.github.dsheirer.source.tuner.fcd.proV1.FCD1TunerController.LNAEnhance; +import io.github.dsheirer.source.tuner.fcd.proV1.FCD1TunerController.LNAGain; +import io.github.dsheirer.source.tuner.fcd.proV1.FCD1TunerController.MixerGain; import net.miginfocom.swing.MigLayout; import org.slf4j.Logger; import org.slf4j.LoggerFactory; -import javax.swing.*; +import javax.swing.JButton; +import javax.swing.JComboBox; +import javax.swing.JLabel; +import javax.swing.JOptionPane; +import javax.swing.JSeparator; +import javax.swing.JSpinner; +import javax.swing.JTextField; +import javax.swing.SpinnerModel; +import javax.swing.SpinnerNumberModel; +import javax.swing.SwingConstants; import javax.swing.event.ChangeEvent; import javax.swing.event.ChangeListener; import java.awt.event.ActionEvent; @@ -38,30 +51,30 @@ import java.text.DecimalFormat; public class FCD1TunerEditor extends TunerConfigurationEditor { - private static final long serialVersionUID = 1L; + private static final long serialVersionUID = 1L; private final static Logger mLog = LoggerFactory.getLogger( FCD1TunerEditor.class ); - - private JTextField mConfigurationName; - private JButton mTunerInfo; - private JSpinner mFrequencyCorrection; - private JComboBox mComboLNAGain; - private JComboBox mComboLNAEnhance; - private JComboBox mComboMixerGain; - private CorrectionSpinner mCorrectionDCI; - private CorrectionSpinner mCorrectionDCQ; - private CorrectionSpinner mCorrectionGain; - private CorrectionSpinner mCorrectionPhase; - private boolean mLoading; - private FCD1TunerController mController; + private JTextField mConfigurationName; + private JButton mTunerInfo; + private JSpinner mFrequencyCorrection; + private JComboBox mComboLNAGain; + private JComboBox mComboLNAEnhance; + private JComboBox mComboMixerGain; + private CorrectionSpinner mCorrectionDCI; + private CorrectionSpinner mCorrectionDCQ; + private CorrectionSpinner mCorrectionGain; + private CorrectionSpinner mCorrectionPhase; + private boolean mLoading; - public FCD1TunerEditor(TunerConfigurationModel tunerConfigurationModel, FCDTuner tuner ) - { - super( tunerConfigurationModel ); - mController = (FCD1TunerController)tuner.getController(); - - init(); - } + private FCD1TunerController mController; + + public FCD1TunerEditor( TunerConfigurationModel tunerConfigurationModel, FCDTuner tuner ) + { + super( tunerConfigurationModel ); + mController = (FCD1TunerController)tuner.getController(); + + init(); + } private FCD1TunerConfiguration getConfiguration() { @@ -69,33 +82,33 @@ public class FCD1TunerEditor extends TunerConfigurationEditor { return (FCD1TunerConfiguration)getItem(); } - + return null; } private void init() - { - setLayout( new MigLayout( "fill,wrap 4", "[right][grow,fill][right][grow,fill]", - "[][][][][][][][][][][][grow]" ) ); - + { + setLayout( new MigLayout( "fill,wrap 4", "[right][grow,fill][right][grow,fill]", + "[][][][][][][][][][][][grow]" ) ); + add( new JLabel( "FCD Pro Tuner Configuration" ), "span,align center" ); mConfigurationName = new JTextField(); mConfigurationName.setEnabled( false ); - mConfigurationName.addFocusListener( new FocusListener() + mConfigurationName.addFocusListener( new FocusListener() { @Override - public void focusLost( FocusEvent e ) - { + public void focusLost( FocusEvent e ) + { save(); - } + } @Override - public void focusGained( FocusEvent e ) {} + public void focusGained( FocusEvent e ) {} } ); - + add( new JLabel( "Name:" ) ); add( mConfigurationName, "span 2" ); - + mTunerInfo = new JButton( "Tuner Info" ); mTunerInfo.setEnabled( false ); mTunerInfo.addActionListener( new ActionListener() @@ -103,189 +116,189 @@ public class FCD1TunerEditor extends TunerConfigurationEditor @Override public void actionPerformed( ActionEvent e ) { - JOptionPane.showMessageDialog( FCD1TunerEditor.this, getTunerInfo(), - "Tuner Info", JOptionPane.INFORMATION_MESSAGE ); + JOptionPane.showMessageDialog( FCD1TunerEditor.this, getTunerInfo(), + "Tuner Info", JOptionPane.INFORMATION_MESSAGE ); } } ); add( mTunerInfo ); - SpinnerModel model = new SpinnerNumberModel( 0.0, //initial value - -1000.0, //min - 1000.0, //max - 0.1 ); //step + SpinnerModel model = new SpinnerNumberModel( 0.0, //initial value + -1000.0, //min + 1000.0, //max + 0.1 ); //step - mFrequencyCorrection = new JSpinner( model ); - mFrequencyCorrection.setEnabled( false ); - JSpinner.NumberEditor editor = - (JSpinner.NumberEditor)mFrequencyCorrection.getEditor(); + mFrequencyCorrection = new JSpinner( model ); + mFrequencyCorrection.setEnabled( false ); + JSpinner.NumberEditor editor = + (JSpinner.NumberEditor)mFrequencyCorrection.getEditor(); - DecimalFormat format = editor.getFormat(); - format.setMinimumFractionDigits( 1 ); - editor.getTextField().setHorizontalAlignment( SwingConstants.CENTER ); + DecimalFormat format = editor.getFormat(); + format.setMinimumFractionDigits( 1 ); + editor.getTextField().setHorizontalAlignment( SwingConstants.CENTER ); - mFrequencyCorrection.addChangeListener( new ChangeListener() - { + mFrequencyCorrection.addChangeListener( new ChangeListener() + { @Override - public void stateChanged( ChangeEvent e ) - { + public void stateChanged( ChangeEvent e ) + { double value = ((SpinnerNumberModel)mFrequencyCorrection - .getModel()).getNumber().doubleValue(); + .getModel()).getNumber().doubleValue(); try - { - mController.setFrequencyCorrection( value ); - save(); - } - catch ( SourceException e1 ) - { - JOptionPane.showMessageDialog( FCD1TunerEditor.this, "FCD Pro Tuner " - + "Controller - couldn't apply frequency correction value: " + value + - e1.getLocalizedMessage() ); - - mLog.error( "FuncubeDonglePro Controller - couldn't apply " - + "frequency correction value: " + value, e1 ); - } - } - } ); - - add( new JLabel( "PPM:" ) ); - add( mFrequencyCorrection, "wrap" ); - - add( new JSeparator( JSeparator.HORIZONTAL ), "span,grow" ); + { + mController.setFrequencyCorrection( value ); + save(); + } + catch ( SourceException e1 ) + { + JOptionPane.showMessageDialog( FCD1TunerEditor.this, "FCD Pro Tuner " + + "Controller - couldn't apply frequency correction value: " + value + + e1.getLocalizedMessage() ); + + mLog.error( "FuncubeDonglePro Controller - couldn't apply " + + "frequency correction value: " + value, e1 ); + } + } + } ); + + add( new JLabel( "PPM:" ) ); + add( mFrequencyCorrection, "wrap" ); + + add( new JSeparator( JSeparator.HORIZONTAL ), "span,grow" ); add( new JLabel( "Gain" ), "span,align center" ); - mComboLNAGain = new JComboBox( FCD1TunerController.LNAGain.values() ); - mComboLNAGain.setEnabled( false ); - mComboLNAGain.setToolTipText( "Adjust the low noise amplifier gain setting." ); - mComboLNAGain.addActionListener( new ActionListener() - { + mComboLNAGain = new JComboBox( LNAGain.values() ); + mComboLNAGain.setEnabled( false ); + mComboLNAGain.setToolTipText( "Adjust the low noise amplifier gain setting." ); + mComboLNAGain.addActionListener( new ActionListener() + { @Override - public void actionPerformed( ActionEvent arg0 ) - { - FCD1TunerController.LNAGain gain = (FCD1TunerController.LNAGain)mComboLNAGain.getSelectedItem(); - - + public void actionPerformed( ActionEvent arg0 ) + { + LNAGain gain = (LNAGain)mComboLNAGain.getSelectedItem(); + + try - { + { mController.setLNAGain( gain ); save(); - } - catch ( Exception e ) - { - JOptionPane.showMessageDialog( FCD1TunerEditor.this, "FCD Pro Tuner Controller" - + " - error setting LNA gain [" + gain.toString() + "]" ); - - mLog.error( "FuncubeDonglePro Controller - error setting gain [" + gain.toString() + "]", e ); - } - } - } ); - - add( new JLabel( "LNA:" ) ); - add( mComboLNAGain ); - - /** - * LNA Enhance - */ - mComboLNAEnhance = new JComboBox( FCD1TunerController.LNAEnhance.values() ); - mComboLNAEnhance.setToolTipText( "Adjust the LNA enhance setting. Default value is OFF" ); + } + catch ( Exception e ) + { + JOptionPane.showMessageDialog( FCD1TunerEditor.this, "FCD Pro Tuner Controller" + + " - error setting LNA gain [" + gain.toString() + "]" ); - mComboLNAEnhance.addActionListener( new ActionListener() - { + mLog.error( "FuncubeDonglePro Controller - error setting gain [" + gain.toString() + "]", e ); + } + } + } ); + + add( new JLabel( "LNA:" ) ); + add( mComboLNAGain ); + + /** + * LNA Enhance + */ + mComboLNAEnhance = new JComboBox( LNAEnhance.values() ); + mComboLNAEnhance.setToolTipText( "Adjust the LNA enhance setting. Default value is OFF" ); + + mComboLNAEnhance.addActionListener( new ActionListener() + { @Override - public void actionPerformed( ActionEvent arg0 ) - { - FCD1TunerController.LNAEnhance enhance = (FCD1TunerController.LNAEnhance)mComboLNAEnhance.getSelectedItem(); + public void actionPerformed( ActionEvent arg0 ) + { + LNAEnhance enhance = (LNAEnhance)mComboLNAEnhance.getSelectedItem(); try - { + { mController.setLNAEnhance( enhance ); save(); - } - catch ( Exception e1 ) - { - JOptionPane.showMessageDialog( FCD1TunerEditor.this, "FCD Pro Tuner" - + " error setting LNA enhance gain [" + enhance.toString() + "]" ); - - mLog.error( "FCDPro - error setting LNA enhance [" + enhance.toString() + "]", e1 ); - } - } - } ); - - add( new JLabel( "Enhance:" ) ); - add( mComboLNAEnhance ); - - /** - * Mixer Gain - */ - mComboMixerGain = new JComboBox( FCD1TunerController.MixerGain.values() ); - mComboMixerGain.setToolTipText( "Adjust mixer gain setting" ); - + } + catch ( Exception e1 ) + { + JOptionPane.showMessageDialog( FCD1TunerEditor.this, "FCD Pro Tuner" + + " error setting LNA enhance gain [" + enhance.toString() + "]" ); - mComboMixerGain.addActionListener( new ActionListener() - { + mLog.error( "FCDPro - error setting LNA enhance [" + enhance.toString() + "]", e1 ); + } + } + } ); + + add( new JLabel( "Enhance:" ) ); + add( mComboLNAEnhance ); + + /** + * Mixer Gain + */ + mComboMixerGain = new JComboBox( MixerGain.values() ); + mComboMixerGain.setToolTipText( "Adjust mixer gain setting" ); + + + mComboMixerGain.addActionListener( new ActionListener() + { @Override - public void actionPerformed( ActionEvent arg0 ) - { - FCD1TunerController.MixerGain gain = (FCD1TunerController.MixerGain)mComboMixerGain.getSelectedItem(); - + public void actionPerformed( ActionEvent arg0 ) + { + MixerGain gain = (MixerGain)mComboMixerGain.getSelectedItem(); + try - { + { mController.setMixerGain( gain ); save(); - } - catch ( Exception e1 ) - { - JOptionPane.showMessageDialog( FCD1TunerEditor.this, "FCDPro - error setting" - + " mixer gain [" + gain.toString() + "]" ); - - mLog.error( "FCDPro - error setting mixer gain [" + gain.toString() + "]", e1 ); - } - } - } ); - - add( new JLabel( "Mixer:" ) ); - add( mComboMixerGain, "wrap" ); + } + catch ( Exception e1 ) + { + JOptionPane.showMessageDialog( FCD1TunerEditor.this, "FCDPro - error setting" + + " mixer gain [" + gain.toString() + "]" ); - add( new JSeparator( JSeparator.HORIZONTAL ), "span,grow" ); + mLog.error( "FCDPro - error setting mixer gain [" + gain.toString() + "]", e1 ); + } + } + } ); + + add( new JLabel( "Mixer:" ) ); + add( mComboMixerGain, "wrap" ); + + add( new JSeparator( JSeparator.HORIZONTAL ), "span,grow" ); add( new JLabel( "Correction" ), "span,align center" ); - /** - * Inphase DC Correction - */ - mCorrectionDCI = new CorrectionSpinner( Correction.DC_INPHASE, 0.0, 0.00001, 5 ); - mCorrectionDCI.setEnabled( false ); - mCorrectionDCI.setToolTipText( "DC Bias Correction/Inphase " - + "Component: valid values are -1.0 to 1.0 (default: 0.0)" ); - add( new JLabel( "DC Inphase:" ) ); - add( mCorrectionDCI ); + /** + * Inphase DC Correction + */ + mCorrectionDCI = new CorrectionSpinner( Correction.DC_INPHASE, 0.0, 0.00001, 5 ); + mCorrectionDCI.setEnabled( false ); + mCorrectionDCI.setToolTipText( "DC Bias Correction/Inphase " + + "Component: valid values are -1.0 to 1.0 (default: 0.0)" ); + add( new JLabel( "DC Inphase:" ) ); + add( mCorrectionDCI ); - /** - * Quadrature DC Correction - */ - mCorrectionDCQ = new CorrectionSpinner( Correction.DC_QUADRATURE, 0.0, 0.00001, 5 ); - mCorrectionDCQ.setToolTipText( "DC Bias Correction/Quadrature " - + "Component: valid values are -1.0 to 1.0 (default: 0.0)" ); - add( new JLabel( "DC Quadrature:" ) ); - add( mCorrectionDCQ ); + /** + * Quadrature DC Correction + */ + mCorrectionDCQ = new CorrectionSpinner( Correction.DC_QUADRATURE, 0.0, 0.00001, 5 ); + mCorrectionDCQ.setToolTipText( "DC Bias Correction/Quadrature " + + "Component: valid values are -1.0 to 1.0 (default: 0.0)" ); + add( new JLabel( "DC Quadrature:" ) ); + add( mCorrectionDCQ ); - /** - * Gain Correction - */ - mCorrectionGain = new CorrectionSpinner( Correction.GAIN, 0.0, 0.00001, 5 ); - mCorrectionGain.setToolTipText( "Gain Correction: valid values are " - + "-1.0 to 1.0 (default: 0.0)" ); - add( new JLabel( "Gain:" ) ); - add( mCorrectionGain ); + /** + * Gain Correction + */ + mCorrectionGain = new CorrectionSpinner( Correction.GAIN, 0.0, 0.00001, 5 ); + mCorrectionGain.setToolTipText( "Gain Correction: valid values are " + + "-1.0 to 1.0 (default: 0.0)" ); + add( new JLabel( "Gain:" ) ); + add( mCorrectionGain ); - /** - * Phase Correction - */ - mCorrectionPhase = new CorrectionSpinner( Correction.PHASE, 0.0, 0.00001, 5 ); - mCorrectionPhase.setToolTipText( "Phase Correction: valid values are " - + "-1.0 to 1.0 (default: 0.0)" ); - add( new JLabel( "Phase:" ) ); - add( mCorrectionPhase ); - } + /** + * Phase Correction + */ + mCorrectionPhase = new CorrectionSpinner( Correction.PHASE, 0.0, 0.00001, 5 ); + mCorrectionPhase.setToolTipText( "Phase Correction: valid values are " + + "-1.0 to 1.0 (default: 0.0)" ); + add( new JLabel( "Phase:" ) ); + add( mCorrectionPhase ); + } /** * Sets each of the tuner configuration controls to the enabled argument state @@ -296,12 +309,12 @@ public class FCD1TunerEditor extends TunerConfigurationEditor { mConfigurationName.setEnabled( enabled ); } - + if( mTunerInfo.isEnabled() != enabled ) { mTunerInfo.setEnabled( enabled ); } - + if( mFrequencyCorrection.isEnabled() != enabled ) { mFrequencyCorrection.setEnabled( enabled ); @@ -311,32 +324,32 @@ public class FCD1TunerEditor extends TunerConfigurationEditor { mComboLNAEnhance.setEnabled( enabled ); } - + if( mComboLNAGain.isEnabled() != enabled ) { mComboLNAGain.setEnabled( enabled ); } - + if( mComboMixerGain.isEnabled() != enabled ) { mComboMixerGain.setEnabled( enabled ); } - + if( mCorrectionDCI.isEnabled() != enabled ) { mCorrectionDCI.setEnabled( enabled ); } - + if( mCorrectionDCQ.isEnabled() != enabled ) { mCorrectionDCQ.setEnabled( enabled ); } - + if( mCorrectionGain.isEnabled() != enabled ) { mCorrectionGain.setEnabled( enabled ); } - + if( mCorrectionPhase.isEnabled() != enabled ) { mCorrectionPhase.setEnabled( enabled ); @@ -348,7 +361,7 @@ public class FCD1TunerEditor extends TunerConfigurationEditor StringBuilder sb = new StringBuilder(); sb.append( "

Funcube Dongle Pro Tuner

" ); - + sb.append( "USB ID: " ); sb.append( mController.getUSBID() ); sb.append( "
" ); @@ -371,37 +384,37 @@ public class FCD1TunerEditor extends TunerConfigurationEditor return sb.toString(); } - + @Override public void save() { - if( hasItem() && !mLoading ) - { - FCD1TunerConfiguration config = getConfiguration(); + if( hasItem() && !mLoading ) + { + FCD1TunerConfiguration config = getConfiguration(); - config.setName( mConfigurationName.getText() ); + config.setName( mConfigurationName.getText() ); double value = ((SpinnerNumberModel)mFrequencyCorrection - .getModel()).getNumber().doubleValue(); - config.setFrequencyCorrection( value ); - config.setLNAEnhance( (FCD1TunerController.LNAEnhance)mComboLNAEnhance.getSelectedItem() ); - config.setLNAGain( (FCD1TunerController.LNAGain)mComboLNAGain.getSelectedItem() ); - config.setMixerGain( (FCD1TunerController.MixerGain)mComboMixerGain.getSelectedItem() ); + .getModel()).getNumber().doubleValue(); + config.setFrequencyCorrection( value ); + config.setLNAEnhance( (LNAEnhance)mComboLNAEnhance.getSelectedItem() ); + config.setLNAGain( (LNAGain)mComboLNAGain.getSelectedItem() ); + config.setMixerGain( (MixerGain)mComboMixerGain.getSelectedItem() ); double dci = ((SpinnerNumberModel)mCorrectionDCI.getModel()).getNumber().doubleValue(); - config.setInphaseDCCorrection( dci ); - + config.setInphaseDCCorrection( dci ); + double dcq = ((SpinnerNumberModel)mCorrectionDCQ.getModel()).getNumber().doubleValue(); - config.setQuadratureDCCorrection( dcq ); - + config.setQuadratureDCCorrection( dcq ); + double gain = ((SpinnerNumberModel)mCorrectionGain.getModel()).getNumber().doubleValue(); - config.setGainCorrection( gain ); - + config.setGainCorrection( gain ); + double phase = ((SpinnerNumberModel)mCorrectionPhase.getModel()).getNumber().doubleValue(); - config.setPhaseCorrection( phase ); - - getTunerConfigurationModel().broadcast( new TunerConfigurationEvent( getConfiguration(), TunerConfigurationEvent.Event.CHANGE ) ); - } + config.setPhaseCorrection( phase ); + + getTunerConfigurationModel().broadcast( new TunerConfigurationEvent( getConfiguration(), Event.CHANGE ) ); + } } @Override @@ -415,19 +428,19 @@ public class FCD1TunerEditor extends TunerConfigurationEditor if( hasItem() ) { FCD1TunerConfiguration config = getConfiguration(); - + if( tunerConfiguration.isAssigned() ) { setControlsEnabled( true ); mConfigurationName.setText( config.getName() ); - mFrequencyCorrection.setValue( config.getFrequencyCorrection() ); - mComboLNAEnhance.setSelectedItem( config.getLNAEnhance() ); - mComboLNAGain.setSelectedItem( config.getLNAGain() ); - mComboMixerGain.setSelectedItem( config.getMixerGain() ); - mCorrectionDCI.setValue( config.getInphaseDCCorrection() ); - mCorrectionDCQ.setValue( config.getQuadratureDCCorrection() ); - mCorrectionGain.setValue( config.getGainCorrection() ); - mCorrectionPhase.setValue( config.getPhaseCorrection() ); + mFrequencyCorrection.setValue( config.getFrequencyCorrection() ); + mComboLNAEnhance.setSelectedItem( config.getLNAEnhance() ); + mComboLNAGain.setSelectedItem( config.getLNAGain() ); + mComboMixerGain.setSelectedItem( config.getMixerGain() ); + mCorrectionDCI.setValue( config.getInphaseDCCorrection() ); + mCorrectionDCQ.setValue( config.getQuadratureDCCorrection() ); + mCorrectionGain.setValue( config.getGainCorrection() ); + mCorrectionPhase.setValue( config.getPhaseCorrection() ); } else { @@ -440,44 +453,44 @@ public class FCD1TunerEditor extends TunerConfigurationEditor setControlsEnabled( false ); mConfigurationName.setText( "" ); } - + mLoading = false; } - - public enum Correction { GAIN, PHASE, DC_INPHASE, DC_QUADRATURE }; - - public class CorrectionSpinner extends JSpinner - { - private static final long serialVersionUID = 1L; - private static final double MIN_VALUE = -1.0d; - private static final double MAX_VALUE = 1.0d; - - private Correction mCorrectionComponent; + + public enum Correction { GAIN, PHASE, DC_INPHASE, DC_QUADRATURE }; + + public class CorrectionSpinner extends JSpinner + { + private static final long serialVersionUID = 1L; + private static final double MIN_VALUE = -1.0d; + private static final double MAX_VALUE = 1.0d; + + private Correction mCorrectionComponent; public CorrectionSpinner( Correction component, double initialValue, double step, int decimalPlaces ) - { + { mCorrectionComponent = component; - - SpinnerModel model = new SpinnerNumberModel( initialValue, MIN_VALUE, MAX_VALUE, step ); - setModel( model ); - JSpinner.NumberEditor editor = (JSpinner.NumberEditor)getEditor(); - - DecimalFormat format = editor.getFormat(); - format.setMinimumFractionDigits( decimalPlaces ); - - editor.getTextField().setHorizontalAlignment( SwingConstants.CENTER ); + SpinnerModel model = new SpinnerNumberModel( initialValue, MIN_VALUE, MAX_VALUE, step ); + setModel( model ); - addChangeListener( new ChangeListener() - { + JSpinner.NumberEditor editor = (JSpinner.NumberEditor)getEditor(); + + DecimalFormat format = editor.getFormat(); + format.setMinimumFractionDigits( decimalPlaces ); + + editor.getTextField().setHorizontalAlignment( SwingConstants.CENTER ); + + addChangeListener( new ChangeListener() + { @Override - public void stateChanged( ChangeEvent e ) - { - double value = + public void stateChanged( ChangeEvent e ) + { + double value = ((SpinnerNumberModel)getModel()).getNumber().doubleValue(); try - { + { switch( mCorrectionComponent ) { case DC_INPHASE: @@ -493,20 +506,20 @@ public class FCD1TunerEditor extends TunerConfigurationEditor mController.setPhaseCorrection( value ); break; } - - save(); - } - catch ( Exception e1 ) - { - JOptionPane.showMessageDialog( FCD1TunerEditor.this, "FCDPro - error " - + "applying " + mCorrectionComponent.toString() + " correction value [" - + value + "]" ); - - mLog.error( "FCDPro - error applying "+ mCorrectionComponent.toString() - + " correction value [" + value + "]", e1 ); - } - } - } ); - } - } -} + + save(); + } + catch ( Exception e1 ) + { + JOptionPane.showMessageDialog( FCD1TunerEditor.this, "FCDPro - error " + + "applying " + mCorrectionComponent.toString() + " correction value [" + + value + "]" ); + + mLog.error( "FCDPro - error applying "+ mCorrectionComponent.toString() + + " correction value [" + value + "]", e1 ); + } + } + } ); + } + } +} \ No newline at end of file diff --git a/src/main/java/io/github/dsheirer/source/tuner/fcd/proplusV2/FCD2TunerController.java b/src/main/java/io/github/dsheirer/source/tuner/fcd/proplusV2/FCD2TunerController.java index 5efbc669..c8e71140 100644 --- a/src/main/java/io/github/dsheirer/source/tuner/fcd/proplusV2/FCD2TunerController.java +++ b/src/main/java/io/github/dsheirer/source/tuner/fcd/proplusV2/FCD2TunerController.java @@ -19,6 +19,7 @@ package io.github.dsheirer.source.tuner.fcd.proplusV2; import io.github.dsheirer.source.SourceException; +import io.github.dsheirer.source.tuner.MixerTunerDataLine; import io.github.dsheirer.source.tuner.MixerTunerType; import io.github.dsheirer.source.tuner.TunerClass; import io.github.dsheirer.source.tuner.TunerType; @@ -41,11 +42,10 @@ public class FCD2TunerController extends FCDTunerController public static final int MAXIMUM_TUNABLE_FREQUENCY = 2050000000; public static final int SAMPLE_RATE = 192000; - public FCD2TunerController(Device device, DeviceDescriptor descriptor) + public FCD2TunerController(MixerTunerDataLine mixerTDL, Device device, DeviceDescriptor descriptor) { - super(device, descriptor, - (int)MixerTunerType.FUNCUBE_DONGLE_PRO_PLUS.getAudioFormat().getSampleRate(), - MINIMUM_TUNABLE_FREQUENCY, MAXIMUM_TUNABLE_FREQUENCY); + super(mixerTDL, device, descriptor, MixerTunerType.FUNCUBE_DONGLE_PRO_PLUS.getDisplayString(), + MINIMUM_TUNABLE_FREQUENCY, MAXIMUM_TUNABLE_FREQUENCY, MixerTunerType.FUNCUBE_DONGLE_PRO.getAudioFormat()); } public void init() throws SourceException @@ -64,7 +64,7 @@ public class FCD2TunerController extends FCDTunerController } } - public int getCurrentSampleRate() + public double getCurrentSampleRate() { return SAMPLE_RATE; } @@ -200,39 +200,4 @@ public class FCD2TunerController extends FCDTunerController mLog.error("error setting IQ correction to [" + value + "]", e); } } - - public enum Block - { - CELLULAR_BAND_BLOCKED("Blocked"), - NO_BAND_BLOCK("Unblocked"), - UNKNOWN("Unknown"); - - private String mLabel; - - private Block(String label) - { - mLabel = label; - } - - public String getLabel() - { - return mLabel; - } - - public static Block getBlock(String block) - { - Block retVal = UNKNOWN; - - if(block.equalsIgnoreCase("No blk")) - { - retVal = NO_BAND_BLOCK; - } - else if(block.equalsIgnoreCase("Cell blk")) - { - retVal = CELLULAR_BAND_BLOCKED; - } - - return retVal; - } - } -} +} \ No newline at end of file diff --git a/src/main/java/io/github/dsheirer/source/tuner/fcd/proplusV2/FCD2TunerEditor.java b/src/main/java/io/github/dsheirer/source/tuner/fcd/proplusV2/FCD2TunerEditor.java index 97db4f17..ce8d6fb7 100644 --- a/src/main/java/io/github/dsheirer/source/tuner/fcd/proplusV2/FCD2TunerEditor.java +++ b/src/main/java/io/github/dsheirer/source/tuner/fcd/proplusV2/FCD2TunerEditor.java @@ -28,7 +28,16 @@ import net.miginfocom.swing.MigLayout; import org.slf4j.Logger; import org.slf4j.LoggerFactory; -import javax.swing.*; +import javax.swing.JButton; +import javax.swing.JCheckBox; +import javax.swing.JLabel; +import javax.swing.JOptionPane; +import javax.swing.JSeparator; +import javax.swing.JSpinner; +import javax.swing.JTextField; +import javax.swing.SpinnerModel; +import javax.swing.SpinnerNumberModel; +import javax.swing.SwingConstants; import javax.swing.event.ChangeEvent; import javax.swing.event.ChangeListener; import java.awt.event.ActionEvent; @@ -40,24 +49,24 @@ import java.text.DecimalFormat; public class FCD2TunerEditor extends TunerConfigurationEditor { private final static Logger mLog = LoggerFactory.getLogger( FCD2TunerEditor.class ); - private static final long serialVersionUID = 1L; - - private JTextField mConfigurationName; - private JButton mTunerInfo; - private JSpinner mFrequencyCorrection; - private JCheckBox mLNAGain; - private JCheckBox mMixerGain; - private boolean mLoading; + private static final long serialVersionUID = 1L; - private FCD2TunerController mController; + private JTextField mConfigurationName; + private JButton mTunerInfo; + private JSpinner mFrequencyCorrection; + private JCheckBox mLNAGain; + private JCheckBox mMixerGain; + private boolean mLoading; - public FCD2TunerEditor( TunerConfigurationModel tunerConfigurationModel, FCDTuner tuner ) - { - super( tunerConfigurationModel ); - mController = (FCD2TunerController)tuner.getController(); - - init(); - } + private FCD2TunerController mController; + + public FCD2TunerEditor( TunerConfigurationModel tunerConfigurationModel, FCDTuner tuner ) + { + super( tunerConfigurationModel ); + mController = (FCD2TunerController)tuner.getController(); + + init(); + } private FCD2TunerConfiguration getConfiguration() { @@ -65,33 +74,33 @@ public class FCD2TunerEditor extends TunerConfigurationEditor { return (FCD2TunerConfiguration)getItem(); } - + return null; } - private void init() - { - setLayout( new MigLayout( "fill,wrap 4", "[right][grow,fill][right][grow,fill]", - "[][][][][][grow]" ) ); - + private void init() + { + setLayout( new MigLayout( "fill,wrap 4", "[right][grow,fill][right][grow,fill]", + "[][][][][][grow]" ) ); + add( new JLabel( "FCD Pro+ Tuner Configuration" ), "span,align center" ); mConfigurationName = new JTextField(); mConfigurationName.setEnabled( false ); - mConfigurationName.addFocusListener( new FocusListener() + mConfigurationName.addFocusListener( new FocusListener() { @Override - public void focusLost( FocusEvent e ) - { + public void focusLost( FocusEvent e ) + { save(); - } + } @Override - public void focusGained( FocusEvent e ) {} + public void focusGained( FocusEvent e ) {} } ); - + add( new JLabel( "Name:" ) ); add( mConfigurationName, "span 2" ); - + mTunerInfo = new JButton( "Tuner Info" ); mTunerInfo.setEnabled( false ); mTunerInfo.addActionListener( new ActionListener() @@ -99,103 +108,103 @@ public class FCD2TunerEditor extends TunerConfigurationEditor @Override public void actionPerformed( ActionEvent e ) { - JOptionPane.showMessageDialog( FCD2TunerEditor.this, getTunerInfo(), - "Tuner Info", JOptionPane.INFORMATION_MESSAGE ); + JOptionPane.showMessageDialog( FCD2TunerEditor.this, getTunerInfo(), + "Tuner Info", JOptionPane.INFORMATION_MESSAGE ); } } ); add( mTunerInfo ); - SpinnerModel model = new SpinnerNumberModel( 0.0, //initial value - -1000.0, //min - 1000.0, //max - 0.1 ); //step + SpinnerModel model = new SpinnerNumberModel( 0.0, //initial value + -1000.0, //min + 1000.0, //max + 0.1 ); //step - mFrequencyCorrection = new JSpinner( model ); - mFrequencyCorrection.setEnabled( false ); - JSpinner.NumberEditor editor = - (JSpinner.NumberEditor)mFrequencyCorrection.getEditor(); + mFrequencyCorrection = new JSpinner( model ); + mFrequencyCorrection.setEnabled( false ); + JSpinner.NumberEditor editor = + (JSpinner.NumberEditor)mFrequencyCorrection.getEditor(); - DecimalFormat format = editor.getFormat(); - format.setMinimumFractionDigits( 1 ); - editor.getTextField().setHorizontalAlignment( SwingConstants.CENTER ); + DecimalFormat format = editor.getFormat(); + format.setMinimumFractionDigits( 1 ); + editor.getTextField().setHorizontalAlignment( SwingConstants.CENTER ); - mFrequencyCorrection.addChangeListener( new ChangeListener() - { + mFrequencyCorrection.addChangeListener( new ChangeListener() + { @Override - public void stateChanged( ChangeEvent e ) - { + public void stateChanged( ChangeEvent e ) + { double value = ((SpinnerNumberModel)mFrequencyCorrection - .getModel()).getNumber().doubleValue(); + .getModel()).getNumber().doubleValue(); try - { - mController.setFrequencyCorrection( value ); - save(); - } - catch ( SourceException e1 ) - { - JOptionPane.showMessageDialog( FCD2TunerEditor.this, "FCD Pro Plus Tuner " - + "Controller - couldn't apply frequency correction value: " + value + - e1.getLocalizedMessage() ); - - mLog.error( "FuncubeDongleProPlus Controller - couldn't apply " - + "frequency correction value: " + value, e1 ); - } - } - } ); - - add( new JLabel( "PPM:" ) ); - add( mFrequencyCorrection, "wrap" ); - - add( new JSeparator( JSeparator.HORIZONTAL ), "span,grow" ); + { + mController.setFrequencyCorrection( value ); + save(); + } + catch ( SourceException e1 ) + { + JOptionPane.showMessageDialog( FCD2TunerEditor.this, "FCD Pro Plus Tuner " + + "Controller - couldn't apply frequency correction value: " + value + + e1.getLocalizedMessage() ); - mLNAGain = new JCheckBox( "LNA Gain" ); - mLNAGain.setEnabled( false ); - mLNAGain.addActionListener( new ActionListener() - { - @Override - public void actionPerformed( ActionEvent event ) - { - try - { - mController.setLNAGain( mLNAGain.isSelected() ); - save(); - } - catch( SourceException e ) - { - mLog.error( "Couldn't set LNA gain for FCD2", e ); - } - } - } ); + mLog.error( "FuncubeDongleProPlus Controller - couldn't apply " + + "frequency correction value: " + value, e1 ); + } + } + } ); - add( new JLabel( "" ) ); - add( mLNAGain ); + add( new JLabel( "PPM:" ) ); + add( mFrequencyCorrection, "wrap" ); - /** - * Mixer Gain - */ - mMixerGain = new JCheckBox( "Mixer Gain" ); - - mMixerGain.addActionListener( new ActionListener() - { - @Override - public void actionPerformed( ActionEvent event ) - { - try - { - mController.setMixerGain( mMixerGain.isSelected() ); - save(); - } - catch( SourceException e ) - { - mLog.error( "Couldn't set mixer gain for FCD2", e ); - } - } - } ); + add( new JSeparator( JSeparator.HORIZONTAL ), "span,grow" ); - add( new JLabel( "" ) ); - add( mMixerGain ); - } + mLNAGain = new JCheckBox( "LNA Gain" ); + mLNAGain.setEnabled( false ); + mLNAGain.addActionListener( new ActionListener() + { + @Override + public void actionPerformed( ActionEvent event ) + { + try + { + mController.setLNAGain( mLNAGain.isSelected() ); + save(); + } + catch( SourceException e ) + { + mLog.error( "Couldn't set LNA gain for FCD2", e ); + } + } + } ); + + add( new JLabel( "" ) ); + add( mLNAGain ); + + /** + * Mixer Gain + */ + mMixerGain = new JCheckBox( "Mixer Gain" ); + + mMixerGain.addActionListener( new ActionListener() + { + @Override + public void actionPerformed( ActionEvent event ) + { + try + { + mController.setMixerGain( mMixerGain.isSelected() ); + save(); + } + catch( SourceException e ) + { + mLog.error( "Couldn't set mixer gain for FCD2", e ); + } + } + } ); + + add( new JLabel( "" ) ); + add( mMixerGain ); + } /** * Sets each of the tuner configuration controls to the enabled argument state @@ -206,12 +215,12 @@ public class FCD2TunerEditor extends TunerConfigurationEditor { mConfigurationName.setEnabled( enabled ); } - + if( mTunerInfo.isEnabled() != enabled ) { mTunerInfo.setEnabled( enabled ); } - + if( mFrequencyCorrection.isEnabled() != enabled ) { mFrequencyCorrection.setEnabled( enabled ); @@ -221,7 +230,7 @@ public class FCD2TunerEditor extends TunerConfigurationEditor { mLNAGain.setEnabled( enabled ); } - + if( mMixerGain.isEnabled() != enabled ) { mMixerGain.setEnabled( enabled ); @@ -233,7 +242,7 @@ public class FCD2TunerEditor extends TunerConfigurationEditor StringBuilder sb = new StringBuilder(); sb.append( "

Funcube Dongle Pro Plus Tuner

" ); - + sb.append( "USB ID: " ); sb.append( mController.getUSBID() ); sb.append( "
" ); @@ -256,26 +265,26 @@ public class FCD2TunerEditor extends TunerConfigurationEditor return sb.toString(); } - + @Override public void save() { - if( hasItem() && !mLoading ) - { - FCD2TunerConfiguration config = getConfiguration(); + if( hasItem() && !mLoading ) + { + FCD2TunerConfiguration config = getConfiguration(); - config.setName( mConfigurationName.getText() ); + config.setName( mConfigurationName.getText() ); double value = ((SpinnerNumberModel)mFrequencyCorrection - .getModel()).getNumber().doubleValue(); - config.setFrequencyCorrection( value ); - - config.setGainLNA( mLNAGain.isSelected() ); - config.setGainMixer( mMixerGain.isSelected() ); - - getTunerConfigurationModel().broadcast( + .getModel()).getNumber().doubleValue(); + config.setFrequencyCorrection( value ); + + config.setGainLNA( mLNAGain.isSelected() ); + config.setGainMixer( mMixerGain.isSelected() ); + + getTunerConfigurationModel().broadcast( new TunerConfigurationEvent( getConfiguration(), Event.CHANGE ) ); - } + } } @Override @@ -289,15 +298,15 @@ public class FCD2TunerEditor extends TunerConfigurationEditor if( hasItem() ) { FCD2TunerConfiguration config = getConfiguration(); - - + + if( tunerConfiguration.isAssigned() ) { setControlsEnabled( true ); mConfigurationName.setText( config.getName() ); - mFrequencyCorrection.setValue( config.getFrequencyCorrection() ); - mLNAGain.setSelected( config.getGainLNA() ); - mMixerGain.setSelected( config.getGainMixer() ); + mFrequencyCorrection.setValue( config.getFrequencyCorrection() ); + mLNAGain.setSelected( config.getGainLNA() ); + mMixerGain.setSelected( config.getGainMixer() ); } else { @@ -310,7 +319,7 @@ public class FCD2TunerEditor extends TunerConfigurationEditor setControlsEnabled( false ); mConfigurationName.setText( "" ); } - + mLoading = false; } -} +} \ No newline at end of file diff --git a/src/main/java/io/github/dsheirer/source/tuner/frequency/DCTrackingFrequencyControl.java b/src/main/java/io/github/dsheirer/source/tuner/frequency/DCTrackingFrequencyControl.java index 74c5367f..ff92b14a 100644 --- a/src/main/java/io/github/dsheirer/source/tuner/frequency/DCTrackingFrequencyControl.java +++ b/src/main/java/io/github/dsheirer/source/tuner/frequency/DCTrackingFrequencyControl.java @@ -1,129 +1,127 @@ package io.github.dsheirer.source.tuner.frequency; /******************************************************************************* - * SDR Trunk - * Copyright (C) 2016 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 + * 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 + * ******************************************************************************/ import io.github.dsheirer.module.Module; import io.github.dsheirer.sample.Listener; import io.github.dsheirer.sample.real.RealBuffer; -import io.github.dsheirer.source.tuner.frequency.FrequencyChangeEvent.Event; +import io.github.dsheirer.source.ISourceEventProvider; +import io.github.dsheirer.source.SourceEvent; -import java.util.concurrent.ScheduledExecutorService; - -public class DCTrackingFrequencyControl extends Module - implements Listener, IFrequencyChangeProvider +public class DCTrackingFrequencyControl extends Module implements Listener, ISourceEventProvider { - private static float COARSE_THRESHOLD = 0.1f; - private static float LOOP_GAIN = 0.1f; - private static float COARSE_GAIN = 1000.0f; - private static float FINE_GAIN = 100.0f; - private int mErrorCorrection = 0; - private int mMaximumFrequencyCorrection = 0; - private Listener mListener; - - /** - * DC offset Automatic Frequency Control. Monitors DC bias present in an FM - * demodulated audio stream and issues automatic frequency corrections to - * maintain the DC bias close to zero. - * - * @param maximum - maximum allowable (+/-) frequency correction value. - */ - public DCTrackingFrequencyControl( int maximum ) - { - mMaximumFrequencyCorrection = maximum; - } - - public void dispose() - { - mListener = null; - } + private static float COARSE_THRESHOLD = 0.1f; + private static float LOOP_GAIN = 0.1f; + private static float COARSE_GAIN = 1000.0f; + private static float FINE_GAIN = 100.0f; + private int mErrorCorrection = 0; + private int mMaximumFrequencyCorrection = 0; + private Listener mListener; - @Override - public void receive( RealBuffer buffer ) - { - double sum = 0.0d; - - for( float sample: buffer.getSamples() ) - { - sum += sample; - } - - float mean = (float)( sum / (double)buffer.getSamples().length ); - - float adjustment = 0.0f; - - if( mean > COARSE_THRESHOLD || mean < - COARSE_THRESHOLD ) - { - adjustment = mean * COARSE_GAIN; - } - else - { - adjustment = mean * FINE_GAIN; - } + /** + * DC offset Automatic Frequency Control. Monitors DC bias present in an FM + * demodulated audio stream and issues automatic frequency corrections to + * maintain the DC bias close to zero. + * + * @param maximum - maximum allowable (+/-) frequency correction value. + */ + public DCTrackingFrequencyControl(int maximum) + { + mMaximumFrequencyCorrection = maximum; + } - setErrorCorrection( mErrorCorrection + (int)( (float)adjustment * LOOP_GAIN ) ); - } - - private void setErrorCorrection( int correction ) - { - if( mListener != null ) - { - if( correction > mMaximumFrequencyCorrection ) - { - correction = mMaximumFrequencyCorrection; - } - else if( correction < -mMaximumFrequencyCorrection ) - { - correction = -mMaximumFrequencyCorrection; - } - - if( correction != mErrorCorrection ) - { - mListener.receive( new FrequencyChangeEvent( - Event.REQUEST_CHANNEL_FREQUENCY_CORRECTION_CHANGE, correction ) ); - } - } - } - - @Override - public void setFrequencyChangeListener( Listener listener ) - { - mListener = listener; - } + public void dispose() + { + mListener = null; + } - @Override - public void removeFrequencyChangeListener() - { - mListener = null; - } + @Override + public void receive(RealBuffer buffer) + { + double sum = 0.0d; - @Override - public void start( ScheduledExecutorService executor ) - { - } + for(float sample : buffer.getSamples()) + { + sum += sample; + } - @Override - public void stop() - { - } + float mean = (float)(sum / (double)buffer.getSamples().length); - @Override - public void reset() - { - setErrorCorrection( 0 ); - } + float adjustment = 0.0f; + + if(mean > COARSE_THRESHOLD || mean < -COARSE_THRESHOLD) + { + adjustment = mean * COARSE_GAIN; + } + else + { + adjustment = mean * FINE_GAIN; + } + + setErrorCorrection(mErrorCorrection + (int)((float)adjustment * LOOP_GAIN)); + } + + private void setErrorCorrection(int correction) + { + if(mListener != null) + { + if(correction > mMaximumFrequencyCorrection) + { + correction = mMaximumFrequencyCorrection; + } + else if(correction < -mMaximumFrequencyCorrection) + { + correction = -mMaximumFrequencyCorrection; + } + + if(correction != mErrorCorrection) + { + mListener.receive(SourceEvent.channelFrequencyCorrectionRequest(correction)); + } + } + } + + @Override + public void setSourceEventListener(Listener listener) + { + mListener = listener; + } + + @Override + public void removeSourceEventListener() + { + mListener = null; + } + + @Override + public void start() + { + } + + @Override + public void stop() + { + } + + @Override + public void reset() + { + setErrorCorrection(0); + } } diff --git a/src/main/java/io/github/dsheirer/source/tuner/frequency/FrequencyChangeEvent.java b/src/main/java/io/github/dsheirer/source/tuner/frequency/FrequencyChangeEvent.java deleted file mode 100644 index f0a9b94f..00000000 --- a/src/main/java/io/github/dsheirer/source/tuner/frequency/FrequencyChangeEvent.java +++ /dev/null @@ -1,34 +0,0 @@ -package io.github.dsheirer.source.tuner.frequency; - -public class FrequencyChangeEvent -{ - public enum Event - { - NOTIFICATION_FREQUENCY_CHANGE, - NOTIFICATION_FREQUENCY_CORRECTION_CHANGE, - NOTIFICATION_CHANNEL_FREQUENCY_CORRECTION_CHANGE, - NOTIFICATION_SAMPLE_RATE_CHANGE, - - REQUEST_FREQUENCY_CHANGE, - REQUEST_CHANNEL_FREQUENCY_CORRECTION_CHANGE; - } - - private Event mEvent; - private Number mValue; - - public FrequencyChangeEvent( Event event, Number value ) - { - mEvent = event; - mValue = value; - } - - public Event getEvent() - { - return mEvent; - } - - public Number getValue() - { - return mValue; - } -} diff --git a/src/main/java/io/github/dsheirer/source/tuner/frequency/FrequencyChangeProcessorWrapper.java b/src/main/java/io/github/dsheirer/source/tuner/frequency/FrequencyChangeProcessorWrapper.java deleted file mode 100644 index 4f1baec0..00000000 --- a/src/main/java/io/github/dsheirer/source/tuner/frequency/FrequencyChangeProcessorWrapper.java +++ /dev/null @@ -1,44 +0,0 @@ -package io.github.dsheirer.source.tuner.frequency; - -import io.github.dsheirer.sample.Listener; -import io.github.dsheirer.source.SourceException; -import org.slf4j.Logger; -import org.slf4j.LoggerFactory; - -public class FrequencyChangeProcessorWrapper - implements IFrequencyChangeListener, Listener -{ - private final static Logger mLog = LoggerFactory.getLogger( FrequencyChangeProcessorWrapper.class ); - - private IFrequencyChangeProcessor mProcessor; - - /** - * Provides IFrequencyChangeListener wrapper for an IFrequencyChangeProcessor - */ - public FrequencyChangeProcessorWrapper( IFrequencyChangeProcessor handler ) - { - mProcessor = handler; - } - - @Override - public Listener getFrequencyChangeListener() - { - return this; - } - - @Override - public void receive( FrequencyChangeEvent event ) - { - if( mProcessor != null ) - { - try - { - mProcessor.frequencyChanged( event ); - } - catch( SourceException se ) - { - mLog.error("Error sending frequency change event to embedded processor", se); - } - } - } -} diff --git a/src/main/java/io/github/dsheirer/source/tuner/frequency/FrequencyController.java b/src/main/java/io/github/dsheirer/source/tuner/frequency/FrequencyController.java index 217afa00..74d36f8f 100644 --- a/src/main/java/io/github/dsheirer/source/tuner/frequency/FrequencyController.java +++ b/src/main/java/io/github/dsheirer/source/tuner/frequency/FrequencyController.java @@ -1,48 +1,46 @@ /******************************************************************************* - * SDR Trunk - * Copyright (C) 2014 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 + * ********************************************************************************************************************* + * sdr-trunk + * 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 io.github.dsheirer.source.tuner.frequency; +import io.github.dsheirer.source.ISourceEventProcessor; import io.github.dsheirer.source.InvalidFrequencyException; +import io.github.dsheirer.source.SourceEvent; import io.github.dsheirer.source.SourceException; -import io.github.dsheirer.source.tuner.frequency.FrequencyChangeEvent.Event; import org.slf4j.Logger; import org.slf4j.LoggerFactory; import java.util.ArrayList; +import java.util.List; public class FrequencyController { - private final static Logger mLog = LoggerFactory.getLogger( FrequencyController.class ); + private final static Logger mLog = LoggerFactory.getLogger(FrequencyController.class); private Tunable mTunable; private long mFrequency = 101100000; private long mTunedFrequency = 101100000; private long mMinimumFrequency; private long mMaximumFrequency; - private double mFrequencyCorrection = 0d; - private int mSampleRate = 0; - - private ArrayList mProcessors = new ArrayList<>(); - - public FrequencyController( Tunable tunable, - long minFrequency, - long maxFrequency, - double frequencyCorrection ) + private double mFrequencyCorrection = 0.0d; + private double mSampleRate = 0.0d; + + private List mProcessors = new ArrayList<>(); + + public FrequencyController(Tunable tunable, long minFrequency, long maxFrequency, double frequencyCorrection) { mTunable = tunable; mMinimumFrequency = minFrequency; @@ -51,23 +49,31 @@ public class FrequencyController } /** - * Get bandwidth/sample rate in hertz + * Get bandwidth in hertz */ public int getBandwidth() { - return mSampleRate; + return (int)mSampleRate; } /** * Set sample rate in hertz */ - public void setSampleRate( int sampleRate ) throws SourceException + public void setSampleRate(int sampleRate) throws SourceException { mSampleRate = sampleRate; - + broadcastSampleRateChange(); } + /** + * Get sample rate in hertz + */ + public double getSampleRate() + { + return mSampleRate; + } + /** * Get frequency in hertz */ @@ -78,13 +84,24 @@ public class FrequencyController /** * Set frequency + * * @param frequency in hertz * @throws SourceException if tunable doesn't support tuning a corrected - * version of the requested frequency + * version of the requested frequency */ - public void setFrequency( long frequency ) throws SourceException + public void setFrequency(long frequency) throws SourceException { - setFrequency( frequency, true ); + setFrequency(frequency, true); + } + + /** + * Indicates if the specified frequency can be tuned (ie is within min/max frequency) by this controller. + * @param frequency to evaluate + * @return true if the frequency falls within the tuning range of this controller + */ + public boolean canTune(long frequency) + { + return getMinimumFrequency() <= frequency && frequency <= getMaximumFrequency(); } /** @@ -92,149 +109,147 @@ public class FrequencyController * method supports changing the frequency correction value without * broadcasting a frequency change event. */ - private void setFrequency( long frequency, boolean broadcastChange ) throws SourceException + private void setFrequency(long frequency, boolean broadcastChange) throws SourceException { - long tunedFrequency = getTunedFrequency( frequency ); - - if( tunedFrequency < mMinimumFrequency ) + long tunedFrequency = getTunedFrequency(frequency); + + if(tunedFrequency < mMinimumFrequency) { - throw new InvalidFrequencyException( "Requested frequency not valid", frequency, mMinimumFrequency ); + throw new InvalidFrequencyException("Requested frequency not valid", frequency, mMinimumFrequency); } - if( tunedFrequency > mMaximumFrequency ) + if(tunedFrequency > mMaximumFrequency) { - throw new InvalidFrequencyException( "Requested frequency not valid", frequency, mMaximumFrequency ); + throw new InvalidFrequencyException("Requested frequency not valid", frequency, mMaximumFrequency); } mFrequency = frequency; mTunedFrequency = tunedFrequency; - if( mTunable != null ) + if(mTunable != null) { - mTunable.setTunedFrequency( mTunedFrequency ); + mTunable.setTunedFrequency(mTunedFrequency); } /* Broadcast to all listeners that the frequency has changed */ - if( broadcastChange ) + if(broadcastChange) { broadcastFrequencyChange(); } } - + public long getTunedFrequency() { return mTunedFrequency; } - + public long getMinimumFrequency() { return mMinimumFrequency; } - + public long getMaximumFrequency() { return mMaximumFrequency; } - - /** - * Calculate the tuned frequency by adding frequency correction to the - * corrected frequency. - * @param correctedFrequency - */ - private long getTunedFrequency( long correctedFrequency ) - { - return (long)( (double)correctedFrequency / - ( 1.0 + ( mFrequencyCorrection / 1000000.0 ) ) ); - } - /** - * Calculate the corrected frequency by subtracting frequency correction - * from the tuned frequency. - * @param tunedFrequency - */ - @SuppressWarnings("unused") - private long getCorrectedFrequency( long tunedFrequency ) - { - return (long)( (double)tunedFrequency / - ( 1.0 - ( mFrequencyCorrection / 1000000.0 ) ) ); - } - - public double getFrequencyCorrection() - { - return mFrequencyCorrection; - } - - public void setFrequencyCorrection( double correction ) throws SourceException - { - mFrequencyCorrection = correction; + /** + * Calculate the tuned frequency by adding frequency correction to the + * corrected frequency. + * + * @param correctedFrequency + */ + private long getTunedFrequency(long correctedFrequency) + { + return (long)((double)correctedFrequency / + (1.0 + (mFrequencyCorrection / 1000000.0))); + } - if( mFrequency > 0 ) - { - setFrequency( mFrequency, true ); - } + /** + * Calculate the corrected frequency by subtracting frequency correction + * from the tuned frequency. + * + * @param tunedFrequency + */ + @SuppressWarnings("unused") + private long getCorrectedFrequency(long tunedFrequency) + { + return (long)((double)tunedFrequency / + (1.0 - (mFrequencyCorrection / 1000000.0))); + } + + public double getFrequencyCorrection() + { + return mFrequencyCorrection; + } + + public void setFrequencyCorrection(double correction) throws SourceException + { + mFrequencyCorrection = correction; + + if(mFrequency > 0) + { + setFrequency(mFrequency, true); + } + + broadcastFrequencyCorrectionChange(); + } - broadcastFrequencyErrorChange(); - } - /** * Adds listener to receive frequency change events */ -//TODO: rename this to addFrequencyChangeProcessor() - public void addListener( IFrequencyChangeProcessor processor ) + public void addListener(ISourceEventProcessor processor) { - if( !mProcessors.contains( processor ) ) + if(!mProcessors.contains(processor)) { - mProcessors.add( processor ); + mProcessors.add(processor); } } /** * Removes listener from receiving frequency change events */ - public void removeFrequencyChangeProcessor( IFrequencyChangeProcessor processor ) + public void removeFrequencyChangeProcessor(ISourceEventProcessor processor) { - mProcessors.remove( processor ); + mProcessors.remove(processor); } - - - /** - * Broadcasts a change/update to the current (uncorrected) frequency or the - * bandwidth/sample rate value. - */ - protected void broadcastFrequencyChange() throws SourceException - { - broadcastFrequencyChangeEvent( - new FrequencyChangeEvent( Event.NOTIFICATION_FREQUENCY_CHANGE, mFrequency ) ); - } - - /** - * Broadcast a frequency error/correction value change - */ - protected void broadcastFrequencyErrorChange() throws SourceException - { - broadcastFrequencyChangeEvent( - new FrequencyChangeEvent( Event.NOTIFICATION_FREQUENCY_CORRECTION_CHANGE, mFrequencyCorrection ) ); - } - - /** - * Broadcasts a sample rate change - */ - protected void broadcastSampleRateChange() throws SourceException - { - broadcastFrequencyChangeEvent( - new FrequencyChangeEvent( Event.NOTIFICATION_SAMPLE_RATE_CHANGE, mSampleRate ) ); - } - - public void broadcastFrequencyChangeEvent( FrequencyChangeEvent event ) throws SourceException - { - for( IFrequencyChangeProcessor processor: mProcessors ) - { - processor.frequencyChanged( event ); - } - } - - + + + /** + * Broadcasts a change/update to the current (uncorrected) frequency or the + * bandwidth/sample rate value. + */ + protected void broadcastFrequencyChange() throws SourceException + { + broadcast(SourceEvent.frequencyChange(mFrequency)); + } + + /** + * Broadcast a frequency error/correction value change + */ + protected void broadcastFrequencyCorrectionChange() throws SourceException + { + broadcast(SourceEvent.frequencyCorrectionChange((int)mFrequencyCorrection)); + } + + /** + * Broadcasts a sample rate change + */ + protected void broadcastSampleRateChange() throws SourceException + { + broadcast(SourceEvent.sampleRateChange(mSampleRate)); + } + + public void broadcast(SourceEvent event) throws SourceException + { + for(ISourceEventProcessor processor : mProcessors) + { + processor.process(event); + } + } + + public interface Tunable { /** @@ -245,11 +260,16 @@ public class FrequencyController /** * Sets the tuned frequency of the device */ - public void setTunedFrequency( long frequency ) throws SourceException; + public void setTunedFrequency(long frequency) throws SourceException; /** * Gets the current bandwidth setting of the device */ - public int getCurrentSampleRate() throws SourceException; + public double getCurrentSampleRate() throws SourceException; + + /** + * Indicates if this tunable can tune the frequency + */ + public boolean canTune(long frequency); } -} +} \ No newline at end of file diff --git a/src/main/java/io/github/dsheirer/source/tuner/frequency/IFrequencyChangeListener.java b/src/main/java/io/github/dsheirer/source/tuner/frequency/IFrequencyChangeListener.java deleted file mode 100644 index eda2e784..00000000 --- a/src/main/java/io/github/dsheirer/source/tuner/frequency/IFrequencyChangeListener.java +++ /dev/null @@ -1,34 +0,0 @@ -/******************************************************************************* - * SDR Trunk - * Copyright (C) 2014 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 io.github.dsheirer.source.tuner.frequency; - -import io.github.dsheirer.sample.Listener; - - -/** - * Interface for broadcasting changes to frequency, bandwidth, sample rate, - * and actual sample rate values to all registered listeners. - * - * Note: broadcasted frequency should be the uncorrected, or displayable - * frequency value. All frequency correction aspects should be handled within - * the device that implements the tuning of the frequency. - */ -public interface IFrequencyChangeListener -{ - public Listener getFrequencyChangeListener(); -} diff --git a/src/main/java/io/github/dsheirer/source/tuner/frequency/IFrequencyChangeProcessor.java b/src/main/java/io/github/dsheirer/source/tuner/frequency/IFrequencyChangeProcessor.java deleted file mode 100644 index b47c47f6..00000000 --- a/src/main/java/io/github/dsheirer/source/tuner/frequency/IFrequencyChangeProcessor.java +++ /dev/null @@ -1,8 +0,0 @@ -package io.github.dsheirer.source.tuner.frequency; - -import io.github.dsheirer.source.SourceException; - -public interface IFrequencyChangeProcessor -{ - public void frequencyChanged( FrequencyChangeEvent event ) throws SourceException; -} diff --git a/src/main/java/io/github/dsheirer/source/tuner/frequency/IFrequencyChangeProvider.java b/src/main/java/io/github/dsheirer/source/tuner/frequency/IFrequencyChangeProvider.java deleted file mode 100644 index f3dcb6fd..00000000 --- a/src/main/java/io/github/dsheirer/source/tuner/frequency/IFrequencyChangeProvider.java +++ /dev/null @@ -1,30 +0,0 @@ -/******************************************************************************* - * 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 io.github.dsheirer.source.tuner.frequency; - -import io.github.dsheirer.sample.Listener; - -/** - * Provider of Frequency Change Events - */ -public interface IFrequencyChangeProvider -{ - void setFrequencyChangeListener( Listener listener ); - void removeFrequencyChangeListener(); -} diff --git a/src/main/java/io/github/dsheirer/source/tuner/hackrf/HackRFTuner.java b/src/main/java/io/github/dsheirer/source/tuner/hackrf/HackRFTuner.java index ce79bdb7..2e1516fe 100644 --- a/src/main/java/io/github/dsheirer/source/tuner/hackrf/HackRFTuner.java +++ b/src/main/java/io/github/dsheirer/source/tuner/hackrf/HackRFTuner.java @@ -1,124 +1,76 @@ /******************************************************************************* - * SDR Trunk - * Copyright (C) 2014-2016 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 + * sdr-trunk + * 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 io.github.dsheirer.source.tuner.hackrf; -import io.github.dsheirer.sample.Listener; -import io.github.dsheirer.sample.complex.ComplexBuffer; import io.github.dsheirer.source.SourceException; import io.github.dsheirer.source.tuner.Tuner; -import io.github.dsheirer.source.tuner.TunerChannel; -import io.github.dsheirer.source.tuner.TunerChannelSource; import io.github.dsheirer.source.tuner.TunerClass; -import io.github.dsheirer.source.tuner.TunerEvent; import io.github.dsheirer.source.tuner.TunerType; +import io.github.dsheirer.source.tuner.hackrf.HackRFTunerController.BoardID; import org.slf4j.Logger; import org.slf4j.LoggerFactory; -import java.util.concurrent.RejectedExecutionException; - public class HackRFTuner extends Tuner { - private final static Logger mLog = LoggerFactory.getLogger( HackRFTuner.class ); + private final static Logger mLog = LoggerFactory.getLogger( HackRFTuner.class ); - public HackRFTuner( HackRFTunerController controller ) throws SourceException - { - super( "HackRF", controller ); - } - - public HackRFTunerController getController() - { - return (HackRFTunerController)getTunerController(); - } + public HackRFTuner( HackRFTunerController controller ) throws SourceException + { + super( "HackRF", controller ); + } - public void dispose() - { - //TODO: dispose of something here - } - - @Override + public HackRFTunerController getController() + { + return (HackRFTunerController)getTunerController(); + } + + public void dispose() + { + } + + @Override public TunerClass getTunerClass() { - return TunerClass.HACKRF_ONE; + return TunerClass.HACKRF_ONE; } - @Override + @Override public TunerType getTunerType() { - return TunerType.HACKRF; + return TunerType.HACKRF; } - @Override - public double getSampleSize() - { - return 11.0; - } - - @Override - public TunerChannelSource getChannel(TunerChannel channel ) - throws RejectedExecutionException, SourceException + @Override + public double getSampleSize() { - TunerChannelSource source = getController().getChannel( this, channel ); - - if( source != null ) - { - broadcast( new TunerEvent( this, TunerEvent.Event.CHANNEL_COUNT ) ); - } - - return source; + return 11.0; } - @Override - public void releaseChannel( TunerChannelSource source ) - { - /* Unregister for receiving samples */ - removeListener( (Listener)source ); - - /* Tell the controller to release the channel and cleanup */ - if( source != null ) - { - getController().releaseChannel( source ); - } - } - - @Override + @Override public String getUniqueID() { - try - { - return getController().getSerial().getSerialNumber(); - } - catch( Exception e ) - { - mLog.error( "error gettting serial number", e ); - } - - return HackRFTunerController.BoardID.HACKRF_ONE.getLabel(); + try + { + return getController().getSerial().getSerialNumber(); + } + catch( Exception e ) + { + mLog.error( "error gettting serial number", e ); + } + + return BoardID.HACKRF_ONE.getLabel(); } - - @Override - public void addListener( Listener listener ) - { - getController().addListener( listener ); - } - - @Override - public void removeListener( Listener listener ) - { - getController().removeListener( listener ); - } -} +} \ No newline at end of file diff --git a/src/main/java/io/github/dsheirer/source/tuner/hackrf/HackRFTunerController.java b/src/main/java/io/github/dsheirer/source/tuner/hackrf/HackRFTunerController.java index fe232ec4..01773f25 100644 --- a/src/main/java/io/github/dsheirer/source/tuner/hackrf/HackRFTunerController.java +++ b/src/main/java/io/github/dsheirer/source/tuner/hackrf/HackRFTunerController.java @@ -53,13 +53,11 @@ ******************************************************************************/ package io.github.dsheirer.source.tuner.hackrf; -import io.github.dsheirer.sample.Listener; import io.github.dsheirer.sample.adapter.ByteSampleAdapter; -import io.github.dsheirer.sample.complex.ComplexBuffer; import io.github.dsheirer.source.SourceException; -import io.github.dsheirer.source.tuner.TunerController; import io.github.dsheirer.source.tuner.configuration.TunerConfiguration; import io.github.dsheirer.source.tuner.usb.USBTransferProcessor; +import io.github.dsheirer.source.tuner.usb.USBTunerController; import org.apache.commons.io.EndianUtils; import org.slf4j.Logger; import org.slf4j.LoggerFactory; @@ -74,7 +72,7 @@ import java.nio.ByteBuffer; import java.nio.ByteOrder; import java.util.Arrays; -public class HackRFTunerController extends TunerController +public class HackRFTunerController extends USBTunerController { private final static Logger mLog = LoggerFactory.getLogger(HackRFTunerController.class); @@ -158,6 +156,12 @@ public class HackRFTunerController extends TunerController mUSBTransferProcessor = new USBTransferProcessor(name, mDeviceHandle, mSampleAdapter, USB_TRANSFER_BUFFER_SIZE); } + @Override + protected USBTransferProcessor getUSBTransferProcessor() + { + return mUSBTransferProcessor; + } + /** * Claims the USB interface. If another application currently has * the interface claimed, the USB_FORCE_CLAIM_INTERFACE setting @@ -328,7 +332,7 @@ public class HackRFTunerController extends TunerController } @Override - public int getCurrentSampleRate() throws SourceException + public double getCurrentSampleRate() { return mSampleRate.getRate(); } @@ -522,7 +526,7 @@ public class HackRFTunerController extends TunerController write(Request.SET_SAMPLE_RATE, 0, 0, buffer); } - public int getSampleRate() + public double getSampleRate() { return mSampleRate.getRate(); } @@ -872,23 +876,4 @@ public class HackRFTunerController extends TunerController return sb.toString(); } } - - /** - * Adds a sample listener. If the buffer processing thread is - * not currently running, starts it running in a new thread. - */ - public void addListener(Listener listener) - { - mUSBTransferProcessor.addListener(listener); - } - - /** - * Removes the sample listener. If this is the last registered listener, - * shuts down the buffer processing thread. - */ - public void removeListener(Listener listener) - { - mUSBTransferProcessor.removeListener(listener); - } - -} +} \ No newline at end of file diff --git a/src/main/java/io/github/dsheirer/source/tuner/hackrf/HackRFTunerEditor.java b/src/main/java/io/github/dsheirer/source/tuner/hackrf/HackRFTunerEditor.java index 12bb1cf4..9aea1db3 100644 --- a/src/main/java/io/github/dsheirer/source/tuner/hackrf/HackRFTunerEditor.java +++ b/src/main/java/io/github/dsheirer/source/tuner/hackrf/HackRFTunerEditor.java @@ -1,19 +1,17 @@ /******************************************************************************* - * SDR Trunk - * Copyright (C) 2014-2016 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 + * sdr-trunk + * 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 io.github.dsheirer.source.tuner.hackrf; @@ -22,11 +20,24 @@ import io.github.dsheirer.source.tuner.configuration.TunerConfiguration; import io.github.dsheirer.source.tuner.configuration.TunerConfigurationEditor; import io.github.dsheirer.source.tuner.configuration.TunerConfigurationEvent; import io.github.dsheirer.source.tuner.configuration.TunerConfigurationModel; +import io.github.dsheirer.source.tuner.hackrf.HackRFTunerController.HackRFLNAGain; +import io.github.dsheirer.source.tuner.hackrf.HackRFTunerController.HackRFSampleRate; +import io.github.dsheirer.source.tuner.hackrf.HackRFTunerController.HackRFVGAGain; import net.miginfocom.swing.MigLayout; import org.slf4j.Logger; import org.slf4j.LoggerFactory; -import javax.swing.*; +import javax.swing.JButton; +import javax.swing.JComboBox; +import javax.swing.JLabel; +import javax.swing.JOptionPane; +import javax.swing.JSeparator; +import javax.swing.JSpinner; +import javax.swing.JTextField; +import javax.swing.JToggleButton; +import javax.swing.SpinnerModel; +import javax.swing.SpinnerNumberModel; +import javax.swing.SwingConstants; import javax.swing.event.ChangeEvent; import javax.swing.event.ChangeListener; import javax.usb.UsbException; @@ -40,196 +51,196 @@ public class HackRFTunerEditor extends TunerConfigurationEditor { private static final long serialVersionUID = 1L; - private final static Logger mLog = LoggerFactory.getLogger( HackRFTunerEditor.class ); + private final static Logger mLog = LoggerFactory.getLogger( HackRFTunerEditor.class ); private JTextField mConfigurationName; private JButton mTunerInfo; - private JComboBox mComboSampleRate; + private JComboBox mComboSampleRate; private JSpinner mFrequencyCorrection; private JToggleButton mAmplifier; - private JComboBox mComboLNAGain; - private JComboBox mComboVGAGain; + private JComboBox mComboLNAGain; + private JComboBox mComboVGAGain; private boolean mLoading; private HackRFTunerController mController; - public HackRFTunerEditor(TunerConfigurationModel tunerConfigurationModel, HackRFTuner tuner ) - { - super( tunerConfigurationModel ); - - mController = tuner.getController(); - - init(); - } + public HackRFTunerEditor( TunerConfigurationModel tunerConfigurationModel, HackRFTuner tuner ) + { + super( tunerConfigurationModel ); - private HackRFTunerConfiguration getConfiguration() - { - if( hasItem() ) - { - return (HackRFTunerConfiguration)getItem(); - } - - return null; - } + mController = tuner.getController(); + + init(); + } + + private HackRFTunerConfiguration getConfiguration() + { + if( hasItem() ) + { + return (HackRFTunerConfiguration)getItem(); + } + + return null; + } private void init() { - setLayout( new MigLayout( "fill,wrap 4", "[right][grow,fill][right][grow,fill]", - "[][][][][][][grow]" ) ); - - add( new JLabel( "HackRF Tuner Configuration" ), "span,align center" ); + setLayout( new MigLayout( "fill,wrap 4", "[right][grow,fill][right][grow,fill]", + "[][][][][][][grow]" ) ); - mConfigurationName = new JTextField(); - mConfigurationName.setEnabled( false ); - mConfigurationName.addFocusListener( new FocusListener() - { - @Override + add( new JLabel( "HackRF Tuner Configuration" ), "span,align center" ); + + mConfigurationName = new JTextField(); + mConfigurationName.setEnabled( false ); + mConfigurationName.addFocusListener( new FocusListener() + { + @Override public void focusLost( FocusEvent e ) { - save(); + save(); } - @Override + @Override public void focusGained( FocusEvent e ) {} - } ); - - add( new JLabel( "Name:" ) ); - add( mConfigurationName, "span 2" ); - - mTunerInfo = new JButton( "Tuner Info" ); - mTunerInfo.setEnabled( false ); - mTunerInfo.addActionListener( new ActionListener() - { - @Override - public void actionPerformed( ActionEvent e ) - { - JOptionPane.showMessageDialog( HackRFTunerEditor.this, getTunerInfo(), - "Tuner Info", JOptionPane.INFORMATION_MESSAGE ); - } - } ); - add( mTunerInfo ); - - mComboSampleRate = new JComboBox<>( HackRFTunerController.HackRFSampleRate.values() ); - mComboSampleRate.setEnabled( false ); - mComboSampleRate.addActionListener( new ActionListener() + } ); + + add( new JLabel( "Name:" ) ); + add( mConfigurationName, "span 2" ); + + mTunerInfo = new JButton( "Tuner Info" ); + mTunerInfo.setEnabled( false ); + mTunerInfo.addActionListener( new ActionListener() { - @Override + @Override public void actionPerformed( ActionEvent e ) { - HackRFTunerController.HackRFSampleRate sampleRate = - (HackRFTunerController.HackRFSampleRate)mComboSampleRate.getSelectedItem(); - try + JOptionPane.showMessageDialog( HackRFTunerEditor.this, getTunerInfo(), + "Tuner Info", JOptionPane.INFORMATION_MESSAGE ); + } + } ); + add( mTunerInfo ); + + mComboSampleRate = new JComboBox<>( HackRFSampleRate.values() ); + mComboSampleRate.setEnabled( false ); + mComboSampleRate.addActionListener( new ActionListener() + { + @Override + public void actionPerformed( ActionEvent e ) + { + HackRFSampleRate sampleRate = + (HackRFSampleRate)mComboSampleRate.getSelectedItem(); + try { - mController.setSampleRate( sampleRate ); - save(); + mController.setSampleRate( sampleRate ); + save(); } - catch ( SourceException |UsbException e2 ) + catch ( SourceException|UsbException e2 ) { - JOptionPane.showMessageDialog( HackRFTunerEditor.this, "HackRF Tuner Controller" - + " - couldn't apply the sample rate setting [" + sampleRate.getLabel() + - "] " + e2.getLocalizedMessage() ); - - mLog.error( "HackRF Tuner Controller - couldn't apply sample rate setting [" + - sampleRate.getLabel() + "]", e ); + JOptionPane.showMessageDialog( HackRFTunerEditor.this, "HackRF Tuner Controller" + + " - couldn't apply the sample rate setting [" + sampleRate.getLabel() + + "] " + e2.getLocalizedMessage() ); + + mLog.error( "HackRF Tuner Controller - couldn't apply sample rate setting [" + + sampleRate.getLabel() + "]", e ); } } } ); add( new JLabel( "Sample Rate:" ) ); add( mComboSampleRate ); - + SpinnerModel model = new SpinnerNumberModel( 0.0, //initial value - -1000.0, //min - 1000.0, //max - 0.1 ); //step + -1000.0, //min + 1000.0, //max + 0.1 ); //step mFrequencyCorrection = new JSpinner( model ); mFrequencyCorrection.setEnabled( false ); - JSpinner.NumberEditor editor = (JSpinner.NumberEditor)mFrequencyCorrection.getEditor(); - - DecimalFormat format = editor.getFormat(); - format.setMinimumFractionDigits( 1 ); - editor.getTextField().setHorizontalAlignment( SwingConstants.CENTER ); + JSpinner.NumberEditor editor = (JSpinner.NumberEditor)mFrequencyCorrection.getEditor(); - mFrequencyCorrection.addChangeListener( new ChangeListener() + DecimalFormat format = editor.getFormat(); + format.setMinimumFractionDigits( 1 ); + editor.getTextField().setHorizontalAlignment( SwingConstants.CENTER ); + + mFrequencyCorrection.addChangeListener( new ChangeListener() { - @Override + @Override public void stateChanged( ChangeEvent e ) { - final double value = ((SpinnerNumberModel)mFrequencyCorrection - .getModel()).getNumber().doubleValue(); + final double value = ((SpinnerNumberModel)mFrequencyCorrection + .getModel()).getNumber().doubleValue(); - try + try { - mController.setFrequencyCorrection( value ); - save(); + mController.setFrequencyCorrection( value ); + save(); } catch ( SourceException e1 ) { - JOptionPane.showMessageDialog( HackRFTunerEditor.this, "HackRF Tuner Controller" - + " - couldn't apply frequency correction value: " + value + - e1.getLocalizedMessage() ); - - mLog.error( "HackRF Tuner Controller - couldn't apply frequency correction " - + "value: " + value, e1 ); + JOptionPane.showMessageDialog( HackRFTunerEditor.this, "HackRF Tuner Controller" + + " - couldn't apply frequency correction value: " + value + + e1.getLocalizedMessage() ); + + mLog.error( "HackRF Tuner Controller - couldn't apply frequency correction " + + "value: " + value, e1 ); } } } ); - + add( new JLabel( "PPM:" ) ); add( mFrequencyCorrection ); add( new JSeparator( JSeparator.HORIZONTAL ), "span,grow" ); add( new JLabel( "Gain" ) ); add( new JLabel( "" ), "span 2" ); //filler - + mAmplifier = new JToggleButton( "Amplifier" ); mAmplifier.setEnabled( false ); - mAmplifier.addActionListener( new ActionListener() + mAmplifier.addActionListener( new ActionListener() { - @Override + @Override public void actionPerformed( ActionEvent arg0 ) { - try + try { - mController.setAmplifierEnabled( mAmplifier.isSelected() ); - save(); + mController.setAmplifierEnabled( mAmplifier.isSelected() ); + save(); } catch ( UsbException e ) { - mLog.error( "couldn't enable/disable amplifier", e ); - - JOptionPane.showMessageDialog( HackRFTunerEditor.this, "Couldn't change amplifier setting", - "Error changing amplifier setting", JOptionPane.ERROR_MESSAGE ); + mLog.error( "couldn't enable/disable amplifier", e ); + + JOptionPane.showMessageDialog( HackRFTunerEditor.this, "Couldn't change amplifier setting", + "Error changing amplifier setting", JOptionPane.ERROR_MESSAGE ); } } } ); add( mAmplifier ); - - mComboLNAGain = new JComboBox( HackRFTunerController.HackRFLNAGain.values() ); + + mComboLNAGain = new JComboBox( HackRFLNAGain.values() ); mComboLNAGain.setEnabled( false ); - mComboLNAGain.addActionListener( new ActionListener() + mComboLNAGain.addActionListener( new ActionListener() { - @Override + @Override public void actionPerformed( ActionEvent arg0 ) { - try + try { - HackRFTunerController.HackRFLNAGain lnaGain = (HackRFTunerController.HackRFLNAGain)mComboLNAGain.getSelectedItem(); - - if ( lnaGain == null ) - { - lnaGain = HackRFTunerController.HackRFLNAGain.GAIN_0; - } - - mController.setLNAGain( lnaGain ); - save(); + HackRFLNAGain lnaGain = (HackRFLNAGain)mComboLNAGain.getSelectedItem(); + + if ( lnaGain == null ) + { + lnaGain = HackRFLNAGain.GAIN_0; + } + + mController.setLNAGain( lnaGain ); + save(); } catch ( UsbException e ) { - JOptionPane.showMessageDialog( HackRFTunerEditor.this, "HackRF Tuner Controller" - + " - couldn't apply the LNA gain setting - " + e.getLocalizedMessage() ); - - mLog.error( "HackRF Tuner Controller - couldn't apply LNA gain setting - ", e ); + JOptionPane.showMessageDialog( HackRFTunerEditor.this, "HackRF Tuner Controller" + + " - couldn't apply the LNA gain setting - " + e.getLocalizedMessage() ); + + mLog.error( "HackRF Tuner Controller - couldn't apply LNA gain setting - ", e ); } } } ); @@ -237,30 +248,30 @@ public class HackRFTunerEditor extends TunerConfigurationEditor add( new JLabel( "LNA:" ) ); add( mComboLNAGain ); - mComboVGAGain = new JComboBox( HackRFTunerController.HackRFVGAGain.values() ); - mComboVGAGain.addActionListener( new ActionListener() + mComboVGAGain = new JComboBox( HackRFVGAGain.values() ); + mComboVGAGain.addActionListener( new ActionListener() { - @Override + @Override public void actionPerformed( ActionEvent arg0 ) { - try + try { - HackRFTunerController.HackRFVGAGain vgaGain = (HackRFTunerController.HackRFVGAGain)mComboVGAGain.getSelectedItem(); + HackRFVGAGain vgaGain = (HackRFVGAGain)mComboVGAGain.getSelectedItem(); - if( vgaGain == null ) - { - vgaGain = HackRFTunerController.HackRFVGAGain.GAIN_4; - } - - mController.setVGAGain( vgaGain ); - save(); + if( vgaGain == null ) + { + vgaGain = HackRFVGAGain.GAIN_4; + } + + mController.setVGAGain( vgaGain ); + save(); } catch ( UsbException e ) { - JOptionPane.showMessageDialog( HackRFTunerEditor.this, "HackRF Tuner Controller" - + " - couldn't apply the VGA gain setting - " + e.getLocalizedMessage() ); - - mLog.error( "HackRF Tuner Controller - couldn't apply VGA gain setting", e ); + JOptionPane.showMessageDialog( HackRFTunerEditor.this, "HackRF Tuner Controller" + + " - couldn't apply the VGA gain setting - " + e.getLocalizedMessage() ); + + mLog.error( "HackRF Tuner Controller - couldn't apply VGA gain setting", e ); } } } ); @@ -269,177 +280,177 @@ public class HackRFTunerEditor extends TunerConfigurationEditor add( mComboVGAGain ); } - /** - * Sets each of the tuner configuration controls to the enabled argument state - */ - private void setControlsEnabled( boolean enabled ) - { - if( mConfigurationName.isEnabled() != enabled ) - { - mConfigurationName.setEnabled( enabled ); - } - - if( mTunerInfo.isEnabled() != enabled ) - { - mTunerInfo.setEnabled( enabled ); - } - - if( mFrequencyCorrection.isEnabled() != enabled ) - { - mFrequencyCorrection.setEnabled( enabled ); - } + /** + * Sets each of the tuner configuration controls to the enabled argument state + */ + private void setControlsEnabled( boolean enabled ) + { + if( mConfigurationName.isEnabled() != enabled ) + { + mConfigurationName.setEnabled( enabled ); + } + + if( mTunerInfo.isEnabled() != enabled ) + { + mTunerInfo.setEnabled( enabled ); + } + + if( mFrequencyCorrection.isEnabled() != enabled ) + { + mFrequencyCorrection.setEnabled( enabled ); + } if( mComboSampleRate.isEnabled() != enabled ) { - mComboSampleRate.setEnabled( enabled ); + mComboSampleRate.setEnabled( enabled ); } - + if( mAmplifier.isEnabled() != enabled ) { - mAmplifier.setEnabled( enabled ); + mAmplifier.setEnabled( enabled ); } if( mComboLNAGain.isEnabled() != enabled ) { - mComboLNAGain.setEnabled( enabled ); + mComboLNAGain.setEnabled( enabled ); } - + if( mComboVGAGain.isEnabled() != enabled ) { - mComboVGAGain.setEnabled( enabled ); + mComboVGAGain.setEnabled( enabled ); } - } + } - private String getTunerInfo() - { - HackRFTunerController.BoardID board = HackRFTunerController.BoardID.INVALID; + private String getTunerInfo() + { + HackRFTunerController.BoardID board = HackRFTunerController.BoardID.INVALID; - try + try { - board = mController.getBoardID(); + board = mController.getBoardID(); } catch ( UsbException e ) { - mLog.error( "couldn't read HackRF board identifier", e ); + mLog.error( "couldn't read HackRF board identifier", e ); } - StringBuilder sb = new StringBuilder(); + StringBuilder sb = new StringBuilder(); - sb.append( "

HackRF Tuner

" ); + sb.append( "

HackRF Tuner

" ); - sb.append( "Board: " ); - sb.append( board.getLabel() ); - sb.append( "
" ); + sb.append( "Board: " ); + sb.append( board.getLabel() ); + sb.append( "
" ); - HackRFTunerController.Serial serial = null; - - try - { - serial = mController.getSerial(); - } - catch( Exception e ) - { - mLog.error( "couldn't read HackRF serial number", e ); - } - - if( serial != null ) - { - sb.append( "Serial: " ); - sb.append( serial.getSerialNumber() ); - sb.append( "
" ); - - sb.append( "Part: " ); - sb.append( serial.getPartID() ); - sb.append( "
" ); - } - else - { - sb.append( "Serial: Unknown
" ); - sb.append( "Part: Unknown
" ); - } - - String firmware = null; - - try - { - firmware = mController.getFirmwareVersion(); - } - catch( Exception e ) - { - mLog.error( "couldn't read HackRF firmware version", e ); - } - - if( firmware != null ) - { - sb.append( "Firmware: " ); - sb.append( firmware ); - sb.append( "
" ); - } - else - { - sb.append( "Firmware: Unknown
" ); - } - - return sb.toString(); - } + HackRFTunerController.Serial serial = null; - @Override - public void setItem( TunerConfiguration tunerConfiguration ) - { - super.setItem( tunerConfiguration ); + try + { + serial = mController.getSerial(); + } + catch( Exception e ) + { + mLog.error( "couldn't read HackRF serial number", e ); + } - //Toggle loading so that we don't fire a change event and schedule a settings file save - mLoading = true; + if( serial != null ) + { + sb.append( "Serial: " ); + sb.append( serial.getSerialNumber() ); + sb.append( "
" ); - if( hasItem() ) - { - HackRFTunerConfiguration config = getConfiguration(); - - if( tunerConfiguration.isAssigned() ) - { - setControlsEnabled( true ); - - mConfigurationName.setText( config.getName() ); - mComboSampleRate.setSelectedItem( config.getSampleRate() ); - mFrequencyCorrection.setValue( config.getFrequencyCorrection() ); - mAmplifier.setSelected( config.getAmplifierEnabled() ); - mComboLNAGain.setSelectedItem( config.getLNAGain() ); - mComboVGAGain.setSelectedItem( config.getVGAGain() ); - } - else - { - setControlsEnabled( false ); - mConfigurationName.setText( config.getName() ); - } - } - else - { - setControlsEnabled( false ); - mConfigurationName.setText( "" ); - } - - mLoading = false; - } + sb.append( "Part: " ); + sb.append( serial.getPartID() ); + sb.append( "
" ); + } + else + { + sb.append( "Serial: Unknown
" ); + sb.append( "Part: Unknown
" ); + } - @Override - public void save() - { - if( hasItem() && !mLoading ) - { - HackRFTunerConfiguration config = getConfiguration(); + String firmware = null; - config.setName( mConfigurationName.getText() ); + try + { + firmware = mController.getFirmwareVersion(); + } + catch( Exception e ) + { + mLog.error( "couldn't read HackRF firmware version", e ); + } - double value = ((SpinnerNumberModel)mFrequencyCorrection - .getModel()).getNumber().doubleValue(); - config.setFrequencyCorrection( value ); - config.setSampleRate( (HackRFTunerController.HackRFSampleRate)mComboSampleRate.getSelectedItem() ); - config.setAmplifierEnabled( mAmplifier.isSelected() ); - config.setLNAGain( (HackRFTunerController.HackRFLNAGain)mComboLNAGain.getSelectedItem() ); - config.setVGAGain( (HackRFTunerController.HackRFVGAGain)mComboVGAGain.getSelectedItem() ); - - getTunerConfigurationModel().broadcast( - new TunerConfigurationEvent( getConfiguration(), TunerConfigurationEvent.Event.CHANGE ) ); - } - } -} + if( firmware != null ) + { + sb.append( "Firmware: " ); + sb.append( firmware ); + sb.append( "
" ); + } + else + { + sb.append( "Firmware: Unknown
" ); + } + + return sb.toString(); + } + + @Override + public void setItem( TunerConfiguration tunerConfiguration ) + { + super.setItem( tunerConfiguration ); + + //Toggle loading so that we don't fire a change event and schedule a settings file save + mLoading = true; + + if( hasItem() ) + { + HackRFTunerConfiguration config = getConfiguration(); + + if( tunerConfiguration.isAssigned() ) + { + setControlsEnabled( true ); + + mConfigurationName.setText( config.getName() ); + mComboSampleRate.setSelectedItem( config.getSampleRate() ); + mFrequencyCorrection.setValue( config.getFrequencyCorrection() ); + mAmplifier.setSelected( config.getAmplifierEnabled() ); + mComboLNAGain.setSelectedItem( config.getLNAGain() ); + mComboVGAGain.setSelectedItem( config.getVGAGain() ); + } + else + { + setControlsEnabled( false ); + mConfigurationName.setText( config.getName() ); + } + } + else + { + setControlsEnabled( false ); + mConfigurationName.setText( "" ); + } + + mLoading = false; + } + + @Override + public void save() + { + if( hasItem() && !mLoading ) + { + HackRFTunerConfiguration config = getConfiguration(); + + config.setName( mConfigurationName.getText() ); + + double value = ((SpinnerNumberModel)mFrequencyCorrection + .getModel()).getNumber().doubleValue(); + config.setFrequencyCorrection( value ); + config.setSampleRate( (HackRFSampleRate)mComboSampleRate.getSelectedItem() ); + config.setAmplifierEnabled( mAmplifier.isSelected() ); + config.setLNAGain( (HackRFLNAGain)mComboLNAGain.getSelectedItem() ); + config.setVGAGain( (HackRFVGAGain)mComboVGAGain.getSelectedItem() ); + + getTunerConfigurationModel().broadcast( + new TunerConfigurationEvent( getConfiguration(), TunerConfigurationEvent.Event.CHANGE ) ); + } + } +} \ No newline at end of file diff --git a/src/main/java/io/github/dsheirer/source/tuner/manager/AbstractSourceManager.java b/src/main/java/io/github/dsheirer/source/tuner/manager/AbstractSourceManager.java new file mode 100644 index 00000000..6a463feb --- /dev/null +++ b/src/main/java/io/github/dsheirer/source/tuner/manager/AbstractSourceManager.java @@ -0,0 +1,108 @@ +/******************************************************************************* + * sdr-trunk + * 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 io.github.dsheirer.source.tuner.manager; + +import io.github.dsheirer.sample.Broadcaster; +import io.github.dsheirer.sample.Listener; +import io.github.dsheirer.sample.complex.ComplexBuffer; +import io.github.dsheirer.sample.complex.IComplexBufferProvider; +import io.github.dsheirer.source.SourceEvent; +import io.github.dsheirer.source.tuner.channel.TunerChannel; +import io.github.dsheirer.source.tuner.channel.TunerChannelSource; + +import java.util.List; + +/** + * Interface to define the functionality of a source manager for handling tuner channel management, complex buffer + * listeners, and source event listeners. + */ +public abstract class AbstractSourceManager implements IComplexBufferProvider, Listener +{ + protected Broadcaster mComplexBufferBroadcaster = new Broadcaster<>(); + + /** + * Indicates if this source manager can tune (ie provide a source for) the specified tuner channel + */ + public abstract boolean canTune(TunerChannel tunerChannel); + + /** + * List of tuner channels being sourced by this source manager + */ + public abstract List getChannels(); + + /** + * Obtains a source for the tuner channel or returns null if the channel cannot be sourced by this tuner. + * Note: use the canTune() method to check if a tuner channel can be sourced/tuned prior to accessing this method. + * + * @param tunerChannel for requested source + * @return tuner channel source or null + */ + public abstract TunerChannelSource getSource(TunerChannel tunerChannel); + + + /** + * Releases the source and all related resources + */ + public abstract void releaseSource(TunerChannelSource source); + + /** + * Adds the listener to receive source events from this source manager + * + * @param sourceEventListener to add + */ + public abstract void addSourceEventListener(Listener sourceEventListener); + + /** + * Removes the listener from receiving source events from this source manager + * @param sourceEventListener to remove + */ + public abstract void removeSourceEventListener(Listener sourceEventListener); + + /** + * Adds the listener to receive complex buffer samples + */ + @Override + public void addComplexBufferListener(Listener listener) + { + mComplexBufferBroadcaster.addListener(listener); + } + + /** + * Removes the listener from receiving complex buffer samples + */ + @Override + public void removeComplexBufferListener(Listener listener) + { + mComplexBufferBroadcaster.removeListener(listener); + } + + /** + * Indicates if there are any complex buffer listeners registered with this source manager + */ + @Override + public boolean hasComplexBufferListeners() + { + return mComplexBufferBroadcaster.hasListeners(); + } + + /** + * Broadcasts the buffer to any registered listeners + */ + protected void broadcast(ComplexBuffer complexBuffer) + { + mComplexBufferBroadcaster.broadcast(complexBuffer); + } +} diff --git a/src/main/java/io/github/dsheirer/source/tuner/manager/TunerSourceManager.java b/src/main/java/io/github/dsheirer/source/tuner/manager/TunerSourceManager.java new file mode 100644 index 00000000..8b7c913d --- /dev/null +++ b/src/main/java/io/github/dsheirer/source/tuner/manager/TunerSourceManager.java @@ -0,0 +1,80 @@ +/******************************************************************************* + * sdr-trunk + * 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 io.github.dsheirer.source.tuner.manager; + +import io.github.dsheirer.dsp.filter.channelizer.PolyphaseChannelManager; +import io.github.dsheirer.sample.Listener; +import io.github.dsheirer.source.SourceEvent; +import io.github.dsheirer.source.tuner.TunerController; +import io.github.dsheirer.source.tuner.channel.TunerChannel; +import io.github.dsheirer.source.tuner.channel.TunerChannelSource; + +import java.util.List; + +public class TunerSourceManager extends AbstractSourceManager +{ + private PolyphaseChannelManager mPolyphaseChannelManager; + private TunerController mTunerController; + + public TunerSourceManager(TunerController tunerController) + { + mTunerController = tunerController; + } + + //TODO: broadcast a tuner event each time we add a new channel to the polyphase manager + + @Override + public boolean canTune(TunerChannel tunerChannel) + { + return false; + } + + @Override + public List getChannels() + { + return null; + } + + @Override + public TunerChannelSource getSource(TunerChannel tunerChannel) + { + return null; + } + + @Override + public void releaseSource(TunerChannelSource source) + { + + } + + @Override + public void addSourceEventListener(Listener sourceEventListener) + { + + } + + @Override + public void removeSourceEventListener(Listener sourceEventListener) + { + + } + + @Override + public void receive(SourceEvent sourceEvent) + { + + } +} diff --git a/src/main/java/io/github/dsheirer/source/tuner/rtl/RTL2832Tuner.java b/src/main/java/io/github/dsheirer/source/tuner/rtl/RTL2832Tuner.java index 7066dbcc..4f84f4e8 100644 --- a/src/main/java/io/github/dsheirer/source/tuner/rtl/RTL2832Tuner.java +++ b/src/main/java/io/github/dsheirer/source/tuner/rtl/RTL2832Tuner.java @@ -17,143 +17,59 @@ ******************************************************************************/ package io.github.dsheirer.source.tuner.rtl; -import io.github.dsheirer.sample.Listener; -import io.github.dsheirer.sample.complex.ComplexBuffer; import io.github.dsheirer.source.SourceException; import io.github.dsheirer.source.tuner.Tuner; -import io.github.dsheirer.source.tuner.TunerChannel; -import io.github.dsheirer.source.tuner.TunerChannelSource; import io.github.dsheirer.source.tuner.TunerClass; -import io.github.dsheirer.source.tuner.TunerEvent; import io.github.dsheirer.source.tuner.TunerType; import org.slf4j.Logger; import org.slf4j.LoggerFactory; -import org.usb4java.LibUsbException; - -import java.util.concurrent.RejectedExecutionException; public class RTL2832Tuner extends Tuner { - private final static Logger mLog = - LoggerFactory.getLogger( RTL2832Tuner.class ); + private final static Logger mLog = LoggerFactory.getLogger(RTL2832Tuner.class); - private TunerClass mTunerClass; + private TunerClass mTunerClass; - public RTL2832Tuner( TunerClass tunerClass, - RTL2832TunerController controller ) - throws SourceException - { - super( tunerClass.getVendorDeviceLabel() + "/" + - controller.getTunerType().getLabel() + " " + - controller.getUniqueID(), controller ); - - mTunerClass = tunerClass; - } - - public void dispose() - { - //TODO: dispose of something here - } - - public RTL2832TunerController getController() - { - return (RTL2832TunerController)getTunerController(); - } + public RTL2832Tuner(TunerClass tunerClass, RTL2832TunerController controller) throws SourceException + { + super(tunerClass.getVendorDeviceLabel() + "/" + controller.getTunerType().getLabel() + " " + + controller.getUniqueID(), controller); - @Override + mTunerClass = tunerClass; + } + + public void dispose() + { + } + + public RTL2832TunerController getController() + { + return (RTL2832TunerController)getTunerController(); + } + + @Override public String getUniqueID() { - return getController().getUniqueID(); + return getController().getUniqueID(); } - @Override + @Override public TunerClass getTunerClass() { - return mTunerClass; + return mTunerClass; } - @Override + @Override public TunerType getTunerType() { - return getController().getTunerType(); + return getController().getTunerType(); } - - public void setSampleRate( RTL2832TunerController.SampleRate sampleRate ) throws SourceException - { - try - { - getController().setSampleRate( sampleRate ); - } - catch( LibUsbException e ) - { - throw new SourceException( "RTL2832 Tuner - error setting " - + "sample rate", e ); - } - } - - @Override - public double getSampleSize() - { - //Note: although sample size is 8, we set it to 11 to align with the - //actual noise floor. - return 11.0; - } - public void setFrequency( int frequency ) throws SourceException - { - getController().setFrequency( frequency ); - } - - @Override - public TunerChannelSource getChannel(TunerChannel channel ) - throws RejectedExecutionException, - SourceException - { - TunerChannelSource source = null; - - try - { - source = getController().getChannel( this, channel ); - - if( source != null ) - { - broadcast( new TunerEvent( this, TunerEvent.Event.CHANNEL_COUNT ) ); - } - } - catch( Exception e ) - { - mLog.error( "couldn't provide source channel", e ); - } - - return source; - } - - /** - * Releases the tuned channel so that the tuner controller can tune to - * other frequencies as needed. - */ - @Override - public void releaseChannel( TunerChannelSource source ) + @Override + public double getSampleSize() { - /* Unregister for receiving samples */ - removeListener( (Listener)source ); - - /* Tell the controller to release the channel and cleanup */ - if( source != null ) - { - getController().releaseChannel( source ); - } + //Note: although sample size is 8, we set it to 11 to align with the + //actual noise floor. + return 11.0; } - - @Override - public void addListener( Listener listener ) - { - getController().addListener( listener ); - } - - @Override - public void removeListener( Listener listener ) - { - getController().removeListener( listener ); - } -} +} \ No newline at end of file diff --git a/src/main/java/io/github/dsheirer/source/tuner/rtl/RTL2832TunerController.java b/src/main/java/io/github/dsheirer/source/tuner/rtl/RTL2832TunerController.java index 7fbfe6c4..c71218a2 100644 --- a/src/main/java/io/github/dsheirer/source/tuner/rtl/RTL2832TunerController.java +++ b/src/main/java/io/github/dsheirer/source/tuner/rtl/RTL2832TunerController.java @@ -26,10 +26,10 @@ import io.github.dsheirer.sample.adapter.ByteSampleAdapter; import io.github.dsheirer.sample.adapter.ISampleAdapter; import io.github.dsheirer.sample.complex.ComplexBuffer; import io.github.dsheirer.source.SourceException; -import io.github.dsheirer.source.tuner.TunerController; import io.github.dsheirer.source.tuner.TunerManager; import io.github.dsheirer.source.tuner.TunerType; import io.github.dsheirer.source.tuner.usb.USBTransferProcessor; +import io.github.dsheirer.source.tuner.usb.USBTunerController; import org.slf4j.Logger; import org.slf4j.LoggerFactory; import org.usb4java.Device; @@ -46,7 +46,7 @@ import java.nio.charset.Charset; import java.util.ArrayList; import java.util.Arrays; -public abstract class RTL2832TunerController extends TunerController +public abstract class RTL2832TunerController extends USBTunerController { private final static Logger mLog = LoggerFactory.getLogger(RTL2832TunerController.class); @@ -162,6 +162,12 @@ public abstract class RTL2832TunerController extends TunerController USB_TRANSFER_BUFFER_SIZE); } + @Override + protected USBTransferProcessor getUSBTransferProcessor() + { + return mUSBTransferProcessor; + } + /** * Claims the USB interface. Attempts to detach the active kernel driver * if one is currently attached. @@ -238,21 +244,21 @@ public abstract class RTL2832TunerController extends TunerController /* Set intermediate frequency to 0 Hz */ setIFFrequency(0); - /* Enable I/Q ADC Input */ + /* Enable I/Q ADC Input */ writeDemodRegister(mDeviceHandle, Page.ZERO, (short) 0x08, (short) 0xCD, 1); - - /* Enable zero-IF mode */ + + /* Enable zero-IF mode */ writeDemodRegister(mDeviceHandle, Page.ONE, (short) 0xB1, (short) 0x1B, 1); - - /* Set default i/q path */ + + /* Set default i/q path */ writeDemodRegister(mDeviceHandle, Page.ZERO, (short) 0x06, @@ -271,21 +277,21 @@ public abstract class RTL2832TunerController extends TunerController long ifFrequency = ((long) TWO_TO_22_POWER * (long) frequency) / (long) mOscillatorFrequency * -1; - /* Write byte 2 (high) */ + /* Write byte 2 (high) */ writeDemodRegister(mDeviceHandle, Page.ONE, (short) 0x19, (short) (Long.rotateRight(ifFrequency, 16) & 0x3F), 1); - - /* Write byte 1 (middle) */ + + /* Write byte 1 (middle) */ writeDemodRegister(mDeviceHandle, Page.ONE, (short) 0x1A, (short) (Long.rotateRight(ifFrequency, 8) & 0xFF), 1); - - /* Write byte 0 (low) */ + + /* Write byte 0 (low) */ writeDemodRegister(mDeviceHandle, Page.ONE, (short) 0x1B, @@ -341,8 +347,8 @@ public abstract class RTL2832TunerController extends TunerController try { claimInterface(handle); - - /* Perform a dummy write to see if the device needs reset */ + + /* Perform a dummy write to see if the device needs reset */ boolean resetRequired = false; try @@ -392,14 +398,14 @@ public abstract class RTL2832TunerController extends TunerController } } - /* Initialize the baseband */ + /* Initialize the baseband */ initBaseband(handle); enableI2CRepeater(handle, true); boolean controlI2CRepeater = false; - /* Test for each tuner type until we find the correct one */ + /* Test for each tuner type until we find the correct one */ if(isTuner(TunerTypeCheck.E4K, handle, controlI2CRepeater)) { tunerClass = TunerType.ELONICS_E4000; @@ -448,7 +454,7 @@ public abstract class RTL2832TunerController extends TunerController { if(mUSBTransferProcessor != null) { - mUSBTransferProcessor.removeAllListeners(); + mUSBTransferProcessor.removeListener(); TunerManager.LIBUSB_TRANSFER_PROCESSOR.unregisterTransferProcessor(mUSBTransferProcessor); } @@ -468,32 +474,32 @@ public abstract class RTL2832TunerController extends TunerController public static void initBaseband(DeviceHandle handle) throws LibUsbException { - /* Initialize USB */ + /* Initialize USB */ writeRegister(handle, Block.USB, Address.USB_SYSCTL.getAddress(), 0x09, 1); writeRegister(handle, Block.USB, Address.USB_EPA_MAXPKT.getAddress(), 0x0002, 2); writeRegister(handle, Block.USB, Address.USB_EPA_CTL.getAddress(), 0x1002, 2); - /* Power on demod */ + /* Power on demod */ writeRegister(handle, Block.SYS, Address.DEMOD_CTL_1.getAddress(), 0x22, 1); writeRegister(handle, Block.SYS, Address.DEMOD_CTL.getAddress(), 0xE8, 1); - /* Reset demod */ + /* Reset demod */ writeDemodRegister(handle, Page.ONE, (short) 0x01, 0x14, 1); //Bit 3 = soft reset writeDemodRegister(handle, Page.ONE, (short) 0x01, 0x10, 1); - - /* Disable spectrum inversion and adjacent channel rejection */ + + /* Disable spectrum inversion and adjacent channel rejection */ writeDemodRegister(handle, Page.ONE, (short) 0x15, 0x00, 1); writeDemodRegister(handle, Page.ONE, (short) 0x16, 0x0000, 2); - /* Clear DDC shift and IF frequency registers */ + /* Clear DDC shift and IF frequency registers */ writeDemodRegister(handle, Page.ONE, (short) 0x16, 0x00, 1); writeDemodRegister(handle, Page.ONE, (short) 0x17, 0x00, 1); writeDemodRegister(handle, Page.ONE, (short) 0x18, 0x00, 1); writeDemodRegister(handle, Page.ONE, (short) 0x19, 0x00, 1); writeDemodRegister(handle, Page.ONE, (short) 0x1A, 0x00, 1); writeDemodRegister(handle, Page.ONE, (short) 0x1B, 0x00, 1); - - /* Set FIR coefficients */ + + /* Set FIR coefficients */ for(int x = 0; x < sFIR_COEFFICIENTS.length; x++) { writeDemodRegister(handle, @@ -502,32 +508,32 @@ public abstract class RTL2832TunerController extends TunerController sFIR_COEFFICIENTS[x], 1); } - - /* Enable SDR mode, disable DAGC (bit 5) */ + + /* Enable SDR mode, disable DAGC (bit 5) */ writeDemodRegister(handle, Page.ZERO, (short) 0x19, 0x05, 1); - - /* Init FSM state-holding register */ + + /* Init FSM state-holding register */ writeDemodRegister(handle, Page.ONE, (short) 0x93, 0xF0, 1); writeDemodRegister(handle, Page.ONE, (short) 0x94, 0x0F, 1); - - /* Disable AGC (en_dagc, bit 0) (seems to have no effect) */ + + /* Disable AGC (en_dagc, bit 0) (seems to have no effect) */ writeDemodRegister(handle, Page.ONE, (short) 0x11, 0x00, 1); - /* Disable RF and IF AGC loop */ + /* Disable RF and IF AGC loop */ writeDemodRegister(handle, Page.ONE, (short) 0x04, 0x00, 1); - - /* Disable PID filter */ + + /* Disable PID filter */ writeDemodRegister(handle, Page.ZERO, (short) 0x61, 0x60, 1); - /* opt_adc_iq = 0, default ADC_I/ADC_Q datapath */ + /* opt_adc_iq = 0, default ADC_I/ADC_Q datapath */ writeDemodRegister(handle, Page.ZERO, (short) 0x06, 0x80, 1); - - /* Enable Zero-if mode (en_bbin bit), - * DC cancellation (en_dc_est), - * IQ estimation/compensation (en_iq_comp, en_iq_est) */ + + /* Enable Zero-if mode (en_bbin bit), + * DC cancellation (en_dc_est), + * IQ estimation/compensation (en_iq_comp, en_iq_est) */ writeDemodRegister(handle, Page.ONE, (short) 0xB1, 0x1B, 1); - /* Disable 4.096 MHz clock output on pin TP_CK0 */ + /* Disable 4.096 MHz clock output on pin TP_CK0 */ writeDemodRegister(handle, Page.ZERO, (short) 0x0D, 0x83, 1); } @@ -896,10 +902,10 @@ public abstract class RTL2832TunerController extends TunerController if(type == TunerTypeCheck.FC0012 || type == TunerTypeCheck.FC2580) { - /* Initialize the GPIOs */ + /* Initialize the GPIOs */ setGPIOOutput(handle, (byte) 0x20); - /* Reset tuner before probing */ + /* Reset tuner before probing */ setGPIOBit(handle, (byte) 0x20, true); setGPIOBit(handle, (byte) 0x20, false); } @@ -924,7 +930,7 @@ public abstract class RTL2832TunerController extends TunerController return false; } - public int getCurrentSampleRate() throws SourceException + public double getCurrentSampleRate() { return mSampleRate.getRate(); } @@ -942,7 +948,7 @@ public abstract class RTL2832TunerController extends TunerController SampleRate sampleRate = SampleRate.getClosest(rate); - /* If we're not currently set to this rate, set it as the current rate */ + /* If we're not currently set to this rate, set it as the current rate */ if(sampleRate.getRate() != rate) { setSampleRate(sampleRate); @@ -961,23 +967,23 @@ public abstract class RTL2832TunerController extends TunerController public void setSampleRate(SampleRate sampleRate) throws SourceException { - /* Write high-order 16-bits of sample rate ratio to demod register */ + /* Write high-order 16-bits of sample rate ratio to demod register */ writeDemodRegister(mDeviceHandle, Page.ONE, (short) 0x9F, sampleRate.getRatioHighBits(), 2); - - /* Write low-order 16-bits of sample rate ratio to demod register. - * Note: none of the defined rates have a low order ratio value, so we - * simply write a zero to the register */ + + /* Write low-order 16-bits of sample rate ratio to demod register. + * Note: none of the defined rates have a low order ratio value, so we + * simply write a zero to the register */ writeDemodRegister(mDeviceHandle, Page.ONE, (short) 0xA1, 0, 2); - - /* Set sample rate correction to 0 */ + + /* Set sample rate correction to 0 */ setSampleRateFrequencyCorrection(0); - /* Reset the demod for the changes to take effect */ + /* Reset the demod for the changes to take effect */ writeDemodRegister(mDeviceHandle, Page.ONE, (short) 0x01, 0x14, 1); writeDemodRegister(mDeviceHandle, Page.ONE, (short) 0x01, 0x10, 1); - /* Apply any tuner specific sample rate filter settings */ + /* Apply any tuner specific sample rate filter settings */ setSampleRateFilters(sampleRate.getRate()); mSampleRate = sampleRate; @@ -999,7 +1005,7 @@ public abstract class RTL2832TunerController extends TunerController (short) 0x3E, (Integer.rotateRight(offset, 8) & 0xFF), 1); - /* Test to retune controller to apply frequency correction */ + /* Test to retune controller to apply frequency correction */ try { mFrequencyController.setFrequency(mFrequencyController.getFrequency()); @@ -1052,7 +1058,7 @@ public abstract class RTL2832TunerController extends TunerController try { - /* Tell the RTL-2832 to address the EEPROM */ + /* Tell the RTL-2832 to address the EEPROM */ writeRegister(handle, Block.I2C, EEPROM_ADDRESS, (byte) offset, 1); } catch(LibUsbException e) @@ -1410,13 +1416,13 @@ public abstract class RTL2832TunerController extends TunerController private int getLabel(int start) { - /* Validate length and check second byte for ETX (0x03) */ + /* Validate length and check second byte for ETX (0x03) */ if(start > 254 || mData[start + 1] != (byte) 0x03) { return 256; } - /* Get label length, including the length and ETX bytes */ + /* Get label length, including the length and ETX bytes */ int length = 0xFF & mData[start]; if(start + length > 255) @@ -1424,10 +1430,10 @@ public abstract class RTL2832TunerController extends TunerController return 256; } - /* Get the label bytes */ + /* Get the label bytes */ byte[] data = Arrays.copyOfRange(mData, start + 2, start + length); - - /* Translate the bytes as UTF-16 Little Endian and store the label */ + + /* Translate the bytes as UTF-16 Little Endian and store the label */ String label = new String(data, Charset.forName("UTF-16LE")); mLabels.add(label); @@ -1489,20 +1495,33 @@ public abstract class RTL2832TunerController extends TunerController } /** - * Adds a sample listener. If the USB transfer processor is not currently running, it will auto-start + * Adds the IQ buffer listener and automatically starts buffer transfer processing, if not already started. */ - public void addListener(Listener listener) + @Override + public void addComplexBufferListener(Listener listener) { - mUSBTransferProcessor.addListener(listener); + boolean hasExistingListeners = hasComplexBufferListeners(); + + super.addComplexBufferListener(listener); + + if(!hasExistingListeners) + { + mUSBTransferProcessor.setListener(this); + } } /** - * Removes the sample listener. If this is the last registered listener, the USB transfer processor will - * halt buffer processing + * Removes the IQ buffer listener and stops buffer transfer processing if there are no more listeners. */ - public void removeListener(Listener listener) + @Override + public void removeComplexBufferListener(Listener listener) { - mUSBTransferProcessor.removeListener(listener); + super.removeComplexBufferListener(listener); + + if(!hasComplexBufferListeners()) + { + mUSBTransferProcessor.removeListener(); + } } /** @@ -1531,4 +1550,4 @@ public abstract class RTL2832TunerController extends TunerController resetUSBBuffer(); } } -} +} \ No newline at end of file diff --git a/src/main/java/io/github/dsheirer/source/tuner/rtl/e4k/E4KTunerController.java b/src/main/java/io/github/dsheirer/source/tuner/rtl/e4k/E4KTunerController.java index 90bef77c..79a6e00a 100644 --- a/src/main/java/io/github/dsheirer/source/tuner/rtl/e4k/E4KTunerController.java +++ b/src/main/java/io/github/dsheirer/source/tuner/rtl/e4k/E4KTunerController.java @@ -44,8 +44,8 @@ public class E4KTunerController extends RTL2832TunerController /* The local oscillator is defined by whole (integer) units of the oscillator * frequency and fractional units representing 1/65536th of the oscillator - * frequency, meaning we can only tune the local oscillator in units of - * 439.453125 hertz. */ + * frequency, meaning we can only tune the local oscillator in units of + * 439.453125 hertz. */ public static final long E4K_PLL_Y = 65536l; /* 16-bit fractional register */ public static final byte MASTER1_RESET = (byte) 0x01; public static final byte MASTER1_NORM_STBY = (byte) 0x02; @@ -191,7 +191,7 @@ public class E4KTunerController extends RTL2832TunerController try { - /* Read the contents of the 256-byte EEPROM */ + /* Read the contents of the 256-byte EEPROM */ eeprom = readEEPROM(mDeviceHandle, (short) 0, 256); } catch(Exception e) @@ -218,7 +218,7 @@ public class E4KTunerController extends RTL2832TunerController try { - /* Dummy write to test USB interface */ + /* Dummy write to test USB interface */ writeRegister(mDeviceHandle, Block.USB, Address.USB_SYSCTL.getAddress(), (short) 0x09, 1); @@ -263,7 +263,7 @@ public class E4KTunerController extends RTL2832TunerController @Override public void setSampleRateFilters(int bandwidth) throws SourceException { - /* Determine repeater state so we can restore it when done */ + /* Determine repeater state so we can restore it when done */ boolean i2CRepeaterEnabled = isI2CRepeaterEnabled(); if(!i2CRepeaterEnabled) @@ -293,7 +293,7 @@ public class E4KTunerController extends RTL2832TunerController { try { - /* Determine repeater state so we can restore it when done */ + /* Determine repeater state so we can restore it when done */ boolean i2CRepeaterEnabled = isI2CRepeaterEnabled(); if(!i2CRepeaterEnabled) @@ -313,7 +313,7 @@ public class E4KTunerController extends RTL2832TunerController int pllSetting = readE4KRegister(Register.SYNTH7, controlI2CRepeater); PLL pll = PLL.fromSetting(pllSetting); - /* Return the repeater to its previous state */ + /* Return the repeater to its previous state */ if(!i2CRepeaterEnabled) { enableI2CRepeater(mDeviceHandle, false); @@ -331,22 +331,22 @@ public class E4KTunerController extends RTL2832TunerController @Override public void setTunedFrequency(long frequency) throws SourceException { - /* Get the phase locked loop setting */ + /* Get the phase locked loop setting */ PLL pll = PLL.fromFrequency(frequency); - /* Z is an integer representing the number of scaled oscillator frequency - * increments the multiplied frequency. */ + /* Z is an integer representing the number of scaled oscillator frequency + * increments the multiplied frequency. */ byte z = (byte) (frequency / pll.getScaledOscillator()); - /* remainder is just as it describes. It is what is left over after we - * carve out scaled oscillator frequency increments (z) from the desired - * frequency. */ + /* remainder is just as it describes. It is what is left over after we + * carve out scaled oscillator frequency increments (z) from the desired + * frequency. */ int remainder = (int) (frequency - ((z & 0xFF) * pll.getScaledOscillator())); - /* X is a 16-bit representation of the remainder */ + /* X is a 16-bit representation of the remainder */ int x = (int) ((double) remainder / (double) pll.getScaledOscillator() * E4K_PLL_Y); - /* Calculate the exact (tunable) frequency and apply that to the tuner */ + /* Calculate the exact (tunable) frequency and apply that to the tuner */ long actualFrequency = calculateActualFrequency(pll, z, x); /** @@ -360,34 +360,34 @@ public class E4KTunerController extends RTL2832TunerController actualFrequency = calculateActualFrequency(pll, z, x); } - /* Apply the actual frequency */ + /* Apply the actual frequency */ try { enableI2CRepeater(mDeviceHandle, true); boolean controlI2CRepeater = false; - /* Write the PLL setting */ + /* Write the PLL setting */ writeE4KRegister(Register.SYNTH7, pll.getIndex(), controlI2CRepeater); - /* Write z (integral) value */ + /* Write z (integral) value */ writeE4KRegister(Register.SYNTH3, z, controlI2CRepeater); - /* Write the x (fractional) value high-order byte to synth4 register */ + /* Write the x (fractional) value high-order byte to synth4 register */ writeE4KRegister(Register.SYNTH4, (byte) (x & 0xFF), controlI2CRepeater); - /* Write the x (fractional) value low-order byte to synth5 register */ + /* Write the x (fractional) value low-order byte to synth5 register */ writeE4KRegister(Register.SYNTH5, (byte) ((Integer.rotateRight(x, 8)) & 0xFF), controlI2CRepeater); - /* Set the band for the new frequency */ + /* Set the band for the new frequency */ setBand(actualFrequency, controlI2CRepeater); - /* Set the filter */ + /* Set the filter */ setRFFilter(actualFrequency, controlI2CRepeater); - /* Check for PLL lock */ + /* Check for PLL lock */ int lock = readE4KRegister(Register.SYNTH1, controlI2CRepeater); if(!((lock & 0x1) == 0x1)) @@ -425,24 +425,24 @@ public class E4KTunerController extends RTL2832TunerController boolean i2CRepeaterControl = false; - /* Perform dummy read */ + /* Perform dummy read */ readE4KRegister(Register.DUMMY, i2CRepeaterControl); - /* Reset everything and clear POR indicator */ - /* NOTE: register value remains 0010 even after we write 0111 to it */ + /* Reset everything and clear POR indicator */ + /* NOTE: register value remains 0010 even after we write 0111 to it */ writeE4KRegister(Register.MASTER1, (byte) (MASTER1_RESET | MASTER1_NORM_STBY | MASTER1_POR_DET), i2CRepeaterControl); - /* Configure clock input */ + /* Configure clock input */ writeE4KRegister(Register.CLK_INP, (byte) 0x00, i2CRepeaterControl); - /* Disable clock output */ + /* Disable clock output */ writeE4KRegister(Register.REF_CLK, (byte) 0x00, i2CRepeaterControl); writeE4KRegister(Register.CLKOUT_PWDN, (byte) 0x96, i2CRepeaterControl); - /* Magic Init */ + /* Magic Init */ magicInit(i2CRepeaterControl); // /* Set common mode voltage a bit higher for more margin - 850 mv */ @@ -456,7 +456,7 @@ public class E4KTunerController extends RTL2832TunerController // writeE4KRegister( Register.DCTIME1, (byte)0x01, i2CRepeaterControl ); // writeE4KRegister( Register.DCTIME2, (byte)0x01, i2CRepeaterControl ); // - /* Set LNA Mode */ + /* Set LNA Mode */ writeE4KRegister(Register.AGC4, (byte) 0x10, i2CRepeaterControl); //High Threshold writeE4KRegister(Register.AGC5, (byte) 0x04, i2CRepeaterControl); //Low Threshold writeE4KRegister(Register.AGC6, (byte) 0x1A, i2CRepeaterControl); //LNA calibrate + loop rate @@ -496,12 +496,12 @@ public class E4KTunerController extends RTL2832TunerController // i2CRepeaterControl ); - /* Use automatic gain as a default */ + /* Use automatic gain as a default */ setLNAGain(E4KLNAGain.AUTOMATIC, i2CRepeaterControl); setMixerGain(E4KMixerGain.AUTOMATIC, i2CRepeaterControl); setEnhanceGain(E4KEnhanceGain.AUTOMATIC, i2CRepeaterControl); - /* Set IF gain stages */ + /* Set IF gain stages */ setIFStage1Gain(IFStage1Gain.GAIN_PLUS6, i2CRepeaterControl); setIFStage2Gain(IFStage2Gain.GAIN_PLUS0, i2CRepeaterControl); setIFStage3Gain(IFStage3Gain.GAIN_PLUS0, i2CRepeaterControl); @@ -509,7 +509,7 @@ public class E4KTunerController extends RTL2832TunerController setIFStage5Gain(IFStage5Gain.GAIN_PLUS9, i2CRepeaterControl); setIFStage6Gain(IFStage6Gain.GAIN_PLUS9, i2CRepeaterControl); - /* Set the most narrow filter we can possible use */ + /* Set the most narrow filter we can possible use */ setMixerFilter(MixerFilter.BW_1M9, i2CRepeaterControl); setRCFilter(RCFilter.BW_1M0, i2CRepeaterControl); setChannelFilter(ChannelFilter.BW_2M15, i2CRepeaterControl); @@ -519,7 +519,7 @@ public class E4KTunerController extends RTL2832TunerController // /* Disable DC detector */ // setDCRangeDetectorEnabled( false, i2CRepeaterControl ); - /* Disable time variant DC correction */ + /* Disable time variant DC correction */ writeMaskedE4KRegister(Register.DC5, (byte) 0x03, (byte) 0, i2CRepeaterControl); writeMaskedE4KRegister(Register.DCTIME1, (byte) 0x03, (byte) 0, i2CRepeaterControl); writeMaskedE4KRegister(Register.DCTIME2, (byte) 0x03, (byte) 0, i2CRepeaterControl); @@ -541,7 +541,7 @@ public class E4KTunerController extends RTL2832TunerController enableI2CRepeater(mDeviceHandle, true); } - /* Capture current gain settings to reapply at the end */ + /* Capture current gain settings to reapply at the end */ IFStage1Gain stage1 = getIFStage1Gain(i2CRepeaterControl); IFStage2Gain stage2 = getIFStage2Gain(i2CRepeaterControl); IFStage3Gain stage3 = getIFStage3Gain(i2CRepeaterControl); @@ -549,15 +549,15 @@ public class E4KTunerController extends RTL2832TunerController IFStage5Gain stage5 = getIFStage5Gain(i2CRepeaterControl); IFStage6Gain stage6 = getIFStage6Gain(i2CRepeaterControl); - /* Set Mixer gain to manual using current auto Mixer gain setting */ + /* Set Mixer gain to manual using current auto Mixer gain setting */ E4KMixerGain mixerGain = getMixerGain(i2CRepeaterControl); setMixerGain(mixerGain, i2CRepeaterControl); - /* Set LNA gain to manual using current auto LNA gain setting */ + /* Set LNA gain to manual using current auto LNA gain setting */ E4KLNAGain lnaGain = getLNAGain(i2CRepeaterControl); setLNAGain(lnaGain, i2CRepeaterControl); - /* Set IF Stage 2 - 6 gains to maximum */ + /* Set IF Stage 2 - 6 gains to maximum */ setIFStage2Gain(IFStage2Gain.GAIN_PLUS9, i2CRepeaterControl); setIFStage3Gain(IFStage3Gain.GAIN_PLUS9, i2CRepeaterControl); setIFStage4Gain(IFStage4Gain.GAIN_PLUS2B, i2CRepeaterControl); @@ -570,17 +570,17 @@ public class E4KTunerController extends RTL2832TunerController */ for(DCGainCombination combo : DCGainCombination.values()) { - /* Set mixer and stage1 gain values */ + /* Set mixer and stage1 gain values */ setMixerGain(combo.getMixerGain(), i2CRepeaterControl); setIFStage1Gain(combo.getIFStage1Gain(), i2CRepeaterControl); - /* Turn on the DC range detector */ + /* Turn on the DC range detector */ setDCRangeDetectorEnabled(true, i2CRepeaterControl); - /* Calibrate the DC value */ + /* Calibrate the DC value */ writeE4KRegister(Register.DC1, (byte) 0x01, i2CRepeaterControl); - /* Get the I/Q offset and range values */ + /* Get the I/Q offset and range values */ byte offsetI = (byte) (readE4KRegister(Register.DC2, i2CRepeaterControl) & 0x3F); byte offsetQ = (byte) (readE4KRegister(Register.DC3, @@ -590,7 +590,7 @@ public class E4KTunerController extends RTL2832TunerController byte rangeI = (byte) (range & 0x3); byte rangeQ = (byte) ((range >> 4) & 0x3); - /* Write the offset and range values to the lookup table */ + /* Write the offset and range values to the lookup table */ writeE4KRegister(combo.getIRegister(), (byte) (offsetI | (rangeI << 6)), i2CRepeaterControl); @@ -735,7 +735,7 @@ public class E4KTunerController extends RTL2832TunerController AGC7_MIX_GAIN_MANUAL, localI2CRepeaterControl); - /* Set the desired manual gain setting */ + /* Set the desired manual gain setting */ writeMaskedE4KRegister(E4KMixerGain.getRegister(), E4KMixerGain.getMask(), gain.getValue(), @@ -945,7 +945,7 @@ public class E4KTunerController extends RTL2832TunerController Band band = Band.fromFrequency(frequency); - /* Set the bias */ + /* Set the bias */ switch(band) { case UHF: @@ -1018,8 +1018,8 @@ public class E4KTunerController extends RTL2832TunerController } /* - * Performs magic initialization ... and that's all we need to know, I guess - */ + * Performs magic initialization ... and that's all we need to know, I guess + */ private void magicInit(boolean controlI2CRepeater) throws UsbException { if(controlI2CRepeater) @@ -1058,7 +1058,7 @@ public class E4KTunerController extends RTL2832TunerController { int temp = readE4KRegister(register, controlI2CRepeater); - /* If the register is not set to the masked value, then change it */ + /* If the register is not set to the masked value, then change it */ if((byte) (temp & mask) != value) { writeE4KRegister(register, @@ -1207,7 +1207,7 @@ public class E4KTunerController extends RTL2832TunerController } /* Returns the PLL setting with the closest frequency that is greater - * than the frequency argument */ + * than the frequency argument */ public static PLL fromFrequency(long frequency) { for(PLL pll : values()) @@ -1288,7 +1288,7 @@ public class E4KTunerController extends RTL2832TunerController M1720(14, 1710000000, 1735000000), /* Note: max frequency is currently limited by the max integer value - * and we set the max frequency for this filter to 2147 MHz */ + * and we set the max frequency for this filter to 2147 MHz */ M1750(15, 1735000000, 2147000000); private int mValue; @@ -1446,7 +1446,7 @@ public class E4KTunerController extends RTL2832TunerController } } - /* default */ + /* default */ return MixerFilter.BW_27M0; } @@ -1557,7 +1557,7 @@ public class E4KTunerController extends RTL2832TunerController } } - /* default */ + /* default */ return RCFilter.BW_21M4; } @@ -1684,7 +1684,7 @@ public class E4KTunerController extends RTL2832TunerController } } - /* default */ + /* default */ return ChannelFilter.BW_5M50; } @@ -2509,4 +2509,4 @@ public class E4KTunerController extends RTL2832TunerController return (byte) mValue; } } -} +} \ No newline at end of file diff --git a/src/main/java/io/github/dsheirer/source/tuner/rtl/e4k/E4KTunerEditor.java b/src/main/java/io/github/dsheirer/source/tuner/rtl/e4k/E4KTunerEditor.java index cdc64bab..394d785d 100644 --- a/src/main/java/io/github/dsheirer/source/tuner/rtl/e4k/E4KTunerEditor.java +++ b/src/main/java/io/github/dsheirer/source/tuner/rtl/e4k/E4KTunerEditor.java @@ -1,19 +1,17 @@ /******************************************************************************* - * SDR Trunk - * Copyright (C) 2014-2016 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 + * sdr-trunk + * 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 io.github.dsheirer.source.tuner.rtl.e4k; @@ -24,6 +22,7 @@ import io.github.dsheirer.source.tuner.configuration.TunerConfigurationEvent; import io.github.dsheirer.source.tuner.configuration.TunerConfigurationModel; import io.github.dsheirer.source.tuner.rtl.RTL2832Tuner; import io.github.dsheirer.source.tuner.rtl.RTL2832TunerController; +import io.github.dsheirer.source.tuner.rtl.RTL2832TunerController.SampleRate; import io.github.dsheirer.source.tuner.rtl.e4k.E4KTunerController.E4KEnhanceGain; import io.github.dsheirer.source.tuner.rtl.e4k.E4KTunerController.E4KGain; import io.github.dsheirer.source.tuner.rtl.e4k.E4KTunerController.E4KLNAGain; @@ -33,7 +32,16 @@ import org.slf4j.Logger; import org.slf4j.LoggerFactory; import org.usb4java.LibUsbException; -import javax.swing.*; +import javax.swing.JButton; +import javax.swing.JComboBox; +import javax.swing.JLabel; +import javax.swing.JOptionPane; +import javax.swing.JSeparator; +import javax.swing.JSpinner; +import javax.swing.JTextField; +import javax.swing.SpinnerModel; +import javax.swing.SpinnerNumberModel; +import javax.swing.SwingConstants; import javax.swing.event.ChangeEvent; import javax.swing.event.ChangeListener; import javax.usb.UsbException; @@ -46,28 +54,28 @@ import java.text.DecimalFormat; public class E4KTunerEditor extends TunerConfigurationEditor { private final static Logger mLog = LoggerFactory.getLogger( E4KTunerEditor.class ); - - private static final long serialVersionUID = 1L; - - private JTextField mConfigurationName; - private JButton mTunerInfo; - private JComboBox mComboSampleRate; - private JSpinner mFrequencyCorrection; - private JComboBox mComboMasterGain; - private JComboBox mComboMixerGain; - private JComboBox mComboLNAGain; - private JComboBox mComboEnhanceGain; - private E4KTunerController mController; - private boolean mLoading; + private static final long serialVersionUID = 1L; - public E4KTunerEditor(TunerConfigurationModel tunerConfigurationModel, RTL2832Tuner tuner ) - { - super( tunerConfigurationModel ); - mController = (E4KTunerController)tuner.getController(); - - init(); - } + private JTextField mConfigurationName; + private JButton mTunerInfo; + private JComboBox mComboSampleRate; + private JSpinner mFrequencyCorrection; + private JComboBox mComboMasterGain; + private JComboBox mComboMixerGain; + private JComboBox mComboLNAGain; + private JComboBox mComboEnhanceGain; + + private E4KTunerController mController; + private boolean mLoading; + + public E4KTunerEditor( TunerConfigurationModel tunerConfigurationModel, RTL2832Tuner tuner ) + { + super( tunerConfigurationModel ); + mController = (E4KTunerController)tuner.getController(); + + init(); + } private E4KTunerConfiguration getConfiguration() { @@ -75,15 +83,15 @@ public class E4KTunerEditor extends TunerConfigurationEditor { return (E4KTunerConfiguration)getItem(); } - + return null; } - private void init() - { - setLayout( new MigLayout( "fill,wrap 4", "[right][grow,fill][right][grow,fill]", - "[][][][][][][][grow]" ) ); - + private void init() + { + setLayout( new MigLayout( "fill,wrap 4", "[right][grow,fill][right][grow,fill]", + "[][][][][][][][grow]" ) ); + add( new JLabel( "E4000 Tuner Configuration" ), "span,align center" ); /** @@ -92,19 +100,19 @@ public class E4KTunerEditor extends TunerConfigurationEditor add( new JLabel( "Name:" ) ); mConfigurationName = new JTextField(); mConfigurationName.setEnabled( false ); - mConfigurationName.addFocusListener( new FocusListener() + mConfigurationName.addFocusListener( new FocusListener() { @Override - public void focusLost( FocusEvent e ) - { + public void focusLost( FocusEvent e ) + { save(); - } + } @Override - public void focusGained( FocusEvent e ) {} + public void focusGained( FocusEvent e ) {} } ); - + add( mConfigurationName, "span 2" ); - + mTunerInfo = new JButton( "Tuner Info" ); mTunerInfo.setEnabled( false ); mTunerInfo.addActionListener( new ActionListener() @@ -112,223 +120,223 @@ public class E4KTunerEditor extends TunerConfigurationEditor @Override public void actionPerformed( ActionEvent e ) { - JOptionPane.showMessageDialog( E4KTunerEditor.this, getTunerInfo(), - "Tuner Info", JOptionPane.INFORMATION_MESSAGE ); + JOptionPane.showMessageDialog( E4KTunerEditor.this, getTunerInfo(), + "Tuner Info", JOptionPane.INFORMATION_MESSAGE ); } } ); add( mTunerInfo ); - mComboSampleRate = new JComboBox<>( RTL2832TunerController.SampleRate.values() ); - mComboSampleRate.setEnabled( false ); - mComboSampleRate.addActionListener( new ActionListener() - { + mComboSampleRate = new JComboBox<>( SampleRate.values() ); + mComboSampleRate.setEnabled( false ); + mComboSampleRate.addActionListener( new ActionListener() + { @Override - public void actionPerformed( ActionEvent e ) - { - RTL2832TunerController.SampleRate sampleRate = (RTL2832TunerController.SampleRate)mComboSampleRate.getSelectedItem(); - + public void actionPerformed( ActionEvent e ) + { + SampleRate sampleRate = (SampleRate)mComboSampleRate.getSelectedItem(); + try - { + { mController.setSampleRate( sampleRate ); - save(); - } - catch ( SourceException | LibUsbException eSampleRate ) - { - JOptionPane.showMessageDialog( E4KTunerEditor.this, - "E4K Tuner Controller - couldn't apply the sample rate setting [" + - sampleRate.getLabel() + "] " + eSampleRate.getLocalizedMessage() ); - - mLog.error( "E4K Tuner Controller - couldn't apply sample rate setting [" + - sampleRate.getLabel() + "]", eSampleRate ); - } - } - } ); - add( new JLabel( "Sample Rate:" ) ); - add( mComboSampleRate ); + save(); + } + catch ( SourceException | LibUsbException eSampleRate ) + { + JOptionPane.showMessageDialog( E4KTunerEditor.this, + "E4K Tuner Controller - couldn't apply the sample rate setting [" + + sampleRate.getLabel() + "] " + eSampleRate.getLocalizedMessage() ); - SpinnerModel model = new SpinnerNumberModel( 0.0, //initial value - -1000.0, //min - 1000.0, //max - 0.1 ); //step + mLog.error( "E4K Tuner Controller - couldn't apply sample rate setting [" + + sampleRate.getLabel() + "]", eSampleRate ); + } + } + } ); + add( new JLabel( "Sample Rate:" ) ); + add( mComboSampleRate ); - mFrequencyCorrection = new JSpinner( model ); - mFrequencyCorrection.setEnabled( false ); - - JSpinner.NumberEditor editor = - (JSpinner.NumberEditor)mFrequencyCorrection.getEditor(); - - DecimalFormat format = editor.getFormat(); - format.setMinimumFractionDigits( 1 ); - editor.getTextField().setHorizontalAlignment( SwingConstants.CENTER ); + SpinnerModel model = new SpinnerNumberModel( 0.0, //initial value + -1000.0, //min + 1000.0, //max + 0.1 ); //step - mFrequencyCorrection.addChangeListener( new ChangeListener() - { + mFrequencyCorrection = new JSpinner( model ); + mFrequencyCorrection.setEnabled( false ); + + JSpinner.NumberEditor editor = + (JSpinner.NumberEditor)mFrequencyCorrection.getEditor(); + + DecimalFormat format = editor.getFormat(); + format.setMinimumFractionDigits( 1 ); + editor.getTextField().setHorizontalAlignment( SwingConstants.CENTER ); + + mFrequencyCorrection.addChangeListener( new ChangeListener() + { @Override - public void stateChanged( ChangeEvent e ) - { + public void stateChanged( ChangeEvent e ) + { final double value = ((SpinnerNumberModel)mFrequencyCorrection - .getModel()).getNumber().doubleValue(); + .getModel()).getNumber().doubleValue(); try - { - mController.setFrequencyCorrection( value ); - save(); - } - catch ( SourceException e1 ) - { - JOptionPane.showMessageDialog( E4KTunerEditor.this, "E4K Tuner Controller - " - + "couldn't apply frequency correction value: " + value + e1.getLocalizedMessage() ); - - mLog.error( "E4K Tuner Controller - couldn't apply frequency correction value:" - + value, e1 ); - } - } - } ); - - add( new JLabel( "PPM:" ) ); - add( mFrequencyCorrection ); + { + mController.setFrequencyCorrection( value ); + save(); + } + catch ( SourceException e1 ) + { + JOptionPane.showMessageDialog( E4KTunerEditor.this, "E4K Tuner Controller - " + + "couldn't apply frequency correction value: " + value + e1.getLocalizedMessage() ); - add( new JSeparator( JSeparator.HORIZONTAL ), "span,grow" ); - add( new JLabel( "Gain" ), "wrap" ); - - mComboMasterGain = new JComboBox( E4KGain.values() ); - mComboMasterGain.setEnabled( false ); - mComboMasterGain.addActionListener( new ActionListener() - { + mLog.error( "E4K Tuner Controller - couldn't apply frequency correction value:" + + value, e1 ); + } + } + } ); + + add( new JLabel( "PPM:" ) ); + add( mFrequencyCorrection ); + + add( new JSeparator( JSeparator.HORIZONTAL ), "span,grow" ); + add( new JLabel( "Gain" ), "wrap" ); + + mComboMasterGain = new JComboBox( E4KGain.values() ); + mComboMasterGain.setEnabled( false ); + mComboMasterGain.addActionListener( new ActionListener() + { @Override - public void actionPerformed( ActionEvent arg0 ) - { + public void actionPerformed( ActionEvent arg0 ) + { try - { + { E4KGain gain = (E4KGain)mComboMasterGain.getSelectedItem(); - - mController.setGain( (E4KGain)mComboMasterGain.getSelectedItem(), true ); - - if( gain == E4KGain.MANUAL ) - { - mComboMixerGain.setSelectedItem( mController.getMixerGain( true ) ); - mComboMixerGain.setEnabled( true ); - - mComboLNAGain.setSelectedItem( mController.getLNAGain( true ) ); - mComboLNAGain.setEnabled( true ); - mComboEnhanceGain.setSelectedItem( mController.getEnhanceGain( true ) ); - mComboEnhanceGain.setEnabled( true ); - } - else - { - mComboMixerGain.setEnabled( false ); - mComboMixerGain.setSelectedItem( gain.getMixerGain() ); + mController.setGain( (E4KGain)mComboMasterGain.getSelectedItem(), true ); - mComboLNAGain.setEnabled( false ); - mComboLNAGain.setSelectedItem( gain.getLNAGain() ); + if( gain == E4KGain.MANUAL ) + { + mComboMixerGain.setSelectedItem( mController.getMixerGain( true ) ); + mComboMixerGain.setEnabled( true ); - mComboEnhanceGain.setEnabled( false ); - mComboEnhanceGain.setSelectedItem( gain.getEnhanceGain() ); - } - - save(); - } - catch ( UsbException e ) - { - JOptionPane.showMessageDialog( E4KTunerEditor.this, "E4K Tuner Controller - " - + "couldn't apply the gain setting - " + e.getLocalizedMessage() ); - - mLog.error( "E4K Tuner Controller - couldn't apply gain setting", e ); - } - } - } ); - - mComboMasterGain.setToolTipText( "Select AUTOMATIC for auto " - + "gain, MANUAL to enable
independent control of " - + "Mixer, LNA and Enhance gain
" - + "settings, or one of the individual gain settings for
" - + "semi-manual gain control" ); - add( new JLabel( "Master:" ) ); - add( mComboMasterGain ); + mComboLNAGain.setSelectedItem( mController.getLNAGain( true ) ); + mComboLNAGain.setEnabled( true ); - mComboMixerGain = new JComboBox( E4KMixerGain.values() ); - mComboMixerGain.addActionListener( new ActionListener() - { + mComboEnhanceGain.setSelectedItem( mController.getEnhanceGain( true ) ); + mComboEnhanceGain.setEnabled( true ); + } + else + { + mComboMixerGain.setEnabled( false ); + mComboMixerGain.setSelectedItem( gain.getMixerGain() ); + + mComboLNAGain.setEnabled( false ); + mComboLNAGain.setSelectedItem( gain.getLNAGain() ); + + mComboEnhanceGain.setEnabled( false ); + mComboEnhanceGain.setSelectedItem( gain.getEnhanceGain() ); + } + + save(); + } + catch ( UsbException e ) + { + JOptionPane.showMessageDialog( E4KTunerEditor.this, "E4K Tuner Controller - " + + "couldn't apply the gain setting - " + e.getLocalizedMessage() ); + + mLog.error( "E4K Tuner Controller - couldn't apply gain setting", e ); + } + } + } ); + + mComboMasterGain.setToolTipText( "Select AUTOMATIC for auto " + + "gain, MANUAL to enable
independent control of " + + "Mixer, LNA and Enhance gain
" + + "settings, or one of the individual gain settings for
" + + "semi-manual gain control" ); + add( new JLabel( "Master:" ) ); + add( mComboMasterGain ); + + mComboMixerGain = new JComboBox( E4KMixerGain.values() ); + mComboMixerGain.addActionListener( new ActionListener() + { @Override - public void actionPerformed( ActionEvent arg0 ) - { + public void actionPerformed( ActionEvent arg0 ) + { try - { + { E4KMixerGain mixerGain = (E4KMixerGain)mComboMixerGain.getSelectedItem(); mController.setMixerGain( mixerGain, true ); - save(); - } - catch ( UsbException e ) - { - JOptionPane.showMessageDialog( E4KTunerEditor.this, "E4K Tuner Controller - " - + "couldn't apply the mixer gain setting - " + e.getLocalizedMessage() ); - - mLog.error( "E4K Tuner Controller - couldn't apply mixer gain setting", e ); - } - } - } ); - mComboMixerGain.setToolTipText( "Mixer Gain. Set master gain " - + "to MASTER to enable adjustment" ); - mComboMixerGain.setEnabled( false ); - add( new JLabel( "Mixer:" ) ); - add( mComboMixerGain ); + save(); + } + catch ( UsbException e ) + { + JOptionPane.showMessageDialog( E4KTunerEditor.this, "E4K Tuner Controller - " + + "couldn't apply the mixer gain setting - " + e.getLocalizedMessage() ); - mComboLNAGain = new JComboBox( E4KLNAGain.values() ); - mComboLNAGain.addActionListener( new ActionListener() - { + mLog.error( "E4K Tuner Controller - couldn't apply mixer gain setting", e ); + } + } + } ); + mComboMixerGain.setToolTipText( "Mixer Gain. Set master gain " + + "to MASTER to enable adjustment" ); + mComboMixerGain.setEnabled( false ); + add( new JLabel( "Mixer:" ) ); + add( mComboMixerGain ); + + mComboLNAGain = new JComboBox( E4KLNAGain.values() ); + mComboLNAGain.addActionListener( new ActionListener() + { @Override - public void actionPerformed( ActionEvent arg0 ) - { + public void actionPerformed( ActionEvent arg0 ) + { try - { + { E4KLNAGain lnaGain = (E4KLNAGain)mComboLNAGain.getSelectedItem(); mController.setLNAGain( lnaGain, true ); - save(); - } - catch ( UsbException e ) - { - JOptionPane.showMessageDialog( E4KTunerEditor.this, "E4K Tuner Controller - " - + "couldn't apply the LNA gain setting - " + e.getLocalizedMessage() ); - - mLog.error( "E4K Tuner Controller - couldn't apply LNA gain setting - ", e ); - } - } - } ); - mComboLNAGain.setToolTipText( "LNA Gain. Set master gain " - + "to MANUAL to enable adjustment" ); - mComboLNAGain.setEnabled( false ); - add( new JLabel( "LNA:" ) ); - add( mComboLNAGain ); + save(); + } + catch ( UsbException e ) + { + JOptionPane.showMessageDialog( E4KTunerEditor.this, "E4K Tuner Controller - " + + "couldn't apply the LNA gain setting - " + e.getLocalizedMessage() ); - mComboEnhanceGain = new JComboBox( E4KEnhanceGain.values() ); - mComboEnhanceGain.addActionListener( new ActionListener() - { + mLog.error( "E4K Tuner Controller - couldn't apply LNA gain setting - ", e ); + } + } + } ); + mComboLNAGain.setToolTipText( "LNA Gain. Set master gain " + + "to MANUAL to enable adjustment" ); + mComboLNAGain.setEnabled( false ); + add( new JLabel( "LNA:" ) ); + add( mComboLNAGain ); + + mComboEnhanceGain = new JComboBox( E4KEnhanceGain.values() ); + mComboEnhanceGain.addActionListener( new ActionListener() + { @Override - public void actionPerformed( ActionEvent arg0 ) - { + public void actionPerformed( ActionEvent arg0 ) + { try - { + { E4KEnhanceGain enhance = (E4KEnhanceGain)mComboEnhanceGain.getSelectedItem(); mController.setEnhanceGain( enhance, true ); - save(); - } - catch ( UsbException e ) - { - JOptionPane.showMessageDialog( E4KTunerEditor.this, "E4K Tuner Controller - " - + "couldn't apply the enhance gain setting - " + e.getLocalizedMessage() ); - - mLog.error( "E4K Tuner Controller - couldn't apply enhance gain setting", e ); - } - } - } ); - mComboEnhanceGain.setToolTipText( "Enhance Gain. Set master gain " - + "to MANUAL to enable adjustment" ); - mComboEnhanceGain.setEnabled( false ); - add( new JLabel( "Enhance:" ) ); - add( mComboEnhanceGain ); - } - + save(); + } + catch ( UsbException e ) + { + JOptionPane.showMessageDialog( E4KTunerEditor.this, "E4K Tuner Controller - " + + "couldn't apply the enhance gain setting - " + e.getLocalizedMessage() ); + + mLog.error( "E4K Tuner Controller - couldn't apply enhance gain setting", e ); + } + } + } ); + mComboEnhanceGain.setToolTipText( "Enhance Gain. Set master gain " + + "to MANUAL to enable adjustment" ); + mComboEnhanceGain.setEnabled( false ); + add( new JLabel( "Enhance:" ) ); + add( mComboEnhanceGain ); + } + /** * Sets each of the tuner configuration controls to the enabled argument state */ @@ -338,51 +346,51 @@ public class E4KTunerEditor extends TunerConfigurationEditor { mConfigurationName.setEnabled( enabled ); } - + if( mTunerInfo.isEnabled() != enabled ) { mTunerInfo.setEnabled( enabled ); } - + if( mFrequencyCorrection.isEnabled() != enabled ) { mFrequencyCorrection.setEnabled( enabled ); } - if( mComboSampleRate.isEnabled() != enabled ) - { - mComboSampleRate.setEnabled( enabled ); - } + if( mComboSampleRate.isEnabled() != enabled ) + { + mComboSampleRate.setEnabled( enabled ); + } - if( mComboMasterGain.isEnabled() != enabled ) - { - mComboMasterGain.setEnabled( enabled ); - } + if( mComboMasterGain.isEnabled() != enabled ) + { + mComboMasterGain.setEnabled( enabled ); + } - if( mComboMixerGain.isEnabled() != enabled ) - { - mComboMixerGain.setEnabled( enabled ); - } - - if( mComboLNAGain.isEnabled() != enabled ) - { - mComboLNAGain.setEnabled( enabled ); - } - - if( mComboEnhanceGain.isEnabled() != enabled ) - { - mComboEnhanceGain.setEnabled( enabled ); - } + if( mComboMixerGain.isEnabled() != enabled ) + { + mComboMixerGain.setEnabled( enabled ); + } + + if( mComboLNAGain.isEnabled() != enabled ) + { + mComboLNAGain.setEnabled( enabled ); + } + + if( mComboEnhanceGain.isEnabled() != enabled ) + { + mComboEnhanceGain.setEnabled( enabled ); + } } - + private String getTunerInfo() { StringBuilder sb = new StringBuilder(); RTL2832TunerController.Descriptor descriptor = mController.getDescriptor(); - + sb.append( "

RTL-2832 with E4000 Tuner

" ); - + if( descriptor == null ) { sb.append( "No EEPROM Descriptor Available" ); @@ -422,27 +430,27 @@ public class E4KTunerEditor extends TunerConfigurationEditor @Override public void save() { - if( hasItem() && !mLoading ) - { - E4KTunerConfiguration config = getConfiguration(); + if( hasItem() && !mLoading ) + { + E4KTunerConfiguration config = getConfiguration(); - config.setName( mConfigurationName.getText() ); + config.setName( mConfigurationName.getText() ); - config.setSampleRate( (RTL2832TunerController.SampleRate)mComboSampleRate.getSelectedItem() ); + config.setSampleRate( (SampleRate)mComboSampleRate.getSelectedItem() ); - double value = ((SpinnerNumberModel)mFrequencyCorrection - .getModel()).getNumber().doubleValue(); - config.setFrequencyCorrection( value ); + double value = ((SpinnerNumberModel)mFrequencyCorrection + .getModel()).getNumber().doubleValue(); + config.setFrequencyCorrection( value ); - config.setMasterGain( (E4KGain)mComboMasterGain.getSelectedItem() ); - config.setMixerGain( (E4KMixerGain)mComboMixerGain.getSelectedItem() ); + config.setMasterGain( (E4KGain)mComboMasterGain.getSelectedItem() ); + config.setMixerGain( (E4KMixerGain)mComboMixerGain.getSelectedItem() ); config.setLNAGain( (E4KLNAGain)mComboLNAGain.getSelectedItem() ); config.setEnhanceGain( (E4KEnhanceGain)mComboEnhanceGain.getSelectedItem() ); - + getTunerConfigurationModel().broadcast( new TunerConfigurationEvent( - getConfiguration(), TunerConfigurationEvent.Event.CHANGE ) ); - } - + getConfiguration(), TunerConfigurationEvent.Event.CHANGE ) ); + } + } @Override @@ -456,18 +464,18 @@ public class E4KTunerEditor extends TunerConfigurationEditor if( hasItem() ) { E4KTunerConfiguration config = getConfiguration(); - + if( tunerConfiguration.isAssigned() ) { - setControlsEnabled( true ); + setControlsEnabled( true ); mConfigurationName.setText( config.getName() ); - mFrequencyCorrection.setValue( config.getFrequencyCorrection() ); - mComboSampleRate.setSelectedItem( config.getSampleRate() ); - mComboMasterGain.setSelectedItem( config.getMasterGain() ); - mComboMixerGain.setSelectedItem( config.getMixerGain() ); - mComboLNAGain.setSelectedItem( config.getLNAGain() ); - mComboEnhanceGain.setSelectedItem( config.getEnhanceGain() ); + mFrequencyCorrection.setValue( config.getFrequencyCorrection() ); + mComboSampleRate.setSelectedItem( config.getSampleRate() ); + mComboMasterGain.setSelectedItem( config.getMasterGain() ); + mComboMixerGain.setSelectedItem( config.getMixerGain() ); + mComboLNAGain.setSelectedItem( config.getLNAGain() ); + mComboEnhanceGain.setSelectedItem( config.getEnhanceGain() ); } else { @@ -480,7 +488,7 @@ public class E4KTunerEditor extends TunerConfigurationEditor setControlsEnabled( false ); mConfigurationName.setText( "" ); } - + mLoading = false; } -} +} \ No newline at end of file diff --git a/src/main/java/io/github/dsheirer/source/tuner/rtl/r820t/R820TTunerController.java b/src/main/java/io/github/dsheirer/source/tuner/rtl/r820t/R820TTunerController.java index bda2e908..c440ba04 100644 --- a/src/main/java/io/github/dsheirer/source/tuner/rtl/r820t/R820TTunerController.java +++ b/src/main/java/io/github/dsheirer/source/tuner/rtl/r820t/R820TTunerController.java @@ -1,6 +1,6 @@ /******************************************************************************* * SDR Trunk - * Copyright (C) 2014-2016 Dennis Sheirer + * Copyright (C) 2014-2017 Dennis Sheirer * * Java port of librtlsdr * @@ -32,14 +32,13 @@ import org.usb4java.Device; import org.usb4java.DeviceDescriptor; import org.usb4java.LibUsbException; -import javax.swing.*; +import javax.swing.JPanel; import javax.usb.UsbException; import java.nio.ByteBuffer; public class R820TTunerController extends RTL2832TunerController { - private final static Logger mLog = - LoggerFactory.getLogger(R820TTunerController.class); + private final static Logger mLog = LoggerFactory.getLogger(R820TTunerController.class); public static final long MIN_FREQUENCY = 3180000; public static final long MAX_FREQUENCY = 1782030000; @@ -188,14 +187,14 @@ public class R820TTunerController extends RTL2832TunerController /* Set intermediate frequency to R820T IF frequency */ setIFFrequency(R820T_IF_FREQUENCY); - /* Enable spectrum inversion */ + /* Enable spectrum inversion */ writeDemodRegister(mDeviceHandle, Page.ONE, (short) 0x15, (short) 0x01, 1); - - /* Set default i/q path */ + + /* Set default i/q path */ writeDemodRegister(mDeviceHandle, Page.ZERO, (short) 0x06, @@ -215,23 +214,23 @@ public class R820TTunerController extends RTL2832TunerController { FrequencyRange range = FrequencyRange.getRangeForFrequency(frequency); - /* Set open drain */ + /* Set open drain */ writeR820TRegister(Register.DRAIN, range.getOpenDrain(), controlI2C); - /* RF_MUX, Polymux */ + /* RF_MUX, Polymux */ writeR820TRegister(Register.RF_POLY_MUX, range.getRFMuxPolyMux(), controlI2C); - - /* TF Band */ + + /* TF Band */ writeR820TRegister(Register.TF_BAND, range.getTFC(), controlI2C); - - /* XTAL CAP & Drive */ + + /* XTAL CAP & Drive */ writeR820TRegister(Register.PLL_XTAL_CAPACITOR_AND_DRIVE, range.getXTALHighCap0P(), controlI2C); - - /* Register 8 - what is it? */ + + /* Register 8 - what is it? */ writeR820TRegister(Register.UNKNOWN_REGISTER_8, (byte) 0x00, controlI2C); - - /* Register 9 - what is it? */ + + /* Register 9 - what is it? */ writeR820TRegister(Register.UNKNOWN_REGISTER_9, (byte) 0x00, controlI2C); } @@ -240,7 +239,7 @@ public class R820TTunerController extends RTL2832TunerController */ public void init() throws SourceException { - /* Initialize the super class to open and claim the usb interface*/ + /* Initialize the super class to open and claim the usb interface*/ super.init(); try @@ -266,24 +265,24 @@ public class R820TTunerController extends RTL2832TunerController */ public void initTuner(boolean controlI2C) throws UsbException { - /* Disable zero IF mode */ + /* Disable zero IF mode */ writeDemodRegister(mDeviceHandle, Page.ONE, (short) 0xB1, (short) 0x1A, 1); - - /* Only enable in-phase ADC input */ + + /* Only enable in-phase ADC input */ writeDemodRegister(mDeviceHandle, Page.ZERO, (short) 0x08, (short) 0x4D, 1); - - /* Set intermediate frequency to R820T IF frequency (3.57 MHz) */ + + /* Set intermediate frequency to R820T IF frequency (3.57 MHz) */ setIFFrequency(R820T_IF_FREQUENCY); - /* Enable spectrum inversion */ + /* Enable spectrum inversion */ writeDemodRegister(mDeviceHandle, Page.ONE, (short) 0x15, @@ -303,12 +302,12 @@ public class R820TTunerController extends RTL2832TunerController */ private void setTVStandard(boolean controlI2C) throws UsbException { - /* Init Flag & Xtal check Result */ + /* Init Flag & Xtal check Result */ writeR820TRegister(Register.XTAL_CHECK, (byte) 0x00, controlI2C); - - /* Set version */ + + /* Set version */ writeR820TRegister(Register.VERSION, VERSION, controlI2C); - + /* LT Gain Test */ writeR820TRegister(Register.LNA_TOP, (byte) 0x00, controlI2C); @@ -326,7 +325,7 @@ public class R820TTunerController extends RTL2832TunerController writeR820TRegister(Register.PLL_XTAL_CAPACITOR, (byte) 0x00, controlI2C); setPLL(56000 * 1000, controlI2C); - + /* Start trigger */ writeR820TRegister(Register.CALIBRATION_TRIGGER, (byte) 0x10, controlI2C); @@ -349,36 +348,36 @@ public class R820TTunerController extends RTL2832TunerController { calibrationCode = 0; } - - /* Write calibration code */ + + /* Write calibration code */ byte filt_q = 0x10; writeR820TRegister(Register.FILTER_CALIBRATION_CODE, (byte) (calibrationCode | filt_q), controlI2C); - /* Set BW, Filter gain & HP Corner */ + /* Set BW, Filter gain & HP Corner */ writeR820TRegister(Register.BANDWIDTH_FILTER_GAIN_HIGHPASS_FILTER_CORNER, (byte) 0x6B, controlI2C); - /* Set Image_R */ + /* Set Image_R */ writeR820TRegister(Register.IMAGE_REVERSE, (byte) 0x00, controlI2C); - - /* Set filter_3db, V6MHz */ + + /* Set filter_3db, V6MHz */ writeR820TRegister(Register.FILTER_GAIN, (byte) 0x10, controlI2C); - /* Channel filter extension */ + /* Channel filter extension */ writeR820TRegister(Register.CHANNEL_FILTER_EXTENSION, (byte) 0x60, controlI2C); - /* Loop through */ + /* Loop through */ writeR820TRegister(Register.LOOP_THROUGH, (byte) 0x00, controlI2C); - /* Loop through attenuation */ + /* Loop through attenuation */ writeR820TRegister(Register.LOOP_THROUGH_ATTENUATION, (byte) 0x00, controlI2C); - - /* Filter extension widest */ + + /* Filter extension widest */ writeR820TRegister(Register.FILTER_EXTENSION_WIDEST, (byte) 0x00, controlI2C); - /* RF poly filter current */ + /* RF poly filter current */ writeR820TRegister(Register.RF_POLY_FILTER_CURRENT, (byte) 0x60, controlI2C); } @@ -404,7 +403,7 @@ public class R820TTunerController extends RTL2832TunerController private void systemFrequencySelect(long frequency, boolean controlI2C) throws UsbException { - /* LNA top? */ + /* LNA top? */ writeR820TRegister(Register.LNA_TOP2, (byte) 0xE5, controlI2C); byte mixer_top; @@ -431,8 +430,8 @@ public class R820TTunerController extends RTL2832TunerController writeR820TRegister(Register.LNA_VTH_L, (byte) 0x53, controlI2C); writeR820TRegister(Register.MIXER_VTH_L, (byte) 0x75, controlI2C); - - /* Air-In only for Astrometa */ + + /* Air-In only for Astrometa */ writeR820TRegister(Register.AIR_CABLE1_INPUT_SELECTOR, (byte) 0x00, controlI2C); writeR820TRegister(Register.CABLE2_INPUT_SELECTOR, (byte) 0x00, controlI2C); @@ -443,7 +442,7 @@ public class R820TTunerController extends RTL2832TunerController writeR820TRegister(Register.FILTER_CURRENT, (byte) 0x40, controlI2C); - /* if( type != TUNER_ANALOG_TV ) ... */ + /* if( type != TUNER_ANALOG_TV ) ... */ writeR820TRegister(Register.LNA_TOP, (byte) 0x00, controlI2C); @@ -457,10 +456,10 @@ public class R820TTunerController extends RTL2832TunerController writeR820TRegister(Register.MIXER_TOP2, mixer_top, controlI2C); - /* LNA discharge current */ + /* LNA discharge current */ writeR820TRegister(Register.LNA_DISCHARGE_CURRENT, (byte) 0x14, controlI2C); - - /* AGC clock 1 khz, external det1 cap 1u */ + + /* AGC clock 1 khz, external det1 cap 1u */ writeR820TRegister(Register.AGC_CLOCK, (byte) 0x20, controlI2C); } @@ -474,7 +473,7 @@ public class R820TTunerController extends RTL2832TunerController */ private void setPLL(long frequency, boolean controlI2C) throws UsbException { - /* Set reference divider to 0 */ + /* Set reference divider to 0 */ writeR820TRegister(Register.REFERENCE_DIVIDER_2, (byte) 0x00, controlI2C); /* Set PLL autotune to 128kHz */ @@ -525,7 +524,7 @@ public class R820TTunerController extends RTL2832TunerController * settings */ if(!isPLLLocked(controlI2C)) { - /* Increase VCO current */ + /* Increase VCO current */ writeR820TRegister(Register.VCO_CURRENT, (byte) 0x60, controlI2C); if(!isPLLLocked(controlI2C)) @@ -612,8 +611,8 @@ public class R820TTunerController extends RTL2832TunerController mShadowRegister[register.getRegister()] = value; -// Log.info( "R820T writing register " + -// String.format( "%02X", register.getRegister() ) + " value " + +// Log.info( "R820T writing register " + +// String.format( "%02X", register.getRegister() ) + " value " + // String.format( "%02X", value ) ); } @@ -1293,4 +1292,4 @@ public class R820TTunerController extends RTL2832TunerController "] must be in the range 0 - 31"); } } -} +} \ No newline at end of file diff --git a/src/main/java/io/github/dsheirer/source/tuner/rtl/r820t/R820TTunerEditor.java b/src/main/java/io/github/dsheirer/source/tuner/rtl/r820t/R820TTunerEditor.java index 6558271f..43fa49d2 100644 --- a/src/main/java/io/github/dsheirer/source/tuner/rtl/r820t/R820TTunerEditor.java +++ b/src/main/java/io/github/dsheirer/source/tuner/rtl/r820t/R820TTunerEditor.java @@ -24,12 +24,26 @@ import io.github.dsheirer.source.tuner.configuration.TunerConfigurationEvent; import io.github.dsheirer.source.tuner.configuration.TunerConfigurationModel; import io.github.dsheirer.source.tuner.rtl.RTL2832Tuner; import io.github.dsheirer.source.tuner.rtl.RTL2832TunerController; +import io.github.dsheirer.source.tuner.rtl.RTL2832TunerController.SampleRate; +import io.github.dsheirer.source.tuner.rtl.r820t.R820TTunerController.R820TGain; +import io.github.dsheirer.source.tuner.rtl.r820t.R820TTunerController.R820TLNAGain; +import io.github.dsheirer.source.tuner.rtl.r820t.R820TTunerController.R820TMixerGain; +import io.github.dsheirer.source.tuner.rtl.r820t.R820TTunerController.R820TVGAGain; import net.miginfocom.swing.MigLayout; import org.slf4j.Logger; import org.slf4j.LoggerFactory; import org.usb4java.LibUsbException; -import javax.swing.*; +import javax.swing.JButton; +import javax.swing.JComboBox; +import javax.swing.JLabel; +import javax.swing.JOptionPane; +import javax.swing.JSeparator; +import javax.swing.JSpinner; +import javax.swing.JTextField; +import javax.swing.SpinnerModel; +import javax.swing.SpinnerNumberModel; +import javax.swing.SwingConstants; import javax.swing.event.ChangeEvent; import javax.swing.event.ChangeListener; import javax.usb.UsbException; @@ -44,61 +58,61 @@ public class R820TTunerEditor extends TunerConfigurationEditor private final static Logger mLog = LoggerFactory.getLogger( R820TTunerEditor.class ); private static final long serialVersionUID = 1L; - private static final R820TTunerController.R820TGain DEFAULT_GAIN = R820TTunerController.R820TGain.GAIN_279; - - private R820TTunerController mController; + private static final R820TGain DEFAULT_GAIN = R820TGain.GAIN_279; - private JTextField mConfigurationName; - private JButton mTunerInfo; - private JComboBox mComboSampleRate; - private JSpinner mFrequencyCorrection; - private JComboBox mComboMasterGain; - private JComboBox mComboMixerGain; - private JComboBox mComboLNAGain; - private JComboBox mComboVGAGain; - private boolean mLoading; + private R820TTunerController mController; + + private JTextField mConfigurationName; + private JButton mTunerInfo; + private JComboBox mComboSampleRate; + private JSpinner mFrequencyCorrection; + private JComboBox mComboMasterGain; + private JComboBox mComboMixerGain; + private JComboBox mComboLNAGain; + private JComboBox mComboVGAGain; + private boolean mLoading; + + public R820TTunerEditor( TunerConfigurationModel tunerConfigurationModel, RTL2832Tuner tuner ) + { + super( tunerConfigurationModel ); + mController = (R820TTunerController)tuner.getController(); + + init(); + } - public R820TTunerEditor(TunerConfigurationModel tunerConfigurationModel, RTL2832Tuner tuner ) - { - super( tunerConfigurationModel ); - mController = (R820TTunerController)tuner.getController(); - - init(); - } - private R820TTunerConfiguration getConfiguration() { if( hasItem() ) { return (R820TTunerConfiguration)getItem(); } - + return null; } private void init() - { - setLayout( new MigLayout( "fill,wrap 4", "[right][grow,fill][right][grow,fill]", - "[][][][][][][][grow]" ) ); - + { + setLayout( new MigLayout( "fill,wrap 4", "[right][grow,fill][right][grow,fill]", + "[][][][][][][][grow]" ) ); + add( new JLabel( "R820T Tuner Configuration" ), "span,align center" ); mConfigurationName = new JTextField(); mConfigurationName.setEnabled( false ); - mConfigurationName.addFocusListener( new FocusListener() + mConfigurationName.addFocusListener( new FocusListener() { @Override - public void focusLost( FocusEvent e ) - { + public void focusLost( FocusEvent e ) + { save(); - } + } @Override - public void focusGained( FocusEvent e ) {} + public void focusGained( FocusEvent e ) {} } ); - + add( new JLabel( "Name:" ) ); add( mConfigurationName, "span 2" ); - + mTunerInfo = new JButton( "Tuner Info" ); mTunerInfo.setEnabled( false ); mTunerInfo.addActionListener( new ActionListener() @@ -106,279 +120,279 @@ public class R820TTunerEditor extends TunerConfigurationEditor @Override public void actionPerformed( ActionEvent e ) { - JOptionPane.showMessageDialog( R820TTunerEditor.this, getTunerInfo(), - "Tuner Info", JOptionPane.INFORMATION_MESSAGE ); + JOptionPane.showMessageDialog( R820TTunerEditor.this, getTunerInfo(), + "Tuner Info", JOptionPane.INFORMATION_MESSAGE ); } } ); add( mTunerInfo ); - - mComboSampleRate = new JComboBox<>( RTL2832TunerController.SampleRate.values() ); - mComboSampleRate.setEnabled( false ); - mComboSampleRate.addActionListener( new ActionListener() - { + + mComboSampleRate = new JComboBox<>( SampleRate.values() ); + mComboSampleRate.setEnabled( false ); + mComboSampleRate.addActionListener( new ActionListener() + { @Override - public void actionPerformed( ActionEvent e ) - { - RTL2832TunerController.SampleRate sampleRate = - (RTL2832TunerController.SampleRate)mComboSampleRate.getSelectedItem(); + public void actionPerformed( ActionEvent e ) + { + SampleRate sampleRate = + (SampleRate)mComboSampleRate.getSelectedItem(); try - { + { mController.setSampleRate( sampleRate ); - save(); - } - catch ( SourceException | LibUsbException eSampleRate ) - { - JOptionPane.showMessageDialog( - R820TTunerEditor.this, - "R820T Tuner Controller - couldn't apply the sample " - + "rate setting [" + sampleRate.getLabel() + "] " + - eSampleRate.getLocalizedMessage() ); - - mLog.error( "R820T Tuner Controller - couldn't apply sample " - + "rate setting [" + sampleRate.getLabel() + "]", - eSampleRate ); - } - } - } ); - add( new JLabel( "Sample Rate:" ) ); - add( mComboSampleRate ); + save(); + } + catch ( SourceException | LibUsbException eSampleRate ) + { + JOptionPane.showMessageDialog( + R820TTunerEditor.this, + "R820T Tuner Controller - couldn't apply the sample " + + "rate setting [" + sampleRate.getLabel() + "] " + + eSampleRate.getLocalizedMessage() ); - /** - * Frequency Correction - */ - SpinnerModel model = - new SpinnerNumberModel( 0.0, //initial value - -1000.0, //min - 1000.0, //max - 0.1 ); //step + mLog.error( "R820T Tuner Controller - couldn't apply sample " + + "rate setting [" + sampleRate.getLabel() + "]", + eSampleRate ); + } + } + } ); + add( new JLabel( "Sample Rate:" ) ); + add( mComboSampleRate ); - mFrequencyCorrection = new JSpinner( model ); - mFrequencyCorrection.setEnabled( false ); + /** + * Frequency Correction + */ + SpinnerModel model = + new SpinnerNumberModel( 0.0, //initial value + -1000.0, //min + 1000.0, //max + 0.1 ); //step - JSpinner.NumberEditor editor = - (JSpinner.NumberEditor)mFrequencyCorrection.getEditor(); - - DecimalFormat format = editor.getFormat(); - format.setMinimumFractionDigits( 1 ); - editor.getTextField().setHorizontalAlignment( SwingConstants.CENTER ); + mFrequencyCorrection = new JSpinner( model ); + mFrequencyCorrection.setEnabled( false ); - mFrequencyCorrection.addChangeListener( new ChangeListener() - { + JSpinner.NumberEditor editor = + (JSpinner.NumberEditor)mFrequencyCorrection.getEditor(); + + DecimalFormat format = editor.getFormat(); + format.setMinimumFractionDigits( 1 ); + editor.getTextField().setHorizontalAlignment( SwingConstants.CENTER ); + + mFrequencyCorrection.addChangeListener( new ChangeListener() + { @Override - public void stateChanged( ChangeEvent e ) - { + public void stateChanged( ChangeEvent e ) + { final double value = ((SpinnerNumberModel)mFrequencyCorrection - .getModel()).getNumber().doubleValue(); - - try + .getModel()).getNumber().doubleValue(); + + try { mController.setFrequencyCorrection( value ); - } - catch ( SourceException e1 ) + } + catch ( SourceException e1 ) { mLog.error( "Error setting frequency correction value", e1 ); } - - save(); - } - } ); - - add( new JLabel( "PPM:" ) ); - add( mFrequencyCorrection ); - add( new JSeparator( JSeparator.HORIZONTAL ), "span,grow" ); - - /** - * Gain Controls - */ - add( new JLabel( "Gain" ), "wrap" ); - - /* Master Gain Control */ - mComboMasterGain = new JComboBox( R820TTunerController.R820TGain.values() ); - mComboMasterGain.setEnabled( false ); - mComboMasterGain.addActionListener( new ActionListener() - { + save(); + } + } ); + + add( new JLabel( "PPM:" ) ); + add( mFrequencyCorrection ); + + add( new JSeparator( JSeparator.HORIZONTAL ), "span,grow" ); + + /** + * Gain Controls + */ + add( new JLabel( "Gain" ), "wrap" ); + + /* Master Gain Control */ + mComboMasterGain = new JComboBox( R820TGain.values() ); + mComboMasterGain.setEnabled( false ); + mComboMasterGain.addActionListener( new ActionListener() + { @Override - public void actionPerformed( ActionEvent arg0 ) - { + public void actionPerformed( ActionEvent arg0 ) + { try - { - R820TTunerController.R820TGain gain = (R820TTunerController.R820TGain)mComboMasterGain.getSelectedItem(); - - mController.setGain( (R820TTunerController.R820TGain)mComboMasterGain.getSelectedItem(), true ); - - if( gain == R820TTunerController.R820TGain.MANUAL ) - { - mComboMixerGain.setSelectedItem( gain.getMixerGain() ); - mComboMixerGain.setEnabled( true ); - - mComboLNAGain.setSelectedItem( gain.getLNAGain() ); - mComboLNAGain.setEnabled( true ); + { + R820TGain gain = (R820TGain)mComboMasterGain.getSelectedItem(); - mComboVGAGain.setSelectedItem( gain.getVGAGain() ); - mComboVGAGain.setEnabled( true ); - } - else - { - mComboMixerGain.setEnabled( false ); - mComboMixerGain.setSelectedItem( gain.getMixerGain() ); + mController.setGain( (R820TGain)mComboMasterGain.getSelectedItem(), true ); - mComboLNAGain.setEnabled( false ); - mComboLNAGain.setSelectedItem( gain.getLNAGain() ); + if( gain == R820TGain.MANUAL ) + { + mComboMixerGain.setSelectedItem( gain.getMixerGain() ); + mComboMixerGain.setEnabled( true ); - mComboVGAGain.setEnabled( false ); - mComboVGAGain.setSelectedItem( gain.getVGAGain() ); - } + mComboLNAGain.setSelectedItem( gain.getLNAGain() ); + mComboLNAGain.setEnabled( true ); - save(); - } - catch ( UsbException e ) - { - JOptionPane.showMessageDialog( - R820TTunerEditor.this, - "R820T Tuner Controller - couldn't apply the gain " - + "setting - " + e.getLocalizedMessage() ); - - mLog.error( "R820T Tuner Controller - couldn't apply " - + "gain setting - ", e ); - } - } - } ); - mComboMasterGain.setToolTipText( "Select AUTOMATIC for auto " - + "gain, MANUAL to enable
independent control of " - + "Mixer, LNA and Enhance gain
" - + "settings, or one of the individual gain settings for
" - + "semi-manual gain control" ); - add( new JLabel( "Master:" ) ); - add( mComboMasterGain ); + mComboVGAGain.setSelectedItem( gain.getVGAGain() ); + mComboVGAGain.setEnabled( true ); + } + else + { + mComboMixerGain.setEnabled( false ); + mComboMixerGain.setSelectedItem( gain.getMixerGain() ); - /* Mixer Gain Control */ - mComboMixerGain = new JComboBox( R820TTunerController.R820TMixerGain.values() ); - mComboMixerGain.setEnabled( false ); - mComboMixerGain.addActionListener( new ActionListener() - { + mComboLNAGain.setEnabled( false ); + mComboLNAGain.setSelectedItem( gain.getLNAGain() ); + + mComboVGAGain.setEnabled( false ); + mComboVGAGain.setSelectedItem( gain.getVGAGain() ); + } + + save(); + } + catch ( UsbException e ) + { + JOptionPane.showMessageDialog( + R820TTunerEditor.this, + "R820T Tuner Controller - couldn't apply the gain " + + "setting - " + e.getLocalizedMessage() ); + + mLog.error( "R820T Tuner Controller - couldn't apply " + + "gain setting - ", e ); + } + } + } ); + mComboMasterGain.setToolTipText( "Select AUTOMATIC for auto " + + "gain, MANUAL to enable
independent control of " + + "Mixer, LNA and Enhance gain
" + + "settings, or one of the individual gain settings for
" + + "semi-manual gain control" ); + add( new JLabel( "Master:" ) ); + add( mComboMasterGain ); + + /* Mixer Gain Control */ + mComboMixerGain = new JComboBox( R820TMixerGain.values() ); + mComboMixerGain.setEnabled( false ); + mComboMixerGain.addActionListener( new ActionListener() + { @Override - public void actionPerformed( ActionEvent arg0 ) - { + public void actionPerformed( ActionEvent arg0 ) + { try - { - R820TTunerController.R820TMixerGain mixerGain = - (R820TTunerController.R820TMixerGain)mComboMixerGain.getSelectedItem(); - + { + R820TMixerGain mixerGain = + (R820TMixerGain)mComboMixerGain.getSelectedItem(); + if( mixerGain == null ) { mixerGain = DEFAULT_GAIN.getMixerGain(); } - + if( mComboMixerGain.isEnabled() ) { mController.setMixerGain( mixerGain, true ); } - save(); - } - catch ( UsbException e ) - { - JOptionPane.showMessageDialog( R820TTunerEditor.this, - "R820T Tuner Controller - couldn't apply the mixer " - + "gain setting - " + e.getLocalizedMessage() ); - - mLog.error( "R820T Tuner Controller - couldn't apply mixer " - + "gain setting - ", e ); - } - } - } ); - mComboMixerGain.setToolTipText( "Mixer Gain. Set master gain " - + "to MANUAL to enable adjustment" ); - - add( new JLabel( "Mixer:" ) ); - add( mComboMixerGain ); + save(); + } + catch ( UsbException e ) + { + JOptionPane.showMessageDialog( R820TTunerEditor.this, + "R820T Tuner Controller - couldn't apply the mixer " + + "gain setting - " + e.getLocalizedMessage() ); - /* LNA Gain Control */ - mComboLNAGain = new JComboBox( R820TTunerController.R820TLNAGain.values() ); - mComboLNAGain.setEnabled( false ); - mComboLNAGain.addActionListener( new ActionListener() - { + mLog.error( "R820T Tuner Controller - couldn't apply mixer " + + "gain setting - ", e ); + } + } + } ); + mComboMixerGain.setToolTipText( "Mixer Gain. Set master gain " + + "to MANUAL to enable adjustment" ); + + add( new JLabel( "Mixer:" ) ); + add( mComboMixerGain ); + + /* LNA Gain Control */ + mComboLNAGain = new JComboBox( R820TLNAGain.values() ); + mComboLNAGain.setEnabled( false ); + mComboLNAGain.addActionListener( new ActionListener() + { @Override - public void actionPerformed( ActionEvent arg0 ) - { + public void actionPerformed( ActionEvent arg0 ) + { try - { - R820TTunerController.R820TLNAGain lnaGain = - (R820TTunerController.R820TLNAGain)mComboLNAGain.getSelectedItem(); - + { + R820TLNAGain lnaGain = + (R820TLNAGain)mComboLNAGain.getSelectedItem(); + if ( lnaGain == null ) { lnaGain = DEFAULT_GAIN.getLNAGain(); } - + if( mComboLNAGain.isEnabled() ) { mController.setLNAGain( lnaGain, true ); } - save(); - } - catch ( UsbException e ) - { - JOptionPane.showMessageDialog( R820TTunerEditor.this, - "R820T Tuner Controller - couldn't apply the LNA " - + "gain setting - " + e.getLocalizedMessage() ); - - mLog.error( "R820T Tuner Controller - couldn't apply LNA " - + "gain setting - ", e ); - } - } - } ); - mComboLNAGain.setToolTipText( "LNA Gain. Set master gain " - + "to MANUAL to enable adjustment" ); - - add( new JLabel( "LNA:" ) ); - add( mComboLNAGain ); + save(); + } + catch ( UsbException e ) + { + JOptionPane.showMessageDialog( R820TTunerEditor.this, + "R820T Tuner Controller - couldn't apply the LNA " + + "gain setting - " + e.getLocalizedMessage() ); - /* VGA Gain Control */ - mComboVGAGain = new JComboBox( R820TTunerController.R820TVGAGain.values() ); - mComboVGAGain.setEnabled( false ); - mComboVGAGain.addActionListener( new ActionListener() - { + mLog.error( "R820T Tuner Controller - couldn't apply LNA " + + "gain setting - ", e ); + } + } + } ); + mComboLNAGain.setToolTipText( "LNA Gain. Set master gain " + + "to MANUAL to enable adjustment" ); + + add( new JLabel( "LNA:" ) ); + add( mComboLNAGain ); + + /* VGA Gain Control */ + mComboVGAGain = new JComboBox( R820TVGAGain.values() ); + mComboVGAGain.setEnabled( false ); + mComboVGAGain.addActionListener( new ActionListener() + { @Override - public void actionPerformed( ActionEvent arg0 ) - { + public void actionPerformed( ActionEvent arg0 ) + { try - { - R820TTunerController.R820TVGAGain vgaGain = - (R820TTunerController.R820TVGAGain)mComboVGAGain.getSelectedItem(); + { + R820TVGAGain vgaGain = + (R820TVGAGain)mComboVGAGain.getSelectedItem(); if( vgaGain == null ) { vgaGain = DEFAULT_GAIN.getVGAGain(); } - + if( mComboVGAGain.isEnabled() ) { mController.setVGAGain( vgaGain, true ); } - - save(); - } - catch ( UsbException e ) - { - JOptionPane.showMessageDialog( R820TTunerEditor.this, - "R820T Tuner Controller - couldn't apply the VGA " - + "gain setting - " + e.getLocalizedMessage() ); - - mLog.error( "R820T Tuner Controller - couldn't apply VGA " - + "gain setting", e ); - } - } - } ); - mComboVGAGain.setToolTipText( "VGA Gain. Set master gain " - + "to MANUAL to enable adjustment" ); - add( new JLabel( "VGA:" ) ); - add( mComboVGAGain ); - } - + + save(); + } + catch ( UsbException e ) + { + JOptionPane.showMessageDialog( R820TTunerEditor.this, + "R820T Tuner Controller - couldn't apply the VGA " + + "gain setting - " + e.getLocalizedMessage() ); + + mLog.error( "R820T Tuner Controller - couldn't apply VGA " + + "gain setting", e ); + } + } + } ); + mComboVGAGain.setToolTipText( "VGA Gain. Set master gain " + + "to MANUAL to enable adjustment" ); + add( new JLabel( "VGA:" ) ); + add( mComboVGAGain ); + } + /** * Sets each of the tuner configuration controls to the enabled argument state */ @@ -388,41 +402,41 @@ public class R820TTunerEditor extends TunerConfigurationEditor { mConfigurationName.setEnabled( enabled ); } - + if( mTunerInfo.isEnabled() != enabled ) { mTunerInfo.setEnabled( enabled ); } - + if( mFrequencyCorrection.isEnabled() != enabled ) { mFrequencyCorrection.setEnabled( enabled ); } - if( mComboSampleRate.isEnabled() != enabled ) - { - mComboSampleRate.setEnabled( enabled ); - } + if( mComboSampleRate.isEnabled() != enabled ) + { + mComboSampleRate.setEnabled( enabled ); + } - if( mComboMasterGain.isEnabled() != enabled ) - { - mComboMasterGain.setEnabled( enabled ); - } + if( mComboMasterGain.isEnabled() != enabled ) + { + mComboMasterGain.setEnabled( enabled ); + } - if( mComboMixerGain.isEnabled() != enabled ) - { - mComboMixerGain.setEnabled( enabled ); - } - - if( mComboLNAGain.isEnabled() != enabled ) - { - mComboLNAGain.setEnabled( enabled ); - } - - if( mComboVGAGain.isEnabled() != enabled ) - { - mComboVGAGain.setEnabled( enabled ); - } + if( mComboMixerGain.isEnabled() != enabled ) + { + mComboMixerGain.setEnabled( enabled ); + } + + if( mComboLNAGain.isEnabled() != enabled ) + { + mComboLNAGain.setEnabled( enabled ); + } + + if( mComboVGAGain.isEnabled() != enabled ) + { + mComboVGAGain.setEnabled( enabled ); + } } private String getTunerInfo() @@ -430,9 +444,9 @@ public class R820TTunerEditor extends TunerConfigurationEditor StringBuilder sb = new StringBuilder(); RTL2832TunerController.Descriptor descriptor = mController.getDescriptor(); - + sb.append( "

RTL-2832 with R820T Tuner

" ); - + if( descriptor == null ) { sb.append( "No EEPROM Descriptor Available" ); @@ -468,37 +482,37 @@ public class R820TTunerEditor extends TunerConfigurationEditor return sb.toString(); } - + @Override public void save() { - if( hasItem() && !mLoading ) - { - R820TTunerConfiguration config = getConfiguration(); + if( hasItem() && !mLoading ) + { + R820TTunerConfiguration config = getConfiguration(); - config.setName( mConfigurationName.getText() ); + config.setName( mConfigurationName.getText() ); double value = ((SpinnerNumberModel)mFrequencyCorrection - .getModel()).getNumber().doubleValue(); - config.setFrequencyCorrection( value ); - - config.setSampleRate( (RTL2832TunerController.SampleRate)mComboSampleRate.getSelectedItem() ); - - R820TTunerController.R820TGain gain = (R820TTunerController.R820TGain)mComboMasterGain.getSelectedItem(); + .getModel()).getNumber().doubleValue(); + config.setFrequencyCorrection( value ); + + config.setSampleRate( (SampleRate)mComboSampleRate.getSelectedItem() ); + + R820TGain gain = (R820TGain)mComboMasterGain.getSelectedItem(); config.setMasterGain( gain ); - - R820TTunerController.R820TMixerGain mixerGain = (R820TTunerController.R820TMixerGain)mComboMixerGain.getSelectedItem(); + + R820TMixerGain mixerGain = (R820TMixerGain)mComboMixerGain.getSelectedItem(); config.setMixerGain( mixerGain ); - - R820TTunerController.R820TLNAGain lnaGain = (R820TTunerController.R820TLNAGain)mComboLNAGain.getSelectedItem(); + + R820TLNAGain lnaGain = (R820TLNAGain)mComboLNAGain.getSelectedItem(); config.setLNAGain( lnaGain ); - R820TTunerController.R820TVGAGain vgaGain = (R820TTunerController.R820TVGAGain)mComboVGAGain.getSelectedItem(); + R820TVGAGain vgaGain = (R820TVGAGain)mComboVGAGain.getSelectedItem(); config.setVGAGain( vgaGain ); - - getTunerConfigurationModel().broadcast( + + getTunerConfigurationModel().broadcast( new TunerConfigurationEvent( getConfiguration(), TunerConfigurationEvent.Event.CHANGE ) ); - } + } } @Override @@ -508,21 +522,21 @@ public class R820TTunerEditor extends TunerConfigurationEditor //Toggle loading so that we don't fire a change event and schedule a settings file save mLoading = true; - + if( hasItem() ) { R820TTunerConfiguration config = getConfiguration(); - + if( tunerConfiguration.isAssigned() ) { setControlsEnabled( true ); mConfigurationName.setText( config.getName() ); - mFrequencyCorrection.setValue( config.getFrequencyCorrection() ); - mComboSampleRate.setSelectedItem( config.getSampleRate() ); - mComboMasterGain.setSelectedItem( config.getMasterGain() ); - mComboMixerGain.setSelectedItem( config.getMixerGain() ); - mComboLNAGain.setSelectedItem( config.getLNAGain() ); - mComboVGAGain.setSelectedItem( config.getVGAGain() ); + mFrequencyCorrection.setValue( config.getFrequencyCorrection() ); + mComboSampleRate.setSelectedItem( config.getSampleRate() ); + mComboMasterGain.setSelectedItem( config.getMasterGain() ); + mComboMixerGain.setSelectedItem( config.getMixerGain() ); + mComboLNAGain.setSelectedItem( config.getLNAGain() ); + mComboVGAGain.setSelectedItem( config.getVGAGain() ); } else { @@ -535,7 +549,7 @@ public class R820TTunerEditor extends TunerConfigurationEditor setControlsEnabled( false ); mConfigurationName.setText( "" ); } - + mLoading = false; } -} +} \ No newline at end of file diff --git a/src/source/tuner/test/SampleGenerator.java b/src/main/java/io/github/dsheirer/source/tuner/test/SampleGenerator.java similarity index 96% rename from src/source/tuner/test/SampleGenerator.java rename to src/main/java/io/github/dsheirer/source/tuner/test/SampleGenerator.java index 15796caf..b08403eb 100644 --- a/src/source/tuner/test/SampleGenerator.java +++ b/src/main/java/io/github/dsheirer/source/tuner/test/SampleGenerator.java @@ -16,14 +16,14 @@ * along with this program. If not, see * ******************************************************************************/ -package source.tuner.test; +package io.github.dsheirer.source.tuner.test; -import dsp.mixer.LowPhaseNoiseOscillator; +import io.github.dsheirer.dsp.mixer.LowPhaseNoiseOscillator; +import io.github.dsheirer.sample.Listener; +import io.github.dsheirer.sample.complex.ComplexBuffer; +import io.github.dsheirer.util.ThreadPool; import org.slf4j.Logger; import org.slf4j.LoggerFactory; -import sample.Listener; -import sample.complex.ComplexBuffer; -import util.ThreadPool; import java.util.concurrent.ScheduledFuture; import java.util.concurrent.TimeUnit; diff --git a/src/source/tuner/test/TestTuner.java b/src/main/java/io/github/dsheirer/source/tuner/test/TestTuner.java similarity index 91% rename from src/source/tuner/test/TestTuner.java rename to src/main/java/io/github/dsheirer/source/tuner/test/TestTuner.java index f43e646b..fa918b56 100644 --- a/src/source/tuner/test/TestTuner.java +++ b/src/main/java/io/github/dsheirer/source/tuner/test/TestTuner.java @@ -16,13 +16,13 @@ * along with this program. If not, see * ******************************************************************************/ -package source.tuner.test; +package io.github.dsheirer.source.tuner.test; +import io.github.dsheirer.source.tuner.Tuner; +import io.github.dsheirer.source.tuner.TunerClass; +import io.github.dsheirer.source.tuner.TunerType; import org.slf4j.Logger; import org.slf4j.LoggerFactory; -import source.tuner.Tuner; -import source.tuner.TunerClass; -import source.tuner.TunerType; /** * Testing tuner that implements an internal oscillator to output a unity gain tone at a specified frequency offset diff --git a/src/source/tuner/test/TestTunerController.java b/src/main/java/io/github/dsheirer/source/tuner/test/TestTunerController.java similarity index 93% rename from src/source/tuner/test/TestTunerController.java rename to src/main/java/io/github/dsheirer/source/tuner/test/TestTunerController.java index 98ff7387..0c94dd5d 100644 --- a/src/source/tuner/test/TestTunerController.java +++ b/src/main/java/io/github/dsheirer/source/tuner/test/TestTunerController.java @@ -16,15 +16,15 @@ * along with this program. If not, see * ******************************************************************************/ -package source.tuner.test; +package io.github.dsheirer.source.tuner.test; +import io.github.dsheirer.sample.Listener; +import io.github.dsheirer.sample.complex.ComplexBuffer; +import io.github.dsheirer.source.SourceException; +import io.github.dsheirer.source.tuner.TunerController; +import io.github.dsheirer.source.tuner.configuration.TunerConfiguration; import org.slf4j.Logger; import org.slf4j.LoggerFactory; -import sample.Listener; -import sample.complex.ComplexBuffer; -import source.SourceException; -import source.tuner.TunerController; -import source.tuner.configuration.TunerConfiguration; public class TestTunerController extends TunerController { diff --git a/src/main/java/io/github/dsheirer/source/tuner/usb/USBTransferProcessor.java b/src/main/java/io/github/dsheirer/source/tuner/usb/USBTransferProcessor.java index 3331c184..b1bf58a0 100644 --- a/src/main/java/io/github/dsheirer/source/tuner/usb/USBTransferProcessor.java +++ b/src/main/java/io/github/dsheirer/source/tuner/usb/USBTransferProcessor.java @@ -18,7 +18,6 @@ ******************************************************************************/ package io.github.dsheirer.source.tuner.usb; -import io.github.dsheirer.sample.Broadcaster; import io.github.dsheirer.sample.Listener; import io.github.dsheirer.sample.OverflowableTransferQueue; import io.github.dsheirer.sample.adapter.ISampleAdapter; @@ -69,10 +68,10 @@ public class USBTransferProcessor implements TransferCallback //Byte array transfer buffers size in bytes private int mBufferSize; - private Broadcaster mComplexBufferBroadcaster = new Broadcaster<>(); private OverflowableTransferQueue mFilledBuffers; private LinkedTransferQueue mAvailableTransfers = new LinkedTransferQueue<>(); private LinkedTransferQueue mTransfersInProgress = new LinkedTransferQueue<>(); + private Listener mComplexBufferListener; private AtomicBoolean mRunning = new AtomicBoolean(); @@ -233,36 +232,26 @@ public class USBTransferProcessor implements TransferCallback } /** - * Indicates if there are complex buffer listeners registered with this processor + * Sets the listener and auto-starts the buffer processor */ - public boolean hasListeners() + public void setListener(Listener listener) { - return mComplexBufferBroadcaster.hasListeners(); - } - - public void addListener(Listener listener) - { - mComplexBufferBroadcaster.addListener(listener); - + mComplexBufferListener = listener; start(); } - public void removeListener(Listener listener) + /** + * Auto-stops the buffer processor and removes the listener + */ + public void removeListener() { - mComplexBufferBroadcaster.removeListener(listener); - - if(!hasListeners()) + if(mComplexBufferListener != null) { stop(); + mComplexBufferListener = null; } } - public void removeAllListeners() - { - mComplexBufferBroadcaster.clear(); - stop(); - } - /** * Prepares (allocates) a set of transfer buffers for use in transferring data from the USB device via the bulk * interface. Since we're using direct allocation (native), buffers are retained and reused across multiple @@ -424,7 +413,10 @@ public class USBTransferProcessor implements TransferCallback { float[] complexSamples = mSampleAdapter.convert(buffer); - mComplexBufferBroadcaster.broadcast(new ComplexBuffer(complexSamples)); + if(mComplexBufferListener != null) + { + mComplexBufferListener.receive(new ComplexBuffer(complexSamples)); + } } } catch(Exception e) diff --git a/src/source/tuner/usb/USBTunerController.java b/src/main/java/io/github/dsheirer/source/tuner/usb/USBTunerController.java similarity index 92% rename from src/source/tuner/usb/USBTunerController.java rename to src/main/java/io/github/dsheirer/source/tuner/usb/USBTunerController.java index f731783b..19d60729 100644 --- a/src/source/tuner/usb/USBTunerController.java +++ b/src/main/java/io/github/dsheirer/source/tuner/usb/USBTunerController.java @@ -16,11 +16,11 @@ * along with this program. If not, see * ******************************************************************************/ -package source.tuner.usb; +package io.github.dsheirer.source.tuner.usb; -import sample.Listener; -import sample.complex.ComplexBuffer; -import source.tuner.TunerController; +import io.github.dsheirer.sample.Listener; +import io.github.dsheirer.sample.complex.ComplexBuffer; +import io.github.dsheirer.source.tuner.TunerController; public abstract class USBTunerController extends TunerController { diff --git a/src/main/java/io/github/dsheirer/source/wave/ComplexWaveSource.java b/src/main/java/io/github/dsheirer/source/wave/ComplexWaveSource.java index 8ece68b6..e219d85b 100644 --- a/src/main/java/io/github/dsheirer/source/wave/ComplexWaveSource.java +++ b/src/main/java/io/github/dsheirer/source/wave/ComplexWaveSource.java @@ -18,14 +18,13 @@ ******************************************************************************/ package io.github.dsheirer.source.wave; -import io.github.dsheirer.channel.heartbeat.Heartbeat; import io.github.dsheirer.sample.ConversionUtils; import io.github.dsheirer.sample.Listener; import io.github.dsheirer.sample.complex.ComplexBuffer; import io.github.dsheirer.source.ComplexSource; import io.github.dsheirer.source.IControllableFileSource; import io.github.dsheirer.source.IFrameLocationListener; -import io.github.dsheirer.source.tuner.frequency.FrequencyChangeEvent; +import io.github.dsheirer.source.SourceEvent; import org.slf4j.Logger; import org.slf4j.LoggerFactory; @@ -36,12 +35,10 @@ import javax.sound.sampled.UnsupportedAudioFileException; import java.io.File; import java.io.IOException; import java.util.Arrays; -import java.util.concurrent.ScheduledExecutorService; public class ComplexWaveSource extends ComplexSource implements IControllableFileSource { - private final static Logger mLog = - LoggerFactory.getLogger(ComplexWaveSource.class); + private final static Logger mLog = LoggerFactory.getLogger(ComplexWaveSource.class); private IFrameLocationListener mFrameLocationListener; private int mBytesPerFrame; @@ -57,49 +54,33 @@ public class ComplexWaveSource extends ComplexSource implements IControllableFil } @Override - public void setFrequencyChangeListener(Listener listener) + public void setSourceEventListener(Listener listener) { //Not implemented } @Override - public void removeFrequencyChangeListener() + public void removeSourceEventListener() { //Not implemented } @Override - public Listener getFrequencyChangeListener() + public Listener getSourceEventListener() { //Not implemented return null; } - /** - * Not implemented - */ - @Override - public void setHeartbeatListener(Listener listener) - { - } - - /** - * Not implemented - */ - @Override - public void removeHeartbeatListener() - { - } - @Override public void reset() { stop(); - start(null); + start(); } @Override - public void start(ScheduledExecutorService executor) + public void start() { try { @@ -132,11 +113,11 @@ public class ComplexWaveSource extends ComplexSource implements IControllableFil } @Override - public int getSampleRate() + public double getSampleRate() { if(mInputStream != null) { - return (int)mInputStream.getFormat().getSampleRate(); + return mInputStream.getFormat().getSampleRate(); } return 0; diff --git a/src/main/java/io/github/dsheirer/source/wave/RealWaveSource.java b/src/main/java/io/github/dsheirer/source/wave/RealWaveSource.java index b41042b8..8a94520c 100644 --- a/src/main/java/io/github/dsheirer/source/wave/RealWaveSource.java +++ b/src/main/java/io/github/dsheirer/source/wave/RealWaveSource.java @@ -17,14 +17,13 @@ ******************************************************************************/ package io.github.dsheirer.source.wave; -import io.github.dsheirer.channel.heartbeat.Heartbeat; import io.github.dsheirer.sample.ConversionUtils; import io.github.dsheirer.sample.Listener; import io.github.dsheirer.sample.real.RealBuffer; import io.github.dsheirer.source.IControllableFileSource; import io.github.dsheirer.source.IFrameLocationListener; import io.github.dsheirer.source.RealSource; -import io.github.dsheirer.source.tuner.frequency.FrequencyChangeEvent; +import io.github.dsheirer.source.SourceEvent; import org.slf4j.Logger; import org.slf4j.LoggerFactory; @@ -35,7 +34,6 @@ import javax.sound.sampled.UnsupportedAudioFileException; import java.io.File; import java.io.IOException; import java.util.Arrays; -import java.util.concurrent.ScheduledExecutorService; public class RealWaveSource extends RealSource implements IControllableFileSource, AutoCloseable { @@ -54,52 +52,35 @@ public class RealWaveSource extends RealSource implements IControllableFileSourc mFile = file; } - @Override - public void setFrequencyChangeListener(Listener listener) - { - //Not implemented - } + @Override + public void setSourceEventListener(Listener listener) + { + //Not implemented + } - @Override - public void removeFrequencyChangeListener() - { - //Not implemented - } - - @Override - public Listener getFrequencyChangeListener() - { - //Not implemented - return null; - } - - /** - * Not implemented - */ - @Override - public void setHeartbeatListener(Listener listener) - { - } - - /** - * Not implemented - */ - @Override - public void removeHeartbeatListener() - { - } + @Override + public void removeSourceEventListener() + { + //Not implemented + } + @Override + public Listener getSourceEventListener() + { + //Not implemented + return null; + } @Override public void reset() { stop(); - start(null); + start(); } @Override - public void start(ScheduledExecutorService executor) + public void start() { try { @@ -129,16 +110,15 @@ public class RealWaveSource extends RealSource implements IControllableFileSourc @Override public long getFrameCount() throws IOException { - // TODO Auto-generated method stub return 0; } @Override - public int getSampleRate() + public double getSampleRate() { if(mInputStream != null) { - return (int)mInputStream.getFormat().getSampleRate(); + return mInputStream.getFormat().getSampleRate(); } return 0; diff --git a/src/main/java/io/github/dsheirer/spectrum/ChannelSpectrumPanel.java b/src/main/java/io/github/dsheirer/spectrum/ChannelSpectrumPanel.java index 7b7bf9d0..08a4e355 100644 --- a/src/main/java/io/github/dsheirer/spectrum/ChannelSpectrumPanel.java +++ b/src/main/java/io/github/dsheirer/spectrum/ChannelSpectrumPanel.java @@ -1,3 +1,18 @@ +/******************************************************************************* + * sdr-trunk + * 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 io.github.dsheirer.spectrum; import io.github.dsheirer.controller.channel.Channel; @@ -18,8 +33,7 @@ import io.github.dsheirer.settings.ColorSettingMenuItem; import io.github.dsheirer.settings.Setting; import io.github.dsheirer.settings.SettingChangeListener; import io.github.dsheirer.settings.SettingsManager; -import io.github.dsheirer.source.tuner.frequency.FrequencyChangeEvent; -import io.github.dsheirer.source.tuner.frequency.FrequencyChangeEvent.Event; +import io.github.dsheirer.source.SourceEvent; import io.github.dsheirer.spectrum.converter.DFTResultsConverter; import io.github.dsheirer.spectrum.converter.RealDecibelConverter; import io.github.dsheirer.spectrum.menu.AveragingItem; @@ -30,8 +44,13 @@ import io.github.dsheirer.spectrum.menu.SmoothingItem; import io.github.dsheirer.spectrum.menu.SmoothingTypeItem; import net.miginfocom.swing.MigLayout; -import javax.swing.*; -import java.awt.*; +import javax.swing.JLayeredPane; +import javax.swing.JMenu; +import javax.swing.JPanel; +import javax.swing.JPopupMenu; +import javax.swing.JSeparator; +import javax.swing.SwingUtilities; +import java.awt.Component; import java.awt.event.ComponentEvent; import java.awt.event.ComponentListener; import java.awt.event.MouseEvent; @@ -39,446 +58,456 @@ import java.awt.event.MouseListener; import java.awt.event.MouseMotionListener; import java.util.concurrent.atomic.AtomicBoolean; -public class ChannelSpectrumPanel extends JPanel - implements ChannelEventListener, - Listener, - SettingChangeListener, - SpectralDisplayAdjuster +public class ChannelSpectrumPanel extends JPanel implements ChannelEventListener, + Listener, SettingChangeListener, SpectralDisplayAdjuster { - private static final long serialVersionUID = 1L; + private static final long serialVersionUID = 1L; - private DFTProcessor mDFTProcessor = new DFTProcessor( SampleType.REAL ); + private DFTProcessor mDFTProcessor = new DFTProcessor(SampleType.REAL); private DFTResultsConverter mDFTConverter = new RealDecibelConverter(); private JLayeredPane mLayeredPane; private SpectrumPanel mSpectrumPanel; private ChannelOverlayPanel mOverlayPanel; - + private Channel mCurrentChannel; - + private int mSampleBufferSize = 2400; - private HalfBandFilter_RB_RB mDecimatingFilter = new HalfBandFilter_RB_RB( - Filters.FIR_HALF_BAND_31T_ONE_EIGHTH_FCO.getCoefficients(), 1.0f, true ); - + private HalfBandFilter_RB_RB mDecimatingFilter = new HalfBandFilter_RB_RB( + Filters.FIR_HALF_BAND_31T_ONE_EIGHTH_FCO.getCoefficients(), 1.0f, true); + private AtomicBoolean mEnabled = new AtomicBoolean(); - + private SettingsManager mSettingsManager; private ChannelProcessingManager mChannelProcessingManager; - public ChannelSpectrumPanel( SettingsManager settingsManager, - ChannelProcessingManager channelProcessingManager ) + public ChannelSpectrumPanel(SettingsManager settingsManager, + ChannelProcessingManager channelProcessingManager) { - mSettingsManager = settingsManager; - mChannelProcessingManager = channelProcessingManager; - - if( mSettingsManager != null ) - { - mSettingsManager.addListener( this ); - } - - mSpectrumPanel = new SpectrumPanel( mSettingsManager ); - mSpectrumPanel.setAveraging( 1 ); + mSettingsManager = settingsManager; + mChannelProcessingManager = channelProcessingManager; - mOverlayPanel = new ChannelOverlayPanel( mSettingsManager ); - mDFTProcessor.addConverter( mDFTConverter ); - mDFTConverter.addListener( mSpectrumPanel ); + if(mSettingsManager != null) + { + mSettingsManager.addListener(this); + } - /* Set the DFTProcessor to the decimated 24kHz sample rate */ - mDFTProcessor.frequencyChanged( - new FrequencyChangeEvent( Event.NOTIFICATION_SAMPLE_RATE_CHANGE, 24000 ) ); - - initGui(); + mSpectrumPanel = new SpectrumPanel(mSettingsManager); + mSpectrumPanel.setAveraging(1); + + mOverlayPanel = new ChannelOverlayPanel(mSettingsManager); + mDFTProcessor.addConverter(mDFTConverter); + mDFTConverter.addListener(mSpectrumPanel); + + /* Set the DFTProcessor to the decimated 24kHz sample rate */ + mDFTProcessor.process(SourceEvent.sampleRateChange(24000.0)); + + initGui(); } - + public void dispose() { - setEnabled( false ); - - mDFTProcessor.dispose(); + setEnabled(false); - if( mSettingsManager != null ) - { - mSettingsManager.removeListener( this ); - } - - mSettingsManager = null; - mCurrentChannel = null; - mDFTProcessor = null; - mSpectrumPanel = null; + mDFTProcessor.dispose(); + + if(mSettingsManager != null) + { + mSettingsManager.removeListener(this); + } + + mSettingsManager = null; + mCurrentChannel = null; + mDFTProcessor = null; + mSpectrumPanel = null; } - - public void setFrameRate( int framesPerSecond ) + + public void setFrameRate(int framesPerSecond) { - mSampleBufferSize = (int)( 48000 / framesPerSecond ); - - mDFTProcessor.setFrameRate( framesPerSecond ); + mSampleBufferSize = (int)(48000 / framesPerSecond); + + mDFTProcessor.setFrameRate(framesPerSecond); } - + private void initGui() { - setLayout( new MigLayout( "insets 0 0 0 0 ", - "[grow,fill]", - "[grow,fill]") ); - - mLayeredPane = new JLayeredPane(); - mLayeredPane.addComponentListener( new ResizeListener() ); - - MouseEventProcessor mouser = new MouseEventProcessor(); - - mOverlayPanel.addMouseListener( mouser ); - mOverlayPanel.addMouseMotionListener( mouser ); - - mLayeredPane.add( mSpectrumPanel, new Integer( 0 ), 0 ); - mLayeredPane.add( mOverlayPanel, new Integer( 1 ), 0 ); + setLayout(new MigLayout("insets 0 0 0 0 ", + "[grow,fill]", + "[grow,fill]")); - add( mLayeredPane ); + mLayeredPane = new JLayeredPane(); + mLayeredPane.addComponentListener(new ResizeListener()); + + MouseEventProcessor mouser = new MouseEventProcessor(); + + mOverlayPanel.addMouseListener(mouser); + mOverlayPanel.addMouseMotionListener(mouser); + + mLayeredPane.add(mSpectrumPanel, new Integer(0), 0); + mLayeredPane.add(mOverlayPanel, new Integer(1), 0); + + add(mLayeredPane); } - - public void setEnabled( boolean enabled ) + + public void setEnabled(boolean enabled) { - if( enabled && mEnabled.compareAndSet( false, true ) ) - { - start(); - } - else if( !enabled && mEnabled.compareAndSet( true, false ) ) - { - stop(); - } + if(enabled && mEnabled.compareAndSet(false, true)) + { + start(); + } + else if(!enabled && mEnabled.compareAndSet(true, false)) + { + stop(); + } } @Override - @SuppressWarnings( "incomplete-switch" ) - public void channelChanged( ChannelEvent event ) + @SuppressWarnings("incomplete-switch") + public void channelChanged(ChannelEvent event) { - switch( event.getEvent() ) - { - case NOTIFICATION_SELECTION_CHANGE: - //ChannelSelectionManager ensures that only 1 channel can be - //selected and any previously selected channel will be first - //deselected before we get a new selection event - if( event.getChannel().isSelected() ) - { - if( mCurrentChannel != null ) - { - stop(); - mCurrentChannel = null; - } - - mCurrentChannel = event.getChannel(); - - if( mEnabled.get() ) - { - start(); - } - } - else - { - stop(); - mCurrentChannel = null; - } - break; - case NOTIFICATION_PROCESSING_STOP: - if( event.getChannel() == mCurrentChannel ) - { - if( mEnabled.get() ) - { - stop(); - } - - mCurrentChannel = null; - } - break; - } - } - - private void start() - { - if( mEnabled.get() && mCurrentChannel != null && mCurrentChannel.getEnabled() ) - { - ProcessingChain processingChain = mChannelProcessingManager - .getProcessingChain( mCurrentChannel ); - - if( processingChain != null ) - { - processingChain.addRealBufferListener( this ); - - mDFTProcessor.start(); - } - } - } - - private void stop() - { - if( mCurrentChannel != null && mCurrentChannel.getEnabled() ) - { - ProcessingChain processingChain = mChannelProcessingManager - .getProcessingChain( mCurrentChannel ); - - if( processingChain != null ) - { - processingChain.removeRealBufferListener( this ); - } - } - - mDFTProcessor.stop(); - - mSpectrumPanel.clearSpectrum(); - } - - @Override - public void settingChanged( Setting setting ) - { - if( mSpectrumPanel != null ) - { - mSpectrumPanel.settingChanged( setting ); - } - if( mOverlayPanel != null ) - { - mOverlayPanel.settingChanged( setting ); - } - } - - @Override - public void settingDeleted( Setting setting ) - { - if( mSpectrumPanel != null ) - { - mSpectrumPanel.settingDeleted( setting ); - } - - if( mOverlayPanel != null ) - { - mOverlayPanel.settingDeleted( setting ); - } - } - - @Override - public void receive( RealBuffer buffer ) - { - RealBuffer decimated = mDecimatingFilter.filter( buffer ); - - //Hack: we're placing real samples in a complex buffer that the DFT - //processor is expecting. - mDFTProcessor.receive( new ComplexBuffer( decimated.getSamples() ) ); - } - - /** - * Monitors the sizing of the layered pane and resizes the spectrum and - * channel panels whenever the layered pane is resized - */ - public class ResizeListener implements ComponentListener - { - @Override - public void componentResized( ComponentEvent e ) + switch(event.getEvent()) { - Component c = e.getComponent(); - - mSpectrumPanel.setBounds( 0, 0, c.getWidth(), c.getHeight() ); - mOverlayPanel.setBounds( 0, 0, c.getWidth(), c.getHeight() ); + case NOTIFICATION_SELECTION_CHANGE: + //ChannelSelectionManager ensures that only 1 channel can be + //selected and any previously selected channel will be first + //deselected before we get a new selection event + if(event.getChannel().isSelected()) + { + if(mCurrentChannel != null) + { + stop(); + mCurrentChannel = null; + } + + mCurrentChannel = event.getChannel(); + + if(mEnabled.get()) + { + start(); + } + } + else + { + stop(); + mCurrentChannel = null; + } + break; + case NOTIFICATION_PROCESSING_STOP: + if(event.getChannel() == mCurrentChannel) + { + if(mEnabled.get()) + { + stop(); + } + + mCurrentChannel = null; + } + break; + } + } + + private void start() + { + if(mEnabled.get() && mCurrentChannel != null && mCurrentChannel.getEnabled()) + { + ProcessingChain processingChain = mChannelProcessingManager + .getProcessingChain(mCurrentChannel); + + if(processingChain != null) + { + processingChain.addRealBufferListener(this); + + mDFTProcessor.start(); + } + } + } + + private void stop() + { + if(mCurrentChannel != null && mCurrentChannel.getEnabled()) + { + ProcessingChain processingChain = mChannelProcessingManager + .getProcessingChain(mCurrentChannel); + + if(processingChain != null) + { + processingChain.removeRealBufferListener(this); + } } - @Override - public void componentHidden( ComponentEvent arg0 ) {} - @Override - public void componentMoved( ComponentEvent arg0 ) {} - @Override - public void componentShown( ComponentEvent arg0 ) {} - } - - /** - * Mouse event handler for the channel panel. - */ - public class MouseEventProcessor implements MouseMotionListener, MouseListener - { - @Override - public void mouseMoved( MouseEvent event ) + mDFTProcessor.stop(); + + mSpectrumPanel.clearSpectrum(); + } + + @Override + public void settingChanged(Setting setting) + { + if(mSpectrumPanel != null) { - update( event ); + mSpectrumPanel.settingChanged(setting); } - @Override - public void mouseDragged( MouseEvent event ) - { - update( event ); - } - - private void update( MouseEvent event ) - { - if( event.getComponent() == mOverlayPanel ) - { - mOverlayPanel.setCursorLocation( event.getPoint() ); - } - } - - @Override - public void mouseEntered( MouseEvent e ) - { - if( e.getComponent() == mOverlayPanel ) - { - mOverlayPanel.setCursorVisible( true ); - } - } - - @Override - public void mouseExited( MouseEvent e ) - { - mOverlayPanel.setCursorVisible( false ); - } - - /** - * Displays the context menu. - */ - @Override - public void mouseClicked( MouseEvent event ) + if(mOverlayPanel != null) { - if( SwingUtilities.isRightMouseButton( event ) ) - { - JPopupMenu contextMenu = new JPopupMenu(); - - /** - * Color Menus - */ - JMenu colorMenu = new JMenu( "Color" ); + mOverlayPanel.settingChanged(setting); + } + } - colorMenu.add( new ColorSettingMenuItem( mSettingsManager, - ColorSettingName.SPECTRUM_CURSOR ) ); - - colorMenu.add( new ColorSettingMenuItem( mSettingsManager, - ColorSettingName.SPECTRUM_LINE ) ); - - colorMenu.add( new ColorSettingMenuItem( mSettingsManager, - ColorSettingName.SPECTRUM_BACKGROUND ) ); - - colorMenu.add( new ColorSettingMenuItem( mSettingsManager, - ColorSettingName.SPECTRUM_GRADIENT_BOTTOM ) ); - - colorMenu.add( new ColorSettingMenuItem( mSettingsManager, - ColorSettingName.SPECTRUM_GRADIENT_TOP ) ); - - contextMenu.add( colorMenu ); - - /** - * Display items: fft and frame rate - */ - JMenu displayMenu = new JMenu( "Display" ); - contextMenu.add( displayMenu ); - - /** - * Averaging menu - */ - JMenu averagingMenu = new JMenu( "Averaging" ); - averagingMenu.add( - new AveragingItem( ChannelSpectrumPanel.this, 2 ) ); - displayMenu.add( averagingMenu ); - - /** - * FFT width - */ - JMenu fftWidthMenu = new JMenu( "FFT Width" ); - displayMenu.add( fftWidthMenu ); - - for( DFTSize width: DFTSize.values() ) - { - fftWidthMenu.add( new DFTSizeItem( mDFTProcessor, width ) ); - } - - /** - * DFT Processor Frame Rate - */ - JMenu frameRateMenu = new JMenu( "Frame Rate" ); - displayMenu.add( frameRateMenu ); - - frameRateMenu.add( new FrameRateItem( mDFTProcessor, 14 ) ); - frameRateMenu.add( new FrameRateItem( mDFTProcessor, 16 ) ); - frameRateMenu.add( new FrameRateItem( mDFTProcessor, 18 ) ); - frameRateMenu.add( new FrameRateItem( mDFTProcessor, 20 ) ); - frameRateMenu.add( new FrameRateItem( mDFTProcessor, 25 ) ); - frameRateMenu.add( new FrameRateItem( mDFTProcessor, 30 ) ); - frameRateMenu.add( new FrameRateItem( mDFTProcessor, 40 ) ); - frameRateMenu.add( new FrameRateItem( mDFTProcessor, 50 ) ); - - /** - * FFT Window Type - */ - JMenu fftWindowType = new JMenu( "Window Type" ); - displayMenu.add( fftWindowType ); - - for( WindowType type: WindowType.values() ) - { - fftWindowType.add( - new FFTWindowTypeItem( mDFTProcessor, type ) ); - } - - /** - * Smoothing menu - */ - JMenu smoothingMenu = new JMenu( "Smoothing" ); - - if( mSpectrumPanel.getSmoothingType() != SmoothingType.NONE ) - { - smoothingMenu.add( new SmoothingItem( ChannelSpectrumPanel.this, 5 ) ); - smoothingMenu.add( new JSeparator() ); - } - - smoothingMenu.add( new SmoothingTypeItem( ChannelSpectrumPanel.this, SmoothingType.GAUSSIAN ) ); - smoothingMenu.add( new SmoothingTypeItem( ChannelSpectrumPanel.this, SmoothingType.TRIANGLE ) ); - smoothingMenu.add( new SmoothingTypeItem( ChannelSpectrumPanel.this, SmoothingType.RECTANGLE ) ); - smoothingMenu.add( new SmoothingTypeItem( ChannelSpectrumPanel.this, SmoothingType.NONE ) ); - - displayMenu.add( smoothingMenu ); - - if( contextMenu != null ) - { - contextMenu.show( mOverlayPanel, - event.getX(), - event.getY() ); - } - } + @Override + public void settingDeleted(Setting setting) + { + if(mSpectrumPanel != null) + { + mSpectrumPanel.settingDeleted(setting); } - @Override - public void mousePressed( MouseEvent e ) {} - @Override - public void mouseReleased( MouseEvent e ) {} - } + if(mOverlayPanel != null) + { + mOverlayPanel.settingDeleted(setting); + } + } - @Override + @Override + public void receive(RealBuffer buffer) + { + RealBuffer decimated = mDecimatingFilter.filter(buffer); + + //Hack: we're placing real samples in a complex buffer that the DFT + //processor is expecting. + mDFTProcessor.receive(new ComplexBuffer(decimated.getSamples())); + } + + /** + * Monitors the sizing of the layered pane and resizes the spectrum and + * channel panels whenever the layered pane is resized + */ + public class ResizeListener implements ComponentListener + { + @Override + public void componentResized(ComponentEvent e) + { + Component c = e.getComponent(); + + mSpectrumPanel.setBounds(0, 0, c.getWidth(), c.getHeight()); + mOverlayPanel.setBounds(0, 0, c.getWidth(), c.getHeight()); + } + + @Override + public void componentHidden(ComponentEvent arg0) + { + } + + @Override + public void componentMoved(ComponentEvent arg0) + { + } + + @Override + public void componentShown(ComponentEvent arg0) + { + } + } + + /** + * Mouse event handler for the channel panel. + */ + public class MouseEventProcessor implements MouseMotionListener, MouseListener + { + @Override + public void mouseMoved(MouseEvent event) + { + update(event); + } + + @Override + public void mouseDragged(MouseEvent event) + { + update(event); + } + + private void update(MouseEvent event) + { + if(event.getComponent() == mOverlayPanel) + { + mOverlayPanel.setCursorLocation(event.getPoint()); + } + } + + @Override + public void mouseEntered(MouseEvent e) + { + if(e.getComponent() == mOverlayPanel) + { + mOverlayPanel.setCursorVisible(true); + } + } + + @Override + public void mouseExited(MouseEvent e) + { + mOverlayPanel.setCursorVisible(false); + } + + /** + * Displays the context menu. + */ + @Override + public void mouseClicked(MouseEvent event) + { + if(SwingUtilities.isRightMouseButton(event)) + { + JPopupMenu contextMenu = new JPopupMenu(); + + /** + * Color Menus + */ + JMenu colorMenu = new JMenu("Color"); + + colorMenu.add(new ColorSettingMenuItem(mSettingsManager, + ColorSettingName.SPECTRUM_CURSOR)); + + colorMenu.add(new ColorSettingMenuItem(mSettingsManager, + ColorSettingName.SPECTRUM_LINE)); + + colorMenu.add(new ColorSettingMenuItem(mSettingsManager, + ColorSettingName.SPECTRUM_BACKGROUND)); + + colorMenu.add(new ColorSettingMenuItem(mSettingsManager, + ColorSettingName.SPECTRUM_GRADIENT_BOTTOM)); + + colorMenu.add(new ColorSettingMenuItem(mSettingsManager, + ColorSettingName.SPECTRUM_GRADIENT_TOP)); + + contextMenu.add(colorMenu); + + /** + * Display items: fft and frame rate + */ + JMenu displayMenu = new JMenu("Display"); + contextMenu.add(displayMenu); + + /** + * Averaging menu + */ + JMenu averagingMenu = new JMenu("Averaging"); + averagingMenu.add( + new AveragingItem(ChannelSpectrumPanel.this, 2)); + displayMenu.add(averagingMenu); + + /** + * FFT width + */ + JMenu fftWidthMenu = new JMenu("FFT Width"); + displayMenu.add(fftWidthMenu); + + for(DFTSize width : DFTSize.values()) + { + fftWidthMenu.add(new DFTSizeItem(mDFTProcessor, width)); + } + + /** + * DFT Processor Frame Rate + */ + JMenu frameRateMenu = new JMenu("Frame Rate"); + displayMenu.add(frameRateMenu); + + frameRateMenu.add(new FrameRateItem(mDFTProcessor, 14)); + frameRateMenu.add(new FrameRateItem(mDFTProcessor, 16)); + frameRateMenu.add(new FrameRateItem(mDFTProcessor, 18)); + frameRateMenu.add(new FrameRateItem(mDFTProcessor, 20)); + frameRateMenu.add(new FrameRateItem(mDFTProcessor, 25)); + frameRateMenu.add(new FrameRateItem(mDFTProcessor, 30)); + frameRateMenu.add(new FrameRateItem(mDFTProcessor, 40)); + frameRateMenu.add(new FrameRateItem(mDFTProcessor, 50)); + + /** + * FFT Window Type + */ + JMenu fftWindowType = new JMenu("Window Type"); + displayMenu.add(fftWindowType); + + for(WindowType type : WindowType.values()) + { + fftWindowType.add( + new FFTWindowTypeItem(mDFTProcessor, type)); + } + + /** + * Smoothing menu + */ + JMenu smoothingMenu = new JMenu("Smoothing"); + + if(mSpectrumPanel.getSmoothingType() != SmoothingType.NONE) + { + smoothingMenu.add(new SmoothingItem(ChannelSpectrumPanel.this, 5)); + smoothingMenu.add(new JSeparator()); + } + + smoothingMenu.add(new SmoothingTypeItem(ChannelSpectrumPanel.this, SmoothingType.GAUSSIAN)); + smoothingMenu.add(new SmoothingTypeItem(ChannelSpectrumPanel.this, SmoothingType.TRIANGLE)); + smoothingMenu.add(new SmoothingTypeItem(ChannelSpectrumPanel.this, SmoothingType.RECTANGLE)); + smoothingMenu.add(new SmoothingTypeItem(ChannelSpectrumPanel.this, SmoothingType.NONE)); + + displayMenu.add(smoothingMenu); + + if(contextMenu != null) + { + contextMenu.show(mOverlayPanel, + event.getX(), + event.getY()); + } + } + } + + @Override + public void mousePressed(MouseEvent e) + { + } + + @Override + public void mouseReleased(MouseEvent e) + { + } + } + + @Override public int getAveraging() { - return mSpectrumPanel.getAveraging(); + return mSpectrumPanel.getAveraging(); } - @Override - public void setAveraging( int averaging ) + @Override + public void setAveraging(int averaging) { - mSpectrumPanel.setAveraging( averaging ); + mSpectrumPanel.setAveraging(averaging); } - public void setSampleSize( double sampleSize ) - { - mSpectrumPanel.setSampleSize( sampleSize ); - } + public void setSampleSize(double sampleSize) + { + mSpectrumPanel.setSampleSize(sampleSize); + } - @Override - public int getSmoothing() - { - return mSpectrumPanel.getSmoothing(); - } + @Override + public int getSmoothing() + { + return mSpectrumPanel.getSmoothing(); + } - @Override - public void setSmoothing( int smoothing ) - { - mSpectrumPanel.setSmoothing( smoothing ); - } + @Override + public void setSmoothing(int smoothing) + { + mSpectrumPanel.setSmoothing(smoothing); + } - @Override - public SmoothingType getSmoothingType() - { - return mSpectrumPanel.getSmoothingType(); - } + @Override + public SmoothingType getSmoothingType() + { + return mSpectrumPanel.getSmoothingType(); + } - @Override - public void setSmoothingType( SmoothingType type ) - { - mSpectrumPanel.setSmoothingType( type ); - } + @Override + public void setSmoothingType(SmoothingType type) + { + mSpectrumPanel.setSmoothingType(type); + } } diff --git a/src/main/java/io/github/dsheirer/spectrum/DFTProcessor.java b/src/main/java/io/github/dsheirer/spectrum/DFTProcessor.java index b69628be..d2bf9205 100644 --- a/src/main/java/io/github/dsheirer/spectrum/DFTProcessor.java +++ b/src/main/java/io/github/dsheirer/spectrum/DFTProcessor.java @@ -1,19 +1,16 @@ /******************************************************************************* - * sdrtrunk + * sdr-trunk * 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 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 + * You should have received a copy of the GNU General Public License along with this program. + * If not, see * ******************************************************************************/ package io.github.dsheirer.spectrum; @@ -25,8 +22,8 @@ import io.github.dsheirer.sample.Buffer; import io.github.dsheirer.sample.Listener; import io.github.dsheirer.sample.SampleType; import io.github.dsheirer.sample.complex.ComplexBuffer; -import io.github.dsheirer.source.tuner.frequency.FrequencyChangeEvent; -import io.github.dsheirer.source.tuner.frequency.IFrequencyChangeProcessor; +import io.github.dsheirer.source.ISourceEventProcessor; +import io.github.dsheirer.source.SourceEvent; import io.github.dsheirer.spectrum.converter.DFTResultsConverter; import io.github.dsheirer.util.ThreadPool; import org.jtransforms.fft.FloatFFT_1D; @@ -46,7 +43,7 @@ import java.util.concurrent.atomic.AtomicBoolean; * Processes both complex samples or float samples and dispatches a float array * of DFT results, using configurable fft size and output dispatch timelines. */ -public class DFTProcessor implements Listener, IFrequencyChangeProcessor, IDFTWidthChangeProcessor +public class DFTProcessor implements Listener, ISourceEventProcessor, IDFTWidthChangeProcessor { private final static Logger mLog = LoggerFactory.getLogger(DFTProcessor.class); @@ -70,7 +67,7 @@ public class DFTProcessor implements Listener, IFrequencyChangePr /* The Cosine and Hanning windows seem to offer the best spectral display * with minimal bin leakage/smearing */ - private WindowType mWindowType = Window.WindowType.HANNING; + private WindowType mWindowType = WindowType.BLACKMAN_HARRIS_7; private FloatFFT_1D mFFT = new FloatFFT_1D(mDFTSize.getSize()); @@ -452,7 +449,7 @@ public class DFTProcessor implements Listener, IFrequencyChangePr } @Override - public void frequencyChanged(FrequencyChangeEvent event) + public void process(SourceEvent event) { switch(event.getEvent()) { diff --git a/src/main/java/io/github/dsheirer/spectrum/OverlayPanel.java b/src/main/java/io/github/dsheirer/spectrum/OverlayPanel.java index d1317e11..37b607df 100644 --- a/src/main/java/io/github/dsheirer/spectrum/OverlayPanel.java +++ b/src/main/java/io/github/dsheirer/spectrum/OverlayPanel.java @@ -28,14 +28,22 @@ import io.github.dsheirer.settings.ColorSetting.ColorSettingName; import io.github.dsheirer.settings.Setting; import io.github.dsheirer.settings.SettingChangeListener; import io.github.dsheirer.settings.SettingsManager; -import io.github.dsheirer.source.tuner.TunerChannel; -import io.github.dsheirer.source.tuner.frequency.FrequencyChangeEvent; -import io.github.dsheirer.source.tuner.frequency.IFrequencyChangeProcessor; +import io.github.dsheirer.source.ISourceEventProcessor; +import io.github.dsheirer.source.SourceEvent; +import io.github.dsheirer.source.tuner.channel.TunerChannel; import org.slf4j.Logger; import org.slf4j.LoggerFactory; -import javax.swing.*; -import java.awt.*; +import javax.swing.JPanel; +import java.awt.BasicStroke; +import java.awt.Color; +import java.awt.Font; +import java.awt.FontMetrics; +import java.awt.Graphics; +import java.awt.Graphics2D; +import java.awt.Point; +import java.awt.RenderingHints; +import java.awt.Stroke; import java.awt.event.ComponentEvent; import java.awt.event.ComponentListener; import java.awt.geom.Line2D; @@ -44,7 +52,7 @@ import java.text.DecimalFormat; import java.util.ArrayList; import java.util.concurrent.CopyOnWriteArrayList; -public class OverlayPanel extends JPanel implements ChannelEventListener, IFrequencyChangeProcessor, +public class OverlayPanel extends JPanel implements ChannelEventListener, ISourceEventProcessor, SettingChangeListener { private static final long serialVersionUID = 1L; @@ -635,7 +643,7 @@ public class OverlayPanel extends JPanel implements ChannelEventListener, IFrequ * Frequency change event handler */ @Override - public void frequencyChanged(FrequencyChangeEvent event) + public void process(SourceEvent event) { switch(event.getEvent()) { diff --git a/src/main/java/io/github/dsheirer/spectrum/SpectralDisplayPanel.java b/src/main/java/io/github/dsheirer/spectrum/SpectralDisplayPanel.java index 6ade9244..c9719429 100644 --- a/src/main/java/io/github/dsheirer/spectrum/SpectralDisplayPanel.java +++ b/src/main/java/io/github/dsheirer/spectrum/SpectralDisplayPanel.java @@ -1,19 +1,16 @@ /******************************************************************************* - * sdrtrunk + * sdr-trunk * 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 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 + * You should have received a copy of the GNU General Public License along with this program. + * If not, see * ******************************************************************************/ package io.github.dsheirer.spectrum; @@ -33,10 +30,9 @@ import io.github.dsheirer.sample.complex.ComplexBuffer; import io.github.dsheirer.settings.ColorSetting.ColorSettingName; import io.github.dsheirer.settings.ColorSettingMenuItem; import io.github.dsheirer.settings.SettingsManager; +import io.github.dsheirer.source.ISourceEventProcessor; +import io.github.dsheirer.source.SourceEvent; import io.github.dsheirer.source.tuner.Tuner; -import io.github.dsheirer.source.tuner.frequency.FrequencyChangeEvent; -import io.github.dsheirer.source.tuner.frequency.FrequencyChangeEvent.Event; -import io.github.dsheirer.source.tuner.frequency.IFrequencyChangeProcessor; import io.github.dsheirer.spectrum.OverlayPanel.ChannelDisplay; import io.github.dsheirer.spectrum.converter.ComplexDecibelConverter; import io.github.dsheirer.spectrum.converter.DFTResultsConverter; @@ -50,11 +46,25 @@ import net.miginfocom.swing.MigLayout; import org.slf4j.Logger; import org.slf4j.LoggerFactory; -import javax.swing.*; +import javax.swing.JCheckBoxMenuItem; +import javax.swing.JComponent; +import javax.swing.JLabel; +import javax.swing.JLayeredPane; +import javax.swing.JMenu; +import javax.swing.JMenuItem; +import javax.swing.JPanel; +import javax.swing.JPopupMenu; +import javax.swing.JScrollPane; +import javax.swing.JSeparator; +import javax.swing.JSlider; +import javax.swing.JSplitPane; +import javax.swing.SwingUtilities; import javax.swing.event.ChangeEvent; import javax.swing.event.ChangeListener; import javax.swing.event.MouseInputAdapter; -import java.awt.*; +import java.awt.Component; +import java.awt.Dimension; +import java.awt.EventQueue; import java.awt.event.ActionEvent; import java.awt.event.ActionListener; import java.awt.event.ComponentEvent; @@ -65,7 +75,7 @@ import java.text.DecimalFormat; import java.util.ArrayList; import java.util.Hashtable; -public class SpectralDisplayPanel extends JPanel implements Listener, IFrequencyChangeProcessor, IDFTWidthChangeProcessor +public class SpectralDisplayPanel extends JPanel implements Listener, ISourceEventProcessor, IDFTWidthChangeProcessor { private static final long serialVersionUID = 1L; @@ -223,7 +233,7 @@ public class SpectralDisplayPanel extends JPanel implements Listener) mDFTProcessor); + mTuner.getSourceManager().addComplexBufferListener(mDFTProcessor); mSpectrumPanel.setSampleSize(mTuner.getSampleSize()); //Fire frequency and sample rate change events so that the spectrum //and overlay panels can synchronize - frequencyChanged(new FrequencyChangeEvent( - Event.NOTIFICATION_FREQUENCY_CHANGE, - mTuner.getTunerController().getFrequency())); + process(SourceEvent.frequencyChange(mTuner.getTunerController().getFrequency())); - frequencyChanged(new FrequencyChangeEvent( - Event.NOTIFICATION_SAMPLE_RATE_CHANGE, - mTuner.getTunerController().getSampleRate())); + process(SourceEvent.sampleRateChange(mTuner.getTunerController().getSampleRate())); } } @@ -452,7 +458,7 @@ public class SpectralDisplayPanel extends JPanel implements Listener) mDFTProcessor); + mTuner.getTunerController().removeComplexBufferListener(mDFTProcessor); mTuner = null; } } @@ -508,7 +514,7 @@ public class SpectralDisplayPanel extends JPanel implements Listener - ******************************************************************************/ - -package module; - -import java.util.concurrent.ScheduledExecutorService; - -public abstract class Module -{ - /** - * Defines the basic component level class for all processing, demodulation - * and decoding components that can operate within a processing chain. - * - * Modules can optionally implement any of the following interfaces: - * - * IAudioPacketListener consumes audio packets - * IAudioPacketProvider produces audio packets - * ICallEventListener consumes call events - * ICallEventProvider provides call events - * IChannelEventListener consumes channel events - * IChannelEventProvider provides channel events (reset, selection, start, etc) - * IComplexBufferListener consumes complex samples and normally - * produces demodulated real sample buffers - * IFrequencyCorrectionController provides tuned frequency error corrections - * IMessageProvider produces messages - * IRealBufferListener consumes demodulated real sample buffers - * IRealBufferListener produces demodulated real sample buffers - * ISquelchStateListener consumes squelch states - * ISquelchStateProvider provides squelch states - */ - public Module() - { - } - - /** - * Initialize or reset all internal states to default - prepare to start - * processing or resume processing, potentially with a different source. - * - * This method is invoked after constructing a module, and following any stop() - * calls and before any start() calls. This allows the module to prior to - * starting a module and starting it again processing a newly tuned channel, - * like a traffic channel. - */ - public abstract void reset(); - - /** - * Start processing. - */ - public abstract void start(); - - /** - * Stop processing - */ - public abstract void stop(); - - /** - * Dispose of all resources and listeners and prepare for garbage collection - */ - public abstract void dispose(); - -} diff --git a/src/module/ProcessingChain.java b/src/module/ProcessingChain.java deleted file mode 100644 index fb4a8722..00000000 --- a/src/module/ProcessingChain.java +++ /dev/null @@ -1,821 +0,0 @@ -/******************************************************************************* - * 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 module; - -import audio.AudioPacket; -import audio.IAudioPacketListener; -import audio.IAudioPacketProvider; -import audio.squelch.ISquelchStateListener; -import audio.squelch.ISquelchStateProvider; -import audio.squelch.SquelchState; -import source.heartbeat.Heartbeat; -import source.heartbeat.IHeartbeatListener; -import source.heartbeat.IHeartbeatProvider; -import channel.metadata.AttributeChangeRequest; -import channel.metadata.IAttributeChangeRequestListener; -import channel.metadata.IAttributeChangeRequestProvider; -import channel.state.ChannelState; -import channel.state.DecoderState; -import channel.state.DecoderStateEvent; -import channel.state.IDecoderStateEventListener; -import channel.state.IDecoderStateEventProvider; -import controller.channel.Channel.ChannelType; -import controller.channel.ChannelEvent; -import controller.channel.IChannelEventListener; -import controller.channel.IChannelEventProvider; -import message.IMessageListener; -import message.IMessageProvider; -import message.Message; -import module.decode.event.CallEvent; -import module.decode.event.CallEventModel; -import module.decode.event.ICallEventListener; -import module.decode.event.ICallEventProvider; -import module.decode.event.MessageActivityModel; -import module.log.EventLogger; -import org.slf4j.Logger; -import org.slf4j.LoggerFactory; -import record.wave.ComplexBufferWaveRecorder; -import record.wave.RealBufferWaveRecorder; -import sample.Broadcaster; -import sample.Listener; -import sample.complex.ComplexBuffer; -import sample.complex.IComplexBufferListener; -import sample.real.IFilteredRealBufferListener; -import sample.real.IFilteredRealBufferProvider; -import sample.real.IUnFilteredRealBufferListener; -import sample.real.IUnFilteredRealBufferProvider; -import sample.real.RealBuffer; -import source.ComplexSource; -import source.RealSource; -import source.Source; -import source.ISourceEventListener; -import source.ISourceEventProvider; -import source.SourceEvent; -import util.ThreadPool; - -import java.util.ArrayList; -import java.util.List; -import java.util.concurrent.atomic.AtomicBoolean; - -/** - * Processing chain provides a framework for connecting a complex or real sample - * source to a set of one primary decoder and zero or more auxiliary decoders. - * All decoded messages and call events produced by the decoders and the decoder - * call states are aggregated by the various broadcasters. You can register - * listeners to receive aggregated messages, call events, and audio packets. - * - * Normal setup sequence: - * - * 1) Add one or more modules - * 2) Register listeners to receive messages, call events, audio, etc. - * 3) Add a valid source - * 4) Invoke the start() method to start processing. - * 5) Invoke the stop() method to stop processing. - * - * Optional: if you want to reuse the processing chain with a new sample source, - * invoke the following method sequence: stop(), setSource(), start() - */ -public class ProcessingChain implements IChannelEventListener -{ - private final static Logger mLog = LoggerFactory.getLogger(ProcessingChain.class); - - private Broadcaster mAttributeChangeRequestBroadcaster = new Broadcaster<>(); - private Broadcaster mAudioPacketBroadcaster = new Broadcaster<>(); - private Broadcaster mCallEventBroadcaster = new Broadcaster<>(); - private Broadcaster mChannelEventBroadcaster = new Broadcaster<>(); - private Broadcaster mComplexBufferBroadcaster = new Broadcaster<>(); - private Broadcaster mDecoderStateEventBroadcaster = new Broadcaster<>(); - private Broadcaster mHeartbeatBroadcaster = new Broadcaster<>(); - private Broadcaster mFrequencyChangeEventBroadcaster = new Broadcaster<>(); - private Broadcaster mMessageBroadcaster = new Broadcaster<>(); - private Broadcaster mFilteredRealBufferBroadcaster = new Broadcaster<>(); - private Broadcaster mUnFilteredRealBufferBroadcaster = new Broadcaster<>(); - private Broadcaster mSquelchStateBroadcaster = new Broadcaster<>(); - - private AtomicBoolean mRunning = new AtomicBoolean(); - - protected Source mSource; - private List mModules = new ArrayList<>(); - private CallEventModel mCallEventModel; - private ChannelState mChannelState; - private MessageActivityModel mMessageActivityModel; - - /** - * Creates a processing chain for managing a set of modules - * - * @param channelType - */ - public ProcessingChain(ChannelType channelType) - { - mChannelState = new ChannelState(channelType); - addModule(mChannelState); - - mCallEventModel = new CallEventModel(); - addCallEventListener(mCallEventModel); - } - - public CallEventModel getCallEventModel() - { - return mCallEventModel; - } - - public ChannelState getChannelState() - { - return mChannelState; - } - - public MessageActivityModel getMessageActivityModel() - { - return mMessageActivityModel; - } - - public void setMessageActivityModel(MessageActivityModel model) - { - mMessageActivityModel = model; - - addMessageListener(mMessageActivityModel); - } - - public void dispose() - { - stop(); - - for(Module module : mModules) - { - module.dispose(); - } - - mModules.clear(); - - mAudioPacketBroadcaster.dispose(); - mCallEventBroadcaster.dispose(); - mChannelEventBroadcaster.dispose(); - mComplexBufferBroadcaster.dispose(); - mMessageBroadcaster.dispose(); - mFilteredRealBufferBroadcaster.dispose(); - mSquelchStateBroadcaster.dispose(); - } - - /** - * Indicates if this processing chain is currently receiving samples from - * a source and sending those samples to the decoders. - */ - public boolean isProcessing() - { - return mRunning.get(); - } - - /** - * Indicates if this chain currently has a valid sample source. - */ - public boolean hasSource() - { - return mSource != null; - } - - /** - * Applies a sample source to this processing chain. Processing won't - * start until the start() method is invoked. - * - * @param source - real or complex sample source - * @throws IllegalStateException if the processing chain is currently - * processing with another source. Invoke stop() before applying a new - * source. - */ - public void setSource(Source source) throws IllegalStateException - { - if(isProcessing()) - { - throw new IllegalStateException("Processing chain is currently processing. Invoke stop() on the " + - "processing chain before applying a new sample source"); - } - - mSource = source; - - addModule(mSource); - } - - /** - * List of current modules for this processing chain - */ - public List getModules() - { - return mModules; - } - - /** - * List of decoder states for this processing chain - */ - public List getDecoderStates() - { - List decoderStates = new ArrayList<>(); - - for(Module module : mModules) - { - if(module instanceof DecoderState) - { - decoderStates.add((DecoderState) module); - } - } - - return decoderStates; - } - - /** - * Adds the list of modules to this processing chain - */ - public void addModules(List modules) - { - for(Module module : modules) - { - addModule(module); - } - } - - /** - * Adds a module to the processing chain. Each module is tested for the - * interfaces that it supports and is registered or receives a listener - * to consume or produce the supported interface data type. All elements - * and events that are produced by any module are automatically routed to - * all other components that support the corresponding listener interface. - * - * At least one module should consume complex samples and either produce - * decoded messages and/or audio, or produce decoded real sample buffers - * for all other modules to consume. - * - * @param module - processing module, demodulator, decoder, source, state - * machine, etc. - */ - public void addModule(Module module) - { - mModules.add(module); - - registerListeners(module); - registerProviders(module); - } - - /** - * Removes the module from the processing chain and deregisters the module as a listener and - * as a provider - */ - public void removeModule(Module module) - { - unregisterListeners(module); - unregisterProviders(module); - - mModules.remove(module); - } - - /** - * Registers the module as a listener to each of the broadcasters that - * provide the data interface(s) supported by the module. - */ - private void registerListeners(Module module) - { - if(module instanceof IAttributeChangeRequestListener) - { - mAttributeChangeRequestBroadcaster.addListener(((IAttributeChangeRequestListener)module).getAttributeChangeRequestListener()); - } - - if(module instanceof IAudioPacketListener) - { - mAudioPacketBroadcaster.addListener(((IAudioPacketListener) module).getAudioPacketListener()); - } - - if(module instanceof ICallEventListener) - { - mCallEventBroadcaster.addListener(((ICallEventListener) module).getCallEventListener()); - } - - if(module instanceof IChannelEventListener) - { - mChannelEventBroadcaster.addListener(((IChannelEventListener) module).getChannelEventListener()); - } - - if(module instanceof IComplexBufferListener) - { - mComplexBufferBroadcaster.addListener(((IComplexBufferListener) module).getComplexBufferListener()); - } - - if(module instanceof IDecoderStateEventListener) - { - mDecoderStateEventBroadcaster.addListener(((IDecoderStateEventListener) module).getDecoderStateListener()); - } - - if(module instanceof ISourceEventListener) - { - Listener listener = ((ISourceEventListener)module).getSourceEventListener(); - - if(listener != null) - { - mFrequencyChangeEventBroadcaster.addListener(listener); - } - } - - if(module instanceof IHeartbeatListener) - { - mHeartbeatBroadcaster.addListener(((IHeartbeatListener)module).getHeartbeatListener()); - } - - if(module instanceof IMessageListener) - { - mMessageBroadcaster.addListener(((IMessageListener) module).getMessageListener()); - } - - if(module instanceof IFilteredRealBufferListener) - { - mFilteredRealBufferBroadcaster.addListener(((IFilteredRealBufferListener) module).getFilteredRealBufferListener()); - } - - if(module instanceof ISquelchStateListener) - { - mSquelchStateBroadcaster.addListener(((ISquelchStateListener) module).getSquelchStateListener()); - } - - if(module instanceof IUnFilteredRealBufferListener) - { - mUnFilteredRealBufferBroadcaster.addListener(((IUnFilteredRealBufferListener) module).getUnFilteredRealBufferListener()); - } - } - - /** - * Registers the module as a listener to each of the broadcasters that - * provide the data interface(s) supported by the module. - */ - private void unregisterListeners(Module module) - { - if(module instanceof IAttributeChangeRequestListener) - { - mAttributeChangeRequestBroadcaster.removeListener(((IAttributeChangeRequestListener) module).getAttributeChangeRequestListener()); - } - - if(module instanceof IAudioPacketListener) - { - mAudioPacketBroadcaster.removeListener(((IAudioPacketListener) module).getAudioPacketListener()); - } - - if(module instanceof ICallEventListener) - { - mCallEventBroadcaster.removeListener(((ICallEventListener) module).getCallEventListener()); - } - - if(module instanceof IChannelEventListener) - { - mChannelEventBroadcaster.removeListener(((IChannelEventListener) module).getChannelEventListener()); - } - - if(module instanceof IComplexBufferListener) - { - mComplexBufferBroadcaster.removeListener(((IComplexBufferListener) module).getComplexBufferListener()); - } - - if(module instanceof IDecoderStateEventListener) - { - mDecoderStateEventBroadcaster.removeListener(((IDecoderStateEventListener) module).getDecoderStateListener()); - } - - if(module instanceof ISourceEventListener) - { - mFrequencyChangeEventBroadcaster.removeListener(((ISourceEventListener) module).getSourceEventListener()); - } - - if(module instanceof IHeartbeatListener) - { - mHeartbeatBroadcaster.removeListener(((IHeartbeatListener)module).getHeartbeatListener()); - } - - if(module instanceof IMessageListener) - { - mMessageBroadcaster.removeListener(((IMessageListener) module).getMessageListener()); - } - - if(module instanceof IFilteredRealBufferListener) - { - mFilteredRealBufferBroadcaster.removeListener(((IFilteredRealBufferListener) module).getFilteredRealBufferListener()); - } - - if(module instanceof ISquelchStateListener) - { - mSquelchStateBroadcaster.removeListener(((ISquelchStateListener) module).getSquelchStateListener()); - } - - if(module instanceof IUnFilteredRealBufferListener) - { - mUnFilteredRealBufferBroadcaster.removeListener(((IUnFilteredRealBufferListener) module).getUnFilteredRealBufferListener()); - } - } - - /** - * Registers the broadcaster(s) as listeners to the module for each - * provider interface that is supported by the module. - */ - private void registerProviders(Module module) - { - if(module instanceof IAttributeChangeRequestProvider) - { - ((IAttributeChangeRequestProvider)module).setAttributeChangeRequestListener(mAttributeChangeRequestBroadcaster); - } - - if(module instanceof IAudioPacketProvider) - { - ((IAudioPacketProvider) module).setAudioPacketListener(mAudioPacketBroadcaster); - } - - if(module instanceof ICallEventProvider) - { - ((ICallEventProvider) module).addCallEventListener(mCallEventBroadcaster); - } - - if(module instanceof IChannelEventProvider) - { - ((IChannelEventProvider) module).setChannelEventListener(mChannelEventBroadcaster); - } - - if(module instanceof IDecoderStateEventProvider) - { - ((IDecoderStateEventProvider) module).setDecoderStateListener(mDecoderStateEventBroadcaster); - } - - if(module instanceof IHeartbeatProvider) - { - ((IHeartbeatProvider)module).getHeartbeatManager().addHeartbeatListener(mHeartbeatBroadcaster); - } - - if(module instanceof IMessageProvider) - { - ((IMessageProvider) module).setMessageListener(mMessageBroadcaster); - } - - if(module instanceof IFilteredRealBufferProvider) - { - ((IFilteredRealBufferProvider) module).setFilteredRealBufferListener(mFilteredRealBufferBroadcaster); - } - - if(module instanceof IMessageProvider) - { - ((IMessageProvider) module).setMessageListener(mMessageBroadcaster); - } - - if(module instanceof ISourceEventProvider) - { - ((ISourceEventProvider) module).setSourceEventListener(mFrequencyChangeEventBroadcaster); - } - - if(module instanceof ISquelchStateProvider) - { - ((ISquelchStateProvider) module).setSquelchStateListener(mSquelchStateBroadcaster); - } - - if(module instanceof IUnFilteredRealBufferProvider) - { - ((IUnFilteredRealBufferProvider) module).setUnFilteredRealBufferListener(mUnFilteredRealBufferBroadcaster); - } - } - - /** - * Unregisters the broadcaster(s) as listeners to the module for each - * provider interface that is supported by the module. - */ - private void unregisterProviders(Module module) - { - if(module instanceof IAttributeChangeRequestProvider) - { - ((IAttributeChangeRequestProvider)module).removeAttributeChangeRequestListener(mAttributeChangeRequestBroadcaster); - } - - if(module instanceof IAudioPacketProvider) - { - ((IAudioPacketProvider) module).setAudioPacketListener(null); - } - - if(module instanceof ICallEventProvider) - { - ((ICallEventProvider) module).removeCallEventListener(mCallEventBroadcaster); - } - - if(module instanceof IChannelEventProvider) - { - ((IChannelEventProvider) module).setChannelEventListener(null); - } - - if(module instanceof IDecoderStateEventProvider) - { - ((IDecoderStateEventProvider) module).setDecoderStateListener(null); - } - - if(module instanceof IHeartbeatProvider) - { - ((IHeartbeatProvider)module).getHeartbeatManager().removeHeartbeatListener(mHeartbeatBroadcaster); - } - - if(module instanceof IMessageProvider) - { - ((IMessageProvider) module).setMessageListener(null); - } - - if(module instanceof IFilteredRealBufferProvider) - { - ((IFilteredRealBufferProvider) module).setFilteredRealBufferListener(null); - } - - if(module instanceof IMessageProvider) - { - ((IMessageProvider) module).setMessageListener(null); - } - - if(module instanceof ISourceEventProvider) - { - ((ISourceEventProvider) module).setSourceEventListener(null); - } - - if(module instanceof ISquelchStateProvider) - { - ((ISquelchStateProvider) module).setSquelchStateListener(null); - } - - if(module instanceof IUnFilteredRealBufferProvider) - { - ((IUnFilteredRealBufferProvider) module).setUnFilteredRealBufferListener(null); - } - } - - /** - * Starts processing if the chain has a valid source. Invocations on an - * already started chain have no effect. - */ - public void start() - { - if(mRunning.compareAndSet(false, true)) - { - if(mSource != null) - { - /* Reset each of the modules */ - for(Module module : mModules) - { - module.reset(); - } - - //Setup the channel state to monitor source overflow conditions - mSource.setOverflowListener(mChannelState); - - /* Register with the source to receive sample data. Setup a - * timer task to process the buffer queues 50 times a second - * (every 20 ms) */ - switch(mSource.getSampleType()) - { - case COMPLEX: - ((ComplexSource) mSource).setListener(mComplexBufferBroadcaster); - break; - case REAL: - ((RealSource) mSource).setListener(mFilteredRealBufferBroadcaster); - break; - default: - throw new IllegalArgumentException("Unrecognized source " - + "sample type - cannot start processing chain"); - } - - /* Start each of the modules */ - for(Module module : mModules) - { - try - { - module.start(); - } - catch(Exception e) - { - mLog.error("Error starting module", e); - } - } - } - else - { - mLog.error("Source is null on start()"); - } - } - } - - /** - * Stops processing if the chain is currently processing. Invocations on an already stopped chain have no effect. - */ - public void stop() - { - if(mRunning.compareAndSet(true, false)) - { - /* Stop each of the modules */ - for(Module module : mModules) - { - module.stop(); - } - - if(mSource != null) - { - removeModule(mSource); - - mSource.setOverflowListener(null); - - switch(mSource.getSampleType()) - { - case COMPLEX: - ((ComplexSource) mSource).removeListener(mComplexBufferBroadcaster); - break; - case REAL: - ((RealSource) mSource).removeListener(mFilteredRealBufferBroadcaster); - break; - default: - throw new IllegalArgumentException("Unrecognized source sample type - cannot start processing " + - "chain"); - } - - /* Release the source */ - mSource.dispose(); - mSource = null; - } - } - } - - /** - * Removes any logging modules that are currently registered with this processing chain - */ - public void removeEventLoggingModules() - { - List eventLoggingModules = new ArrayList<>(); - - for(Module module : mModules) - { - if(module instanceof EventLogger) - { - eventLoggingModules.add(module); - } - } - - for(Module eventLoggingModule : eventLoggingModules) - { - removeModule(eventLoggingModule); - } - } - - /** - * Removes any recording modules that are currently registered with this processing chain - */ - public void removeRecordingModules() - { - List recordingModules = new ArrayList<>(); - - for(Module module : mModules) - { - if(module instanceof RealBufferWaveRecorder || module instanceof ComplexBufferWaveRecorder) - { - recordingModules.add(module); - } - } - - for(Module recordingModule : recordingModules) - { - removeModule(recordingModule); - } - } - - /** - * Adds the listener to receive audio packets from all modules. - */ - public void addAudioPacketListener(Listener listener) - { - mAudioPacketBroadcaster.addListener(listener); - } - - public void removeAudioPacketListener(Listener listener) - { - mAudioPacketBroadcaster.removeListener(listener); - } - - /** - * Adds the listener to receive call events from all modules. - */ - public void addCallEventListener(Listener listener) - { - mCallEventBroadcaster.addListener(listener); - } - - public void removeCallEventListener(Listener listener) - { - mCallEventBroadcaster.removeListener(listener); - } - - /** - * Adds the listener to receive call events from all modules. - */ - public void addChannelEventListener(Listener listener) - { - mChannelEventBroadcaster.addListener(listener); - } - - public void removeChannelEventListener(Listener listener) - { - mChannelEventBroadcaster.removeListener(listener); - } - - /** - * Adds the listener to receive decoder state events from decoder modules - */ - public void addDecoderStateEventListener(Listener listener) - { - mDecoderStateEventBroadcaster.addListener(listener); - } - - public void removeDecoderStateEventListener(Listener listener) - { - mDecoderStateEventBroadcaster.removeListener(listener); - } - - public Listener getDecoderStateEventListener() - { - return mDecoderStateEventBroadcaster; - } - - /** - * Adds the listener to receive decoded messages from all decoders. - */ - public void addMessageListener(Listener listener) - { - mMessageBroadcaster.addListener(listener); - } - - /** - * Adds the list of listeners to receive decoded messages from all decoders. - */ - public void addMessageListeners(List> listeners) - { - for(Listener listener : listeners) - { - mMessageBroadcaster.addListener(listener); - } - } - - public void removeMessageListener(Listener listener) - { - mMessageBroadcaster.removeListener(listener); - } - - /** - * Adds the listener to receive frequency change events from the processing chain - * @param listener to receive events - */ - public void addFrequencyChangeListener(Listener listener) - { - mFrequencyChangeEventBroadcaster.addListener(listener); - } - - /** - * Removes the listener from receiving frequency change events - * @param listener to remove - */ - public void removeFrequencyChangeListener(Listener listener) - { - mFrequencyChangeEventBroadcaster.removeListener(listener); - } - - /** - * Adds the listener to receive call events from all modules. - */ - public void addSquelchStateListener(Listener listener) - { - mSquelchStateBroadcaster.addListener(listener); - } - - public void removeSquelchStateListener(Listener listener) - { - mSquelchStateBroadcaster.removeListener(listener); - } - - public void addRealBufferListener(Listener listener) - { - mFilteredRealBufferBroadcaster.addListener(listener); - } - - public void removeRealBufferListener(Listener listener) - { - mFilteredRealBufferBroadcaster.removeListener(listener); - } - - @Override - public Listener getChannelEventListener() - { - return mChannelEventBroadcaster; - } -} diff --git a/src/module/decode/Decoder.java b/src/module/decode/Decoder.java deleted file mode 100644 index 3a029035..00000000 --- a/src/module/decode/Decoder.java +++ /dev/null @@ -1,100 +0,0 @@ -/******************************************************************************* - * 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 module.decode; - -import message.IMessageProvider; -import message.Message; -import module.Module; -import sample.Listener; - -public abstract class Decoder extends Module implements IMessageProvider, Listener -{ - /* This has to be a broadcaster in order for references to persist */ - protected Listener mMessageListener; - - /** - * Decoder - parent class for all decoders, demodulators and components. - */ - public Decoder() - { - } - - @Override - public void dispose() - { - mMessageListener = null; - } - - /** - * Identifies the decoder type (ie protocol) - */ - public abstract DecoderType getDecoderType(); - - - /** - * Adds a listener for receiving decoded messages from this decoder - */ - @Override - public void setMessageListener( Listener listener ) - { - mMessageListener = listener; - } - - /** - * Removes the listener from receiving decoded messages from all attached - * decoders - */ - @Override - public void removeMessageListener() - { - mMessageListener = null; - } - - /** - * Broadcasts the message to the registered message listener - */ - public void broadcast( Message message ) - { - if( mMessageListener != null ) - { - mMessageListener.receive( message ); - } - } - - @Override - public void receive( Message message ) - { - broadcast( message ); - } - - @Override - public void reset() - { - } - - @Override - public void start() - { - } - - @Override - public void stop() - { - } -} diff --git a/src/module/decode/am/AMDecoder.java b/src/module/decode/am/AMDecoder.java deleted file mode 100644 index 29c5b52d..00000000 --- a/src/module/decode/am/AMDecoder.java +++ /dev/null @@ -1,53 +0,0 @@ -/******************************************************************************* - * 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 module.decode.am; - -import module.decode.DecoderType; -import module.decode.PrimaryDecoder; -import module.decode.config.DecodeConfiguration; - -public class AMDecoder extends PrimaryDecoder -{ - public AMDecoder(DecodeConfiguration config) - { - super(config); - } - - @Override - public DecoderType getDecoderType() - { - return DecoderType.AM; - } - - @Override - public void reset() - { - } - - @Override - public void start() - { - } - - @Override - public void stop() - { - } -} diff --git a/src/module/decode/fleetsync2/Fleetsync2Decoder.java b/src/module/decode/fleetsync2/Fleetsync2Decoder.java deleted file mode 100644 index c27d4a97..00000000 --- a/src/module/decode/fleetsync2/Fleetsync2Decoder.java +++ /dev/null @@ -1,177 +0,0 @@ -/******************************************************************************* - * SDR Trunk - * Copyright (C) 2014,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 - ******************************************************************************/ -package module.decode.fleetsync2; - -import instrument.Instrumentable; -import instrument.tap.Tap; -import instrument.tap.TapGroup; -import instrument.tap.stream.BinaryTap; -import instrument.tap.stream.FloatBufferTap; - -import java.util.ArrayList; -import java.util.List; -import java.util.concurrent.ScheduledExecutorService; - -import module.decode.Decoder; -import module.decode.DecoderType; -import sample.Listener; -import sample.real.IFilteredRealBufferListener; -import sample.real.RealBuffer; -import alias.AliasList; -import bits.MessageFramer; -import bits.SyncPattern; -import dsp.filter.Filters; -import dsp.filter.fir.real.RealFIRFilter_RB_RB; -import dsp.filter.halfband.real.HalfBandFilter_RB_RB; -import dsp.fsk.FSK2Decoder; -import dsp.fsk.FSK2Decoder.Output; - -/** - * Fleetsync II Decoder - 1200 baud 2FSK decoder that can process 48k sample rate - * floating point samples and output fully framed Fleetsync II messages - */ -public class Fleetsync2Decoder extends Decoder implements IFilteredRealBufferListener, - Instrumentable -{ - /* Decimated sample rate ( 48,000 / 2 = 24,000 ) feeding the decoder */ - private static final int sDECIMATED_SAMPLE_RATE = 24000; - - /* Baud or Symbol Rate */ - private static final int sSYMBOL_RATE = 1200; - - /* Message length - 5 x REVS + 16 x SYNC + 8 x 64Bit Blocks */ - private static final int sMESSAGE_LENGTH = 537; - - /* Instrumentation Taps */ - private ArrayList mAvailableTaps; - private static final String INSTRUMENT_BANDPASS_FILTER_TO_FSK2_DEMOD = - "Tap Point: Bandpass Filter > < FSK2 Decoder"; - private static final String INSTRUMENT_FSK2_DECODER_TO_MESSAGE_FRAMER = - "Tap Point: FSK2 Decoder > < Message Framer"; - - private FSK2Decoder mFSKDecoder; - private HalfBandFilter_RB_RB mDecimationFilter; - private RealFIRFilter_RB_RB mBandPassFilter; - private MessageFramer mMessageFramer; - private Fleetsync2MessageProcessor mMessageProcessor; - - public Fleetsync2Decoder( AliasList aliasList ) - { - mDecimationFilter = new HalfBandFilter_RB_RB( - Filters.FIR_HALF_BAND_31T_ONE_EIGHTH_FCO.getCoefficients(), 1.0f, true ); - - mBandPassFilter = new RealFIRFilter_RB_RB( - Filters.FIRBP_1200FSK_24000FS.getCoefficients(), 1.02f ); - - mDecimationFilter.setListener( mBandPassFilter ); - - mFSKDecoder = new FSK2Decoder( sDECIMATED_SAMPLE_RATE, - sSYMBOL_RATE, Output.INVERTED ); - mBandPassFilter.setListener( mFSKDecoder ); - - mMessageFramer = new MessageFramer( - SyncPattern.FLEETSYNC2.getPattern(), sMESSAGE_LENGTH ); - mFSKDecoder.setListener( mMessageFramer ); - - mMessageProcessor = new Fleetsync2MessageProcessor( aliasList ); - mMessageFramer.addMessageListener( mMessageProcessor ); - - mMessageProcessor.setMessageListener( this ); - } - - public void dispose() - { - super.dispose(); - - mBandPassFilter.dispose(); - mDecimationFilter.dispose(); - mFSKDecoder.dispose(); - mMessageFramer.dispose(); - mMessageProcessor.dispose(); - } - - @Override - public DecoderType getDecoderType() - { - return DecoderType.FLEETSYNC2; - } - - @Override - public List getTapGroups() - { - if( mAvailableTaps == null ) - { - mAvailableTaps = new ArrayList(); - - TapGroup group = new TapGroup( "Fleetsync 2 Decoder" ); - - group.add( new FloatBufferTap( - INSTRUMENT_BANDPASS_FILTER_TO_FSK2_DEMOD, 0, 0.5f ) ); - group.add( new BinaryTap( - INSTRUMENT_FSK2_DECODER_TO_MESSAGE_FRAMER, 0, 0.025f ) ); - - mAvailableTaps.add( group ); - - mAvailableTaps.addAll( mFSKDecoder.getTapGroups() ); - } - - return mAvailableTaps; - } - - @Override - public void registerTap( Tap tap ) - { - mFSKDecoder.registerTap( tap ); - - switch( tap.getName() ) - { - case INSTRUMENT_BANDPASS_FILTER_TO_FSK2_DEMOD: - FloatBufferTap bpTap = (FloatBufferTap)tap; - mBandPassFilter.setListener( bpTap ); - bpTap.setListener( mFSKDecoder ); - break; - case INSTRUMENT_FSK2_DECODER_TO_MESSAGE_FRAMER: - BinaryTap decoderTap = (BinaryTap)tap; - mFSKDecoder.setListener( decoderTap ); - decoderTap.setListener( mMessageFramer ); - break; - } - } - - @Override - public void unregisterTap( Tap tap ) - { - mFSKDecoder.unregisterTap( tap ); - - switch( tap.getName() ) - { - case INSTRUMENT_BANDPASS_FILTER_TO_FSK2_DEMOD: - mBandPassFilter.setListener( mFSKDecoder ); - break; - case INSTRUMENT_FSK2_DECODER_TO_MESSAGE_FRAMER: - mFSKDecoder.setListener( mMessageFramer ); - break; - } - } - - @Override - public Listener getFilteredRealBufferListener() - { - return mDecimationFilter; - } -} diff --git a/src/module/decode/fleetsync2/Fleetsync2DecoderState.java b/src/module/decode/fleetsync2/Fleetsync2DecoderState.java deleted file mode 100644 index d72346e1..00000000 --- a/src/module/decode/fleetsync2/Fleetsync2DecoderState.java +++ /dev/null @@ -1,301 +0,0 @@ -/******************************************************************************* - * 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 module.decode.fleetsync2; - -import alias.Alias; -import alias.AliasList; -import alias.id.AliasIDType; -import channel.metadata.AliasedStringAttributeMonitor; -import channel.metadata.Attribute; -import channel.metadata.AttributeChangeRequest; -import channel.state.DecoderState; -import channel.state.DecoderStateEvent; -import channel.state.DecoderStateEvent.Event; -import channel.state.State; -import message.Message; -import module.decode.DecoderType; -import util.StringUtils; - -import java.util.Iterator; -import java.util.TreeSet; -import java.util.concurrent.ScheduledExecutorService; - -public class Fleetsync2DecoderState extends DecoderState -{ - private TreeSet mIdents = new TreeSet(); - private TreeSet mEmergencyIdents = new TreeSet(); - - private AliasedStringAttributeMonitor mFromAttribute; - private AliasedStringAttributeMonitor mToAttribute; - private String mMessage; - private String mMessageType; - private long mFrequency; - - public Fleetsync2DecoderState(AliasList aliasList) - { - super(aliasList); - - mFromAttribute = new AliasedStringAttributeMonitor(Attribute.SECONDARY_ADDRESS_FROM, - getAttributeChangeRequestListener(), getAliasList(), AliasIDType.FLEETSYNC); - mToAttribute = new AliasedStringAttributeMonitor(Attribute.SECONDARY_ADDRESS_TO, - getAttributeChangeRequestListener(), getAliasList(), AliasIDType.FLEETSYNC); - } - - @Override - public DecoderType getDecoderType() - { - return DecoderType.FLEETSYNC2; - } - - @Override - public void dispose() - { - super.dispose(); - } - - /** - * Resets the overall decoder state and clears any accumulated event details - */ - @Override - public void reset() - { - mIdents.clear(); - mEmergencyIdents.clear(); - - resetState(); - } - - /** - * Resets this decoder state - */ - public void resetState() - { - mFromAttribute.reset(); - mToAttribute.reset(); - mMessage = null; - mMessageType = null; - } - - @Override - public void receive(Message message) - { - if(message instanceof FleetsyncMessage) - { - FleetsyncMessage fleetsync = (FleetsyncMessage) message; - - if(fleetsync.isValid()) - { - State state = State.CALL; - - mFromAttribute.process(fleetsync.getFromID()); - mIdents.add(fleetsync.getFromID()); - - FleetsyncMessageType type = fleetsync.getMessageType(); - - if(type != FleetsyncMessageType.ANI) - { - mToAttribute.process(fleetsync.getToID()); - mIdents.add(fleetsync.getToID()); - } - - setMessageType(type.getLabel()); - - switch(type) - { - case GPS: - setMessage(fleetsync.getGPSLocation()); - state = State.DATA; - break; - case STATUS: - StringBuilder sb = new StringBuilder(); - sb.append(fleetsync.getStatus()); - - Alias status = fleetsync.getStatusAlias(); - - if(status != null) - { - sb.append("/"); - sb.append(status.getName()); - } - - setMessage(sb.toString()); - state = State.DATA; - break; - case EMERGENCY: - case LONE_WORKER_EMERGENCY: - mEmergencyIdents.add(fleetsync.getFromID()); - state = State.DATA; - break; - default: - break; - } - - FleetsyncCallEvent fsCallEvent = - FleetsyncCallEvent.getFleetsync2Event(fleetsync, mFrequency); - - fsCallEvent.setAliasList(getAliasList()); - - broadcast(fsCallEvent); - - /* Broadcast decode event so that the channel state will - * kick in and reset everything after a short delay */ - broadcast(new DecoderStateEvent(this, Event.DECODE, state)); - } - } - } - - public String getMessage() - { - return mMessage; - } - - public void setMessage(String message) - { - if(!StringUtils.isEqual(mMessage, message)) - { - mMessage = message; - broadcast(new AttributeChangeRequest(Attribute.MESSAGE, mMessage)); - } - } - - public String getMessageType() - { - return "Fleetsync " + mMessageType; - } - - public void setMessageType(String type) - { - if(!StringUtils.isEqual(mMessageType, type)) - { - mMessageType = type; - broadcast(new AttributeChangeRequest(Attribute.MESSAGE_TYPE, getMessageType())); - } - } - - /** - * Responds to reset events issued by the channel state - */ - @Override - public void receiveDecoderStateEvent(DecoderStateEvent event) - { - switch(event.getEvent()) - { - case RESET: - resetState(); - break; - case SOURCE_FREQUENCY: - mFrequency = event.getFrequency(); - break; - default: - break; - } - } - - @Override - public String getActivitySummary() - { - StringBuilder sb = new StringBuilder(); - - sb.append("=============================\n"); - sb.append("Decoder:\tFleetsync II\n\n"); - sb.append("Fleetsync Idents\n"); - - if(mIdents.isEmpty()) - { - sb.append(" None\n"); - } - else - { - Iterator it = mIdents.iterator(); - - while(it.hasNext()) - { - String ident = it.next(); - - sb.append(" "); - sb.append(formatIdent(ident)); - - if(hasAliasList()) - { - Alias alias = getAliasList().getFleetsyncAlias(ident); - - if(alias != null) - { - sb.append(" "); - sb.append(alias.getName()); - } - } - - sb.append("\n"); - } - } - - sb.append("\nFleetsync Emergency Activations\n"); - - if(mEmergencyIdents.isEmpty()) - { - sb.append(" None\n"); - } - else - { - Iterator it = mEmergencyIdents.iterator(); - - while(it.hasNext()) - { - String ident = it.next(); - - sb.append(" "); - sb.append(formatIdent(ident)); - - if(hasAliasList()) - { - Alias alias = getAliasList().getFleetsyncAlias(ident); - - if(alias != null) - { - sb.append(" "); - sb.append(alias.getName()); - } - - sb.append("\n"); - } - } - } - - return sb.toString(); - } - - public static String formatIdent(String ident) - { - StringBuilder sb = new StringBuilder(); - - if(ident.length() == 7) - { - sb.append(ident.substring(0, 3)); - sb.append("-"); - sb.append(ident.substring(3, 7)); - - return sb.toString(); - } - else - { - return ident; - } - } -} diff --git a/src/module/decode/lj1200/LJ1200Decoder.java b/src/module/decode/lj1200/LJ1200Decoder.java deleted file mode 100644 index 2e936984..00000000 --- a/src/module/decode/lj1200/LJ1200Decoder.java +++ /dev/null @@ -1,195 +0,0 @@ -/******************************************************************************* - * SDR Trunk - * Copyright (C) 2014,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 - ******************************************************************************/ -package module.decode.lj1200; - -import instrument.Instrumentable; -import instrument.tap.Tap; -import instrument.tap.TapGroup; -import instrument.tap.stream.BinaryTap; -import instrument.tap.stream.FloatBufferTap; -import instrument.tap.stream.FloatTap; - -import java.util.ArrayList; -import java.util.List; -import java.util.concurrent.ScheduledExecutorService; - -import module.decode.Decoder; -import module.decode.DecoderType; -import sample.Broadcaster; -import sample.Listener; -import sample.real.IFilteredRealBufferListener; -import sample.real.RealBuffer; -import sample.real.RealSampleListener; -import alias.AliasList; -import bits.MessageFramer; -import bits.SyncPattern; -import dsp.filter.Filters; -import dsp.filter.fir.real.RealFIRFilter_RB_RB; -import dsp.filter.halfband.real.HalfBandFilter_RB_RB; -import dsp.fsk.FSK2Decoder; -import dsp.fsk.FSK2Decoder.Output; - -/** - * LJ1200 - 1200 baud 2FSK decoder - */ -public class LJ1200Decoder extends Decoder implements IFilteredRealBufferListener, - Instrumentable -{ - /* Decimated sample rate ( 48,000 / 2 = 24,000 ) feeding the decoder */ - private static final int DECIMATED_SAMPLE_RATE = 24000; - - /* Baud or Symbol Rate */ - private static final int SYMBOL_RATE = 1200; - - /* Message length - 16-bit sync plus 64 bit message */ - private static final int MESSAGE_LENGTH = 80; - - /* Instrumentation Taps */ - private ArrayList mAvailableTaps; - private static final String INSTRUMENT_BANDPASS_FILTER_TO_FSK2_DEMOD = - "Tap Point: Bandpass Filter > < FSK2 Decoder"; - private static final String INSTRUMENT_FSK2_DECODER_TO_MESSAGE_FRAMER = - "Tap Point: FSK2 Decoder > < Message Framer"; - - private FSK2Decoder mFSKDecoder; - private Broadcaster mFSKDecoderBroadcaster = - new Broadcaster(); - - private HalfBandFilter_RB_RB mDecimationFilter; - private RealFIRFilter_RB_RB mBandPassFilter; - private MessageFramer mTowerMessageFramer; - private MessageFramer mTransponderMessageFramer; - private LJ1200MessageProcessor mMessageProcessor; - - public LJ1200Decoder( AliasList aliasList ) - { - mDecimationFilter = new HalfBandFilter_RB_RB( - Filters.FIR_HALF_BAND_31T_ONE_EIGHTH_FCO.getCoefficients(), 1.0f, true ); - - mBandPassFilter = new RealFIRFilter_RB_RB( - Filters.FIRBP_1200FSK_24000FS.getCoefficients(), 1.02f ); - mDecimationFilter.setListener( mBandPassFilter ); - - mFSKDecoder = new FSK2Decoder( DECIMATED_SAMPLE_RATE, - SYMBOL_RATE, Output.INVERTED ); - mBandPassFilter.setListener( mFSKDecoder ); - - mFSKDecoder.setListener( mFSKDecoderBroadcaster ); - - mTowerMessageFramer = new MessageFramer( - SyncPattern.LJ1200.getPattern(), MESSAGE_LENGTH ); - mTransponderMessageFramer = new MessageFramer( - SyncPattern.LJ1200_TRANSPONDER.getPattern(), MESSAGE_LENGTH ); - mFSKDecoderBroadcaster.addListener( mTowerMessageFramer ); - mFSKDecoderBroadcaster.addListener( mTransponderMessageFramer ); - - mMessageProcessor = new LJ1200MessageProcessor( aliasList ); - mTowerMessageFramer.addMessageListener( mMessageProcessor ); - mTransponderMessageFramer.addMessageListener( mMessageProcessor ); - mMessageProcessor.setMessageListener( this ); - } - - @Override - public DecoderType getDecoderType() - { - return DecoderType.LJ_1200; - } - - public void dispose() - { - super.dispose(); - - mDecimationFilter.dispose(); - mBandPassFilter.dispose(); - mFSKDecoder.dispose(); - mTowerMessageFramer.dispose(); - mMessageProcessor.dispose(); - } - - /** - * Returns a float listener interface for connecting this decoder to a - * float stream provider - */ - public RealSampleListener getRealReceiver() - { - return (RealSampleListener)mDecimationFilter; - } - - @Override - public List getTapGroups() - { - if( mAvailableTaps == null ) - { - mAvailableTaps = new ArrayList(); - - TapGroup group = new TapGroup( "LJ-1200 Decoder" ); - - group.add( new FloatTap( - INSTRUMENT_BANDPASS_FILTER_TO_FSK2_DEMOD, 0, 0.5f ) ); - group.add( new BinaryTap( - INSTRUMENT_FSK2_DECODER_TO_MESSAGE_FRAMER, 0, 0.025f ) ); - - mAvailableTaps.add( group ); - mAvailableTaps.addAll( mFSKDecoder.getTapGroups() ); - } - - return mAvailableTaps; - } - - @Override - public void registerTap( Tap tap ) - { - mFSKDecoder.registerTap( tap ); - - switch( tap.getName() ) - { - case INSTRUMENT_BANDPASS_FILTER_TO_FSK2_DEMOD: - FloatBufferTap bpTap = (FloatBufferTap)tap; - mBandPassFilter.setListener( bpTap ); - bpTap.setListener( mFSKDecoder ); - break; - case INSTRUMENT_FSK2_DECODER_TO_MESSAGE_FRAMER: - BinaryTap decoderTap = (BinaryTap)tap; - mFSKDecoder.setListener( decoderTap ); - decoderTap.setListener( mTowerMessageFramer ); - break; - } - } - - @Override - public void unregisterTap( Tap tap ) - { - mFSKDecoder.unregisterTap( tap ); - - switch( tap.getName() ) - { - case INSTRUMENT_BANDPASS_FILTER_TO_FSK2_DEMOD: - mBandPassFilter.setListener( mFSKDecoder ); - break; - case INSTRUMENT_FSK2_DECODER_TO_MESSAGE_FRAMER: - mFSKDecoder.setListener( mTowerMessageFramer ); - break; - } - } - - @Override - public Listener getFilteredRealBufferListener() - { - return mDecimationFilter; - } -} diff --git a/src/module/decode/lj1200/LJ1200DecoderState.java b/src/module/decode/lj1200/LJ1200DecoderState.java deleted file mode 100644 index a5cac1b8..00000000 --- a/src/module/decode/lj1200/LJ1200DecoderState.java +++ /dev/null @@ -1,138 +0,0 @@ -/******************************************************************************* - * 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 module.decode.lj1200; - -import alias.Alias; -import alias.AliasList; -import alias.id.AliasIDType; -import channel.metadata.AliasedStringAttributeMonitor; -import channel.metadata.Attribute; -import channel.metadata.AttributeChangeRequest; -import channel.state.DecoderState; -import channel.state.DecoderStateEvent; -import channel.state.DecoderStateEvent.Event; -import channel.state.State; -import message.Message; -import module.decode.DecoderType; -import org.slf4j.Logger; -import org.slf4j.LoggerFactory; -import util.StringUtils; - -import java.util.ArrayList; -import java.util.Collections; -import java.util.List; -import java.util.Set; -import java.util.TreeSet; -import java.util.concurrent.ScheduledExecutorService; - -public class LJ1200DecoderState extends DecoderState -{ - private final static Logger mLog = LoggerFactory.getLogger(LJ1200DecoderState.class); - - private Set mAddresses = new TreeSet(); - - private AliasedStringAttributeMonitor mAddressAttribute; - - public LJ1200DecoderState(AliasList aliasList) - { - super(aliasList); - - mAddressAttribute = new AliasedStringAttributeMonitor(Attribute.SECONDARY_ADDRESS_TO, - getAttributeChangeRequestListener(), getAliasList(), AliasIDType.SITE); - } - - @Override - public DecoderType getDecoderType() - { - return DecoderType.LJ_1200; - } - - @Override - public void receive(Message message) - { - if(message instanceof LJ1200Message) - { - LJ1200Message lj = (LJ1200Message) message; - - if(lj.isValid()) - { - String address = lj.getAddress(); - - mAddressAttribute.process(address); - mAddresses.add(address); - - broadcast(LJ1200CallEvent.getLJ1200Event(lj)); - - broadcast(new DecoderStateEvent(this, Event.DECODE, State.DATA)); - } - } - else if(message instanceof LJ1200TransponderMessage) - { - - } - } - - @Override - public String getActivitySummary() - { - StringBuilder sb = new StringBuilder(); - - sb.append("=============================\n"); - sb.append("Decoder:\tLJ-1200I\n\n"); - - if(!mAddresses.isEmpty()) - { - sb.append("Transponder Addresses:\n"); - - List addresses = new ArrayList(mAddresses); - - Collections.sort(addresses); - - for(String address : addresses) - { - sb.append("\t"); - sb.append(address); - sb.append("\n"); - } - } - - return sb.toString(); - } - - @Override - public void receiveDecoderStateEvent(DecoderStateEvent event) - { - if(event.getEvent() == Event.RESET) - { - resetState(); - } - } - - private void resetState() - { - mAddressAttribute.reset(); - } - - @Override - public void reset() - { - mAddresses.clear(); - resetState(); - } -} diff --git a/src/module/decode/ltrnet/LTRNetDecoder.java b/src/module/decode/ltrnet/LTRNetDecoder.java deleted file mode 100644 index bf1fbcd4..00000000 --- a/src/module/decode/ltrnet/LTRNetDecoder.java +++ /dev/null @@ -1,123 +0,0 @@ -/******************************************************************************* - * SDR Trunk - * Copyright (C) 2014 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 module.decode.ltrnet; - -import instrument.Instrumentable; -import instrument.tap.Tap; -import instrument.tap.TapGroup; - -import java.util.ArrayList; -import java.util.List; -import java.util.concurrent.ScheduledExecutorService; - -import message.MessageDirection; -import module.decode.Decoder; -import module.decode.DecoderType; -import sample.Listener; -import sample.real.IUnFilteredRealBufferListener; -import sample.real.RealBuffer; -import alias.AliasList; -import bits.MessageFramer; -import bits.SyncPattern; -import dsp.fsk.LTRFSKDecoder; - -public class LTRNetDecoder extends Decoder - implements IUnFilteredRealBufferListener, Instrumentable -{ - public static final int LTR_STANDARD_MESSAGE_LENGTH = 40; - private LTRFSKDecoder mLTRFSKDecoder; - private MessageFramer mLTRMessageFramer; - private LTRNetMessageProcessor mLTRMessageProcessor; - - private List mAvailableTaps; - - /** - * LTR-Net Decoder. Decodes unfiltered (e.g. demodulated but with no DC or - * audio filtering) samples and produces LTR-Net messages. - */ - public LTRNetDecoder( DecodeConfigLTRNet config, AliasList aliasList ) - { - mLTRFSKDecoder = new LTRFSKDecoder(); - - - if( config.getMessageDirection() == MessageDirection.OSW ) - { - mLTRMessageFramer = - new MessageFramer( SyncPattern.LTR_STANDARD_OSW.getPattern(), - LTR_STANDARD_MESSAGE_LENGTH ); - } - else - { - mLTRMessageFramer = - new MessageFramer( SyncPattern.LTR_STANDARD_ISW.getPattern(), - LTR_STANDARD_MESSAGE_LENGTH ); - } - - mLTRFSKDecoder.addListener( mLTRMessageFramer ); - - mLTRMessageProcessor = new LTRNetMessageProcessor( - config.getMessageDirection(), aliasList ); - - mLTRMessageFramer.addMessageListener( mLTRMessageProcessor ); - - mLTRMessageProcessor.setMessageListener( this ); - } - - @Override - public DecoderType getDecoderType() - { - return DecoderType.LTR_NET; - } - - @Override - public Listener getUnFilteredRealBufferListener() - { - return mLTRFSKDecoder; - } - - @Override - public List getTapGroups() - { - if( mAvailableTaps == null ) - { - mAvailableTaps = new ArrayList(); - - mAvailableTaps.addAll( mLTRFSKDecoder.getTapGroups() ); - } - - return mAvailableTaps; - } - - @Override - public void registerTap( Tap tap ) - { - mLTRFSKDecoder.registerTap( tap ); - } - - @Override - public void unregisterTap( Tap tap ) - { - mLTRFSKDecoder.unregisterTap( tap ); - } - - @Override - public void reset() - { - mLTRMessageFramer.reset(); - } -} diff --git a/src/module/decode/ltrnet/LTRNetDecoderState.java b/src/module/decode/ltrnet/LTRNetDecoderState.java deleted file mode 100644 index 544302bc..00000000 --- a/src/module/decode/ltrnet/LTRNetDecoderState.java +++ /dev/null @@ -1,783 +0,0 @@ -/******************************************************************************* - * SDR Trunk - * Copyright (C) 2014,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 - ******************************************************************************/ -package module.decode.ltrnet; - -import alias.Alias; -import alias.AliasList; -import alias.id.AliasIDType; -import channel.metadata.AliasedIntegerAttributeMonitor; -import channel.metadata.AliasedStringAttributeMonitor; -import channel.metadata.Attribute; -import channel.metadata.AttributeChangeRequest; -import channel.state.DecoderState; -import channel.state.DecoderStateEvent; -import channel.state.DecoderStateEvent.Event; -import channel.state.State; -import message.Message; -import module.decode.DecoderType; -import module.decode.event.CallEvent; -import module.decode.event.CallEvent.CallEventType; -import org.slf4j.Logger; -import org.slf4j.LoggerFactory; -import util.StringUtils; - -import java.text.DecimalFormat; -import java.util.HashMap; -import java.util.Iterator; -import java.util.TreeSet; -import java.util.concurrent.ScheduledExecutorService; - -public class LTRNetDecoderState extends DecoderState -{ - private final static Logger mLog = LoggerFactory.getLogger(LTRNetDecoderState.class); - - private DecimalFormat mDecimalFormatter = new DecimalFormat("0.00000"); - - private TreeSet mTalkgroups = new TreeSet(); - private TreeSet mTalkgroupsFirstHeard = new TreeSet(); - private TreeSet mESNs = new TreeSet(); - private TreeSet mUniqueIDs = new TreeSet(); - private TreeSet mNeighborIDs = new TreeSet(); - private TreeSet mSiteIDs = new TreeSet(); - private HashMap mReceiveFrequencies = - new HashMap(); - private HashMap mTransmitFrequencies = - new HashMap(); - private HashMap mActiveCalls = new HashMap(); - - private AliasedStringAttributeMonitor mToAttribute; - private AliasedIntegerAttributeMonitor mFromUIDAttribute; - private AliasedStringAttributeMonitor mESNAttribute; - private String mMessage; - private String mMessageType; - private int mChannelNumber; - private long mFrequency = 0; - - public LTRNetDecoderState(AliasList aliasList) - { - super(aliasList); - - mToAttribute = new AliasedStringAttributeMonitor(Attribute.PRIMARY_ADDRESS_TO, - getAttributeChangeRequestListener(), getAliasList(), AliasIDType.TALKGROUP); - mToAttribute.addIllegalValue("0-00-000"); - - mFromUIDAttribute = new AliasedIntegerAttributeMonitor(Attribute.PRIMARY_ADDRESS_TO, - getAttributeChangeRequestListener(), getAliasList(), AliasIDType.LTR_NET_UID); - - mESNAttribute = new AliasedStringAttributeMonitor(Attribute.SECONDARY_ADDRESS_FROM, - getAttributeChangeRequestListener(), getAliasList(), AliasIDType.ESN); - } - - @Override - public DecoderType getDecoderType() - { - return DecoderType.LTR_NET; - } - - @Override - public void receive(Message message) - { - if(message.isValid()) - { - State state = State.IDLE; - - if(message instanceof LTRNetOSWMessage) - { - LTRNetOSWMessage ltr = (LTRNetOSWMessage) message; - - switch(ltr.getMessageType()) - { - case CA_ENDD: - if(mChannelNumber == 0) - { - setChannelNumber(ltr.getChannel()); - } - - /* Process FCC Station ID Events */ - if(ltr.getGroup() == 254) - { - if(mCurrentCallEvent == null || - mCurrentCallEvent.getCallEventType() != - CallEventType.STATION_ID) - { - mCurrentCallEvent = new LTRCallEvent.Builder( - DecoderType.LTR_NET, CallEventType.STATION_ID) - .aliasList(getAliasList()) - .channel(String.valueOf(mChannelNumber)) - .frequency(mFrequency) - .to(ltr.getTalkgroupID()) - .build(); - - broadcast(mCurrentCallEvent); - - broadcast(new DecoderStateEvent(this, Event.START, State.DATA)); - } - else - { - broadcast(new DecoderStateEvent(this, Event.CONTINUATION, State.DATA)); - } - } - else - { - processCallEndMessage(ltr); - } - break; - case CA_STRT: - if(mChannelNumber == 0) - { - setChannelNumber(ltr.getChannel()); - } - - /* If the call event channel matches our current channel - * then it's a call, otherwise it's a call detect. */ - if(ltr.getChannel() == mChannelNumber) - { - processCallMessage(ltr); - } - else - { - processCallDetectMessage(ltr); - } - - break; - case SY_IDLE: - if(mChannelNumber != ltr.getChannel()) - { - setChannelNumber(ltr.getChannel()); - } - break; - case MA_CHNH: - break; - case MA_CHNL: - break; - case FQ_RXHI: - case FQ_RXLO: - if(ltr.getFrequency() > 0) - { - mReceiveFrequencies.put(ltr.getHomeRepeater(), ltr.getFrequency()); - } - break; - case FQ_TXHI: - case FQ_TXLO: - if(ltr.getFrequency() > 0) - { - mTransmitFrequencies.put(ltr.getHomeRepeater(), ltr.getFrequency()); - } - break; - case ID_NBOR: - String neighborID = ltr.getNeighborID(); - - if(neighborID != null) - { - mNeighborIDs.add(neighborID); - } - break; - case ID_UNIQ: - state = State.DATA; - - int uniqueID = ltr.getRadioUniqueID(); - - if(uniqueID != LTRNetOSWMessage.INT_NULL_VALUE) - { - mUniqueIDs.add(uniqueID); - } - - if(getCurrentLTRCallEvent() == null) - { - mCurrentCallEvent = new LTRCallEvent.Builder( - DecoderType.LTR_NET, CallEventType.REGISTER) - .aliasList(getAliasList()) - .channel(String.valueOf(mChannelNumber)) - .frequency(mFrequency) - .from(String.valueOf(uniqueID)) - .build(); - } - else - { - mCurrentCallEvent.setFromID( - String.valueOf(ltr.getRadioUniqueID())); - mCurrentCallEvent.setDetails("Unique ID"); - - broadcast(mCurrentCallEvent); - } - - mFromUIDAttribute.process(uniqueID); - break; - case ID_SITE: - String siteID = ltr.getSiteID(); - - if(siteID != null) - { - mSiteIDs.add(siteID); - } - break; - default: - break; - } - } - else if(message instanceof LTRNetISWMessage) - { - LTRNetISWMessage ltr = ((LTRNetISWMessage) message); - - switch(ltr.getMessageType()) - { - case CA_STRT: - processCallMessage(ltr); - break; - case CA_ENDD: - processCallEndMessage(ltr); - break; - case ID_ESNH: - case ID_ESNL: - state = State.DATA; - - String esn = ltr.getESN(); - - if(!esn.contains("xxxx")) - { - mESNs.add(ltr.getESN()); - } - - setMessageType("ESN"); - mESNAttribute.process(ltr.getESN()); - - broadcast(new DecoderStateEvent(this, Event.DECODE, State.DATA)); - - if(mCurrentCallEvent == null) - { - mCurrentCallEvent = new LTRCallEvent.Builder( - DecoderType.LTR_NET, CallEventType.REGISTER_ESN) - .aliasList(getAliasList()) - .details("ESN:" + ltr.getESN()) - .frequency(mFrequency) - .from(ltr.getESN()) - .build(); - - broadcast(mCurrentCallEvent); - } - - break; - case ID_UNIQ: - state = State.DATA; - - int uniqueid = ltr.getRadioUniqueID(); - - if(uniqueid != LTRNetISWMessage.INT_NULL_VALUE) - { - mUniqueIDs.add(uniqueid); - - setMessageType("REGISTER UID"); - mFromUIDAttribute.process(uniqueid); - - if(getCurrentLTRCallEvent() == null) - { - mCurrentCallEvent = new LTRCallEvent.Builder( - DecoderType.LTR_NET, CallEventType.REGISTER) - .aliasList(getAliasList()) - .channel(String.valueOf(mChannelNumber)) - .frequency(mFrequency) - .from(String.valueOf(uniqueid)) - .build(); - } - else - { - mCurrentCallEvent.setFromID( - String.valueOf(ltr.getRadioUniqueID())); - mCurrentCallEvent.setDetails("Unique ID"); - - broadcast(mCurrentCallEvent); - } - } - break; - default: - break; - } - } - } - } - - public LTRCallEvent getCurrentLTRCallEvent() - { - if(mCurrentCallEvent != null) - { - return (LTRCallEvent) mCurrentCallEvent; - } - - return null; - } - - @Override - public String getActivitySummary() - { - StringBuilder sb = new StringBuilder(); - - sb.append("Activity Summary\n"); - sb.append("Decoder:\tLTR-Net\n\n"); - - if(mSiteIDs.isEmpty()) - { - sb.append("Site:\tUnknown\n"); - } - else - { - Iterator it = mSiteIDs.iterator(); - - while(it.hasNext()) - { - sb.append("Site:\t"); - - String siteID = it.next(); - - sb.append(siteID); - - if(hasAliasList()) - { - Alias siteAlias = getAliasList().getSiteID(String.valueOf(siteID)); - - if(siteAlias != null) - { - sb.append(" "); - sb.append(siteAlias.getName()); - } - } - - sb.append("\n"); - } - } - - sb.append("\nLCNs (transmit | receive)\n"); - - if(mReceiveFrequencies.isEmpty() && mTransmitFrequencies.isEmpty()) - { - sb.append(" None\n"); - } - else - { - for(int x = 1; x < 21; x++) - { - long rcv = 0; - - if(mReceiveFrequencies.containsKey(x)) - { - rcv = mReceiveFrequencies.get(x); - } - - long xmt = 0; - - if(mTransmitFrequencies.containsKey(x)) - { - xmt = mTransmitFrequencies.get(x); - } - - if(rcv > 0 || xmt > 0) - { - if(x < 10) - { - sb.append(" "); - } - - sb.append(x); - sb.append(": "); - - if(xmt == 0) - { - sb.append("---.-----"); - } - else - { - sb.append(mDecimalFormatter.format((double) xmt / 1E6d)); - } - sb.append(" | "); - - if(rcv == 0) - { - sb.append("---.-----"); - } - else - { - sb.append(mDecimalFormatter.format((double) rcv / 1E6d)); - } - - if(x == mChannelNumber) - { - sb.append(" **"); - } - - sb.append("\n"); - } - } - } - - sb.append("\nTalkgroups\n"); - - if(mTalkgroups.isEmpty()) - { - sb.append(" None\n"); - } - else - { - Iterator it = mTalkgroups.iterator(); - - while(it.hasNext()) - { - String tgid = it.next(); - - sb.append(" "); - sb.append(tgid); - sb.append(" "); - - if(hasAliasList()) - { - Alias alias = getAliasList().getTalkgroupAlias(tgid); - - if(alias != null) - { - sb.append(alias.getName()); - } - } - - sb.append("\n"); - } - } - - sb.append("\nRadio Unique IDs\n"); - - if(mUniqueIDs.isEmpty()) - { - sb.append(" None\n"); - } - else - { - Iterator it = mUniqueIDs.iterator(); - - while(it.hasNext()) - { - int uid = it.next(); - - sb.append(" "); - sb.append(uid); - sb.append(" "); - - if(hasAliasList()) - { - Alias alias = getAliasList().getUniqueID(uid); - - if(alias != null) - { - sb.append(alias.getName()); - } - } - - sb.append("\n"); - } - } - - sb.append("\nESNs\n"); - - if(mESNs.isEmpty()) - { - sb.append(" None\n"); - } - else - { - Iterator it = mESNs.iterator(); - - while(it.hasNext()) - { - String esn = it.next(); - - sb.append(" "); - sb.append(esn); - sb.append(" "); - - if(hasAliasList()) - { - Alias alias = getAliasList().getESNAlias(esn); - - if(alias != null) - { - sb.append(alias.getName()); - } - } - - sb.append("\n"); - } - } - - sb.append("\nNeighbor Sites\n"); - - if(mNeighborIDs.isEmpty()) - { - sb.append(" None\n"); - } - else - { - Iterator it = mNeighborIDs.iterator(); - - while(it.hasNext()) - { - String neighbor = it.next(); - - sb.append(" "); - sb.append(neighbor); - sb.append(" "); - - if(hasAliasList()) - { - Alias alias = getAliasList().getSiteID(String.valueOf(neighbor)); - - if(alias != null) - { - sb.append(alias.getName()); - } - } - - sb.append("\n"); - } - } - - return sb.toString(); - } - - /** - * Call Detect - current channel messages indicate a call on another channel - */ - private void processCallDetectMessage(LTRNetMessage message) - { - if(!mActiveCalls.containsKey(message.getChannel()) || - !mActiveCalls.get(message.getChannel()).contentEquals(message.getTalkgroupID())) - { - mActiveCalls.put(message.getChannel(), message.getTalkgroupID()); - - int channel = message.getChannel(); - - long frequency = 0; - - if(mTransmitFrequencies.containsKey(channel)) - { - frequency = mTransmitFrequencies.get(channel); - } - - broadcast(new LTRCallEvent.Builder(DecoderType.LTR_NET, CallEventType.CALL_DETECT) - .aliasList(getAliasList()) - .channel(String.valueOf(message.getChannel())) - .frequency(frequency) - .to(message.getTalkgroupID()) - .build()); - } - - broadcast(new DecoderStateEvent(this, Event.CONTINUATION, State.IDLE)); - } - - /** - * Indicates if the talkgroup is different than the talkgroup specified in - * the current call event - */ - private boolean isDifferentTalkgroup(String talkgroup) - { - return talkgroup != null && - mCurrentCallEvent != null && - mCurrentCallEvent.getToID() != null && - !mCurrentCallEvent.getToID().contentEquals(talkgroup); - } - - private boolean isValidTalkgroup(String talkgroup) - { - return talkgroup != null && !talkgroup.matches("0-00-000"); - } - - private void processCallMessage(LTRNetMessage message) - { - int group = message.getGroup(); - - /* Process call registration */ - if(group == 253) - { - setMessageType("REGISTER"); - broadcast(new DecoderStateEvent(this, Event.START, State.DATA)); - } - /* Process call */ - else - { - String talkgroup = message.getTalkgroupID(); - - if(isValidTalkgroup(talkgroup)) - { - mToAttribute.process(talkgroup); - - final LTRCallEvent current = getCurrentLTRCallEvent(); - - //If this is a new call or the talkgroup is different from the current call, create a new call event - if(current == null || isDifferentTalkgroup(talkgroup)) - { - /* Invalidate the current call */ - if(current != null) - { - current.setValid(false); - broadcast(current); - } - - /* A talkgroup must be seen at least once before it will be added - * to the mTalkgroups list that is used in the activity summary, - * so that we don't pollute the summary with one-off error talkgroups */ - if(mTalkgroupsFirstHeard.contains(talkgroup)) - { - mTalkgroups.add(talkgroup); - } - else - { - mTalkgroupsFirstHeard.add(talkgroup); - } - - CallEvent callEvent = new LTRCallEvent.Builder(DecoderType.LTR_NET, CallEventType.CALL) - .aliasList(getAliasList()) - .channel(String.valueOf(message.getChannel())) - .frequency(mFrequency) - .to(message.getTalkgroupID()) - .build(); - - broadcast(callEvent); - - mCurrentCallEvent = callEvent; - - broadcast(new DecoderStateEvent(this, Event.START, State.CALL)); - } - - broadcast(new DecoderStateEvent(this, Event.CONTINUATION, State.CALL)); - } - } - } - - private void processCallEndMessage(LTRNetMessage message) - { - if(mCurrentCallEvent != null && - mCurrentCallEvent.getCallEventType() == CallEventType.CALL) - { - mCurrentCallEvent.setEnd(System.currentTimeMillis()); - broadcast(mCurrentCallEvent); - } - - mCurrentCallEvent = null; - - broadcast(new DecoderStateEvent(this, Event.END, State.FADE)); - } - - /** - * Performs a full reset - */ - public void reset() - { - mChannelNumber = 0; - - mActiveCalls.clear(); - mESNs.clear(); - mNeighborIDs.clear(); - mReceiveFrequencies.clear(); - mSiteIDs.clear(); - mTalkgroups.clear(); - mTransmitFrequencies.clear(); - mUniqueIDs.clear(); - - resetState(); - } - - /** - * Resets the decoder state after a call or other decode event - */ - private void resetState() - { - if(mCurrentCallEvent != null && mCurrentCallEvent - .getCallEventType() == CallEventType.CALL) - { - mCurrentCallEvent.end(); - broadcast(mCurrentCallEvent); - } - - mCurrentCallEvent = null; - - mToAttribute.reset(); - mESNAttribute.reset(); - mFromUIDAttribute.reset(); - mMessage = null; - mMessageType = null; - } - - public String getMessage() - { - return mMessage; - } - - public void setMessage(String message) - { - if(!StringUtils.isEqual(mMessage, message)) - { - mMessage = message; - broadcast(new AttributeChangeRequest(Attribute.MESSAGE, mMessageType)); - } - } - - public String getMessageType() - { - return mMessageType; - } - - private void setMessageType(String messageType) - { - if(!StringUtils.isEqual(mMessageType, messageType)) - { - mMessageType = messageType; - broadcast(new AttributeChangeRequest(Attribute.MESSAGE_TYPE, mMessageType)); - } - } - - public boolean hasChannelNumber() - { - return mChannelNumber != 0; - } - - public int getChannelNumber() - { - return mChannelNumber; - } - - public void setChannelNumber(int channelNumber) - { - if(mChannelNumber != channelNumber) - { - mChannelNumber = channelNumber; - broadcast(new AttributeChangeRequest(Attribute.CHANNEL_FREQUENCY_LABEL, - "LCN:" + getChannelNumber())); - } - } - - @Override - public void receiveDecoderStateEvent(DecoderStateEvent event) - { - switch(event.getEvent()) - { - case RESET: - resetState(); - break; - case SOURCE_FREQUENCY: - mFrequency = event.getFrequency(); - break; - default: - break; - } - } -} diff --git a/src/module/decode/ltrstandard/LTRStandardDecoder.java b/src/module/decode/ltrstandard/LTRStandardDecoder.java deleted file mode 100644 index 40b355c5..00000000 --- a/src/module/decode/ltrstandard/LTRStandardDecoder.java +++ /dev/null @@ -1,87 +0,0 @@ -/******************************************************************************* - * SDR Trunk - * Copyright (C) 2014,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 - ******************************************************************************/ -package module.decode.ltrstandard; - -import java.util.concurrent.ScheduledExecutorService; - -import message.MessageDirection; -import module.decode.Decoder; -import module.decode.DecoderType; -import sample.Listener; -import sample.real.IUnFilteredRealBufferListener; -import sample.real.RealBuffer; -import alias.AliasList; -import bits.MessageFramer; -import bits.SyncPattern; -import dsp.fsk.LTRFSKDecoder; - -public class LTRStandardDecoder extends Decoder implements IUnFilteredRealBufferListener -{ - public static final int LTR_STANDARD_MESSAGE_LENGTH = 40; - - private LTRFSKDecoder mLTRFSKDecoder; - private MessageFramer mLTRMessageFramer; - private LTRStandardMessageProcessor mLTRMessageProcessor; - - /** - * LTR Decoder. Decodes unfiltered (e.g. demodulated but with no DC or - * audio filtering) samples and produces LTR Standard messages. - */ - public LTRStandardDecoder( AliasList aliasList, - MessageDirection direction ) - { - mLTRFSKDecoder = new LTRFSKDecoder(); - - if( direction == MessageDirection.OSW ) - { - mLTRMessageFramer = - new MessageFramer( SyncPattern.LTR_STANDARD_OSW.getPattern(), - LTR_STANDARD_MESSAGE_LENGTH ); - } - else - { - mLTRMessageFramer = - new MessageFramer( SyncPattern.LTR_STANDARD_ISW.getPattern(), - LTR_STANDARD_MESSAGE_LENGTH ); - } - - mLTRFSKDecoder.addListener( mLTRMessageFramer ); - - mLTRMessageProcessor = new LTRStandardMessageProcessor( direction, aliasList ); - mLTRMessageFramer.addMessageListener( mLTRMessageProcessor ); - mLTRMessageProcessor.setMessageListener( this ); - } - - @Override - public DecoderType getDecoderType() - { - return DecoderType.LTR_STANDARD; - } - - @Override - public Listener getUnFilteredRealBufferListener() - { - return mLTRFSKDecoder; - } - - @Override - public void reset() - { - mLTRMessageFramer.reset(); - } -} diff --git a/src/module/decode/ltrstandard/LTRStandardDecoderState.java b/src/module/decode/ltrstandard/LTRStandardDecoderState.java deleted file mode 100644 index 1693b2b6..00000000 --- a/src/module/decode/ltrstandard/LTRStandardDecoderState.java +++ /dev/null @@ -1,466 +0,0 @@ -/******************************************************************************* - * 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 module.decode.ltrstandard; - -import alias.Alias; -import alias.AliasList; -import alias.id.AliasIDType; -import channel.metadata.AliasedStringAttributeMonitor; -import channel.metadata.Attribute; -import channel.metadata.AttributeChangeRequest; -import channel.state.DecoderState; -import channel.state.DecoderStateEvent; -import channel.state.DecoderStateEvent.Event; -import channel.state.State; -import message.Message; -import module.decode.DecoderType; -import module.decode.event.CallEvent.CallEventType; -import module.decode.ltrnet.LTRCallEvent; -import module.decode.ltrstandard.message.CallEndMessage; -import module.decode.ltrstandard.message.CallMessage; -import module.decode.ltrstandard.message.IdleMessage; -import module.decode.ltrstandard.message.LTRStandardMessage; -import org.slf4j.Logger; -import org.slf4j.LoggerFactory; -import util.StringUtils; - -import java.util.ArrayList; -import java.util.HashMap; -import java.util.HashSet; -import java.util.List; -import java.util.Map; -import java.util.Set; -import java.util.TreeSet; -import java.util.concurrent.ScheduledExecutorService; - -public class LTRStandardDecoderState extends DecoderState -{ - private final static Logger mLog = - LoggerFactory.getLogger(LTRStandardDecoderState.class); - - private Map mActiveCalls = new HashMap<>(); - private Set mTalkgroupsFirstHeard = new HashSet<>(); - private Set mTalkgroups = new TreeSet<>(); - - private AliasedStringAttributeMonitor mTalkgroupAttribute; - private long mFrequency; - private LCNTracker mLCNTracker = new LCNTracker(); - - public LTRStandardDecoderState(AliasList aliasList) - { - super(aliasList); - - mTalkgroupAttribute = new AliasedStringAttributeMonitor(Attribute.PRIMARY_ADDRESS_TO, - getAttributeChangeRequestListener(), getAliasList(), AliasIDType.TALKGROUP); - } - - @Override - public DecoderType getDecoderType() - { - return DecoderType.LTR_STANDARD; - } - - @Override - public void receive(Message message) - { - if(message.isValid() && message instanceof LTRStandardMessage) - { - switch(((LTRStandardMessage) message).getMessageType()) - { - case CA_STRT: - CallMessage start = (CallMessage) message; - - int channel = start.getChannel(); - - setChannelNumber(channel); - - mLCNTracker.processFreeChannel(start.getFree()); - - //Only process calls on this LCN, or call detects for - //talkgroups that are homed on this LCN - if(mLCNTracker.isValidChannel(channel) && - (mLCNTracker.isCurrentChannel(channel) || - mLCNTracker.isCurrentChannel(start.getHomeRepeater()))) - { - LTRCallEvent event = mActiveCalls.get(channel); - - if(event == null || !event.isMatchingTalkgroup(start.getToID())) - { - //Check for different talkgroup - if(event != null) - { - event.end(); - mActiveCalls.remove(channel); - } - - boolean current = mLCNTracker.isCurrentChannel(channel); - - event = new LTRCallEvent.Builder( - DecoderType.LTR_STANDARD, - current ? CallEventType.CALL : CallEventType.CALL_DETECT) - .to(start.getToID()) - .aliasList(getAliasList()) - .channel(start.getChannelFormatted()) - .frequency(current ? mFrequency : 0) - .build(); - - mActiveCalls.put(channel, event); - - broadcast(event); - - if(mLCNTracker.isCurrentChannel(channel)) - { - String talkgroup = start.getToID(); - mTalkgroupAttribute.process(talkgroup); - processTalkgroup(talkgroup); - } - } - - broadcast(new DecoderStateEvent(this, Event.CONTINUATION, State.CALL)); - } - break; - case CA_ENDD: - CallEndMessage end = (CallEndMessage) message; - - //Home channel is 31 for call end -- use the free channel - //as the call end channel - int repeater = end.getFree(); - - setChannelNumber(repeater); - - if(mLCNTracker.isCurrentChannel(repeater)) - { - String talkgroup = end.getToID(); - mTalkgroupAttribute.process(talkgroup); - processTalkgroup(talkgroup); - - LTRCallEvent event = mActiveCalls.remove(repeater); - - if(event != null) - { - event.end(); - broadcast(event); - broadcast(new DecoderStateEvent(this, Event.END, State.FADE)); - } - } - break; - case SY_IDLE: - IdleMessage idle = (IdleMessage) message; - - int lcn = idle.getChannel(); - - mLCNTracker.processCallChannel(lcn); - - break; - case UN_KNWN: - default: - break; - } - } - } - - private void processTalkgroup(String talkgroup) - { - if(mTalkgroupsFirstHeard.contains(talkgroup)) - { - mTalkgroups.add(talkgroup); - } - else - { - mTalkgroupsFirstHeard.add(talkgroup); - } - } - - /** - * Performs a full reset - */ - public void reset() - { - mActiveCalls.clear(); - mTalkgroupsFirstHeard.clear(); - mTalkgroups.clear(); - mLCNTracker.reset(); - - resetState(); - } - - /** - * Performs a temporal reset following a call or other decode event - */ - private void resetState() - { - for(Integer key : mActiveCalls.keySet()) - { - LTRCallEvent event = mActiveCalls.get(key); - - if(event != null) - { - event.end(); - - broadcast(event); - } - } - - mActiveCalls.clear(); - - mTalkgroupAttribute.reset(); - } - - public boolean hasChannelNumber() - { - return mLCNTracker.getCurrentChannel() != 0; - } - - public int getChannelNumber() - { - return mLCNTracker.getCurrentChannel(); - } - - private void setChannelNumber(int channel) - { - int original = mLCNTracker.getCurrentChannel(); - - mLCNTracker.processCallChannel(channel); - - if(mLCNTracker.getCurrentChannel() != original) - { - broadcast(new AttributeChangeRequest(Attribute.CHANNEL_FREQUENCY_LABEL, - "LCN:" + mLCNTracker.getCurrentChannel())); - } - } - - @Override - public void receiveDecoderStateEvent(DecoderStateEvent event) - { - switch(event.getEvent()) - { - case RESET: - resetState(); - break; - case SOURCE_FREQUENCY: - mFrequency = event.getFrequency(); - break; - default: - break; - } - } - - @Override - public String getActivitySummary() - { - StringBuilder sb = new StringBuilder(); - - sb.append("Activity Summary\n\n"); - - sb.append("Decoder:\tLTR-Standard\n"); - - sb.append("Monitored LCN: "); - - if(hasChannelNumber()) - { - sb.append(getChannelNumber()); - } - else - { - sb.append("*Insufficient Data*"); - } - - sb.append("\n"); - - sb.append("Active LCNs:\t"); - - List lcns = mLCNTracker.getActiveLCNs(); - - if(lcns.size() > 0) - { - sb.append(mLCNTracker.getActiveLCNs()); - } - else - { - sb.append("*Insufficient Data*"); - } - sb.append("\n\n"); - - sb.append("Talkgroups\n"); - - if(mTalkgroups.isEmpty()) - { - sb.append(" None\n"); - } - else - { - for(String talkgroup : mTalkgroups) - { - sb.append(" "); - sb.append(talkgroup); - sb.append(" "); - - if(hasAliasList()) - { - Alias alias = getAliasList().getTalkgroupAlias(talkgroup); - - if(alias != null) - { - sb.append(alias.getName()); - } - } - - sb.append("\n"); - - } - } - - return sb.toString(); - } - - /** - * Tracks the set of call and free channels for a system in order to - * dynamically determine the current LCN and minimize false triggers from - * bogus decoded LTR messages. Tracks the number of occurances of each - * LCN and uses a dynamic threshold to determine LCN validity. - */ - public class LCNTracker - { - private static final int DEFAULT_COUNT = 10; - private int[] mCallLCNCounts; - private int[] mFreeLCNCounts; - private int mCallHighestCount; - private int mFreeHighestCount; - private int mCurrentLCN; - - public LCNTracker() - { - reset(); - } - - public void logStatistics() - { - for(int x = 1; x <= 20; x++) - { - mLog.debug("Call " + x + ": " + mCallLCNCounts[x]); - } - mLog.debug("Call Highest Count: " + mCallHighestCount); - for(int x = 1; x <= 20; x++) - { - mLog.debug("Free " + x + ": " + mFreeLCNCounts[x]); - } - mLog.debug("Free Highest Count: " + mFreeHighestCount); - - mLog.debug("Current LCN: " + mCurrentLCN); - } - - public void reset() - { - //Dim channel arrays to 21 -- ignore the 0 index - mCallLCNCounts = new int[21]; - mFreeLCNCounts = new int[21]; - mCallHighestCount = 3; - mFreeHighestCount = DEFAULT_COUNT; - mCurrentLCN = 0; - } - - public boolean isCurrentChannel(int channel) - { - if(mCurrentLCN == 0) - { - return true; - } - else - { - return channel == mCurrentLCN; - } - } - - public int getCurrentChannel() - { - return mCurrentLCN; - } - - /** - * Indicates if the channel is valid based on the observed current and - * free channels reported by the system. Tracks the count of each - * reported free channel the highest free channel count is used as a - * threshold where a channel is valid when its count exceeds 20% of the - * highest free channel count. A channel is also valid when it is - * the currently monitored channel. - */ - public boolean isValidChannel(int channel) - { - if(isCurrentChannel(channel)) - { - return true; - } - - int count = mFreeLCNCounts[channel]; - - int threshold = (int) ((double) mFreeHighestCount * 0.2); - - return count >= threshold; - } - - public void processCallChannel(int channel) - { - if(1 <= channel && channel <= 20) - { - mCallLCNCounts[channel]++; - - if(mCallLCNCounts[channel] > mCallHighestCount) - { - mCallHighestCount = mCallLCNCounts[channel]; - mCurrentLCN = channel; - } - } - } - - public void processFreeChannel(int channel) - { - if(1 <= channel && channel <= 20) - { - mFreeLCNCounts[channel]++; - - if(mFreeLCNCounts[channel] > mFreeHighestCount) - { - mFreeHighestCount = mFreeLCNCounts[channel]; - } - } - } - - public List getActiveLCNs() - { - List active = new ArrayList<>(); - - if(mFreeHighestCount > DEFAULT_COUNT) - { - for(int x = 1; x <= 20; x++) - { - if(mCurrentLCN != 0 && isCurrentChannel(x)) - { - active.add(x); - } - else if(isValidChannel(x)) - { - active.add(x); - } - } - } - - return active; - } - } -} \ No newline at end of file diff --git a/src/module/decode/mdc1200/MDCDecoder.java b/src/module/decode/mdc1200/MDCDecoder.java deleted file mode 100644 index 2bd89fba..00000000 --- a/src/module/decode/mdc1200/MDCDecoder.java +++ /dev/null @@ -1,205 +0,0 @@ -/******************************************************************************* - * SDR Trunk - * Copyright (C) 2014 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 module.decode.mdc1200; - -import instrument.Instrumentable; -import instrument.tap.Tap; -import instrument.tap.TapGroup; -import instrument.tap.stream.BinaryTap; -import instrument.tap.stream.FloatBufferTap; -import instrument.tap.stream.FloatTap; - -import java.util.ArrayList; -import java.util.List; -import java.util.concurrent.ScheduledExecutorService; - -import module.decode.Decoder; -import module.decode.DecoderType; -import sample.Listener; -import sample.real.IFilteredRealBufferListener; -import sample.real.RealBuffer; -import alias.AliasList; -import bits.MessageFramer; -import bits.SyncPattern; -import dsp.NRZDecoder; -import dsp.filter.Filters; -import dsp.filter.fir.real.RealFIRFilter_RB_RB; -import dsp.filter.halfband.real.HalfBandFilter_RB_RB; -import dsp.fsk.FSK2Decoder; -import dsp.fsk.FSK2Decoder.Output; - -/** - * MDC1200 Decoder - 1200 baud 2FSK decoder that can process 48k sample rate - * complex or floating point samples and output fully framed MDC1200 messages - */ -public class MDCDecoder extends Decoder implements IFilteredRealBufferListener, - Instrumentable -{ - /* Decimated sample rate ( 48,000 / 2 = 24,000 ) feeding the decoder */ - private static final int sDECIMATED_SAMPLE_RATE = 24000; - - /* Baud or Symbol Rate */ - private static final int sSYMBOL_RATE = 1200; - - /* Message length */ - private static final int sMESSAGE_LENGTH = 304; - - /* Instrumentation Taps */ - private ArrayList mAvailableTaps; - private static final String INSTRUMENT_BANDPASS_FILTER_TO_FSK2_DEMOD = - "Tap Point: Bandpass Filter > < FSK2 Decoder"; - private static final String INSTRUMENT_FSK2_DECODER_TO_NRZ_DECODER = - "Tap Point: FSK2 Decoder > < NRZI Decoder"; - private static final String INSTRUMENT_NRZI_DECODER_TO_MESSAGE_FRAMER = - "Tap Point: NRZ Decoder > < Message Framer"; - - private FSK2Decoder mFSKDecoder; - private HalfBandFilter_RB_RB mDecimationFilter; - private RealFIRFilter_RB_RB mBandPassFilter; - private NRZDecoder mNRZDecoder; - private MessageFramer mMessageFramer; - private MDCMessageProcessor mMessageProcessor; - - public MDCDecoder( AliasList aliasList ) - { - /* Decimation filter: 48000 / 2 = 24000 */ - mDecimationFilter = new HalfBandFilter_RB_RB( - Filters.FIR_HALF_BAND_31T_ONE_EIGHTH_FCO.getCoefficients(), 1.0f, true ); - - /* Bandpass filter - predefined */ - mBandPassFilter = new RealFIRFilter_RB_RB( - Filters.FIRBP_1200FSK_24000FS.getCoefficients(), 1.02f ); - mDecimationFilter.setListener( mBandPassFilter ); - - /* 2FSK Decoder */ - mFSKDecoder = new FSK2Decoder( sDECIMATED_SAMPLE_RATE, - sSYMBOL_RATE, Output.NORMAL ); - mBandPassFilter.setListener( mFSKDecoder ); - - /* NRZ Decoder */ - mNRZDecoder = new NRZDecoder( NRZDecoder.MODE_INVERTED ); - mFSKDecoder.setListener( mNRZDecoder ); - - /* Message Framer */ - mMessageFramer = new MessageFramer( SyncPattern.MDC1200.getPattern(), - sMESSAGE_LENGTH ); - mNRZDecoder.setListener( mMessageFramer ); - - /* Message Processor */ - mMessageProcessor = new MDCMessageProcessor( aliasList ); - mMessageFramer.addMessageListener( mMessageProcessor ); - mMessageProcessor.addMessageListener( this ); - } - - public void dispose() - { - super.dispose(); - - mDecimationFilter.dispose(); - mBandPassFilter.dispose(); - mFSKDecoder.dispose(); - mNRZDecoder.dispose(); - mMessageFramer.dispose(); - mMessageProcessor.dispose(); - } - - /* Instrumentation */ - @Override - public List getTapGroups() - { - if( mAvailableTaps == null ) - { - mAvailableTaps = new ArrayList(); - - TapGroup group = new TapGroup( "MDC-1200 Decoder" ); - - group.add( - new FloatTap( INSTRUMENT_BANDPASS_FILTER_TO_FSK2_DEMOD, 0, 0.5f ) ); - group.add( - new BinaryTap( INSTRUMENT_FSK2_DECODER_TO_NRZ_DECODER, 0, 0.025f ) ); - group.add( - new BinaryTap( INSTRUMENT_NRZI_DECODER_TO_MESSAGE_FRAMER, 0, 0.025f ) ); - - mAvailableTaps.add( group ); - mAvailableTaps.addAll( mFSKDecoder.getTapGroups() ); - } - - return mAvailableTaps; - } - - @Override - public void registerTap( Tap tap ) - { - mFSKDecoder.registerTap( tap ); - - switch( tap.getName() ) - { - case INSTRUMENT_BANDPASS_FILTER_TO_FSK2_DEMOD: - /* wire the tap between the bandpass filter and the demod */ - FloatBufferTap floatTap = (FloatBufferTap)tap; - mBandPassFilter.setListener( floatTap ); - floatTap.setListener( mFSKDecoder ); - break; - case INSTRUMENT_FSK2_DECODER_TO_NRZ_DECODER: - /* wire between FSK2 decoder and NRZ decoder */ - BinaryTap binTap = (BinaryTap)tap; - mFSKDecoder.setListener( binTap ); - binTap.setListener( mNRZDecoder ); - break; - case INSTRUMENT_NRZI_DECODER_TO_MESSAGE_FRAMER: - /* wire between demod and framer */ - BinaryTap binInvTap = (BinaryTap)tap; - mNRZDecoder.setListener( binInvTap ); - binInvTap.setListener( mMessageFramer ); - break; - default: - } - } - - @Override - public void unregisterTap( Tap tap ) - { - mFSKDecoder.unregisterTap( tap ); - - switch( tap.getName() ) - { - case INSTRUMENT_BANDPASS_FILTER_TO_FSK2_DEMOD: - mBandPassFilter.setListener( mFSKDecoder ); - break; - case INSTRUMENT_FSK2_DECODER_TO_NRZ_DECODER: - mFSKDecoder.setListener( mNRZDecoder ); - break; - case INSTRUMENT_NRZI_DECODER_TO_MESSAGE_FRAMER: - mNRZDecoder.setListener( mMessageFramer ); - break; - default: - } - } - - @Override - public DecoderType getDecoderType() - { - return DecoderType.MDC1200; - } - - @Override - public Listener getFilteredRealBufferListener() - { - return mDecimationFilter; - } -} diff --git a/src/module/decode/mdc1200/MDCDecoderState.java b/src/module/decode/mdc1200/MDCDecoderState.java deleted file mode 100644 index a70c8f0f..00000000 --- a/src/module/decode/mdc1200/MDCDecoderState.java +++ /dev/null @@ -1,253 +0,0 @@ -/******************************************************************************* - * 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 module.decode.mdc1200; - -import alias.Alias; -import alias.AliasList; -import alias.id.AliasIDType; -import channel.metadata.AliasedStringAttributeMonitor; -import channel.metadata.Attribute; -import channel.metadata.AttributeChangeRequest; -import channel.state.DecoderState; -import channel.state.DecoderStateEvent; -import channel.state.DecoderStateEvent.Event; -import channel.state.State; -import message.Message; -import module.decode.DecoderType; -import util.StringUtils; - -import java.util.Iterator; -import java.util.TreeSet; -import java.util.concurrent.ScheduledExecutorService; - -public class MDCDecoderState extends DecoderState -{ - private TreeSet mIdents = new TreeSet(); - private TreeSet mEmergencyIdents = new TreeSet(); - - private AliasedStringAttributeMonitor mFromAttribute; - private String mMessage; - private String mMessageType; - - public MDCDecoderState(AliasList aliasList) - { - super(aliasList); - - mFromAttribute = new AliasedStringAttributeMonitor(Attribute.SECONDARY_ADDRESS_FROM, - getAttributeChangeRequestListener(), getAliasList(), AliasIDType.MDC1200); - } - - @Override - public DecoderType getDecoderType() - { - return DecoderType.MDC1200; - } - - @Override - public void reset() - { - mIdents.clear(); - mEmergencyIdents.clear(); - - resetState(); - } - - private void resetState() - { - mFromAttribute.reset(); - mMessage = null; - mMessageType = null; - } - - @Override - public void receive(Message message) - { - if(message instanceof MDCMessage) - { - MDCMessage mdc = (MDCMessage) message; - - mIdents.add(mdc.getUnitID()); - - if(mdc.isEmergency()) - { - mEmergencyIdents.add(mdc.getUnitID()); - } - - mFromAttribute.process(mdc.getFromID()); - - MDCMessageType type = mdc.getMessageType(); - - setMessageType(type.getLabel()); - - StringBuilder sb = new StringBuilder(); - - sb.append("OPCODE "); - sb.append(String.valueOf(mdc.getOpcode())); - - if(mdc.isBOT()) - { - sb.append(" TYPE:BOT"); - } - - if(mdc.isEOT()) - { - sb.append(" TYPE:EOT"); - } - - setMessage(sb.toString()); - - MDCCallEvent event = MDCCallEvent.getMDCCallEvent(mdc); - event.setAliasList(getAliasList()); - broadcast(event); - - switch(type) - { - case ANI: - broadcast(new DecoderStateEvent(this, Event.DECODE, State.CALL)); - break; - case ACKNOWLEDGE: - case EMERGENCY: - case PAGING: - case STATUS: - default: - broadcast(new DecoderStateEvent(this, Event.DECODE, State.DATA)); - break; - } - - } - } - - public String getMessage() - { - return mMessage; - } - - public void setMessage(String message) - { - if(!StringUtils.isEqual(mMessage, message)) - { - mMessage = message; - broadcast(new AttributeChangeRequest(Attribute.MESSAGE, mMessage)); - } - } - - public String getMessageType() - { - return mMessageType; - } - - public void setMessageType(String type) - { - if(!StringUtils.isEqual(mMessageType, type)) - { - mMessageType = type; - broadcast(new AttributeChangeRequest(Attribute.MESSAGE_TYPE, "MDC:" + mMessageType)); - } - } - - @Override - public String getActivitySummary() - { - StringBuilder sb = new StringBuilder(); - - sb.append("=============================\n"); - sb.append("Decoder:\tMDC-1200\n\n"); - sb.append("MDC-1200 Idents\n"); - - if(mIdents.isEmpty()) - { - sb.append(" None\n"); - } - else - { - Iterator it = mIdents.iterator(); - - while(it.hasNext()) - { - String ident = it.next(); - - sb.append(" "); - sb.append(ident); - sb.append(" "); - - if(hasAliasList()) - { - Alias alias = getAliasList().getMDC1200Alias(ident); - - if(alias != null) - { - sb.append(alias.getName()); - } - } - - sb.append("\n"); - } - } - - sb.append("MDC-1200 Emergency Idents\n"); - - if(mEmergencyIdents.isEmpty()) - { - sb.append(" None\n"); - } - else - { - Iterator it = mEmergencyIdents.iterator(); - - while(it.hasNext()) - { - String ident = it.next(); - - sb.append(" "); - sb.append(ident); - sb.append(" "); - - if(hasAliasList()) - { - Alias alias = getAliasList().getMDC1200Alias(ident); - - if(alias != null) - { - sb.append(alias.getName()); - } - } - - sb.append("\n"); - } - } - - return sb.toString(); - } - - /** - * Responds to reset events issued by the channel state - */ - @Override - public void receiveDecoderStateEvent(DecoderStateEvent event) - { - switch(event.getEvent()) - { - case RESET: - resetState(); - break; - default: - break; - } - } -} diff --git a/src/module/decode/mpt1327/MPT1327Decoder.java b/src/module/decode/mpt1327/MPT1327Decoder.java deleted file mode 100644 index 228f8dc9..00000000 --- a/src/module/decode/mpt1327/MPT1327Decoder.java +++ /dev/null @@ -1,300 +0,0 @@ -/******************************************************************************* - * 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 module.decode.mpt1327; - -import alias.AliasList; -import bits.MessageFramer; -import bits.SyncPattern; -import dsp.filter.FilterFactory; -import dsp.filter.Filters; -import dsp.filter.fir.FIRFilterSpecification; -import dsp.filter.fir.real.RealFIRFilter_RB_RB; -import dsp.filter.halfband.real.HalfBandFilter_RB_RB; -import dsp.fsk.FSK2Decoder; -import dsp.fsk.FSK2Decoder.Output; -import instrument.Instrumentable; -import instrument.tap.Tap; -import instrument.tap.TapGroup; -import instrument.tap.stream.BinaryTap; -import instrument.tap.stream.FloatBufferTap; -import instrument.tap.stream.FloatTap; -import module.decode.Decoder; -import module.decode.DecoderType; -import org.slf4j.Logger; -import org.slf4j.LoggerFactory; -import sample.Broadcaster; -import sample.Listener; -import sample.real.IFilteredRealBufferListener; -import sample.real.RealBuffer; - -import java.util.ArrayList; -import java.util.List; -import java.util.concurrent.ScheduledExecutorService; - -/** - * MPT1327 Decoder - 1200 baud 2FSK decoder that can process 48k sample rate - * complex or floating point samples and output fully framed MPT1327 control and - * traffic messages. - */ -public class MPT1327Decoder extends Decoder implements IFilteredRealBufferListener, Instrumentable -{ - private final static Logger mLog = LoggerFactory.getLogger(MPT1327Decoder.class); - - private static final FIRFilterSpecification HIGH_PASS_SPECIFICATION = FIRFilterSpecification.highPassBuilder() - .sampleRate( 24000 ) - .stopBandCutoff( 800 ) - .stopBandAmplitude( 0.0 ) - .stopBandRipple( 0.03 ) - .passBandStart( 1000 ) - .passBandAmplitude( 1.0 ) - .passBandRipple( 0.08 ) - .build(); - - private static float[] HIGHPASS_FILTER; - - static - { - try - { - HIGHPASS_FILTER = FilterFactory.getTaps(HIGH_PASS_SPECIFICATION); - } - catch(Exception e) - { - mLog.error("Couldn't design MPT1327 highpass filter"); - } - } - - /* Decimated sample rate ( 48,000 / 2 = 24,000 ) feeding the decoder */ - private static final int sDECIMATED_SAMPLE_RATE = 24000; - - /* Baud or Symbol Rate */ - private static final int sSYMBOL_RATE = 1200; - - /* Message length -- longest possible message is: - * 4xREVS + 16xSYNC + 64xADD1 + 64xDCW1 + 64xDCW2 + 64xDCW3 + 64xDCW4 */ - private static final int sMESSAGE_LENGTH = 350; - - /* Instrumentation Taps */ - private static final String INSTRUMENT_HB1_FILTER_TO_HIGH_PASS = - "Tap Point: Half-band Decimation Filter > < Low Pass Filter"; - private static final String INSTRUMENT_HIGH_PASS_TO_DECODER = - "Tap Point: Low Pass Filter > < Decoder"; - private static final String INSTRUMENT_DECODER_TO_FRAMER = - "Tap Point: Decoder > < Sync Detect/Message Framer"; - private List mAvailableTaps; - - private HalfBandFilter_RB_RB mDecimationFilter; - private RealFIRFilter_RB_RB mHighPassFilter; - private FSK2Decoder mFSKDecoder; - private Broadcaster mSymbolBroadcaster; - private MessageFramer mControlMessageFramer; - private MessageFramer mTrafficMessageFramer; - private MPT1327MessageProcessor mMessageProcessor; - - public MPT1327Decoder(AliasList aliasList, Sync sync) - { - /** - * Normal: 2FSK Decoder with inverted output - * French: 2FSK Decoder with normal output - */ - if(sync == Sync.NORMAL) - { - mFSKDecoder = new FSK2Decoder(24000, 1200, Output.INVERTED); - } - else if(sync == Sync.FRENCH) - { - mFSKDecoder = new FSK2Decoder(24000, 1200, Output.NORMAL); - } - else - { - throw new IllegalArgumentException("MPT1327 Decoder - unrecognized Sync type"); - } - - /* Decimation filter - 48000 / 2 = 24000 output */ - mDecimationFilter = new HalfBandFilter_RB_RB(Filters.FIR_HALF_BAND_31T_ONE_EIGHTH_FCO.getCoefficients(), 1.0002f, true); - - //High-pass filter the audio to remove any DC component or CTCSS tones. The audio has already been low-pass - //filtered by the AudioDemodulation filter cutoff of 3400 Hz. - mHighPassFilter = new RealFIRFilter_RB_RB(HIGHPASS_FILTER, 1.0f); - mDecimationFilter.setListener(mHighPassFilter); - - mHighPassFilter.setListener(mFSKDecoder); - - mSymbolBroadcaster = new Broadcaster(); - mFSKDecoder.setListener(mSymbolBroadcaster); - - /* Message framer for control channel messages */ - mControlMessageFramer = new MessageFramer(sync.getControlSyncPattern().getPattern(), sMESSAGE_LENGTH); - mSymbolBroadcaster.addListener(mControlMessageFramer); - - /* Message framer for traffic channel massages */ - mTrafficMessageFramer = new MessageFramer(sync.getTrafficSyncPattern().getPattern(), sMESSAGE_LENGTH); - mSymbolBroadcaster.addListener(mTrafficMessageFramer); - - /* Fully decoded and framed messages processor */ - mMessageProcessor = new MPT1327MessageProcessor(aliasList); - mMessageProcessor.setMessageListener(this); - mControlMessageFramer.addMessageListener(mMessageProcessor); - mTrafficMessageFramer.addMessageListener(mMessageProcessor); - } - - @Override - public Listener getFilteredRealBufferListener() - { - return mDecimationFilter; - } - - @Override - public DecoderType getDecoderType() - { - return DecoderType.MPT1327; - } - - /** - * Cleanup method - */ - public void dispose() - { - super.dispose(); - - mSymbolBroadcaster.dispose(); - - mControlMessageFramer.dispose(); - - mFSKDecoder.dispose(); - - mDecimationFilter.dispose(); - - mMessageProcessor.dispose(); - - mTrafficMessageFramer.dispose(); - } - - /* Instrumentation Taps */ - @Override - public List getTapGroups() - { - if(mAvailableTaps == null) - { - mAvailableTaps = new ArrayList(); - - TapGroup group = new TapGroup("MPT-1327 Decoder"); - - group.add(new FloatTap(INSTRUMENT_HB1_FILTER_TO_HIGH_PASS, 0, 0.5f)); - group.add(new FloatTap(INSTRUMENT_HIGH_PASS_TO_DECODER, 0, 0.5f)); - group.add(new BinaryTap(INSTRUMENT_DECODER_TO_FRAMER, 0, 0.025f)); - - mAvailableTaps.add(group); - - /* Add the taps from the FSK decoder */ - mAvailableTaps.addAll(mFSKDecoder.getTapGroups()); - } - - return mAvailableTaps; - } - - @Override - public void registerTap(Tap tap) - { - /* Send request to decoder */ - mFSKDecoder.registerTap(tap); - - switch(tap.getName()) - { - case INSTRUMENT_HB1_FILTER_TO_HIGH_PASS: - FloatBufferTap hb1Tap = (FloatBufferTap) tap; - mDecimationFilter.setListener(hb1Tap); - hb1Tap.setListener(mHighPassFilter); - break; - case INSTRUMENT_HIGH_PASS_TO_DECODER: - FloatBufferTap lowTap = (FloatBufferTap) tap; - mHighPassFilter.setListener(lowTap); - lowTap.setListener(mFSKDecoder); - break; - case INSTRUMENT_DECODER_TO_FRAMER: - BinaryTap decoderTap = (BinaryTap) tap; - mFSKDecoder.setListener(decoderTap); - decoderTap.setListener(mSymbolBroadcaster); - break; - } - } - - @Override - public void unregisterTap(Tap tap) - { - mFSKDecoder.unregisterTap(tap); - - switch(tap.getName()) - { - case INSTRUMENT_HB1_FILTER_TO_HIGH_PASS: - mDecimationFilter.setListener(mHighPassFilter); - break; - case INSTRUMENT_HIGH_PASS_TO_DECODER: - mHighPassFilter.setListener(mFSKDecoder); - break; - case INSTRUMENT_DECODER_TO_FRAMER: - mFSKDecoder.setListener(mSymbolBroadcaster); - break; - } - } - - public enum Sync - { - NORMAL("Normal", SyncPattern.MPT1327_CONTROL, SyncPattern.MPT1327_TRAFFIC), - FRENCH("French", SyncPattern.MPT1327_CONTROL_FRENCH, SyncPattern.MPT1327_TRAFFIC_FRENCH); - - private String mLabel; - private SyncPattern mControlSyncPattern; - private SyncPattern mTrafficSyncPattern; - - private Sync(String label, SyncPattern controlPattern, SyncPattern trafficPattern) - { - mLabel = label; - mControlSyncPattern = controlPattern; - mTrafficSyncPattern = trafficPattern; - } - - public String getLabel() - { - return mLabel; - } - - public SyncPattern getControlSyncPattern() - { - return mControlSyncPattern; - } - - public SyncPattern getTrafficSyncPattern() - { - return mTrafficSyncPattern; - } - - public String toString() - { - return getLabel(); - } - } - - @Override - public void reset() - { - mControlMessageFramer.reset(); - mTrafficMessageFramer.reset(); - } -} diff --git a/src/module/decode/mpt1327/MPT1327DecoderState.java b/src/module/decode/mpt1327/MPT1327DecoderState.java deleted file mode 100644 index 940e1496..00000000 --- a/src/module/decode/mpt1327/MPT1327DecoderState.java +++ /dev/null @@ -1,541 +0,0 @@ -/******************************************************************************* - * 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 module.decode.mpt1327; - -import alias.Alias; -import alias.AliasList; -import alias.id.AliasIDType; -import channel.metadata.AliasedStringAttributeMonitor; -import channel.metadata.Attribute; -import channel.metadata.AttributeChangeRequest; -import channel.state.ChangeChannelTimeoutEvent; -import channel.state.DecoderState; -import channel.state.DecoderStateEvent; -import channel.state.DecoderStateEvent.Event; -import channel.state.State; -import channel.traffic.TrafficChannelAllocationEvent; -import controller.channel.Channel.ChannelType; -import controller.channel.map.ChannelMap; -import message.Message; -import module.decode.DecoderType; -import module.decode.config.DecodeConfiguration; -import module.decode.event.CallEvent; -import module.decode.event.CallEvent.CallEventType; -import module.decode.mpt1327.MPT1327Message.IdentType; -import org.slf4j.Logger; -import org.slf4j.LoggerFactory; -import util.StringUtils; - -import java.util.ArrayList; -import java.util.Collections; -import java.util.HashMap; -import java.util.Iterator; -import java.util.List; -import java.util.TreeSet; -import java.util.concurrent.ScheduledExecutorService; - -public class MPT1327DecoderState extends DecoderState -{ - private final static Logger mLog = LoggerFactory.getLogger(MPT1327DecoderState.class); - - private TreeSet mIdents = new TreeSet(); - private HashMap> mGroups = new HashMap>(); - - private String mSite; - private AliasedStringAttributeMonitor mFromAttribute; - private AliasedStringAttributeMonitor mToAttribute; - - private int mChannelNumber; - private ChannelType mChannelType; - private ChannelMap mChannelMap; - - private long mFrequency = 0; - private long mCallTimeout; - - public MPT1327DecoderState(AliasList aliasList, ChannelMap channelMap, ChannelType channelType, long callTimeout) - { - super(aliasList); - - mChannelMap = channelMap; - mChannelType = channelType; - mCallTimeout = callTimeout; - mFromAttribute = new AliasedStringAttributeMonitor(Attribute.PRIMARY_ADDRESS_FROM, - getAttributeChangeRequestListener(), getAliasList(), AliasIDType.MPT1327); - mToAttribute = new AliasedStringAttributeMonitor(Attribute.PRIMARY_ADDRESS_TO, - getAttributeChangeRequestListener(), getAliasList(), AliasIDType.MPT1327); - } - - @Override - public DecoderType getDecoderType() - { - return DecoderType.MPT1327; - } - - public ChannelType getChannelType() - { - return mChannelType; - } - - public void dispose() - { - super.dispose(); - } - - @Override - public void receive(Message message) - { - if(message.isValid()) - { - if(message instanceof MPT1327Message) - { - MPT1327Message mpt = ((MPT1327Message) message); - - switch(mpt.getMessageType()) - { - case ACK: - mIdents.add(mpt.getFromID()); - - IdentType identType = mpt.getIdent1Type(); - - if(identType == IdentType.REGI) - { - broadcast(new MPT1327CallEvent.Builder(CallEventType.REGISTER) - .aliasList(getAliasList()) - .channel(String.valueOf(mChannelNumber)) - .details("REGISTERED ON NETWORK") - .frequency(mFrequency) - .from(mpt.getToID()) - .to(mpt.getFromID()) - .build()); - } - else - { - broadcast(new MPT1327CallEvent.Builder(CallEventType.RESPONSE) - .aliasList(getAliasList()) - .channel(String.valueOf(mChannelNumber)) - .details("ACK " + identType.getLabel()) - .frequency(mFrequency) - .from(mpt.getFromID()) - .to(mpt.getToID()) - .build()); - } - - broadcast(new DecoderStateEvent(this, Event.CONTINUATION, State.CONTROL)); - break; - case ACKI: - mIdents.add(mpt.getFromID()); - mIdents.add(mpt.getToID()); - - broadcast(new DecoderStateEvent(this, Event.CONTINUATION, State.CONTROL)); - break; - case AHYC: - mIdents.add(mpt.getToID()); - - broadcast(new MPT1327CallEvent.Builder(CallEventType.COMMAND) - .aliasList(getAliasList()) - .channel(String.valueOf(mChannelNumber)) - .details(((MPT1327Message) message).getRequestString()) - .frequency(mFrequency) - .from(mpt.getFromID()) - .to(mpt.getToID()) - .build()); - - broadcast(new DecoderStateEvent(this, Event.CONTINUATION, State.CONTROL)); - break; - case AHYQ: - broadcast(new MPT1327CallEvent.Builder(CallEventType.STATUS) - .aliasList(getAliasList()) - .channel(String.valueOf(mChannelNumber)) - .details(mpt.getStatusMessage()) - .frequency(mFrequency) - .from(mpt.getFromID()) - .to(mpt.getToID()) - .build()); - - broadcast(new DecoderStateEvent(this, Event.CONTINUATION, State.CONTROL)); - break; - case ALH: - setSite(mpt.getSiteID()); - broadcast(new DecoderStateEvent(this, Event.START, State.CONTROL)); - break; - case GTC: - String from = mpt.getFromID(); - String to = mpt.getToID(); - - if(from != null) - { - mIdents.add(from); - } - - if(to != null) - { - mIdents.add(to); - } - - /* Capture the idents that talk to each group */ - if(from != null && to != null) - { - if(mGroups.containsKey(to)) - { - ArrayList talkgroups = mGroups.get(to); - - if(!talkgroups.contains(from)) - { - talkgroups.add(from); - } - } - else - { - ArrayList talkgroups = new ArrayList(); - talkgroups.add(from); - - mGroups.put(to, talkgroups); - } - } - - int channel = mpt.getChannel(); - - long frequency = 0; - - if(getChannelMap() != null) - { - frequency = getChannelMap().getFrequency(channel); - } - - CallEvent event = new MPT1327CallEvent.Builder(CallEventType.CALL) - .aliasList(getAliasList()) - .channel(String.valueOf(channel)) - .details("GTC") - .frequency(frequency) - .from(mpt.getFromID()) - .to(mpt.getToID()) - .build(); - - broadcast(new TrafficChannelAllocationEvent(this, event)); - break; - case HEAD_PLUS1: - case HEAD_PLUS2: - case HEAD_PLUS3: - case HEAD_PLUS4: - broadcast(new MPT1327CallEvent.Builder(CallEventType.SDM) - .aliasList(getAliasList()) - .details(mpt.getMessage()) - .from(mpt.getFromID()) - .to(mpt.getToID()) - .build()); - broadcast(new DecoderStateEvent(this, Event.CONTINUATION, State.CONTROL)); - break; - - /* Traffic Channel Events */ - case CLEAR: - mChannelNumber = mpt.getChannel(); - - broadcast(new DecoderStateEvent(this, Event.END, State.FADE)); - break; - case MAINT: - if(mChannelType == ChannelType.STANDARD) - { - /** - * When we receive a MAINT message and we're configured - * as a standard channel, we need to apply the call - * timeout specified by the user. Otherwise we'll - * be using the shorter default call timeout - */ - broadcast(new ChangeChannelTimeoutEvent(this, - mChannelType, mCallTimeout)); - - if(mCurrentCallEvent == null) - { - mCurrentCallEvent = new MPT1327CallEvent.Builder(CallEventType.CALL) - .aliasList(getAliasList()) - .channel(String.valueOf(mChannelNumber)) - .details("MONITORED TRAFFIC CHANNEL") - .frequency(mFrequency) - .to(mpt.getToID()) - .build(); - - broadcast(mCurrentCallEvent); - } - - mToAttribute.process(mpt.getToID()); - - broadcast(new DecoderStateEvent(this, Event.START, State.CALL)); - } - break; - default: - break; - } - } - } - } - - public void reset() - { - mIdents.clear(); - - resetState(); - } - - private void resetState() - { - mFromAttribute.reset(); - mToAttribute.reset(); - - /** - * If this is a standard channel, reset the fade timeout to the default - * timeout. Once processing is underway, if we get a MAINT message, - * this indicates we're processing a traffic channel as a standard - * channel, so we'll issue a different call timeout then. - */ - if(mChannelType == ChannelType.STANDARD) - { - broadcast(new ChangeChannelTimeoutEvent(this, mChannelType, - DecodeConfiguration.DEFAULT_CALL_TIMEOUT_SECONDS * 1000)); - - if(mCurrentCallEvent != null) - { - mCurrentCallEvent.end(); - broadcast(mCurrentCallEvent); - mCurrentCallEvent = null; - } - } - } - - public String getSite() - { - return mSite; - } - - /** - * Set the site number. - */ - public void setSite(String site) - { - if(!StringUtils.isEqual(mSite, site)) - { - mSite = site; - broadcast(new AttributeChangeRequest(Attribute.NETWORK_ID_1, "SITE:" + getSite(), - getSiteAlias())); - } - } - - public Alias getSiteAlias() - { - if(hasAliasList()) - { - return getAliasList().getSiteID(mSite); - } - - return null; - } - - public ChannelMap getChannelMap() - { - return mChannelMap; - } - - public void setChannelMap(ChannelMap channelMap) - { - mChannelMap = channelMap; - } - - public int getChannelNumber() - { - return mChannelNumber; - } - - /** - * Set the channel number. This is used primarily for traffic channels since - * the channel will already have been identified prior to the traffic - * channel being created. - */ - public void setChannelNumber(int channel) - { - if(mChannelNumber != channel) - { - mChannelNumber = channel; - broadcast(new AttributeChangeRequest(Attribute.CHANNEL_FREQUENCY_LABEL, String.valueOf(mChannelNumber))); - } - } - - @Override - public String getActivitySummary() - { - StringBuilder sb = new StringBuilder(); - - sb.append("Activity Summary - Decoder: MPT-1327\n"); - sb.append(DIVIDER1); - sb.append("Site: "); - - if(mSite != null) - { - sb.append(mSite); - - if(hasAliasList()) - { - Alias siteAlias = getAliasList().getSiteID(mSite); - - if(siteAlias != null) - { - sb.append(" ").append(siteAlias.getName()).append("\n"); - } - } - } - else - { - sb.append("Unknown\n"); - } - - sb.append(DIVIDER2).append("Talkgroups: "); - - if(mGroups.isEmpty()) - { - sb.append("None\n"); - } - else - { - List talkgroups = new ArrayList(mGroups.keySet()); - Collections.sort(talkgroups); - - for(String talkgroup : talkgroups) - { - sb.append("\n ").append(talkgroup); - - if(hasAliasList()) - { - Alias alias = getAliasList().getMPT1327Alias(talkgroup); - - if(alias != null) - { - sb.append(" "); - sb.append(alias.getName()); - } - } - - sb.append("\n"); - - ArrayList members = mGroups.get(talkgroup); - Collections.sort(members); - - for(String member : members) - { - sb.append(" >"); - sb.append(member); - - if(hasAliasList()) - { - Alias alias = getAliasList().getMPT1327Alias(member); - - if(alias != null) - { - sb.append(" "); - sb.append(alias.getName()); - } - } - - sb.append("\n"); - } - } - } - - sb.append(DIVIDER2).append("All Idents: "); - - if(mIdents.isEmpty()) - { - sb.append("None\n"); - } - else - { - sb.append("\n"); - - Iterator it = mIdents.iterator(); - - while(it.hasNext()) - { - String ident = it.next(); - - sb.append(""); - sb.append(ident); - sb.append(" "); - - if(hasAliasList()) - { - Alias alias = getAliasList().getMPT1327Alias(ident); - - if(alias != null) - { - sb.append(alias.getName()); - } - } - - sb.append("\n"); - } - } - - return sb.toString(); - } - - @Override - public void receiveDecoderStateEvent(DecoderStateEvent event) - { - switch(event.getEvent()) - { - case RESET: - resetState(); - break; - case SOURCE_FREQUENCY: - mFrequency = event.getFrequency(); - broadcast(new AttributeChangeRequest(Attribute.CHANNEL_FREQUENCY, event.getFrequency())); - break; - case TRAFFIC_CHANNEL_ALLOCATION: - if(event.getSource() != MPT1327DecoderState.this) - { - if(event instanceof TrafficChannelAllocationEvent) - { - TrafficChannelAllocationEvent allocationEvent = - (TrafficChannelAllocationEvent) event; - - String channel = allocationEvent.getCallEvent().getChannel(); - - if(channel != null) - { - try - { - setChannelNumber(Integer.valueOf(channel)); - } - catch(Exception e) - { - //Do nothing, we couldn't parse the channel number - } - } - - mFrequency = allocationEvent.getCallEvent().getFrequency(); - broadcast(new AttributeChangeRequest(Attribute.CHANNEL_FREQUENCY, - allocationEvent.getCallEvent().getFrequency())); - - mFromAttribute.process(allocationEvent.getCallEvent().getFromID()); - mToAttribute.process(allocationEvent.getCallEvent().getToID()); - } - } - break; - default: - break; - } - } -} \ No newline at end of file diff --git a/src/module/decode/nbfm/NBFMDecoder.java b/src/module/decode/nbfm/NBFMDecoder.java deleted file mode 100644 index dfe83545..00000000 --- a/src/module/decode/nbfm/NBFMDecoder.java +++ /dev/null @@ -1,52 +0,0 @@ -/******************************************************************************* - * 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 module.decode.nbfm; - -import module.decode.DecoderType; -import module.decode.PrimaryDecoder; -import module.decode.config.DecodeConfiguration; - -public class NBFMDecoder extends PrimaryDecoder -{ - public NBFMDecoder( DecodeConfiguration config ) - { - super( config ); - } - - @Override - public DecoderType getDecoderType() - { - return DecoderType.NBFM; - } - - @Override - public void reset() - { - } - - @Override - public void start() - { - } - - @Override - public void stop() - { - } -} diff --git a/src/module/decode/p25/C4FMSymbolFilter.java b/src/module/decode/p25/C4FMSymbolFilter.java deleted file mode 100644 index 70a772e0..00000000 --- a/src/module/decode/p25/C4FMSymbolFilter.java +++ /dev/null @@ -1,508 +0,0 @@ -package module.decode.p25; - -import dsp.gain.DirectGainControl; -import instrument.Instrumentable; -import instrument.tap.Tap; -import instrument.tap.TapGroup; -import instrument.tap.stream.AdditiveFloatTap; -import instrument.tap.stream.FloatTap; -import sample.Listener; -import sample.real.RealBuffer; -import sample.real.RealSampleListener; -import sample.real.RealSampleProvider; -import source.ISourceEventListener; -import source.SourceEvent; - -import java.util.ArrayList; -import java.util.List; - -public class C4FMSymbolFilter implements Listener, ISourceEventListener, RealSampleProvider, Instrumentable -{ - private static final float TAPS[][] = - { - {0.00000e+00f, 0.00000e+00f, 0.00000e+00f, 0.00000e+00f, 1.00000e+00f, 0.00000e+00f, 0.00000e+00f, 0.00000e+00f}, // 0/128 - {-1.54700e-04f, 8.53777e-04f, -2.76968e-03f, 7.89295e-03f, 9.98534e-01f, -5.41054e-03f, 1.24642e-03f, -1.98993e-04f}, // 1/128 - {-3.09412e-04f, 1.70888e-03f, -5.55134e-03f, 1.58840e-02f, 9.96891e-01f, -1.07209e-02f, 2.47942e-03f, -3.96391e-04f}, // 2/128 - {-4.64053e-04f, 2.56486e-03f, -8.34364e-03f, 2.39714e-02f, 9.95074e-01f, -1.59305e-02f, 3.69852e-03f, -5.92100e-04f}, // 3/128 - {-6.18544e-04f, 3.42130e-03f, -1.11453e-02f, 3.21531e-02f, 9.93082e-01f, -2.10389e-02f, 4.90322e-03f, -7.86031e-04f}, // 4/128 - {-7.72802e-04f, 4.27773e-03f, -1.39548e-02f, 4.04274e-02f, 9.90917e-01f, -2.60456e-02f, 6.09305e-03f, -9.78093e-04f}, // 5/128 - {-9.26747e-04f, 5.13372e-03f, -1.67710e-02f, 4.87921e-02f, 9.88580e-01f, -3.09503e-02f, 7.26755e-03f, -1.16820e-03f}, // 6/128 - {-1.08030e-03f, 5.98883e-03f, -1.95925e-02f, 5.72454e-02f, 9.86071e-01f, -3.57525e-02f, 8.42626e-03f, -1.35627e-03f}, // 7/128 - {-1.23337e-03f, 6.84261e-03f, -2.24178e-02f, 6.57852e-02f, 9.83392e-01f, -4.04519e-02f, 9.56876e-03f, -1.54221e-03f}, // 8/128 - {-1.38589e-03f, 7.69462e-03f, -2.52457e-02f, 7.44095e-02f, 9.80543e-01f, -4.50483e-02f, 1.06946e-02f, -1.72594e-03f}, // 9/128 - {-1.53777e-03f, 8.54441e-03f, -2.80746e-02f, 8.31162e-02f, 9.77526e-01f, -4.95412e-02f, 1.18034e-02f, -1.90738e-03f}, // 10/128 - {-1.68894e-03f, 9.39154e-03f, -3.09033e-02f, 9.19033e-02f, 9.74342e-01f, -5.39305e-02f, 1.28947e-02f, -2.08645e-03f}, // 11/128 - {-1.83931e-03f, 1.02356e-02f, -3.37303e-02f, 1.00769e-01f, 9.70992e-01f, -5.82159e-02f, 1.39681e-02f, -2.26307e-03f}, // 12/128 - {-1.98880e-03f, 1.10760e-02f, -3.65541e-02f, 1.09710e-01f, 9.67477e-01f, -6.23972e-02f, 1.50233e-02f, -2.43718e-03f}, // 13/128 - {-2.13733e-03f, 1.19125e-02f, -3.93735e-02f, 1.18725e-01f, 9.63798e-01f, -6.64743e-02f, 1.60599e-02f, -2.60868e-03f}, // 14/128 - {-2.28483e-03f, 1.27445e-02f, -4.21869e-02f, 1.27812e-01f, 9.59958e-01f, -7.04471e-02f, 1.70776e-02f, -2.77751e-03f}, // 15/128 - {-2.43121e-03f, 1.35716e-02f, -4.49929e-02f, 1.36968e-01f, 9.55956e-01f, -7.43154e-02f, 1.80759e-02f, -2.94361e-03f}, // 16/128 - {-2.57640e-03f, 1.43934e-02f, -4.77900e-02f, 1.46192e-01f, 9.51795e-01f, -7.80792e-02f, 1.90545e-02f, -3.10689e-03f}, // 17/128 - {-2.72032e-03f, 1.52095e-02f, -5.05770e-02f, 1.55480e-01f, 9.47477e-01f, -8.17385e-02f, 2.00132e-02f, -3.26730e-03f}, // 18/128 - {-2.86289e-03f, 1.60193e-02f, -5.33522e-02f, 1.64831e-01f, 9.43001e-01f, -8.52933e-02f, 2.09516e-02f, -3.42477e-03f}, // 19/128 - {-3.00403e-03f, 1.68225e-02f, -5.61142e-02f, 1.74242e-01f, 9.38371e-01f, -8.87435e-02f, 2.18695e-02f, -3.57923e-03f}, // 20/128 - {-3.14367e-03f, 1.76185e-02f, -5.88617e-02f, 1.83711e-01f, 9.33586e-01f, -9.20893e-02f, 2.27664e-02f, -3.73062e-03f}, // 21/128 - {-3.28174e-03f, 1.84071e-02f, -6.15931e-02f, 1.93236e-01f, 9.28650e-01f, -9.53307e-02f, 2.36423e-02f, -3.87888e-03f}, // 22/128 - {-3.41815e-03f, 1.91877e-02f, -6.43069e-02f, 2.02814e-01f, 9.23564e-01f, -9.84679e-02f, 2.44967e-02f, -4.02397e-03f}, // 23/128 - {-3.55283e-03f, 1.99599e-02f, -6.70018e-02f, 2.12443e-01f, 9.18329e-01f, -1.01501e-01f, 2.53295e-02f, -4.16581e-03f}, // 24/128 - {-3.68570e-03f, 2.07233e-02f, -6.96762e-02f, 2.22120e-01f, 9.12947e-01f, -1.04430e-01f, 2.61404e-02f, -4.30435e-03f}, // 25/128 - {-3.81671e-03f, 2.14774e-02f, -7.23286e-02f, 2.31843e-01f, 9.07420e-01f, -1.07256e-01f, 2.69293e-02f, -4.43955e-03f}, // 26/128 - {-3.94576e-03f, 2.22218e-02f, -7.49577e-02f, 2.41609e-01f, 9.01749e-01f, -1.09978e-01f, 2.76957e-02f, -4.57135e-03f}, // 27/128 - {-4.07279e-03f, 2.29562e-02f, -7.75620e-02f, 2.51417e-01f, 8.95936e-01f, -1.12597e-01f, 2.84397e-02f, -4.69970e-03f}, // 28/128 - {-4.19774e-03f, 2.36801e-02f, -8.01399e-02f, 2.61263e-01f, 8.89984e-01f, -1.15113e-01f, 2.91609e-02f, -4.82456e-03f}, // 29/128 - {-4.32052e-03f, 2.43930e-02f, -8.26900e-02f, 2.71144e-01f, 8.83893e-01f, -1.17526e-01f, 2.98593e-02f, -4.94589e-03f}, // 30/128 - {-4.44107e-03f, 2.50946e-02f, -8.52109e-02f, 2.81060e-01f, 8.77666e-01f, -1.19837e-01f, 3.05345e-02f, -5.06363e-03f}, // 31/128 - {-4.55932e-03f, 2.57844e-02f, -8.77011e-02f, 2.91006e-01f, 8.71305e-01f, -1.22047e-01f, 3.11866e-02f, -5.17776e-03f}, // 32/128 - {-4.67520e-03f, 2.64621e-02f, -9.01591e-02f, 3.00980e-01f, 8.64812e-01f, -1.24154e-01f, 3.18153e-02f, -5.28823e-03f}, // 33/128 - {-4.78866e-03f, 2.71272e-02f, -9.25834e-02f, 3.10980e-01f, 8.58189e-01f, -1.26161e-01f, 3.24205e-02f, -5.39500e-03f}, // 34/128 - {-4.89961e-03f, 2.77794e-02f, -9.49727e-02f, 3.21004e-01f, 8.51437e-01f, -1.28068e-01f, 3.30021e-02f, -5.49804e-03f}, // 35/128 - {-5.00800e-03f, 2.84182e-02f, -9.73254e-02f, 3.31048e-01f, 8.44559e-01f, -1.29874e-01f, 3.35600e-02f, -5.59731e-03f}, // 36/128 - {-5.11376e-03f, 2.90433e-02f, -9.96402e-02f, 3.41109e-01f, 8.37557e-01f, -1.31581e-01f, 3.40940e-02f, -5.69280e-03f}, // 37/128 - {-5.21683e-03f, 2.96543e-02f, -1.01915e-01f, 3.51186e-01f, 8.30432e-01f, -1.33189e-01f, 3.46042e-02f, -5.78446e-03f}, // 38/128 - {-5.31716e-03f, 3.02507e-02f, -1.04150e-01f, 3.61276e-01f, 8.23188e-01f, -1.34699e-01f, 3.50903e-02f, -5.87227e-03f}, // 39/128 - {-5.41467e-03f, 3.08323e-02f, -1.06342e-01f, 3.71376e-01f, 8.15826e-01f, -1.36111e-01f, 3.55525e-02f, -5.95620e-03f}, // 40/128 - {-5.50931e-03f, 3.13987e-02f, -1.08490e-01f, 3.81484e-01f, 8.08348e-01f, -1.37426e-01f, 3.59905e-02f, -6.03624e-03f}, // 41/128 - {-5.60103e-03f, 3.19495e-02f, -1.10593e-01f, 3.91596e-01f, 8.00757e-01f, -1.38644e-01f, 3.64044e-02f, -6.11236e-03f}, // 42/128 - {-5.68976e-03f, 3.24843e-02f, -1.12650e-01f, 4.01710e-01f, 7.93055e-01f, -1.39767e-01f, 3.67941e-02f, -6.18454e-03f}, // 43/128 - {-5.77544e-03f, 3.30027e-02f, -1.14659e-01f, 4.11823e-01f, 7.85244e-01f, -1.40794e-01f, 3.71596e-02f, -6.25277e-03f}, // 44/128 - {-5.85804e-03f, 3.35046e-02f, -1.16618e-01f, 4.21934e-01f, 7.77327e-01f, -1.41727e-01f, 3.75010e-02f, -6.31703e-03f}, // 45/128 - {-5.93749e-03f, 3.39894e-02f, -1.18526e-01f, 4.32038e-01f, 7.69305e-01f, -1.42566e-01f, 3.78182e-02f, -6.37730e-03f}, // 46/128 - {-6.01374e-03f, 3.44568e-02f, -1.20382e-01f, 4.42134e-01f, 7.61181e-01f, -1.43313e-01f, 3.81111e-02f, -6.43358e-03f}, // 47/128 - {-6.08674e-03f, 3.49066e-02f, -1.22185e-01f, 4.52218e-01f, 7.52958e-01f, -1.43968e-01f, 3.83800e-02f, -6.48585e-03f}, // 48/128 - {-6.15644e-03f, 3.53384e-02f, -1.23933e-01f, 4.62289e-01f, 7.44637e-01f, -1.44531e-01f, 3.86247e-02f, -6.53412e-03f}, // 49/128 - {-6.22280e-03f, 3.57519e-02f, -1.25624e-01f, 4.72342e-01f, 7.36222e-01f, -1.45004e-01f, 3.88454e-02f, -6.57836e-03f}, // 50/128 - {-6.28577e-03f, 3.61468e-02f, -1.27258e-01f, 4.82377e-01f, 7.27714e-01f, -1.45387e-01f, 3.90420e-02f, -6.61859e-03f}, // 51/128 - {-6.34530e-03f, 3.65227e-02f, -1.28832e-01f, 4.92389e-01f, 7.19116e-01f, -1.45682e-01f, 3.92147e-02f, -6.65479e-03f}, // 52/128 - {-6.40135e-03f, 3.68795e-02f, -1.30347e-01f, 5.02377e-01f, 7.10431e-01f, -1.45889e-01f, 3.93636e-02f, -6.68698e-03f}, // 53/128 - {-6.45388e-03f, 3.72167e-02f, -1.31800e-01f, 5.12337e-01f, 7.01661e-01f, -1.46009e-01f, 3.94886e-02f, -6.71514e-03f}, // 54/128 - {-6.50285e-03f, 3.75341e-02f, -1.33190e-01f, 5.22267e-01f, 6.92808e-01f, -1.46043e-01f, 3.95900e-02f, -6.73929e-03f}, // 55/128 - {-6.54823e-03f, 3.78315e-02f, -1.34515e-01f, 5.32164e-01f, 6.83875e-01f, -1.45993e-01f, 3.96678e-02f, -6.75943e-03f}, // 56/128 - {-6.58996e-03f, 3.81085e-02f, -1.35775e-01f, 5.42025e-01f, 6.74865e-01f, -1.45859e-01f, 3.97222e-02f, -6.77557e-03f}, // 57/128 - {-6.62802e-03f, 3.83650e-02f, -1.36969e-01f, 5.51849e-01f, 6.65779e-01f, -1.45641e-01f, 3.97532e-02f, -6.78771e-03f}, // 58/128 - {-6.66238e-03f, 3.86006e-02f, -1.38094e-01f, 5.61631e-01f, 6.56621e-01f, -1.45343e-01f, 3.97610e-02f, -6.79588e-03f}, // 59/128 - {-6.69300e-03f, 3.88151e-02f, -1.39150e-01f, 5.71370e-01f, 6.47394e-01f, -1.44963e-01f, 3.97458e-02f, -6.80007e-03f}, // 60/128 - {-6.71985e-03f, 3.90083e-02f, -1.40136e-01f, 5.81063e-01f, 6.38099e-01f, -1.44503e-01f, 3.97077e-02f, -6.80032e-03f}, // 61/128 - {-6.74291e-03f, 3.91800e-02f, -1.41050e-01f, 5.90706e-01f, 6.28739e-01f, -1.43965e-01f, 3.96469e-02f, -6.79662e-03f}, // 62/128 - {-6.76214e-03f, 3.93299e-02f, -1.41891e-01f, 6.00298e-01f, 6.19318e-01f, -1.43350e-01f, 3.95635e-02f, -6.78902e-03f}, // 63/128 - {-6.77751e-03f, 3.94578e-02f, -1.42658e-01f, 6.09836e-01f, 6.09836e-01f, -1.42658e-01f, 3.94578e-02f, -6.77751e-03f}, // 64/128 - {-6.78902e-03f, 3.95635e-02f, -1.43350e-01f, 6.19318e-01f, 6.00298e-01f, -1.41891e-01f, 3.93299e-02f, -6.76214e-03f}, // 65/128 - {-6.79662e-03f, 3.96469e-02f, -1.43965e-01f, 6.28739e-01f, 5.90706e-01f, -1.41050e-01f, 3.91800e-02f, -6.74291e-03f}, // 66/128 - {-6.80032e-03f, 3.97077e-02f, -1.44503e-01f, 6.38099e-01f, 5.81063e-01f, -1.40136e-01f, 3.90083e-02f, -6.71985e-03f}, // 67/128 - {-6.80007e-03f, 3.97458e-02f, -1.44963e-01f, 6.47394e-01f, 5.71370e-01f, -1.39150e-01f, 3.88151e-02f, -6.69300e-03f}, // 68/128 - {-6.79588e-03f, 3.97610e-02f, -1.45343e-01f, 6.56621e-01f, 5.61631e-01f, -1.38094e-01f, 3.86006e-02f, -6.66238e-03f}, // 69/128 - {-6.78771e-03f, 3.97532e-02f, -1.45641e-01f, 6.65779e-01f, 5.51849e-01f, -1.36969e-01f, 3.83650e-02f, -6.62802e-03f}, // 70/128 - {-6.77557e-03f, 3.97222e-02f, -1.45859e-01f, 6.74865e-01f, 5.42025e-01f, -1.35775e-01f, 3.81085e-02f, -6.58996e-03f}, // 71/128 - {-6.75943e-03f, 3.96678e-02f, -1.45993e-01f, 6.83875e-01f, 5.32164e-01f, -1.34515e-01f, 3.78315e-02f, -6.54823e-03f}, // 72/128 - {-6.73929e-03f, 3.95900e-02f, -1.46043e-01f, 6.92808e-01f, 5.22267e-01f, -1.33190e-01f, 3.75341e-02f, -6.50285e-03f}, // 73/128 - {-6.71514e-03f, 3.94886e-02f, -1.46009e-01f, 7.01661e-01f, 5.12337e-01f, -1.31800e-01f, 3.72167e-02f, -6.45388e-03f}, // 74/128 - {-6.68698e-03f, 3.93636e-02f, -1.45889e-01f, 7.10431e-01f, 5.02377e-01f, -1.30347e-01f, 3.68795e-02f, -6.40135e-03f}, // 75/128 - {-6.65479e-03f, 3.92147e-02f, -1.45682e-01f, 7.19116e-01f, 4.92389e-01f, -1.28832e-01f, 3.65227e-02f, -6.34530e-03f}, // 76/128 - {-6.61859e-03f, 3.90420e-02f, -1.45387e-01f, 7.27714e-01f, 4.82377e-01f, -1.27258e-01f, 3.61468e-02f, -6.28577e-03f}, // 77/128 - {-6.57836e-03f, 3.88454e-02f, -1.45004e-01f, 7.36222e-01f, 4.72342e-01f, -1.25624e-01f, 3.57519e-02f, -6.22280e-03f}, // 78/128 - {-6.53412e-03f, 3.86247e-02f, -1.44531e-01f, 7.44637e-01f, 4.62289e-01f, -1.23933e-01f, 3.53384e-02f, -6.15644e-03f}, // 79/128 - {-6.48585e-03f, 3.83800e-02f, -1.43968e-01f, 7.52958e-01f, 4.52218e-01f, -1.22185e-01f, 3.49066e-02f, -6.08674e-03f}, // 80/128 - {-6.43358e-03f, 3.81111e-02f, -1.43313e-01f, 7.61181e-01f, 4.42134e-01f, -1.20382e-01f, 3.44568e-02f, -6.01374e-03f}, // 81/128 - {-6.37730e-03f, 3.78182e-02f, -1.42566e-01f, 7.69305e-01f, 4.32038e-01f, -1.18526e-01f, 3.39894e-02f, -5.93749e-03f}, // 82/128 - {-6.31703e-03f, 3.75010e-02f, -1.41727e-01f, 7.77327e-01f, 4.21934e-01f, -1.16618e-01f, 3.35046e-02f, -5.85804e-03f}, // 83/128 - {-6.25277e-03f, 3.71596e-02f, -1.40794e-01f, 7.85244e-01f, 4.11823e-01f, -1.14659e-01f, 3.30027e-02f, -5.77544e-03f}, // 84/128 - {-6.18454e-03f, 3.67941e-02f, -1.39767e-01f, 7.93055e-01f, 4.01710e-01f, -1.12650e-01f, 3.24843e-02f, -5.68976e-03f}, // 85/128 - {-6.11236e-03f, 3.64044e-02f, -1.38644e-01f, 8.00757e-01f, 3.91596e-01f, -1.10593e-01f, 3.19495e-02f, -5.60103e-03f}, // 86/128 - {-6.03624e-03f, 3.59905e-02f, -1.37426e-01f, 8.08348e-01f, 3.81484e-01f, -1.08490e-01f, 3.13987e-02f, -5.50931e-03f}, // 87/128 - {-5.95620e-03f, 3.55525e-02f, -1.36111e-01f, 8.15826e-01f, 3.71376e-01f, -1.06342e-01f, 3.08323e-02f, -5.41467e-03f}, // 88/128 - {-5.87227e-03f, 3.50903e-02f, -1.34699e-01f, 8.23188e-01f, 3.61276e-01f, -1.04150e-01f, 3.02507e-02f, -5.31716e-03f}, // 89/128 - {-5.78446e-03f, 3.46042e-02f, -1.33189e-01f, 8.30432e-01f, 3.51186e-01f, -1.01915e-01f, 2.96543e-02f, -5.21683e-03f}, // 90/128 - {-5.69280e-03f, 3.40940e-02f, -1.31581e-01f, 8.37557e-01f, 3.41109e-01f, -9.96402e-02f, 2.90433e-02f, -5.11376e-03f}, // 91/128 - {-5.59731e-03f, 3.35600e-02f, -1.29874e-01f, 8.44559e-01f, 3.31048e-01f, -9.73254e-02f, 2.84182e-02f, -5.00800e-03f}, // 92/128 - {-5.49804e-03f, 3.30021e-02f, -1.28068e-01f, 8.51437e-01f, 3.21004e-01f, -9.49727e-02f, 2.77794e-02f, -4.89961e-03f}, // 93/128 - {-5.39500e-03f, 3.24205e-02f, -1.26161e-01f, 8.58189e-01f, 3.10980e-01f, -9.25834e-02f, 2.71272e-02f, -4.78866e-03f}, // 94/128 - {-5.28823e-03f, 3.18153e-02f, -1.24154e-01f, 8.64812e-01f, 3.00980e-01f, -9.01591e-02f, 2.64621e-02f, -4.67520e-03f}, // 95/128 - {-5.17776e-03f, 3.11866e-02f, -1.22047e-01f, 8.71305e-01f, 2.91006e-01f, -8.77011e-02f, 2.57844e-02f, -4.55932e-03f}, // 96/128 - {-5.06363e-03f, 3.05345e-02f, -1.19837e-01f, 8.77666e-01f, 2.81060e-01f, -8.52109e-02f, 2.50946e-02f, -4.44107e-03f}, // 97/128 - {-4.94589e-03f, 2.98593e-02f, -1.17526e-01f, 8.83893e-01f, 2.71144e-01f, -8.26900e-02f, 2.43930e-02f, -4.32052e-03f}, // 98/128 - {-4.82456e-03f, 2.91609e-02f, -1.15113e-01f, 8.89984e-01f, 2.61263e-01f, -8.01399e-02f, 2.36801e-02f, -4.19774e-03f}, // 99/128 - {-4.69970e-03f, 2.84397e-02f, -1.12597e-01f, 8.95936e-01f, 2.51417e-01f, -7.75620e-02f, 2.29562e-02f, -4.07279e-03f}, // 100/128 - {-4.57135e-03f, 2.76957e-02f, -1.09978e-01f, 9.01749e-01f, 2.41609e-01f, -7.49577e-02f, 2.22218e-02f, -3.94576e-03f}, // 101/128 - {-4.43955e-03f, 2.69293e-02f, -1.07256e-01f, 9.07420e-01f, 2.31843e-01f, -7.23286e-02f, 2.14774e-02f, -3.81671e-03f}, // 102/128 - {-4.30435e-03f, 2.61404e-02f, -1.04430e-01f, 9.12947e-01f, 2.22120e-01f, -6.96762e-02f, 2.07233e-02f, -3.68570e-03f}, // 103/128 - {-4.16581e-03f, 2.53295e-02f, -1.01501e-01f, 9.18329e-01f, 2.12443e-01f, -6.70018e-02f, 1.99599e-02f, -3.55283e-03f}, // 104/128 - {-4.02397e-03f, 2.44967e-02f, -9.84679e-02f, 9.23564e-01f, 2.02814e-01f, -6.43069e-02f, 1.91877e-02f, -3.41815e-03f}, // 105/128 - {-3.87888e-03f, 2.36423e-02f, -9.53307e-02f, 9.28650e-01f, 1.93236e-01f, -6.15931e-02f, 1.84071e-02f, -3.28174e-03f}, // 106/128 - {-3.73062e-03f, 2.27664e-02f, -9.20893e-02f, 9.33586e-01f, 1.83711e-01f, -5.88617e-02f, 1.76185e-02f, -3.14367e-03f}, // 107/128 - {-3.57923e-03f, 2.18695e-02f, -8.87435e-02f, 9.38371e-01f, 1.74242e-01f, -5.61142e-02f, 1.68225e-02f, -3.00403e-03f}, // 108/128 - {-3.42477e-03f, 2.09516e-02f, -8.52933e-02f, 9.43001e-01f, 1.64831e-01f, -5.33522e-02f, 1.60193e-02f, -2.86289e-03f}, // 109/128 - {-3.26730e-03f, 2.00132e-02f, -8.17385e-02f, 9.47477e-01f, 1.55480e-01f, -5.05770e-02f, 1.52095e-02f, -2.72032e-03f}, // 110/128 - {-3.10689e-03f, 1.90545e-02f, -7.80792e-02f, 9.51795e-01f, 1.46192e-01f, -4.77900e-02f, 1.43934e-02f, -2.57640e-03f}, // 111/128 - {-2.94361e-03f, 1.80759e-02f, -7.43154e-02f, 9.55956e-01f, 1.36968e-01f, -4.49929e-02f, 1.35716e-02f, -2.43121e-03f}, // 112/128 - {-2.77751e-03f, 1.70776e-02f, -7.04471e-02f, 9.59958e-01f, 1.27812e-01f, -4.21869e-02f, 1.27445e-02f, -2.28483e-03f}, // 113/128 - {-2.60868e-03f, 1.60599e-02f, -6.64743e-02f, 9.63798e-01f, 1.18725e-01f, -3.93735e-02f, 1.19125e-02f, -2.13733e-03f}, // 114/128 - {-2.43718e-03f, 1.50233e-02f, -6.23972e-02f, 9.67477e-01f, 1.09710e-01f, -3.65541e-02f, 1.10760e-02f, -1.98880e-03f}, // 115/128 - {-2.26307e-03f, 1.39681e-02f, -5.82159e-02f, 9.70992e-01f, 1.00769e-01f, -3.37303e-02f, 1.02356e-02f, -1.83931e-03f}, // 116/128 - {-2.08645e-03f, 1.28947e-02f, -5.39305e-02f, 9.74342e-01f, 9.19033e-02f, -3.09033e-02f, 9.39154e-03f, -1.68894e-03f}, // 117/128 - {-1.90738e-03f, 1.18034e-02f, -4.95412e-02f, 9.77526e-01f, 8.31162e-02f, -2.80746e-02f, 8.54441e-03f, -1.53777e-03f}, // 118/128 - {-1.72594e-03f, 1.06946e-02f, -4.50483e-02f, 9.80543e-01f, 7.44095e-02f, -2.52457e-02f, 7.69462e-03f, -1.38589e-03f}, // 119/128 - {-1.54221e-03f, 9.56876e-03f, -4.04519e-02f, 9.83392e-01f, 6.57852e-02f, -2.24178e-02f, 6.84261e-03f, -1.23337e-03f}, // 120/128 - {-1.35627e-03f, 8.42626e-03f, -3.57525e-02f, 9.86071e-01f, 5.72454e-02f, -1.95925e-02f, 5.98883e-03f, -1.08030e-03f}, // 121/128 - {-1.16820e-03f, 7.26755e-03f, -3.09503e-02f, 9.88580e-01f, 4.87921e-02f, -1.67710e-02f, 5.13372e-03f, -9.26747e-04f}, // 122/128 - {-9.78093e-04f, 6.09305e-03f, -2.60456e-02f, 9.90917e-01f, 4.04274e-02f, -1.39548e-02f, 4.27773e-03f, -7.72802e-04f}, // 123/128 - {-7.86031e-04f, 4.90322e-03f, -2.10389e-02f, 9.93082e-01f, 3.21531e-02f, -1.11453e-02f, 3.42130e-03f, -6.18544e-04f}, // 124/128 - {-5.92100e-04f, 3.69852e-03f, -1.59305e-02f, 9.95074e-01f, 2.39714e-02f, -8.34364e-03f, 2.56486e-03f, -4.64053e-04f}, // 125/128 - {-3.96391e-04f, 2.47942e-03f, -1.07209e-02f, 9.96891e-01f, 1.58840e-02f, -5.55134e-03f, 1.70888e-03f, -3.09412e-04f}, // 126/128 - {-1.98993e-04f, 1.24642e-03f, -5.41054e-03f, 9.98534e-01f, 7.89295e-03f, -2.76968e-03f, 8.53777e-04f, -1.54700e-04f}, // 127/128 - {0.00000e+00f, 0.00000e+00f, 0.00000e+00f, 1.00000e+00f, 0.00000e+00f, 0.00000e+00f, 0.00000e+00f, 0.00000e+00f}, // 128/128 - }; - - /* Instrumentation Taps */ - private static final String INSTRUMENT_SYMBOL_SPREAD = "Tap Point: Symbol Spread (Goal=2.0)"; - private FloatTap mSymbolSpreadTap; - private List mAvailableTaps; - - private static final int NUMBER_FILTER_TAPS = 8; - private static final int NUMBER_FILTER_STEPS = 128; - - private static final int SAMPLE_RATE = 48000; - private static final int SYMBOL_RATE = 4800; - - /* Tracking loop gain constant */ - private static final double K_SYMBOL_SPREAD = 0.0100; - - /* Constraints on symbol spreading */ - private static final float SYMBOL_SPREAD_MAX = 2.4f; // upper range limit: +20% - private static final float SYMBOL_SPREAD_MIN = 1.6f; // lower range limit: -20% - - /* Symbol clock tracking loop gain */ - private static final double K_SYMBOL_TIMING = 0.025; - - /* Coarse and fine frequency tracking constants */ - private static final double K_COARSE_FREQUENCY = 0.00125; - private static final double K_FINE_FREQUENCY = 0.125; - - /* Frequency correction broadcast threshold */ -// private static final double COARSE_FREQUENCY_DEADBAND = 1.66; - private static final double COARSE_FREQUENCY_THRESHOLD = 1.20; - - /* 2.0 symbol spread gives -3, -1, 1, 3 */ - private float mSymbolSpread = 2.0f; - private float mSymbolClock = 0.0f; - private float mSymbolTime = (float)SYMBOL_RATE / (float)SAMPLE_RATE; - - private float mFineFrequencyCorrection = 0.0f; - private float mCoarseFrequencyCorrection = 0.0f; - - private float mHistory[] = new float[NUMBER_FILTER_TAPS]; - private int mHistoryLast = 0; - - private RealSampleListener mListener; - - private DirectGainControl mGainController = new DirectGainControl(15.0f, 0.1f, 35.0f, 0.3f); - - private int mFrequencyAdjustmentRequested = 0; - private int mFrequencyCorrection = 0; - private int mFrequencyCorrectionMaximum = 3000; - private boolean mResetFrequencyTracker = false; - private FrequencyCorrectionProcessor mFrequencyCorrectionProcessor; - private Listener mFrequencyChangeListener; - - /** - * C4FM Symbol Filter - * - * Sample Gain values - the gain of incoming sample values will critically - * impact the performance of this filter. Gain should be adjusted to - * optimize the value of mSymbolSpread toward a value of 2.0. If the value - * is less than 2.0, then increase gain. If the value is over 2.0, then - * decrease gain. - * - * When preceded by the RealAutomaticGainFilter class, an optimal gain - * setting for this filter is 15.4 and that yields a symbol spread that - * is centered on 2.0, ranging 1.92 to 2.08. - */ - public C4FMSymbolFilter(int frequencyCorrectionMaximum) - { - mFrequencyCorrectionMaximum = frequencyCorrectionMaximum; - } - - public void dispose() - { - mGainController = null; -// mFrequencyCorrectionControl = null; - mListener = null; - } - - @Override - public void receive(RealBuffer buffer) - { - for(float sample : buffer.getSamples()) - { - receive(sample); - } - - /* If a frequency correction was requested during the processing of this - * buffer, we'll apply the change and it will be reflected in the next - * arriving buffer. Reset the lock on frequency correction and reset - * the internal frequency correction tracker */ - if(mFrequencyAdjustmentRequested != 0) - { - int correction = mFrequencyCorrection + mFrequencyAdjustmentRequested; - - if(correction > mFrequencyCorrectionMaximum) - { - correction = mFrequencyCorrectionMaximum; - } - else if(correction < -mFrequencyCorrectionMaximum) - { - correction = -mFrequencyCorrectionMaximum; - } - - broadcast(SourceEvent.channelFrequencyCorrectionRequest(correction)); - - mFrequencyAdjustmentRequested = 0; - } - } - - public void receive(float sample) - { - sample = mGainController.correct(sample); - - if(mResetFrequencyTracker) - { - mCoarseFrequencyCorrection = 0.0f; - mFineFrequencyCorrection = 0.0f; - - broadcast(SourceEvent.channelFrequencyCorrectionRequest(0l)); - - mResetFrequencyTracker = false; - } - - mSymbolClock += mSymbolTime; - - mHistory[mHistoryLast++] = sample; - - mHistoryLast %= NUMBER_FILTER_TAPS; - - if(mSymbolClock > 1.0f) - { - mSymbolClock -= 1.0f; - - int imu = (int)Math.floor(0.5 + - ((float)NUMBER_FILTER_STEPS * (mSymbolClock / mSymbolTime))); - - if(imu >= NUMBER_FILTER_STEPS) - { - imu = NUMBER_FILTER_STEPS - 1; - } - - int imu_p1 = imu + 1; - - int j = mHistoryLast; - - double interp = 0.0; - double interp_p1 = 0.0; - - for(int i = 0; i < NUMBER_FILTER_TAPS; i++) - { - interp += TAPS[imu][i] * mHistory[j]; - interp_p1 += TAPS[imu_p1][i] * mHistory[j]; - - j = (j + 1) % NUMBER_FILTER_TAPS; - } - - /* Output symbol will be interpolated value corrected for symbol - * spread and frequency offset */ - interp -= mFineFrequencyCorrection; - interp_p1 -= mFineFrequencyCorrection; - - /* Correct output for symbol deviation (spread) */ - float output = (float)(2.0 * interp / mSymbolSpread); - - /* Detect received symbol error: basically use a hard decision and - * subtract off expected position nominal symbol level which will be - * +/- 0.5 * symbol spread and +/- 1.5 symbol spread. Remember that - * nominal symbol spread will be 2.0 */ - double symbolError; - - if(interp < -mSymbolSpread) - { - /* symbol is -3: Expected at -1.5 * symbol spread */ - symbolError = interp + (1.5 * mSymbolSpread); - mSymbolSpread -= (symbolError * 0.5 * K_SYMBOL_SPREAD); - } - else if(interp < 0.0) - { - /* symbol is -1: Expected at -0.5 * symbol_spread */ - symbolError = interp + (0.5 * mSymbolSpread); - mSymbolSpread -= (symbolError * K_SYMBOL_SPREAD); - } - else if(interp < mSymbolSpread) - { - /* symbol is +1: Expected at +0.5 * symbol_spread */ - symbolError = interp - (0.5 * mSymbolSpread); - mSymbolSpread += (symbolError * K_SYMBOL_SPREAD); - } - else - { - /* symbol is +3: Expected at +1.5 * symbol_spread */ - symbolError = interp - (1.5 * mSymbolSpread); - mSymbolSpread += (symbolError * 0.5 * K_SYMBOL_SPREAD); - } - - /* Symbol clock tracking loop adjustment */ - if(interp_p1 < interp) - { - mSymbolClock += symbolError * K_SYMBOL_TIMING; - } - else - { - mSymbolClock -= symbolError * K_SYMBOL_TIMING; - } - - if(mSymbolSpread < SYMBOL_SPREAD_MIN) - { - mGainController.increase(); - - mSymbolSpread = SYMBOL_SPREAD_MIN; - } - else if(mSymbolSpread > SYMBOL_SPREAD_MAX) - { - mGainController.decrease(); - - mSymbolSpread = SYMBOL_SPREAD_MAX; - } - - mCoarseFrequencyCorrection += ((mFineFrequencyCorrection - - mCoarseFrequencyCorrection) * K_COARSE_FREQUENCY); - - mFineFrequencyCorrection += (symbolError * K_FINE_FREQUENCY); - - /* Queue a frequency adjustment (once per buffer) as needed */ - if(Math.abs(mCoarseFrequencyCorrection) > COARSE_FREQUENCY_THRESHOLD) - { - mFrequencyAdjustmentRequested = - 500 * (mCoarseFrequencyCorrection > 0 ? 1 : -1); - } - - if(mSymbolSpreadTap != null) - { - mSymbolSpreadTap.receive(mSymbolSpread); - } - - /* dispatch the interpolated value to the listener */ - if(mListener != null) - { - mListener.receive(output); - } - } - } - - @Override - public void setListener(RealSampleListener listener) - { - mListener = listener; - } - - @Override - public void removeListener(RealSampleListener listener) - { - mListener = null; - } - - @Override - public List getTapGroups() - { - if(mAvailableTaps == null) - { - mAvailableTaps = new ArrayList<>(); - - /* - * Since the target is 2.0, we subtract 2.0 from the value so that - * a good value is plotted right on the zero center line - */ - TapGroup group = new TapGroup("C4FM Symbol Filter"); - - group.add( - new AdditiveFloatTap(INSTRUMENT_SYMBOL_SPREAD, 0, 0.1f, -2.0f)); - - mAvailableTaps.add(group); - } - - return mAvailableTaps; - } - - @Override - public void registerTap(Tap tap) - { - if(tap.getName().contentEquals(INSTRUMENT_SYMBOL_SPREAD)) - { - mSymbolSpreadTap = (FloatTap)tap; - } - } - - @Override - public void unregisterTap(Tap tap) - { - if(tap.getName().contentEquals(INSTRUMENT_SYMBOL_SPREAD)) - { - mSymbolSpreadTap = null; - } - } - - /** - * Broadcasts a frequency change event to the registered listener - * - * @param event - */ - public void broadcast(SourceEvent event) - { - if(mFrequencyChangeListener != null) - { - mFrequencyChangeListener.receive(event); - } - } - - /** - * Sets the listener for frequency change events. - * - * @param listener - */ - public void setFrequencyChangeListener(Listener listener) - { - mFrequencyChangeListener = listener; - } - - @Override - public Listener getSourceEventListener() - { - if(mFrequencyCorrectionProcessor == null) - { - mFrequencyCorrectionProcessor = new FrequencyCorrectionProcessor(); - } - - return mFrequencyCorrectionProcessor; - } - - /** - * Receives notifications that the channel frequency correction has been - * applied and updates internal tracking value. - */ - public class FrequencyCorrectionProcessor implements Listener - { - @Override - public void receive(SourceEvent event) - { - switch(event.getEvent()) - { - case NOTIFICATION_CHANNEL_FREQUENCY_CORRECTION_CHANGE: - mFrequencyCorrection = event.getValue().intValue(); - - //Reset internal frequency tracking - mCoarseFrequencyCorrection = 0.0f; - mFineFrequencyCorrection = 0.0f; - break; - case NOTIFICATION_FREQUENCY_CORRECTION_CHANGE: - case NOTIFICATION_SAMPLE_RATE_CHANGE: - mResetFrequencyTracker = true; - break; - default: - break; - } - } - } -} diff --git a/src/module/decode/p25/P25Decoder.java b/src/module/decode/p25/P25Decoder.java deleted file mode 100644 index 79ab8f7b..00000000 --- a/src/module/decode/p25/P25Decoder.java +++ /dev/null @@ -1,70 +0,0 @@ -package module.decode.p25; - -import alias.AliasList; -import instrument.Instrumentable; -import module.decode.Decoder; -import module.decode.DecoderType; -import source.ISourceEventListener; -import source.ISourceEventProvider; - -public abstract class P25Decoder extends Decoder implements ISourceEventListener, ISourceEventProvider, Instrumentable -{ - private P25MessageProcessor mMessageProcessor; - - public P25Decoder( AliasList aliasList ) - { - mMessageProcessor = new P25MessageProcessor( aliasList ); - mMessageProcessor.setMessageListener( this ); - } - - public void dispose() - { - super.dispose(); - - mMessageProcessor.dispose(); - mMessageProcessor = null; - } - - @Override - public DecoderType getDecoderType() - { - return DecoderType.P25_PHASE1; - } - - protected P25MessageProcessor getMessageProcessor() - { - return mMessageProcessor; - } - - public abstract Modulation getModulation(); - - public enum Modulation - { - CQPSK( "Simulcast (LSM)", "LSM" ), - C4FM( "Normal (C4FM)", "C4FM" ); - - private String mLabel; - private String mShortLabel; - - private Modulation( String label, String shortLabel ) - { - mLabel = label; - mShortLabel = shortLabel; - } - - public String getLabel() - { - return mLabel; - } - - public String getShortLabel() - { - return mShortLabel; - } - - public String toString() - { - return getLabel(); - } - } -} diff --git a/src/module/decode/p25/P25DecoderState.java b/src/module/decode/p25/P25DecoderState.java deleted file mode 100644 index 2a84255b..00000000 --- a/src/module/decode/p25/P25DecoderState.java +++ /dev/null @@ -1,3622 +0,0 @@ -/******************************************************************************* - * SDR Trunk - * Copyright (C) 2014,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 - ******************************************************************************/ -package module.decode.p25; - -import alias.Alias; -import alias.AliasList; -import alias.id.AliasIDType; -import channel.metadata.AliasedStringAttributeMonitor; -import channel.metadata.Attribute; -import channel.metadata.AttributeChangeRequest; -import channel.state.ChangeChannelTimeoutEvent; -import channel.state.DecoderState; -import channel.state.DecoderStateEvent; -import channel.state.DecoderStateEvent.Event; -import channel.state.State; -import channel.traffic.TrafficChannelAllocationEvent; -import controller.channel.Channel.ChannelType; -import message.Message; -import module.decode.DecoderType; -import module.decode.event.CallEvent.CallEventType; -import module.decode.p25.P25Decoder.Modulation; -import module.decode.p25.message.IAdjacentSite; -import module.decode.p25.message.IBandIdentifier; -import module.decode.p25.message.P25Message; -import module.decode.p25.message.hdu.HDUMessage; -import module.decode.p25.message.ldu.LDU1Message; -import module.decode.p25.message.ldu.LDUMessage; -import module.decode.p25.message.ldu.lc.CallTermination; -import module.decode.p25.message.ldu.lc.TelephoneInterconnectVoiceChannelUser; -import module.decode.p25.message.ldu.lc.UnitToUnitVoiceChannelUser; -import module.decode.p25.message.pdu.PDUMessage; -import module.decode.p25.message.pdu.confirmed.PDUConfirmedMessage; -import module.decode.p25.message.pdu.confirmed.PDUTypeUnknown; -import module.decode.p25.message.pdu.confirmed.PacketData; -import module.decode.p25.message.pdu.confirmed.SNDCPActivateTDSContextAccept; -import module.decode.p25.message.pdu.confirmed.SNDCPActivateTDSContextReject; -import module.decode.p25.message.pdu.confirmed.SNDCPActivateTDSContextRequest; -import module.decode.p25.message.pdu.confirmed.SNDCPDeactivateTDSContext; -import module.decode.p25.message.pdu.confirmed.SNDCPUserData; -import module.decode.p25.message.pdu.osp.control.AdjacentStatusBroadcastExtended; -import module.decode.p25.message.pdu.osp.control.CallAlertExtended; -import module.decode.p25.message.pdu.osp.control.GroupAffiliationQueryExtended; -import module.decode.p25.message.pdu.osp.control.GroupAffiliationResponseExtended; -import module.decode.p25.message.pdu.osp.control.MessageUpdateExtended; -import module.decode.p25.message.pdu.osp.control.NetworkStatusBroadcastExtended; -import module.decode.p25.message.pdu.osp.control.ProtectionParameterBroadcast; -import module.decode.p25.message.pdu.osp.control.RFSSStatusBroadcastExtended; -import module.decode.p25.message.pdu.osp.control.RoamingAddressUpdateExtended; -import module.decode.p25.message.pdu.osp.control.StatusQueryExtended; -import module.decode.p25.message.pdu.osp.control.StatusUpdateExtended; -import module.decode.p25.message.pdu.osp.control.UnitRegistrationResponseExtended; -import module.decode.p25.message.pdu.osp.data.GroupDataChannelGrantExtended; -import module.decode.p25.message.pdu.osp.data.IndividualDataChannelGrantExtended; -import module.decode.p25.message.pdu.osp.voice.GroupVoiceChannelGrantExplicit; -import module.decode.p25.message.pdu.osp.voice.TelephoneInterconnectChannelGrantExplicit; -import module.decode.p25.message.pdu.osp.voice.UnitToUnitAnswerRequestExplicit; -import module.decode.p25.message.pdu.osp.voice.UnitToUnitVoiceChannelGrantExtended; -import module.decode.p25.message.pdu.osp.voice.UnitToUnitVoiceChannelGrantUpdateExtended; -import module.decode.p25.message.tdu.TDUMessage; -import module.decode.p25.message.tdu.lc.AdjacentSiteStatusBroadcast; -import module.decode.p25.message.tdu.lc.AdjacentSiteStatusBroadcastExplicit; -import module.decode.p25.message.tdu.lc.GroupVoiceChannelUpdate; -import module.decode.p25.message.tdu.lc.GroupVoiceChannelUpdateExplicit; -import module.decode.p25.message.tdu.lc.NetworkStatusBroadcast; -import module.decode.p25.message.tdu.lc.NetworkStatusBroadcastExplicit; -import module.decode.p25.message.tdu.lc.SecondaryControlChannelBroadcast; -import module.decode.p25.message.tdu.lc.SecondaryControlChannelBroadcastExplicit; -import module.decode.p25.message.tdu.lc.TDULinkControlMessage; -import module.decode.p25.message.tsbk.TSBKMessage; -import module.decode.p25.message.tsbk.motorola.MotorolaTSBKMessage; -import module.decode.p25.message.tsbk.motorola.PatchGroupAdd; -import module.decode.p25.message.tsbk.motorola.PatchGroupDelete; -import module.decode.p25.message.tsbk.motorola.PatchGroupVoiceChannelGrant; -import module.decode.p25.message.tsbk.motorola.PatchGroupVoiceChannelGrantUpdate; -import module.decode.p25.message.tsbk.osp.control.AcknowledgeResponse; -import module.decode.p25.message.tsbk.osp.control.AdjacentStatusBroadcast; -import module.decode.p25.message.tsbk.osp.control.AuthenticationCommand; -import module.decode.p25.message.tsbk.osp.control.CallAlert; -import module.decode.p25.message.tsbk.osp.control.DenyResponse; -import module.decode.p25.message.tsbk.osp.control.ExtendedFunctionCommand; -import module.decode.p25.message.tsbk.osp.control.GroupAffiliationQuery; -import module.decode.p25.message.tsbk.osp.control.GroupAffiliationResponse; -import module.decode.p25.message.tsbk.osp.control.IdentifierUpdate; -import module.decode.p25.message.tsbk.osp.control.LocationRegistrationResponse; -import module.decode.p25.message.tsbk.osp.control.MessageUpdate; -import module.decode.p25.message.tsbk.osp.control.ProtectionParameterUpdate; -import module.decode.p25.message.tsbk.osp.control.QueuedResponse; -import module.decode.p25.message.tsbk.osp.control.RFSSStatusBroadcast; -import module.decode.p25.message.tsbk.osp.control.RadioUnitMonitorCommand; -import module.decode.p25.message.tsbk.osp.control.RoamingAddressCommand; -import module.decode.p25.message.tsbk.osp.control.StatusQuery; -import module.decode.p25.message.tsbk.osp.control.StatusUpdate; -import module.decode.p25.message.tsbk.osp.control.SystemService; -import module.decode.p25.message.tsbk.osp.control.UnitDeregistrationAcknowledge; -import module.decode.p25.message.tsbk.osp.control.UnitRegistrationCommand; -import module.decode.p25.message.tsbk.osp.control.UnitRegistrationResponse; -import module.decode.p25.message.tsbk.osp.data.GroupDataChannelAnnouncement; -import module.decode.p25.message.tsbk.osp.data.GroupDataChannelAnnouncementExplicit; -import module.decode.p25.message.tsbk.osp.data.GroupDataChannelGrant; -import module.decode.p25.message.tsbk.osp.data.IndividualDataChannelGrant; -import module.decode.p25.message.tsbk.osp.data.SNDCPDataChannelAnnouncementExplicit; -import module.decode.p25.message.tsbk.osp.data.SNDCPDataChannelGrant; -import module.decode.p25.message.tsbk.osp.data.SNDCPDataPageRequest; -import module.decode.p25.message.tsbk.osp.voice.GroupVoiceChannelGrant; -import module.decode.p25.message.tsbk.osp.voice.GroupVoiceChannelGrantUpdate; -import module.decode.p25.message.tsbk.osp.voice.GroupVoiceChannelGrantUpdateExplicit; -import module.decode.p25.message.tsbk.osp.voice.TelephoneInterconnectAnswerRequest; -import module.decode.p25.message.tsbk.osp.voice.TelephoneInterconnectVoiceChannelGrant; -import module.decode.p25.message.tsbk.osp.voice.TelephoneInterconnectVoiceChannelGrantUpdate; -import module.decode.p25.message.tsbk.osp.voice.UnitToUnitAnswerRequest; -import module.decode.p25.message.tsbk.osp.voice.UnitToUnitVoiceChannelGrant; -import module.decode.p25.message.tsbk.osp.voice.UnitToUnitVoiceChannelGrantUpdate; -import module.decode.p25.reference.IPProtocol; -import module.decode.p25.reference.LinkControlOpcode; -import module.decode.p25.reference.Response; -import module.decode.p25.reference.Vendor; -import org.slf4j.Logger; -import org.slf4j.LoggerFactory; - -import java.text.DecimalFormat; -import java.util.ArrayList; -import java.util.Collections; -import java.util.HashMap; -import java.util.List; -import java.util.Map; -import java.util.Set; -import java.util.TreeSet; -import java.util.concurrent.ScheduledExecutorService; - -public class P25DecoderState extends DecoderState -{ - private final static Logger mLog = LoggerFactory.getLogger(P25DecoderState.class); - private static final DecimalFormat mFrequencyFormatter = - new DecimalFormat("0.000000"); - - - private module.decode.p25.message.tsbk.osp.control.NetworkStatusBroadcast mNetworkStatus; - private NetworkStatusBroadcastExtended mNetworkStatusExtended; - private ProtectionParameterBroadcast mProtectionParameterBroadcast; - private RFSSStatusBroadcast mRFSSStatusMessage; - private RFSSStatusBroadcastExtended mRFSSStatusMessageExtended; - private SNDCPDataChannelAnnouncementExplicit mSNDCPDataChannel; - - private Set mSecondaryControlChannels = - new TreeSet<>(); - - private Map mBands = new HashMap<>(); - private Map mRegistrations = new HashMap<>(); - private Map mNeighborMap = new HashMap<>(); - - private String mLastCommandEventID; - private String mLastPageEventID; - private String mLastQueryEventID; - private String mLastRegistrationEventID; - private String mLastResponseEventID; - - //TODO: create multi-attribute monitor for NAC and System - private String mNAC; - private String mSystem; - private AliasedStringAttributeMonitor mSiteAttributeMonitor; - private AliasedStringAttributeMonitor mFromTalkgroupMonitor; - private AliasedStringAttributeMonitor mToTalkgroupMonitor; - private String mCurrentChannel = "CURRENT"; - private long mCurrentChannelFrequency = 0; - - private ChannelType mChannelType; - private Modulation mModulation; - private boolean mIgnoreDataCalls; - private boolean mControlChannelShutdownLogged; - - private P25CallEvent mCurrentCallEvent; - private List mCallDetectTalkgroups = new ArrayList<>(); - private Map mChannelCallMap = new HashMap<>(); - private PatchGroupManager mPatchGroupManager; - - public P25DecoderState(AliasList aliasList, - ChannelType channelType, - Modulation modulation, - boolean ignoreDataCalls) - { - super(aliasList); - mChannelType = channelType; - mModulation = modulation; - mIgnoreDataCalls = ignoreDataCalls; - - mPatchGroupManager = new PatchGroupManager(aliasList, getCallEventBroadcaster()); - mSiteAttributeMonitor = new AliasedStringAttributeMonitor(Attribute.NETWORK_ID_2, - getAttributeChangeRequestListener(), getAliasList(), AliasIDType.SITE); - mFromTalkgroupMonitor = new AliasedStringAttributeMonitor(Attribute.PRIMARY_ADDRESS_FROM, - getAttributeChangeRequestListener(), getAliasList(), AliasIDType.TALKGROUP); - mFromTalkgroupMonitor.addIllegalValue("000000"); - mToTalkgroupMonitor = new AliasedStringAttributeMonitor(Attribute.PRIMARY_ADDRESS_TO, - getAttributeChangeRequestListener(), getAliasList(), AliasIDType.TALKGROUP); - mToTalkgroupMonitor.addIllegalValue("0000"); - mToTalkgroupMonitor.addIllegalValue("000000"); - } - - public Modulation getModulation() - { - return mModulation; - } - - @Override - public DecoderType getDecoderType() - { - return DecoderType.P25_PHASE1; - } - - /** - * Performs a full reset to prepare this object for reuse on a new channel - */ - @Override - public void reset() - { - resetState(); - - mNAC = null; - mSiteAttributeMonitor.reset(); - mSystem = null; - } - - /** - * Resets any temporal state details - */ - private void resetState() - { - mFromTalkgroupMonitor.reset(); - mToTalkgroupMonitor.reset(); - - mCallDetectTalkgroups.clear(); - - if(mCurrentCallEvent != null) - { - mCurrentCallEvent.end(); - } - - mCurrentCallEvent = null; - } - - /** - * Indicates if there a call event exists for the specified channel and - * from and to talkgroup identifiers. - * - * @param channel - channel number - * @param from user id - * @param to talkgroup or user id - * @return true if there is a call event - */ - public boolean hasCallEvent(String channel, String from, String to) - { - boolean hasEvent = false; - - if(mChannelCallMap.containsKey(channel)) - { - P25CallEvent event = mChannelCallMap.get(channel); - - if(to != null && - event.getToID() != null && - to.contentEquals(event.getToID())) - { - if(from != null) - { - if(event.getFromID() == null) - { - hasEvent = true; - } - else if(from.contentEquals(event.getFromID())) - { - hasEvent = true; - } - } - else - { - hasEvent = true; - } - } - } - - return hasEvent; - } - - /** - * Adds the channel and event to the current channel call map. If an entry - * already exists, terminates the event and broadcasts the update. - * - * @param event to place in the map - */ - public void registerCallEvent(P25CallEvent event) - { - String channel = event.getChannel(); - - if(mChannelCallMap.containsKey(event.getChannel())) - { - P25CallEvent previousEvent = mChannelCallMap.remove(event.getChannel()); - - previousEvent.end(); - - broadcast(previousEvent); - } - - mChannelCallMap.put(event.getChannel(), event); - } - - private void updateCallEvent(String channel, String from, String to) - { - P25CallEvent event = mChannelCallMap.get(channel); - - if(event != null && - to != null && - event.getToID() != null && - event.getToID().contentEquals(to)) - { - if(event.getFromID() == null && - from != null && - !from.contentEquals("0000") && - !from.contentEquals("000000")) - { - event.setFromID(from); - broadcast(event); - } - } - } - - /** - * Primary message processing method. - */ - public void receive(Message message) - { - if(message instanceof P25Message) - { - updateNAC(((P25Message)message).getNAC()); - - /* Voice Vocoder messages */ - if(message instanceof LDUMessage) - { - processLDU((LDUMessage)message); - } - - /* Trunking Signalling Messages */ - else if(message instanceof TSBKMessage) - { - processTSBK((TSBKMessage)message); - } - - /* Terminator Data Unit with Link Control Message */ - else if(message instanceof TDULinkControlMessage) - { - processTDULC((TDULinkControlMessage)message); - } - - /* Packet Data Unit Messages */ - else if(message instanceof PDUMessage) - { - processPDU((PDUMessage)message); - } - - /* Header Data Unit Message - preceeds voice LDUs */ - else if(message instanceof HDUMessage) - { - processHDU((HDUMessage)message); - } - - /* Terminator Data Unit, or default message if CRC failed */ - else if(message instanceof TDUMessage) - { - processTDU((TDUMessage)message); - } - } - } - - /** - * Terminator Data Unit - */ - private void processTDU(TDUMessage tdu) - { - } - - /** - * Header Data Unit - first message in a call sequence. - */ - private void processHDU(HDUMessage hdu) - { - if(hdu.isValid()) - { - broadcast(new DecoderStateEvent(this, Event.START, State.CALL)); - - String to = hdu.getToID(); - - mToTalkgroupMonitor.process(to); - - if(mCurrentCallEvent == null) - { - mCurrentCallEvent = new P25CallEvent.Builder(CallEventType.CALL) - .aliasList(getAliasList()) - .channel(mCurrentChannel) - .details((hdu.isEncryptedAudio() ? "ENCRYPTED " : "")) - .frequency(mCurrentChannelFrequency) - .to(to) - .build(); - - broadcast(mCurrentCallEvent); - } - - } - } - - /** - * Terminator Data Unit with Link Control - transmitted multiple times at - * beginning or end of call sequence and includes embedded link control messages - */ - private void processTDULC(TDULinkControlMessage tdulc) - { - if(tdulc.getOpcode() == LinkControlOpcode.CALL_TERMINATION_OR_CANCELLATION) - { - broadcast(new DecoderStateEvent(this, Event.END, State.FADE)); - - if(mCurrentCallEvent != null) - { - mCurrentCallEvent.end(); - mCurrentCallEvent = null; - } - } - else - { - broadcast(new DecoderStateEvent(this, Event.CONTINUATION, State.CALL)); - } - - switch(tdulc.getOpcode()) - { - case ADJACENT_SITE_STATUS_BROADCAST: - if(tdulc instanceof AdjacentSiteStatusBroadcast) - { - IAdjacentSite ias = (IAdjacentSite)tdulc; - - mNeighborMap.put(ias.getUniqueID(), ias); - - updateSystem(ias.getSystemID()); - } - else - { - logAlternateVendorMessage(tdulc); - } - break; - case ADJACENT_SITE_STATUS_BROADCAST_EXPLICIT: - if(tdulc instanceof AdjacentSiteStatusBroadcastExplicit) - { - IAdjacentSite ias = (IAdjacentSite)tdulc; - - mNeighborMap.put(ias.getUniqueID(), ias); - - updateSystem(ias.getSystemID()); - } - break; - case CALL_ALERT: - if(tdulc instanceof module.decode.p25.message.tdu.lc.CallAlert) - { - module.decode.p25.message.tdu.lc.CallAlert ca = - (module.decode.p25.message.tdu.lc.CallAlert)tdulc; - - broadcast(new P25CallEvent.Builder(CallEventType.PAGE) - .aliasList(getAliasList()) - .from(ca.getSourceAddress()) - .to(ca.getTargetAddress()) - .details("CALL ALERT") - .build()); - } - else - { - logAlternateVendorMessage(tdulc); - } - break; - case CALL_TERMINATION_OR_CANCELLATION: - /* This opcode as handled at the beginning of the method */ - break; - case CHANNEL_IDENTIFIER_UPDATE: - //TODO: does the activity summary need this message? - - /* This message is handled by the P25MessageProcessor and - * inserted into any channels needing frequency band info */ - break; - case CHANNEL_IDENTIFIER_UPDATE_EXPLICIT: - //TODO: does the activity summary need this message? - - /* This message is handled by the P25MessageProcessor and - * inserted into any channels needing frequency band info */ - break; - case EXTENDED_FUNCTION_COMMAND: - if(tdulc instanceof module.decode.p25.message.tdu.lc.ExtendedFunctionCommand) - { - module.decode.p25.message.tdu.lc.ExtendedFunctionCommand efc = - (module.decode.p25.message.tdu.lc.ExtendedFunctionCommand)tdulc; - - broadcast(new P25CallEvent.Builder(CallEventType.COMMAND) - .aliasList(getAliasList()) - .to(efc.getTargetAddress()) - .details("FUNCTION:" + efc.getExtendedFunction().getLabel() + - " ARG:" + efc.getArgument()) - .build()); - } - else - { - logAlternateVendorMessage(tdulc); - } - break; - case GROUP_AFFILIATION_QUERY: - if(tdulc instanceof module.decode.p25.message.tdu.lc.GroupAffiliationQuery) - { - module.decode.p25.message.tdu.lc.GroupAffiliationQuery gaq = - (module.decode.p25.message.tdu.lc.GroupAffiliationQuery)tdulc; - - broadcast(new P25CallEvent.Builder(CallEventType.QUERY) - .aliasList(getAliasList()) - .details("GROUP AFFILIATION QUERY") - .from(gaq.getSourceAddress()) - .to(gaq.getTargetAddress()) - .build()); - } - else - { - logAlternateVendorMessage(tdulc); - } - break; - case GROUP_VOICE_CHANNEL_UPDATE: - /* Used only on trunked systems on the outbound channel, to - * reflect user activity on other channels. We process this - * as a call detect */ - if(tdulc instanceof GroupVoiceChannelUpdate) - { - GroupVoiceChannelUpdate gvcu = (GroupVoiceChannelUpdate)tdulc; - - String groupA = gvcu.getGroupAddressA(); - - if(!mCallDetectTalkgroups.contains(groupA)) - { - broadcast(new P25CallEvent.Builder(CallEventType.CALL_DETECT) - .aliasList(getAliasList()) - .channel(gvcu.getChannelA()) - .details((gvcu.isEncrypted() ? "ENCRYPTED" : "")) - .frequency(gvcu.getDownlinkFrequencyA()) - .to(groupA) - .build()); - - mCallDetectTalkgroups.add(groupA); - } - - String groupB = gvcu.getGroupAddressB(); - - if(!mCallDetectTalkgroups.contains(groupB)) - { - broadcast(new P25CallEvent.Builder(CallEventType.CALL_DETECT) - .aliasList(getAliasList()) - .channel(gvcu.getChannelB()) - .details((gvcu.isEncrypted() ? "ENCRYPTED" : "")) - .frequency(gvcu.getDownlinkFrequencyB()) - .to(groupB) - .build()); - - mCallDetectTalkgroups.add(groupB); - } - } - break; - case GROUP_VOICE_CHANNEL_UPDATE_EXPLICIT: - /* Reflects other call activity on the system: CALL DETECT */ - if(mChannelType == ChannelType.STANDARD && - tdulc instanceof GroupVoiceChannelUpdateExplicit) - { - GroupVoiceChannelUpdateExplicit gvcue = - (GroupVoiceChannelUpdateExplicit)tdulc; - - String group = gvcue.getGroupAddress(); - - if(!mCallDetectTalkgroups.contains(group)) - { - broadcast(new P25CallEvent.Builder(CallEventType.CALL_DETECT) - .aliasList(getAliasList()) - .channel(gvcue.getTransmitChannel()) - .details((gvcue.isEncrypted() ? "ENCRYPTED" : "")) - .frequency(gvcue.getDownlinkFrequency()) - .to(group) - .build()); - - mCallDetectTalkgroups.add(group); - } - } - break; - case GROUP_VOICE_CHANNEL_USER: - /* Used on a traffic channel to reflect current call entities */ - if(tdulc instanceof module.decode.p25.message.tdu.lc.GroupVoiceChannelUser) - { - module.decode.p25.message.tdu.lc.GroupVoiceChannelUser gvcuser = - (module.decode.p25.message.tdu.lc.GroupVoiceChannelUser)tdulc; - - broadcast(new DecoderStateEvent(this, Event.CONTINUATION, State.CALL)); - - String from = gvcuser.getSourceAddress(); - String to = gvcuser.getGroupAddress(); - - mFromTalkgroupMonitor.process(from); - mToTalkgroupMonitor.process(to); - - if(mCurrentCallEvent == null) - { - mCurrentCallEvent = new P25CallEvent.Builder(CallEventType.GROUP_CALL) - .aliasList(getAliasList()) - .channel(mCurrentChannel) - .details("TDULC GROUP VOICE CHANNEL USER" + - (gvcuser.isEncrypted() ? "ENCRYPTED " : "") + - (gvcuser.isEmergency() ? "EMERGENCY " : "")) - .frequency(mCurrentChannelFrequency) - .from(from) - .to(to) - .build(); - - broadcast(mCurrentCallEvent); - } - } - break; - case MESSAGE_UPDATE: - if(tdulc instanceof module.decode.p25.message.tdu.lc.MessageUpdate) - { - module.decode.p25.message.tdu.lc.MessageUpdate mu = - (module.decode.p25.message.tdu.lc.MessageUpdate)tdulc; - - broadcast(new P25CallEvent.Builder(CallEventType.SDM) - .aliasList(getAliasList()) - .from(mu.getSourceAddress()) - .to(mu.getTargetAddress()) - .details("MSG: " + mu.getShortDataMessage()) - .build()); - } - else - { - logAlternateVendorMessage(tdulc); - } - break; - case NETWORK_STATUS_BROADCAST: - if(tdulc instanceof NetworkStatusBroadcast) - { - updateSystem(((NetworkStatusBroadcast)tdulc).getSystem()); - } - else - { - logAlternateVendorMessage(tdulc); - } - break; - case NETWORK_STATUS_BROADCAST_EXPLICIT: - if(tdulc instanceof NetworkStatusBroadcastExplicit) - { - updateSystem(((NetworkStatusBroadcastExplicit)tdulc).getSystem()); - } - else - { - logAlternateVendorMessage(tdulc); - } - break; - case PROTECTION_PARAMETER_BROADCAST: - if(tdulc instanceof module.decode.p25.message.tdu.lc.ProtectionParameterBroadcast) - { - module.decode.p25.message.tdu.lc.ProtectionParameterBroadcast ppb = - (module.decode.p25.message.tdu.lc.ProtectionParameterBroadcast)tdulc; - - broadcast(new P25CallEvent.Builder(CallEventType.COMMAND) - .aliasList(getAliasList()) - .to(ppb.getTargetAddress()) - .details("ENCRYPTION: " + - ppb.getEncryption().name() + " KEY:" + - ppb.getEncryptionKey()) - .build()); - } - else - { - logAlternateVendorMessage(tdulc); - } - break; - case RFSS_STATUS_BROADCAST: - if(tdulc instanceof module.decode.p25.message.tdu.lc.RFSSStatusBroadcast) - { - module.decode.p25.message.tdu.lc.RFSSStatusBroadcast rfsssb = - (module.decode.p25.message.tdu.lc.RFSSStatusBroadcast)tdulc; - - updateSystem(rfsssb.getSystem()); - - String site = rfsssb.getRFSubsystemID() + "-" + - rfsssb.getSiteID(); - - mSiteAttributeMonitor.process(site); - - if(mCurrentChannel == null || - !mCurrentChannel.contentEquals(rfsssb.getChannel())) - { - mCurrentChannel = rfsssb.getChannel(); - mCurrentChannelFrequency = rfsssb.getDownlinkFrequency(); - } - } - else - { - logAlternateVendorMessage(tdulc); - } - break; - case RFSS_STATUS_BROADCAST_EXPLICIT: - if(tdulc instanceof module.decode.p25.message.tdu.lc.RFSSStatusBroadcastExplicit) - { - module.decode.p25.message.tdu.lc.RFSSStatusBroadcastExplicit rfsssbe = - (module.decode.p25.message.tdu.lc.RFSSStatusBroadcastExplicit)tdulc; - - String site = rfsssbe.getRFSubsystemID() + "-" + - rfsssbe.getSiteID(); - - mSiteAttributeMonitor.process(site); - - if(mCurrentChannel == null || - !mCurrentChannel.contentEquals(rfsssbe.getTransmitChannel())) - { - mCurrentChannel = rfsssbe.getTransmitChannel(); - mCurrentChannelFrequency = rfsssbe.getDownlinkFrequency(); - } - } - else - { - logAlternateVendorMessage(tdulc); - } - break; - case SECONDARY_CONTROL_CHANNEL_BROADCAST: - if(tdulc instanceof SecondaryControlChannelBroadcast) - { - SecondaryControlChannelBroadcast sccb = - (SecondaryControlChannelBroadcast)tdulc; - - String site = sccb.getRFSubsystemID() + "-" + - sccb.getSiteID(); - - mSiteAttributeMonitor.process(site); - } - else - { - logAlternateVendorMessage(tdulc); - } - break; - case SECONDARY_CONTROL_CHANNEL_BROADCAST_EXPLICIT: - if(tdulc instanceof SecondaryControlChannelBroadcastExplicit) - { - SecondaryControlChannelBroadcastExplicit sccb = - (SecondaryControlChannelBroadcastExplicit)tdulc; - - String site = sccb.getRFSubsystemID() + "-" + - sccb.getSiteID(); - - mSiteAttributeMonitor.process(site); - } - else - { - logAlternateVendorMessage(tdulc); - } - break; - case STATUS_QUERY: - if(tdulc instanceof module.decode.p25.message.tdu.lc.StatusQuery) - { - module.decode.p25.message.tdu.lc.StatusQuery sq = - (module.decode.p25.message.tdu.lc.StatusQuery)tdulc; - - broadcast(new P25CallEvent.Builder(CallEventType.QUERY) - .aliasList(getAliasList()) - .details("STATUS QUERY") - .from(sq.getSourceAddress()) - .to(sq.getTargetAddress()) - .build()); - } - else - { - logAlternateVendorMessage(tdulc); - } - break; - case STATUS_UPDATE: - if(tdulc instanceof module.decode.p25.message.tdu.lc.StatusUpdate) - { - module.decode.p25.message.tdu.lc.StatusUpdate su = - (module.decode.p25.message.tdu.lc.StatusUpdate)tdulc; - - broadcast(new P25CallEvent.Builder(CallEventType.STATUS) - .aliasList(getAliasList()) - .details("STATUS UNIT:" + su.getUnitStatus() + - " USER:" + su.getUserStatus()) - .from(su.getSourceAddress()) - .to(su.getTargetAddress()) - .build()); - } - else - { - logAlternateVendorMessage(tdulc); - } - break; - case SYSTEM_SERVICE_BROADCAST: - /* This message doesn't provide anything we need for channel state */ - break; - case TELEPHONE_INTERCONNECT_ANSWER_REQUEST: - if(tdulc instanceof module.decode.p25.message.tdu.lc.TelephoneInterconnectAnswerRequest) - { - module.decode.p25.message.tdu.lc.TelephoneInterconnectAnswerRequest tiar = - (module.decode.p25.message.tdu.lc.TelephoneInterconnectAnswerRequest)tdulc; - - broadcast(new P25CallEvent.Builder(CallEventType.PAGE) - .aliasList(getAliasList()) - .from(tiar.getTelephoneNumber()) - .to(tiar.getTargetAddress()) - .details("TELEPHONE CALL ALERT") - .build()); - } - else - { - logAlternateVendorMessage(tdulc); - } - break; - case TELEPHONE_INTERCONNECT_VOICE_CHANNEL_USER: - if(mChannelType == ChannelType.STANDARD && - tdulc instanceof module.decode.p25.message.tdu.lc.TelephoneInterconnectVoiceChannelUser) - { - module.decode.p25.message.tdu.lc.TelephoneInterconnectVoiceChannelUser tivcu = - (module.decode.p25.message.tdu.lc.TelephoneInterconnectVoiceChannelUser)tdulc; - - String to = tivcu.getAddress(); - - mToTalkgroupMonitor.process(to); - - if(mCurrentCallEvent == null) - { - mCurrentCallEvent = new P25CallEvent.Builder(CallEventType.TELEPHONE_INTERCONNECT) - .aliasList(getAliasList()) - .channel(mCurrentChannel) - .details((tivcu.isEncrypted() ? "ENCRYPTED" : "") + - (tivcu.isEmergency() ? " EMERGENCY" : "")) - .frequency(mCurrentChannelFrequency) - .to(to) - .build(); - - broadcast(mCurrentCallEvent); - } - } - break; - case UNIT_AUTHENTICATION_COMMAND: - if(tdulc instanceof module.decode.p25.message.tdu.lc.UnitAuthenticationCommand) - { - module.decode.p25.message.tdu.lc.UnitAuthenticationCommand uac = - (module.decode.p25.message.tdu.lc.UnitAuthenticationCommand)tdulc; - - broadcast(new P25CallEvent.Builder(CallEventType.COMMAND) - .aliasList(getAliasList()) - .to(uac.getCompleteTargetAddress()) - .details("AUTHENTICATE") - .build()); - } - else - { - logAlternateVendorMessage(tdulc); - } - break; - case UNIT_REGISTRATION_COMMAND: - if(tdulc instanceof module.decode.p25.message.tdu.lc.UnitRegistrationCommand) - { - module.decode.p25.message.tdu.lc.UnitRegistrationCommand urc = - (module.decode.p25.message.tdu.lc.UnitRegistrationCommand)tdulc; - - broadcast(new P25CallEvent.Builder(CallEventType.COMMAND) - .aliasList(getAliasList()) - .to(urc.getCompleteTargetAddress()) - .details("REGISTER") - .build()); - } - else - { - logAlternateVendorMessage(tdulc); - } - break; - case UNIT_TO_UNIT_ANSWER_REQUEST: - if(tdulc instanceof module.decode.p25.message.tdu.lc.UnitToUnitAnswerRequest) - { - module.decode.p25.message.tdu.lc.UnitToUnitAnswerRequest uuar = - (module.decode.p25.message.tdu.lc.UnitToUnitAnswerRequest)tdulc; - - broadcast(new P25CallEvent.Builder(CallEventType.PAGE) - .aliasList(getAliasList()) - .from(uuar.getSourceAddress()) - .to(uuar.getTargetAddress()) - .details("UNIT TO UNIT CALL ALERT") - .build()); - } - else - { - logAlternateVendorMessage(tdulc); - } - break; - case UNIT_TO_UNIT_VOICE_CHANNEL_USER: - /* Used on traffic channels to indicate the current call entities */ - if(tdulc instanceof module.decode.p25.message.tdu.lc.UnitToUnitVoiceChannelUser) - { - module.decode.p25.message.tdu.lc.UnitToUnitVoiceChannelUser uuvcu = - (module.decode.p25.message.tdu.lc.UnitToUnitVoiceChannelUser)tdulc; - - String from = uuvcu.getSourceAddress(); - mFromTalkgroupMonitor.process(from); - - String to = uuvcu.getTargetAddress(); - mToTalkgroupMonitor.process(to); - - if(mCurrentCallEvent != null) - { - mCurrentCallEvent = new P25CallEvent.Builder(CallEventType.UNIT_TO_UNIT_CALL) - .aliasList(getAliasList()) - .channel(mCurrentChannel) - .details((uuvcu.isEncrypted() ? "ENCRYPTED " : "") + - (uuvcu.isEmergency() ? "EMERGENCY " : "")) - .frequency(mCurrentChannelFrequency) - .from(from) - .to(to) - .build(); - } - } - break; - default: - break; - } - } - - /** - * Log optional vendor format messages that we don't yet support so that we - * can understand those messages and eventually add support. - */ - private void logAlternateVendorMessage(P25Message message) - { -// if( message.isValid() ) -// { -// mLog.info( "PLEASE NOTIFY DEVELOPER - UNRECOGNIZED P25 VENDOR FORMAT -" -// + " DUID:" + message.getDUID() -// + " MSG:" + message.getBinaryMessage() -// + " CLASS:" + message.getClass() ); -// } - } - - /** - * Processes LDU voice frame messages. Sends continuation events to keep - * the channel state synchronized and processes the embedded link control - * messages to capture/broadcast peripheral events like paging. - */ - private void processLDU(LDUMessage ldu) - { - broadcast(new DecoderStateEvent(this, Event.CONTINUATION, State.CALL)); - - if(mCurrentCallEvent == null) - { - mCurrentCallEvent = new P25CallEvent.Builder(CallEventType.CALL) - .aliasList(getAliasList()) - .channel(mCurrentChannel) - .frequency(mCurrentChannelFrequency) - .build(); - - broadcast(mCurrentCallEvent); - } - - if(ldu instanceof LDU1Message) - { - switch(((LDU1Message)ldu).getOpcode()) - { - case ADJACENT_SITE_STATUS_BROADCAST: - if(ldu instanceof module.decode.p25.message.ldu.lc.AdjacentSiteStatusBroadcast) - { - IAdjacentSite ias = (IAdjacentSite)ldu; - - mNeighborMap.put(ias.getUniqueID(), ias); - - updateSystem(ias.getSystemID()); - } - else - { - logAlternateVendorMessage(ldu); - } - - break; - case ADJACENT_SITE_STATUS_BROADCAST_EXPLICIT: - if(ldu instanceof module.decode.p25.message.ldu.lc.AdjacentSiteStatusBroadcastExplicit) - { - IAdjacentSite ias = (IAdjacentSite)ldu; - - mNeighborMap.put(ias.getUniqueID(), ias); - - updateSystem(ias.getSystemID()); - } - else - { - logAlternateVendorMessage(ldu); - } - break; - case CALL_ALERT: - if(ldu instanceof module.decode.p25.message.ldu.lc.CallAlert) - { - module.decode.p25.message.ldu.lc.CallAlert ca = - (module.decode.p25.message.ldu.lc.CallAlert)ldu; - broadcast(new P25CallEvent.Builder(CallEventType.PAGE) - .aliasList(getAliasList()) - .from(ca.getSourceAddress()) - .to(ca.getTargetAddress()) - .details("CALL ALERT") - .build()); - } - else - { - logAlternateVendorMessage(ldu); - } - break; - case CALL_TERMINATION_OR_CANCELLATION: - broadcast(new DecoderStateEvent(this, Event.END, State.FADE)); - mCurrentCallEvent = null; - - if(!(ldu instanceof CallTermination)) - { - logAlternateVendorMessage(ldu); - } - break; - case CHANNEL_IDENTIFIER_UPDATE: - //TODO: do we need this for the activity summary? - - /* This message is handled by the P25MessageProcessor and - * inserted into any channels needing frequency band info */ - break; - case CHANNEL_IDENTIFIER_UPDATE_EXPLICIT: - //TODO: do we need this for the activity summary? - - /* This message is handled by the P25MessageProcessor and - * inserted into any channels needing frequency band info */ - break; - case EXTENDED_FUNCTION_COMMAND: - if(ldu instanceof module.decode.p25.message.ldu.lc.ExtendedFunctionCommand) - { - module.decode.p25.message.ldu.lc.ExtendedFunctionCommand efc = - (module.decode.p25.message.ldu.lc.ExtendedFunctionCommand)ldu; - - broadcast(new P25CallEvent.Builder(CallEventType.COMMAND) - .aliasList(getAliasList()) - .to(efc.getTargetAddress()) - .details("FUNCTION:" + efc.getExtendedFunction().getLabel() + - " ARG:" + efc.getArgument()) - .build()); - } - else - { - logAlternateVendorMessage(ldu); - } - break; - case GROUP_AFFILIATION_QUERY: - if(mChannelType == ChannelType.STANDARD && - ldu instanceof module.decode.p25.message.ldu.lc.GroupAffiliationQuery) - { - module.decode.p25.message.ldu.lc.GroupAffiliationQuery gaq = - (module.decode.p25.message.ldu.lc.GroupAffiliationQuery)ldu; - - broadcast(new P25CallEvent.Builder(CallEventType.QUERY) - .aliasList(getAliasList()) - .details("GROUP AFFILIATION QUERY") - .from(gaq.getSourceAddress()) - .to(gaq.getTargetAddress()) - .build()); - } - else - { - logAlternateVendorMessage(ldu); - } - break; - case GROUP_VOICE_CHANNEL_UPDATE: - if(ldu instanceof module.decode.p25.message.ldu.lc.GroupVoiceChannelUpdate) - { - module.decode.p25.message.ldu.lc.GroupVoiceChannelUpdate gvcu = - (module.decode.p25.message.ldu.lc.GroupVoiceChannelUpdate)ldu; - - String userA = gvcu.getGroupAddressA(); - String userB = gvcu.getGroupAddressB(); - - if(mChannelType == ChannelType.STANDARD) - { - if(!mCallDetectTalkgroups.contains(userA)) - { - broadcast(new P25CallEvent.Builder(CallEventType.CALL_DETECT) - .aliasList(getAliasList()) - .channel(gvcu.getChannelA()) - .details((gvcu.isEncryptedLinkControlWord() ? "ENCRYPTED" : "")) - .frequency(gvcu.getDownlinkFrequencyA()) - .to(userA) - .build()); - - mCallDetectTalkgroups.add(userA); - } - - if(userB != null && - !userB.contentEquals("0000") && - !mCallDetectTalkgroups.contains(userB)) - { - broadcast(new P25CallEvent.Builder(CallEventType.CALL_DETECT) - .aliasList(getAliasList()) - .channel(gvcu.getChannelB()) - .details((gvcu.isEncryptedLinkControlWord() ? "ENCRYPTED" : "")) - .frequency(gvcu.getDownlinkFrequencyB()) - .to(userB) - .build()); - - mCallDetectTalkgroups.add(userB); - } - } - } - else - { - logAlternateVendorMessage(ldu); - } - break; - case GROUP_VOICE_CHANNEL_UPDATE_EXPLICIT: - if(ldu instanceof module.decode.p25.message.ldu.lc.GroupVoiceChannelUpdateExplicit) - { - module.decode.p25.message.ldu.lc.GroupVoiceChannelUpdateExplicit gvcue = - (module.decode.p25.message.ldu.lc.GroupVoiceChannelUpdateExplicit)ldu; - - String group = gvcue.getGroupAddress(); - - if(mChannelType == ChannelType.STANDARD) - { - if(!mCallDetectTalkgroups.contains(group)) - { - broadcast(new P25CallEvent.Builder(CallEventType.CALL_DETECT) - .aliasList(getAliasList()) - .channel(gvcue.getTransmitChannel()) - .details((gvcue.isEncryptedLinkControlWord() ? "ENCRYPTED" : "")) - .frequency(gvcue.getDownlinkFrequency()) - .to(group) - .build()); - - mCallDetectTalkgroups.add(group); - } - } - } - else - { - logAlternateVendorMessage(ldu); - } - break; - case GROUP_VOICE_CHANNEL_USER: - /* Indicates the current user of the current channel */ - if(ldu instanceof module.decode.p25.message.ldu.lc.GroupVoiceChannelUser) - { - module.decode.p25.message.ldu.lc.GroupVoiceChannelUser gvcuser = - (module.decode.p25.message.ldu.lc.GroupVoiceChannelUser)ldu; - - mFromTalkgroupMonitor.process(gvcuser.getSourceAddress()); - - mToTalkgroupMonitor.process(gvcuser.getGroupAddress()); - - if(mChannelType == ChannelType.STANDARD) - { - if(mCurrentCallEvent.getCallEventType() != CallEventType.GROUP_CALL) - { - mCurrentCallEvent.setCallEventType(CallEventType.GROUP_CALL); - broadcast(mCurrentCallEvent); - } - - if(mCurrentCallEvent.getDetails() == null) - { - mCurrentCallEvent.setDetails( - (gvcuser.isEncryptedLinkControlWord() ? "ENCRYPTED " : "") + - (gvcuser.isEmergency() ? "EMERGENCY " : "")); - - broadcast(mCurrentCallEvent); - } - } - } - else - { - logAlternateVendorMessage(ldu); - } - break; - case MESSAGE_UPDATE: - if(ldu instanceof module.decode.p25.message.ldu.lc.MessageUpdate) - { - module.decode.p25.message.ldu.lc.MessageUpdate mu = - (module.decode.p25.message.ldu.lc.MessageUpdate)ldu; - - broadcast(new P25CallEvent.Builder(CallEventType.SDM) - .aliasList(getAliasList()) - .from(mu.getSourceAddress()) - .to(mu.getTargetAddress()) - .details("MSG: " + mu.getShortDataMessage()) - .build()); - } - else - { - logAlternateVendorMessage(ldu); - } - break; - case NETWORK_STATUS_BROADCAST: - if(ldu instanceof module.decode.p25.message.ldu.lc.NetworkStatusBroadcast) - { - updateSystem(((module.decode.p25.message.ldu.lc.NetworkStatusBroadcast)ldu).getSystem()); - } - else - { - logAlternateVendorMessage(ldu); - } - break; - case NETWORK_STATUS_BROADCAST_EXPLICIT: - if(ldu instanceof module.decode.p25.message.ldu.lc.NetworkStatusBroadcastExplicit) - { - updateSystem(((module.decode.p25.message.ldu.lc.NetworkStatusBroadcastExplicit)ldu).getSystem()); - } - else - { - logAlternateVendorMessage(ldu); - } - break; - case PROTECTION_PARAMETER_BROADCAST: - if(ldu instanceof module.decode.p25.message.ldu.lc.ProtectionParameterBroadcast) - { - module.decode.p25.message.ldu.lc.ProtectionParameterBroadcast ppb = - (module.decode.p25.message.ldu.lc.ProtectionParameterBroadcast)ldu; - - broadcast(new P25CallEvent.Builder(CallEventType.COMMAND) - .aliasList(getAliasList()) - .to(ppb.getTargetAddress()) - .details("ENCRYPTION: " + - ppb.getEncryption().name() + " KEY:" + - ppb.getEncryptionKey()) - .build()); - } - else - { - logAlternateVendorMessage(ldu); - } - break; - case RFSS_STATUS_BROADCAST: - if(ldu instanceof module.decode.p25.message.ldu.lc.RFSSStatusBroadcast) - { - module.decode.p25.message.ldu.lc.RFSSStatusBroadcast rfsssb = - (module.decode.p25.message.ldu.lc.RFSSStatusBroadcast)ldu; - - updateSystem(rfsssb.getSystem()); - - String site = rfsssb.getRFSubsystemID() + "-" + - rfsssb.getSiteID(); - - mSiteAttributeMonitor.process(site); - } - else - { - logAlternateVendorMessage(ldu); - } - break; - case RFSS_STATUS_BROADCAST_EXPLICIT: - if(ldu instanceof module.decode.p25.message.ldu.lc.RFSSStatusBroadcastExplicit) - { - module.decode.p25.message.ldu.lc.RFSSStatusBroadcastExplicit rfsssbe = - (module.decode.p25.message.ldu.lc.RFSSStatusBroadcastExplicit)ldu; - - String site = rfsssbe.getRFSubsystemID() + "-" + - rfsssbe.getSiteID(); - - mSiteAttributeMonitor.process(site); - } - else - { - logAlternateVendorMessage(ldu); - } - break; - case SECONDARY_CONTROL_CHANNEL_BROADCAST: - if(ldu instanceof module.decode.p25.message.ldu.lc.SecondaryControlChannelBroadcast) - { - module.decode.p25.message.ldu.lc.SecondaryControlChannelBroadcast sccb = - (module.decode.p25.message.ldu.lc.SecondaryControlChannelBroadcast)ldu; - - String site = sccb.getRFSubsystemID() + "-" + - sccb.getSiteID(); - - mSiteAttributeMonitor.process(site); - } - else - { - logAlternateVendorMessage(ldu); - } - break; - case SECONDARY_CONTROL_CHANNEL_BROADCAST_EXPLICIT: - if(ldu instanceof module.decode.p25.message.ldu.lc.SecondaryControlChannelBroadcastExplicit) - { - module.decode.p25.message.ldu.lc.SecondaryControlChannelBroadcastExplicit sccb = - (module.decode.p25.message.ldu.lc.SecondaryControlChannelBroadcastExplicit)ldu; - - String site = sccb.getRFSubsystemID() + "-" + - sccb.getSiteID(); - - mSiteAttributeMonitor.process(site); - } - else - { - logAlternateVendorMessage(ldu); - } - break; - case STATUS_QUERY: - if(ldu instanceof module.decode.p25.message.ldu.lc.StatusQuery) - { - module.decode.p25.message.ldu.lc.StatusQuery sq = - (module.decode.p25.message.ldu.lc.StatusQuery)ldu; - - broadcast(new P25CallEvent.Builder(CallEventType.QUERY) - .aliasList(getAliasList()) - .details("STATUS QUERY") - .from(sq.getSourceAddress()) - .to(sq.getTargetAddress()) - .build()); - } - else - { - logAlternateVendorMessage(ldu); - } - break; - case STATUS_UPDATE: - if(ldu instanceof module.decode.p25.message.ldu.lc.StatusUpdate) - { - module.decode.p25.message.ldu.lc.StatusUpdate su = - (module.decode.p25.message.ldu.lc.StatusUpdate)ldu; - - broadcast(new P25CallEvent.Builder(CallEventType.STATUS) - .aliasList(getAliasList()) - .details("STATUS UNIT:" + su.getUnitStatus() + - " USER:" + su.getUserStatus()) - .from(su.getSourceAddress()) - .to(su.getTargetAddress()) - .build()); - } - else - { - logAlternateVendorMessage(ldu); - } - break; - case SYSTEM_SERVICE_BROADCAST: - /* This message doesn't provide anything we need for channel state */ - break; - case TELEPHONE_INTERCONNECT_ANSWER_REQUEST: - if(ldu instanceof module.decode.p25.message.ldu.lc.TelephoneInterconnectAnswerRequest) - { - module.decode.p25.message.ldu.lc.TelephoneInterconnectAnswerRequest tiar = - (module.decode.p25.message.ldu.lc.TelephoneInterconnectAnswerRequest)ldu; - - broadcast(new P25CallEvent.Builder(CallEventType.PAGE) - .aliasList(getAliasList()) - .from(tiar.getTelephoneNumber()) - .to(tiar.getTargetAddress()) - .details("TELEPHONE CALL ALERT") - .build()); - } - else - { - logAlternateVendorMessage(ldu); - } - break; - case TELEPHONE_INTERCONNECT_VOICE_CHANNEL_USER: - /* Indicates the current user of the current channel */ - if(mChannelType == ChannelType.STANDARD && - ldu instanceof TelephoneInterconnectVoiceChannelUser) - { - TelephoneInterconnectVoiceChannelUser tivcu = - (TelephoneInterconnectVoiceChannelUser)ldu; - - mToTalkgroupMonitor.process(tivcu.getAddress()); - - if(mCurrentCallEvent.getCallEventType() != CallEventType.TELEPHONE_INTERCONNECT) - { - mCurrentCallEvent.setCallEventType(CallEventType.TELEPHONE_INTERCONNECT); - broadcast(mCurrentCallEvent); - } - - if(mCurrentCallEvent.getDetails() == null) - { - mCurrentCallEvent.setDetails( - (tivcu.isEncryptedLinkControlWord() ? "ENCRYPTED " : "") + - (tivcu.isEmergency() ? "EMERGENCY " : "")); - - broadcast(mCurrentCallEvent); - } - } - else - { - logAlternateVendorMessage(ldu); - } - break; - case UNIT_AUTHENTICATION_COMMAND: - if(ldu instanceof module.decode.p25.message.ldu.lc.UnitAuthenticationCommand) - { - module.decode.p25.message.ldu.lc.UnitAuthenticationCommand uac = - (module.decode.p25.message.ldu.lc.UnitAuthenticationCommand)ldu; - - broadcast(new P25CallEvent.Builder(CallEventType.COMMAND) - .aliasList(getAliasList()) - .to(uac.getCompleteTargetAddress()) - .details("AUTHENTICATE") - .build()); - } - else - { - logAlternateVendorMessage(ldu); - } - break; - case UNIT_REGISTRATION_COMMAND: - if(ldu instanceof module.decode.p25.message.ldu.lc.UnitRegistrationCommand) - { - module.decode.p25.message.ldu.lc.UnitRegistrationCommand urc = - (module.decode.p25.message.ldu.lc.UnitRegistrationCommand)ldu; - - broadcast(new P25CallEvent.Builder(CallEventType.COMMAND) - .aliasList(getAliasList()) - .to(urc.getCompleteTargetAddress()) - .details("REGISTER") - .build()); - } - else - { - logAlternateVendorMessage(ldu); - } - break; - case UNIT_TO_UNIT_ANSWER_REQUEST: - if(ldu instanceof module.decode.p25.message.ldu.lc.UnitToUnitAnswerRequest) - { - module.decode.p25.message.ldu.lc.UnitToUnitAnswerRequest uuar = - (module.decode.p25.message.ldu.lc.UnitToUnitAnswerRequest)ldu; - - broadcast(new P25CallEvent.Builder(CallEventType.PAGE) - .aliasList(getAliasList()) - .from(uuar.getSourceAddress()) - .to(uuar.getTargetAddress()) - .details("UNIT TO UNIT CALL ALERT") - .build()); - } - else - { - logAlternateVendorMessage(ldu); - } - break; - case UNIT_TO_UNIT_VOICE_CHANNEL_USER: - if(mChannelType == ChannelType.STANDARD && - ldu instanceof UnitToUnitVoiceChannelUser) - { - UnitToUnitVoiceChannelUser uuvcu = - (UnitToUnitVoiceChannelUser)ldu; - - mFromTalkgroupMonitor.process(uuvcu.getSourceAddress()); - - mToTalkgroupMonitor.process(uuvcu.getTargetAddress()); - - if(mCurrentCallEvent.getCallEventType() != CallEventType.UNIT_TO_UNIT_CALL) - { - mCurrentCallEvent.setCallEventType(CallEventType.UNIT_TO_UNIT_CALL); - broadcast(mCurrentCallEvent); - } - - if(mCurrentCallEvent.getDetails() == null) - { - mCurrentCallEvent.setDetails( - (uuvcu.isEncryptedLinkControlWord() ? "ENCRYPTED " : "") + - (uuvcu.isEmergency() ? "EMERGENCY " : "")); - - broadcast(mCurrentCallEvent); - } - } - else - { - logAlternateVendorMessage(ldu); - } - break; - default: - break; - } - } - } - - /** - * Process a Trunking Signalling Block message - */ - private void processTSBK(TSBKMessage tsbk) - { - /* Trunking Signalling Block Messages - indicates Control Channel */ - broadcast(new DecoderStateEvent(this, Event.CONTINUATION, State.CONTROL)); - - if(tsbk.getVendor() == Vendor.STANDARD) - { - switch(tsbk.getOpcode()) - { - case ADJACENT_STATUS_BROADCAST: - if(tsbk instanceof AdjacentStatusBroadcast) - { - IAdjacentSite ias = (IAdjacentSite)tsbk; - - mNeighborMap.put(ias.getUniqueID(), ias); - - updateSystem(ias.getSystemID()); - } - break; - case ACKNOWLEDGE_RESPONSE: - processTSBKResponse(tsbk); - break; - case AUTHENTICATION_COMMAND: - processTSBKCommand(tsbk); - break; - case CALL_ALERT: - processTSBKPage(tsbk); - break; - case DENY_RESPONSE: - processTSBKResponse(tsbk); - break; - case EXTENDED_FUNCTION_COMMAND: - processTSBKCommand(tsbk); - break; - case GROUP_AFFILIATION_QUERY: - processTSBKQuery(tsbk); - break; - case GROUP_AFFILIATION_RESPONSE: - processTSBKResponse(tsbk); - break; - case GROUP_DATA_CHANNEL_ANNOUNCEMENT: - case GROUP_DATA_CHANNEL_ANNOUNCEMENT_EXPLICIT: - processTSBKDataChannelAnnouncement(tsbk); - break; - case GROUP_DATA_CHANNEL_GRANT: - case GROUP_VOICE_CHANNEL_GRANT: - case GROUP_VOICE_CHANNEL_GRANT_UPDATE: - case GROUP_VOICE_CHANNEL_GRANT_UPDATE_EXPLICIT: - case INDIVIDUAL_DATA_CHANNEL_GRANT: - case TELEPHONE_INTERCONNECT_VOICE_CHANNEL_GRANT: - case TELEPHONE_INTERCONNECT_VOICE_CHANNEL_GRANT_UPDATE: - case UNIT_TO_UNIT_VOICE_CHANNEL_GRANT: - case UNIT_TO_UNIT_VOICE_CHANNEL_GRANT_UPDATE: - processTSBKChannelGrant(tsbk); - break; - case IDENTIFIER_UPDATE_NON_VUHF: - case IDENTIFIER_UPDATE_VHF_UHF_BANDS: - case IDENTIFIER_UPDATE_TDMA: - IdentifierUpdate iu = (IdentifierUpdate)tsbk; - - if(!mBands.containsKey(iu.getIdentifier())) - { - mBands.put(iu.getIdentifier(), iu); - } - break; - case LOCATION_REGISTRATION_RESPONSE: - case UNIT_DEREGISTRATION_ACKNOWLEDGE: - processTSBKResponse(tsbk); - break; - case MESSAGE_UPDATE: - processTSBKMessage(tsbk); - break; - case NETWORK_STATUS_BROADCAST: - mNetworkStatus = (module.decode.p25.message.tsbk.osp - .control.NetworkStatusBroadcast)tsbk; - break; - case PROTECTION_PARAMETER_UPDATE: - processTSBKResponse(tsbk); - break; - case QUEUED_RESPONSE: - processTSBKResponse(tsbk); - break; - case RADIO_UNIT_MONITOR_COMMAND: - processTSBKCommand(tsbk); - break; - case RFSS_STATUS_BROADCAST: - processTSBKRFSSStatus((RFSSStatusBroadcast)tsbk); - break; - case ROAMING_ADDRESS_COMMAND: - processTSBKCommand(tsbk); - break; - case SECONDARY_CONTROL_CHANNEL_BROADCAST: - module.decode.p25.message.tsbk.osp.control.SecondaryControlChannelBroadcast sccb = - (module.decode.p25.message.tsbk.osp.control.SecondaryControlChannelBroadcast)tsbk; - - if(sccb.getDownlinkFrequency1() > 0) - { - mSecondaryControlChannels.add(sccb); - } - break; - case SNDCP_DATA_CHANNEL_ANNOUNCEMENT_EXPLICIT: - mSNDCPDataChannel = (SNDCPDataChannelAnnouncementExplicit)tsbk; - break; - case SNDCP_DATA_CHANNEL_GRANT: - processTSBKChannelGrant(tsbk); - break; - case STATUS_QUERY: - processTSBKQuery(tsbk); - break; - case STATUS_UPDATE: - processTSBKResponse(tsbk); - break; - case TELEPHONE_INTERCONNECT_ANSWER_REQUEST: - case UNIT_TO_UNIT_ANSWER_REQUEST: - processTSBKPage(tsbk); - break; - case UNIT_REGISTRATION_COMMAND: - processTSBKCommand(tsbk); - break; - case UNIT_REGISTRATION_RESPONSE: - processTSBKResponse(tsbk); - break; - default: - break; - } - } - else if(tsbk.getVendor() == Vendor.MOTOROLA) - { - processMotorolaTSBK((MotorolaTSBKMessage)tsbk); - } - } - - /** - * Process a Packet Data Unit message - */ - private void processPDU(PDUMessage pdu) - { - if(pdu instanceof PacketData || pdu instanceof PDUTypeUnknown) - { - broadcast(new DecoderStateEvent(this, Event.CONTINUATION, State.DATA)); - } - else if(pdu instanceof PDUConfirmedMessage) - { - broadcast(new DecoderStateEvent(this, Event.CONTINUATION, State.DATA)); - - PDUConfirmedMessage pduc = (PDUConfirmedMessage)pdu; - - switch(pduc.getPDUType()) - { - case SNDCP_ACTIVATE_TDS_CONTEXT_ACCEPT: - SNDCPActivateTDSContextAccept satca = - (SNDCPActivateTDSContextAccept)pduc; - - broadcast(new P25CallEvent.Builder(CallEventType.COMMAND) - .aliasList(getAliasList()) - .channel(mCurrentChannel) - .details("ACTIVATE SNDCP USE IP:" + satca.getIPAddress()) - .frequency(mCurrentChannelFrequency) - .to(satca.getLogicalLinkID()) - .build()); - break; - case SNDCP_ACTIVATE_TDS_CONTEXT_REJECT: - SNDCPActivateTDSContextReject satcr = - (SNDCPActivateTDSContextReject)pduc; - - broadcast(new P25CallEvent.Builder(CallEventType.COMMAND) - .aliasList(getAliasList()) - .channel(mCurrentChannel) - .details("REJECT: SNDCP CONTEXT ACTIVATION " - + "REASON:" + satcr.getReason().getLabel()) - .frequency(mCurrentChannelFrequency) - .to(satcr.getLogicalLinkID()) - .build()); - break; - case SNDCP_ACTIVATE_TDS_CONTEXT_REQUEST: - SNDCPActivateTDSContextRequest satcreq = - (SNDCPActivateTDSContextRequest)pduc; - - broadcast(new P25CallEvent.Builder(CallEventType.QUERY) - .aliasList(getAliasList()) - .channel(mCurrentChannel) - .details("REQUEST SNDCP USE IP:" + satcreq.getIPAddress()) - .frequency(mCurrentChannelFrequency) - .from(satcreq.getLogicalLinkID()) - .build()); - break; - case SNDCP_DEACTIVATE_TDS_CONTEXT_ACCEPT: - SNDCPDeactivateTDSContext sdtca = - (SNDCPDeactivateTDSContext)pduc; - - broadcast(new P25CallEvent.Builder(CallEventType.QUERY) - .aliasList(getAliasList()) - .channel(mCurrentChannel) - .details("ACCEPT DEACTIVATE SNDCP CONTEXT") - .frequency(mCurrentChannelFrequency) - .from(sdtca.getLogicalLinkID()) - .build()); - break; - case SNDCP_DEACTIVATE_TDS_CONTEXT_REQUEST: - SNDCPDeactivateTDSContext sdtcreq = - (SNDCPDeactivateTDSContext)pduc; - - broadcast(new P25CallEvent.Builder(CallEventType.QUERY) - .aliasList(getAliasList()) - .channel(mCurrentChannel) - .details("REQUEST DEACTIVATE SNDCP CONTEXT") - .frequency(mCurrentChannelFrequency) - .from(sdtcreq.getLogicalLinkID()) - .build()); - break; - case SNDCP_RF_CONFIRMED_DATA: - if(pduc instanceof SNDCPUserData) - { - SNDCPUserData sud = (SNDCPUserData)pduc; - - StringBuilder sbFrom = new StringBuilder(); - StringBuilder sbTo = new StringBuilder(); - - sbFrom.append(sud.getSourceIPAddress()); - sbTo.append(sud.getDestinationIPAddress()); - - if(sud.getIPProtocol() == IPProtocol.UDP) - { - sbFrom.append(":"); - sbFrom.append(sud.getUDPSourcePort()); - sbTo.append(":"); - sbTo.append(sud.getUDPDestinationPort()); - } - - broadcast(new DecoderStateEvent(this, Event.START, State.DATA)); - - broadcast(new P25CallEvent.Builder(CallEventType.DATA_CALL) - .aliasList(getAliasList()) - .channel(mCurrentChannel) - .details("DATA: " + sud.getPayload() + - " RADIO IP:" + sbTo.toString()) - .frequency(mCurrentChannelFrequency) - .from(sbFrom.toString()) - .to(pduc.getLogicalLinkID()) - .build()); - } - else - { - mLog.error("Error - expected SNDCPUserData instance but class was: " + pduc.getClass() + - " and PDU Type:" + pduc.getPDUType().name()); - } - break; - case SNDCP_RF_UNCONFIRMED_DATA: - break; - default: -// mLog.debug( "PDUC - Unrecognized Message: " + pduc.toString() ); - break; - } - } - else - { - /* These are alternate trunking control blocks in PDU format */ - broadcast(new DecoderStateEvent(this, Event.CONTINUATION, State.CONTROL)); - - switch(pdu.getOpcode()) - { - case GROUP_DATA_CHANNEL_GRANT: - case GROUP_VOICE_CHANNEL_GRANT: - case INDIVIDUAL_DATA_CHANNEL_GRANT: - case TELEPHONE_INTERCONNECT_VOICE_CHANNEL_GRANT: - case UNIT_TO_UNIT_VOICE_CHANNEL_GRANT: - case UNIT_TO_UNIT_VOICE_CHANNEL_GRANT_UPDATE: - processPDUChannelGrant(pdu); - break; - case ADJACENT_STATUS_BROADCAST: - if(pdu instanceof AdjacentStatusBroadcastExtended) - { - IAdjacentSite ias = (IAdjacentSite)pdu; - - mNeighborMap.put(ias.getUniqueID(), ias); - - updateSystem(ias.getSystemID()); - } - break; - case CALL_ALERT: - if(pdu instanceof CallAlertExtended) - { - CallAlertExtended ca = (CallAlertExtended)pdu; - - broadcast(new P25CallEvent.Builder(CallEventType.PAGE) - .aliasList(getAliasList()) - .from(ca.getWACN() + "-" + ca.getSystemID() + "-" + - ca.getSourceID()) - .to(ca.getTargetAddress()) - .build()); - } - else - { - logAlternateVendorMessage(pdu); - } - break; - case GROUP_AFFILIATION_QUERY: - if(pdu instanceof GroupAffiliationQueryExtended) - { - GroupAffiliationQueryExtended gaqe = - (GroupAffiliationQueryExtended)pdu; - - if(mLastQueryEventID == null || !gaqe.getTargetAddress() - .contentEquals(mLastQueryEventID)) - { - broadcast(new P25CallEvent.Builder(CallEventType.QUERY) - .aliasList(getAliasList()) - .details("GROUP AFFILIATION") - .from(gaqe.getWACN() + "-" + gaqe.getSystemID() + - "-" + gaqe.getSourceID()) - .to(gaqe.getTargetAddress()) - .build()); - - mLastQueryEventID = gaqe.getToID(); - } - } - break; - case GROUP_AFFILIATION_RESPONSE: - if(pdu instanceof GroupAffiliationResponseExtended) - { - GroupAffiliationResponseExtended gar = - (GroupAffiliationResponseExtended)pdu; - - if(mLastResponseEventID == null || !gar.getTargetAddress() - .contentEquals(mLastResponseEventID)) - { - broadcast(new P25CallEvent.Builder(CallEventType.RESPONSE) - .aliasList(getAliasList()) - .details("AFFILIATION:" + gar.getResponse().name() + - " FOR GROUP:" + gar.getGroupWACN() + "-" + - gar.getGroupSystemID() + "-" + - gar.getGroupID() + " ANNOUNCEMENT GROUP:" + - gar.getAnnouncementGroupID()) - .from(gar.getSourceWACN() + "-" + - gar.getSourceSystemID() + "-" + - gar.getSourceID()) - .to(gar.getTargetAddress()) - .build()); - - mLastResponseEventID = gar.getTargetAddress(); - } - } - else - { - logAlternateVendorMessage(pdu); - } - break; - case MESSAGE_UPDATE: - if(pdu instanceof MessageUpdateExtended) - { - MessageUpdateExtended mu = (MessageUpdateExtended)pdu; - - broadcast(new P25CallEvent.Builder(CallEventType.SDM) - .aliasList(getAliasList()) - .details("MESSAGE: " + mu.getMessage()) - .from(mu.getSourceWACN() + "-" + mu.getSourceSystemID() + - "-" + mu.getSourceID()) - .to(mu.getTargetAddress()) - .build()); - } - else - { - logAlternateVendorMessage(pdu); - } - break; - case NETWORK_STATUS_BROADCAST: - if(pdu instanceof NetworkStatusBroadcastExtended) - { - mNetworkStatusExtended = (NetworkStatusBroadcastExtended)pdu; - } - else - { - logAlternateVendorMessage(pdu); - } - break; - case PROTECTION_PARAMETER_BROADCAST: - if(pdu instanceof ProtectionParameterBroadcast) - { - mProtectionParameterBroadcast = - (ProtectionParameterBroadcast)pdu; - } - else - { - logAlternateVendorMessage(pdu); - } - break; - case RFSS_STATUS_BROADCAST: - if(pdu instanceof RFSSStatusBroadcastExtended) - { - mRFSSStatusMessageExtended = (RFSSStatusBroadcastExtended)pdu; - - updateNAC(mRFSSStatusMessageExtended.getNAC()); - updateSystem(mRFSSStatusMessageExtended.getSystemID()); - mSiteAttributeMonitor.process(mRFSSStatusMessageExtended.getRFSubsystemID() + - "-" + mRFSSStatusMessageExtended.getSiteID()); - } - else - { - logAlternateVendorMessage(pdu); - } - break; - case ROAMING_ADDRESS_UPDATE: - if(pdu instanceof RoamingAddressUpdateExtended) - { - RoamingAddressUpdateExtended raue = - (RoamingAddressUpdateExtended)pdu; - - StringBuilder sb = new StringBuilder(); - sb.append("ROAMING ADDRESS STACK A:"); - sb.append(raue.getWACNA() + "-" + raue.getSystemIDA()); - - if(raue.isFormat2()) - { - sb.append(" B:"); - sb.append(raue.getWACNB() + "-" + raue.getSystemIDB()); - sb.append(" C:"); - sb.append(raue.getWACNC() + "-" + raue.getSystemIDC()); - sb.append(" D:"); - sb.append(raue.getWACND() + "-" + raue.getSystemIDD()); - } - - if(raue.isFormat3()) - { - sb.append(" E:"); - sb.append(raue.getWACNE() + "-" + raue.getSystemIDE()); - sb.append(" F:"); - sb.append(raue.getWACNF() + "-" + raue.getSystemIDF()); - sb.append(" G:"); - sb.append(raue.getWACNG() + "-" + raue.getSystemIDG()); - } - - broadcast(new P25CallEvent.Builder(CallEventType.RESPONSE) - .aliasList(getAliasList()) - .details(sb.toString()) - .from(raue.getSourceID()) - .to(raue.getTargetAddress()) - .build()); - } - else - { - logAlternateVendorMessage(pdu); - } - break; - case STATUS_QUERY: - if(pdu instanceof StatusQueryExtended) - { - StatusQueryExtended sq = (StatusQueryExtended)pdu; - - if(mLastQueryEventID == null || !sq.getTargetAddress() - .contentEquals(mLastQueryEventID)) - { - broadcast(new P25CallEvent.Builder(CallEventType.QUERY) - .aliasList(getAliasList()) - .details("STATUS QUERY") - .from(sq.getSourceWACN() + "-" + - sq.getSourceSystemID() + "-" + - sq.getSourceID()) - .to(sq.getTargetAddress()) - .build()); - - mLastQueryEventID = sq.getToID(); - } - } - else - { - logAlternateVendorMessage(pdu); - } - break; - case STATUS_UPDATE: - if(pdu instanceof StatusUpdateExtended) - { - StatusUpdateExtended su = (StatusUpdateExtended)pdu; - - if(mLastResponseEventID == null || !mLastResponseEventID - .contentEquals(su.getTargetAddress())) - { - broadcast(new P25CallEvent.Builder(CallEventType.RESPONSE) - .aliasList(getAliasList()) - .details("STATUS USER: " + su.getUserStatus() + - " UNIT: " + su.getUnitStatus()) - .from(su.getSourceWACN() + "-" + - su.getSourceSystemID() + "-" + - su.getSourceID()) - .to(su.getTargetAddress()) - .build()); - - mLastResponseEventID = su.getTargetAddress(); - } - } - else - { - logAlternateVendorMessage(pdu); - } - break; - case UNIT_REGISTRATION_RESPONSE: - if(pdu instanceof UnitRegistrationResponseExtended) - { - UnitRegistrationResponseExtended urr = - (UnitRegistrationResponseExtended)pdu; - - if(urr.getResponse() == Response.ACCEPT) - { - mRegistrations.put(urr.getAssignedSourceAddress(), - System.currentTimeMillis()); - } - - if(mLastRegistrationEventID == null || !mLastRegistrationEventID - .contentEquals(urr.getAssignedSourceAddress())) - { - broadcast(new P25CallEvent.Builder(CallEventType.REGISTER) - .aliasList(getAliasList()) - .details("REGISTRATION:" + urr.getResponse().name() + - " FOR EXTERNAL SYSTEM ADDRESS: " + - urr.getWACN() + "-" + - urr.getSystemID() + "-" + - urr.getSourceAddress() + - " SOURCE ID: " + urr.getSourceID()) - .from(urr.getAssignedSourceAddress()) - .build()); - - mLastRegistrationEventID = urr.getAssignedSourceAddress(); - } - } - else - { - logAlternateVendorMessage(pdu); - } - break; - case UNIT_TO_UNIT_ANSWER_REQUEST: - if(pdu instanceof UnitToUnitAnswerRequestExplicit) - { - UnitToUnitAnswerRequestExplicit utuare = - (UnitToUnitAnswerRequestExplicit)pdu; - - if(mLastPageEventID == null || !mLastPageEventID - .contentEquals(utuare.getTargetAddress())) - { - broadcast(new P25CallEvent.Builder(CallEventType.PAGE) - .aliasList(getAliasList()) - .details((utuare.isEmergency() ? "EMERGENCY" : "")) - .from(utuare.getWACN() + "-" + - utuare.getSystemID() + "-" + - utuare.getSourceID()) - .to(utuare.getTargetAddress()) - .build()); - - mLastPageEventID = utuare.getTargetAddress(); - } - } - else - { - logAlternateVendorMessage(pdu); - } - break; - default: - break; - } - } - } - - private void processPDUChannelGrant(PDUMessage pdu) - { - String channel = null; - String from = null; - String to = null; - - switch(pdu.getOpcode()) - { - case GROUP_DATA_CHANNEL_GRANT: - if(pdu instanceof GroupDataChannelGrantExtended) - { - GroupDataChannelGrantExtended gdcge = - (GroupDataChannelGrantExtended)pdu; - - channel = gdcge.getTransmitChannel(); - from = gdcge.getSourceAddress(); - to = gdcge.getGroupAddress(); - - if(hasCallEvent(channel, from, to)) - { - updateCallEvent(channel, from, to); - } - else - { - P25CallEvent callEvent = new P25CallEvent.Builder(CallEventType.DATA_CALL) - .aliasList(getAliasList()) - .channel(channel) - .details((gdcge.isEncrypted() ? "ENCRYPTED" : "") + - (gdcge.isEmergency() ? " EMERGENCY" : "")) - .frequency(gdcge.getDownlinkFrequency()) - .from(from) - .to(to) - .build(); - - registerCallEvent(callEvent); - broadcast(callEvent); - } - } - else - { - logAlternateVendorMessage(pdu); - } - - if(!mIgnoreDataCalls) - { - broadcast(new TrafficChannelAllocationEvent(this, - mChannelCallMap.get(channel))); - } - break; - case GROUP_VOICE_CHANNEL_GRANT: - if(pdu instanceof GroupVoiceChannelGrantExplicit) - { - GroupVoiceChannelGrantExplicit gvcge = - (GroupVoiceChannelGrantExplicit)pdu; - - channel = gvcge.getTransmitChannel(); - from = gvcge.getSourceAddress(); - to = gvcge.getGroupAddress(); - - if(hasCallEvent(channel, from, to)) - { - updateCallEvent(channel, from, to); - } - else - { - P25CallEvent callEvent = new P25CallEvent.Builder(CallEventType.GROUP_CALL) - .aliasList(getAliasList()) - .channel(channel) - .details((gvcge.isEncrypted() ? "ENCRYPTED" : "") + - (gvcge.isEmergency() ? " EMERGENCY" : "")) - .frequency(gvcge.getDownlinkFrequency()) - .from(from) - .to(to) - .build(); - - registerCallEvent(callEvent); - broadcast(callEvent); - } - - broadcast(new TrafficChannelAllocationEvent(this, - mChannelCallMap.get(channel))); - } - else - { - logAlternateVendorMessage(pdu); - } - break; - case INDIVIDUAL_DATA_CHANNEL_GRANT: - if(pdu instanceof IndividualDataChannelGrantExtended) - { - IndividualDataChannelGrantExtended idcge = - (IndividualDataChannelGrantExtended)pdu; - - channel = idcge.getTransmitChannel(); - from = idcge.getSourceWACN() + "-" + - idcge.getSourceSystemID() + "-" + - idcge.getSourceAddress(); - to = idcge.getTargetAddress(); - - if(hasCallEvent(channel, from, to)) - { - updateCallEvent(channel, from, to); - } - else - { - P25CallEvent callEvent = new P25CallEvent.Builder(CallEventType.DATA_CALL) - .aliasList(getAliasList()) - .channel(channel) - .details((idcge.isEncrypted() ? "ENCRYPTED" : "") + - (idcge.isEmergency() ? " EMERGENCY" : "")) - .frequency(idcge.getDownlinkFrequency()) - .from(from) - .to(to) - .build(); - - registerCallEvent(callEvent); - broadcast(callEvent); - } - - if(!mIgnoreDataCalls) - { - broadcast(new TrafficChannelAllocationEvent(this, - mChannelCallMap.get(channel))); - } - } - else - { - logAlternateVendorMessage(pdu); - } - break; - case TELEPHONE_INTERCONNECT_VOICE_CHANNEL_GRANT: - TelephoneInterconnectChannelGrantExplicit ticge = - (TelephoneInterconnectChannelGrantExplicit)pdu; - - channel = ticge.getTransmitChannel(); - - //We don't know if the subscriber is calling or being called, so - //we use the same address in both from/to fields - from = ticge.getAddress(); - to = ticge.getAddress(); - - if(hasCallEvent(channel, from, to)) - { - updateCallEvent(channel, from, to); - } - else - { - P25CallEvent callEvent = new P25CallEvent.Builder(CallEventType.TELEPHONE_INTERCONNECT) - .aliasList(getAliasList()) - .channel(channel) - .details((ticge.isEncrypted() ? "ENCRYPTED" : "") + - (ticge.isEmergency() ? " EMERGENCY" : "") + - " CALL TIMER:" + ticge.getCallTimer()) - .frequency(ticge.getDownlinkFrequency()) - .from(from) - .to(to) - .build(); - - registerCallEvent(callEvent); - broadcast(callEvent); - } - - broadcast(new TrafficChannelAllocationEvent(this, - mChannelCallMap.get(channel))); - break; - case UNIT_TO_UNIT_VOICE_CHANNEL_GRANT: - if(pdu instanceof UnitToUnitVoiceChannelGrantExtended) - { - UnitToUnitVoiceChannelGrantExtended uuvcge = - (UnitToUnitVoiceChannelGrantExtended)pdu; - - channel = uuvcge.getTransmitChannel(); - from = uuvcge.getSourceWACN() + "-" + - uuvcge.getSourceSystemID() + "-" + - uuvcge.getSourceID(); - to = uuvcge.getTargetAddress(); - - if(hasCallEvent(channel, from, to)) - { - updateCallEvent(channel, from, to); - } - else - { - P25CallEvent callEvent = new P25CallEvent.Builder(CallEventType.UNIT_TO_UNIT_CALL) - .aliasList(getAliasList()) - .channel(channel) - .details((uuvcge.isEncrypted() ? "ENCRYPTED" : "") + - (uuvcge.isEmergency() ? " EMERGENCY" : "")) - .frequency(uuvcge.getDownlinkFrequency()) - .from(from) - .to(to) - .build(); - - registerCallEvent(callEvent); - broadcast(callEvent); - } - - broadcast(new TrafficChannelAllocationEvent(this, - mChannelCallMap.get(channel))); - } - else - { - logAlternateVendorMessage(pdu); - } - break; - case UNIT_TO_UNIT_VOICE_CHANNEL_GRANT_UPDATE: - if(pdu instanceof UnitToUnitVoiceChannelGrantUpdateExtended) - { - UnitToUnitVoiceChannelGrantUpdateExtended uuvcgue = - (UnitToUnitVoiceChannelGrantUpdateExtended)pdu; - - channel = uuvcgue.getTransmitChannel(); - from = uuvcgue.getSourceWACN() + "-" + - uuvcgue.getSourceSystemID() + "-" + - uuvcgue.getSourceID(); - to = uuvcgue.getTargetAddress(); - - if(hasCallEvent(channel, from, to)) - { - updateCallEvent(channel, from, to); - } - else - { - P25CallEvent callEvent = new P25CallEvent.Builder(CallEventType.UNIT_TO_UNIT_CALL) - .aliasList(getAliasList()) - .channel(channel) - .details((uuvcgue.isEncrypted() ? "ENCRYPTED" : "") + - (uuvcgue.isEmergency() ? " EMERGENCY" : "")) - .frequency(uuvcgue.getDownlinkFrequency()) - .from(from) - .to(to) - .build(); - - registerCallEvent(callEvent); - broadcast(callEvent); - } - - broadcast(new TrafficChannelAllocationEvent(this, - mChannelCallMap.get(channel))); - - } - else - { - logAlternateVendorMessage(pdu); - } - break; - default: - break; - } - } - - private void processTSBKCommand(TSBKMessage message) - { - switch(message.getOpcode()) - { - case AUTHENTICATION_COMMAND: - AuthenticationCommand ac = (AuthenticationCommand)message; - - if(mLastCommandEventID == null || !mLastCommandEventID - .contentEquals(ac.getFullTargetID())) - { - broadcast(new P25CallEvent.Builder(CallEventType.COMMAND) - .aliasList(getAliasList()) - .details("AUTHENTICATE") - .to(ac.getWACN() + "-" + ac.getSystemID() + "-" + - ac.getTargetID()) - .build()); - - mLastCommandEventID = ac.getFullTargetID(); - } - break; - case EXTENDED_FUNCTION_COMMAND: - ExtendedFunctionCommand efc = (ExtendedFunctionCommand)message; - - if(mLastCommandEventID == null || !mLastCommandEventID - .contentEquals(efc.getTargetAddress())) - { - broadcast(new P25CallEvent.Builder(CallEventType.FUNCTION) - .aliasList(getAliasList()) - .details("EXTENDED FUNCTION: " + - efc.getExtendedFunction().getLabel()) - .from(efc.getSourceAddress()) - .to(efc.getTargetAddress()) - .build()); - - mLastCommandEventID = efc.getTargetAddress(); - } - break; - case RADIO_UNIT_MONITOR_COMMAND: - RadioUnitMonitorCommand rumc = (RadioUnitMonitorCommand)message; - - if(mLastCommandEventID == null || !mLastCommandEventID - .contentEquals(rumc.getTargetAddress())) - { - broadcast(new P25CallEvent.Builder(CallEventType.COMMAND) - .aliasList(getAliasList()) - .details("RADIO UNIT MONITOR") - .from(rumc.getSourceAddress()) - .to(rumc.getTargetAddress()) - .build()); - - mLastCommandEventID = rumc.getTargetAddress(); - } - break; - case ROAMING_ADDRESS_COMMAND: - RoamingAddressCommand rac = (RoamingAddressCommand)message; - - if(mLastCommandEventID == null || !mLastCommandEventID - .contentEquals(rac.getTargetID())) - { - broadcast(new P25CallEvent.Builder(CallEventType.COMMAND) - .aliasList(getAliasList()) - .details(rac.getStackOperation().name() + - " ROAMING ADDRESS " + rac.getWACN() + "-" + - rac.getSystemID()) - .to(rac.getTargetID()) - .build()); - - mLastCommandEventID = rac.getTargetID(); - } - break; - case UNIT_REGISTRATION_COMMAND: - UnitRegistrationCommand urc = (UnitRegistrationCommand)message; - - if(mLastCommandEventID == null || !mLastCommandEventID - .contentEquals(urc.getTargetAddress())) - { - broadcast(new P25CallEvent.Builder(CallEventType.COMMAND) - .aliasList(getAliasList()) - .details("REGISTER") - .from(urc.getSourceAddress()) - .to(urc.getTargetAddress()) - .build()); - - mLastCommandEventID = urc.getTargetAddress(); - } - break; - default: - break; - } - } - - private void processTSBKMessage(TSBKMessage message) - { - MessageUpdate mu = (MessageUpdate)message; - - broadcast(new P25CallEvent.Builder(CallEventType.SDM) - .aliasList(getAliasList()) - .details("MESSAGE: " + mu.getMessage()) - .from(mu.getSourceAddress()) - .to(mu.getTargetAddress()) - .build()); - } - - private void processTSBKQuery(TSBKMessage message) - { - switch(message.getOpcode()) - { - case GROUP_AFFILIATION_QUERY: - GroupAffiliationQuery gaq = (GroupAffiliationQuery)message; - - if(mLastQueryEventID == null || !mLastQueryEventID - .contentEquals(gaq.getTargetAddress())) - { - broadcast(new P25CallEvent.Builder(CallEventType.QUERY) - .aliasList(getAliasList()) - .details("GROUP AFFILIATION") - .from(gaq.getSourceAddress()) - .to(gaq.getTargetAddress()) - .build()); - } - break; - case STATUS_QUERY: - StatusQuery sq = (StatusQuery)message; - - if(mLastQueryEventID == null || !mLastQueryEventID - .contentEquals(sq.getTargetAddress())) - { - broadcast(new P25CallEvent.Builder(CallEventType.QUERY) - .aliasList(getAliasList()) - .details("STATUS QUERY") - .from(sq.getSourceAddress()) - .to(sq.getTargetAddress()) - .build()); - } - break; - default: - break; - } - } - - private void processTSBKResponse(TSBKMessage message) - { - switch(message.getOpcode()) - { - case ACKNOWLEDGE_RESPONSE: - AcknowledgeResponse ar = (AcknowledgeResponse)message; - - if(mLastResponseEventID == null || !ar.getTargetAddress() - .contentEquals(mLastResponseEventID)) - { - String to = ar.getTargetAddress(); - - if(ar.hasAdditionalInformation() && ar.hasExtendedAddress()) - { - to = ar.getWACN() + "-" + ar.getSystemID() + "-" + - ar.getTargetAddress(); - } - - String from = null; - - if(ar.hasAdditionalInformation() && !ar.hasExtendedAddress()) - { - from = ar.getFromID(); - } - - broadcast(new P25CallEvent.Builder(CallEventType.RESPONSE) - .aliasList(getAliasList()) - .details("ACKNOWLEDGE") - .from(from) - .to(to) - .build()); - - mLastResponseEventID = ar.getTargetAddress(); - } - break; - case DENY_RESPONSE: - DenyResponse dr = (DenyResponse)message; - - if(mLastResponseEventID == null || !dr.getTargetAddress() - .contentEquals(mLastResponseEventID)) - { - broadcast(new P25CallEvent.Builder(CallEventType.RESPONSE) - .aliasList(getAliasList()) - .details("DENY REASON: " + dr.getReason().name() + - " REQUESTED: " + dr.getServiceType().name()) - .from(dr.getSourceAddress()) - .to(dr.getTargetAddress()) - .build()); - - mLastResponseEventID = dr.getTargetAddress(); - } - break; - case GROUP_AFFILIATION_RESPONSE: - GroupAffiliationResponse gar = (GroupAffiliationResponse)message; - - if(mLastResponseEventID == null || !gar.getTargetAddress() - .contentEquals(mLastResponseEventID)) - { - broadcast(new P25CallEvent.Builder(CallEventType.RESPONSE) - .aliasList(getAliasList()) - .details("AFFILIATION:" + gar.getResponse().name() + - " FOR " + gar.getAffiliationScope() + - " GROUP:" + gar.getGroupAddress() + - " ANNOUNCEMENT GROUP:" + - gar.getAnnouncementGroupAddress()) - .to(gar.getTargetAddress()) - .build()); - - mLastResponseEventID = gar.getTargetAddress(); - } - break; - case LOCATION_REGISTRATION_RESPONSE: - LocationRegistrationResponse lrr = - (LocationRegistrationResponse)message; - - if(lrr.getResponse() == Response.ACCEPT) - { - mRegistrations.put(lrr.getTargetAddress(), - System.currentTimeMillis()); - } - - if(mLastRegistrationEventID == null || - !mLastRegistrationEventID.contentEquals(lrr.getTargetAddress())) - { - broadcast(new P25CallEvent.Builder(CallEventType.REGISTER) - .aliasList(getAliasList()) - .details("REGISTRATION:" + lrr.getResponse().name() + - " SITE: " + lrr.getRFSSID() + "-" + - lrr.getSiteID()) - .from(lrr.getTargetAddress()) - .to(lrr.getGroupAddress()) - .build()); - - mLastRegistrationEventID = lrr.getTargetAddress(); - } - break; - case PROTECTION_PARAMETER_UPDATE: - ProtectionParameterUpdate ppu = (ProtectionParameterUpdate)message; - - if(mLastResponseEventID == null || !ppu.getTargetAddress() - .contentEquals(mLastResponseEventID)) - { - broadcast(new P25CallEvent.Builder(CallEventType.RESPONSE) - .aliasList(getAliasList()) - .details("USE ENCRYPTION ALGORITHM:" + - ppu.getAlgorithm().name() + " KEY:" + - ppu.getKeyID()) - .to(ppu.getTargetAddress()) - .build()); - - mLastResponseEventID = ppu.getTargetAddress(); - } - break; - case QUEUED_RESPONSE: - QueuedResponse qr = (QueuedResponse)message; - - if(mLastResponseEventID == null || !qr.getTargetAddress() - .contentEquals(mLastResponseEventID)) - { - broadcast(new P25CallEvent.Builder(CallEventType.RESPONSE) - .aliasList(getAliasList()) - .details("QUEUED REASON: " + qr.getReason().name() + - " REQUESTED: " + qr.getServiceType().name()) - .from(qr.getSourceAddress()) - .to(qr.getTargetAddress()) - .build()); - - mLastResponseEventID = qr.getTargetAddress(); - } - break; - case STATUS_UPDATE: - StatusUpdate su = (StatusUpdate)message; - - if(mLastResponseEventID == null || !su.getTargetAddress() - .contentEquals(mLastResponseEventID)) - { - broadcast(new P25CallEvent.Builder(CallEventType.RESPONSE) - .aliasList(getAliasList()) - .details("STATUS USER: " + su.getUserStatus() + - " UNIT: " + su.getUnitStatus()) - .from(su.getSourceAddress()) - .to(su.getTargetAddress()) - .build()); - - mLastResponseEventID = su.getTargetAddress(); - } - - break; - case UNIT_REGISTRATION_RESPONSE: - UnitRegistrationResponse urr = (UnitRegistrationResponse)message; - - if(urr.getResponse() == Response.ACCEPT) - { - mRegistrations.put(urr.getSourceAddress(), - System.currentTimeMillis()); - } - - if(mLastRegistrationEventID == null || - !mLastRegistrationEventID.contentEquals(urr.getSourceAddress())) - { - broadcast(new P25CallEvent.Builder(CallEventType.REGISTER) - .aliasList(getAliasList()) - .details("REGISTRATION:" + urr.getResponse().name() + - " SYSTEM: " + urr.getSystemID() + - " SOURCE ID: " + urr.getSourceID()) - .from(urr.getSourceAddress()) - .build()); - - mLastRegistrationEventID = urr.getSourceAddress(); - } - break; - case UNIT_DEREGISTRATION_ACKNOWLEDGE: - UnitDeregistrationAcknowledge udr = - (UnitDeregistrationAcknowledge)message; - - if(mLastRegistrationEventID == null || - !mLastRegistrationEventID.contentEquals(udr.getSourceID())) - { - - broadcast(new P25CallEvent.Builder(CallEventType.DEREGISTER) - .aliasList(getAliasList()) - .from(udr.getSourceID()) - .build()); - - List keysToRemove = new ArrayList(); - - /* Remove this radio from the registrations set */ - for(String key : mRegistrations.keySet()) - { - if(key.startsWith(udr.getSourceID())) - { - keysToRemove.add(key); - } - } - - for(String key : keysToRemove) - { - mRegistrations.remove(key); - } - - mLastRegistrationEventID = udr.getSourceID(); - } - break; - default: - break; - } - } - - private void processTSBKRFSSStatus(RFSSStatusBroadcast message) - { - mRFSSStatusMessage = message; - - updateNAC(message.getNAC()); - - updateSystem(message.getSystemID()); - - mSiteAttributeMonitor.process(message.getRFSubsystemID() + "-" + message.getSiteID()); - } - - private void processTSBKDataChannelAnnouncement(TSBKMessage message) - { - switch(message.getOpcode()) - { - case GROUP_DATA_CHANNEL_ANNOUNCEMENT: - GroupDataChannelAnnouncement gdca = (GroupDataChannelAnnouncement)message; - - broadcast(new P25CallEvent.Builder(CallEventType.ANNOUNCEMENT) - .aliasList(getAliasList()) - .channel(gdca.getChannel1()) - .details((gdca.isEncrypted() ? "ENCRYPTED" : "") + - (gdca.isEmergency() ? " EMERGENCY" : "")) - .frequency(gdca.getDownlinkFrequency1()) - .to(gdca.getGroupAddress1()) - .build()); - - if(gdca.hasChannelNumber2()) - { - broadcast(new P25CallEvent.Builder(CallEventType.ANNOUNCEMENT) - .aliasList(getAliasList()) - .channel(gdca.getChannel2()) - .details((gdca.isEncrypted() ? "ENCRYPTED" : "") + - (gdca.isEmergency() ? " EMERGENCY" : "")) - .frequency(gdca.getDownlinkFrequency2()) - .to(gdca.getGroupAddress2()) - .build()); - } - break; - case GROUP_DATA_CHANNEL_ANNOUNCEMENT_EXPLICIT: - GroupDataChannelAnnouncementExplicit gdcae = - (GroupDataChannelAnnouncementExplicit)message; - - broadcast(new P25CallEvent.Builder(CallEventType.DATA_CALL) - .aliasList(getAliasList()) - .channel(gdcae.getTransmitChannel()) - .details((gdcae.isEncrypted() ? "ENCRYPTED" : "") + - (gdcae.isEmergency() ? " EMERGENCY" : "")) - .frequency(gdcae.getDownlinkFrequency()) - .to(gdcae.getGroupAddress()) - .build()); - break; - default: - break; - } - } - - /** - * Process Motorola vendor-specific Trunking Signaling Block messages - */ - private void processMotorolaTSBK(MotorolaTSBKMessage tsbk) - { - String channel; - String from; - String to; - - switch(((MotorolaTSBKMessage)tsbk).getMotorolaOpcode()) - { - case PATCH_GROUP_CHANNEL_GRANT: - //Cleanup patch groups - auto-expire any patch groups before we allocate a channel - mPatchGroupManager.cleanupPatchGroups(); - - PatchGroupVoiceChannelGrant pgvcg = (PatchGroupVoiceChannelGrant)tsbk; - - channel = pgvcg.getChannel(); - from = pgvcg.getSourceAddress(); - to = pgvcg.getPatchGroupAddress(); - - if(hasCallEvent(channel, from, to)) - { - updateCallEvent(channel, from, to); - } - else - { - StringBuilder details = new StringBuilder(); - details.append(pgvcg.isTDMAChannel() ? "TDMA " : ""); - details.append(pgvcg.isEmergency() ? "EMERGENCY " : ""); - details.append("PRI:").append(pgvcg.getPriority()); - details.append(pgvcg.isEncryptedChannel() ? " ENCRYPTED" : ""); - details.append(" ").append(pgvcg.getSessionMode().name()); - - P25CallEvent event = new P25CallEvent.Builder(CallEventType.PATCH_GROUP_CALL) - .aliasList(getAliasList()) - .channel(channel) - .details(details.toString()) - .frequency(pgvcg.getDownlinkFrequency()) - .from(from) - .to(to) - .build(); - - registerCallEvent(event); - broadcast(event); - } - - if(!pgvcg.isTDMAChannel()) - { - broadcast(new TrafficChannelAllocationEvent(this, mChannelCallMap.get(channel))); - } - break; - case PATCH_GROUP_CHANNEL_GRANT_UPDATE: - //Cleanup patch groups - auto-expire any patch groups before we allocate a channel - mPatchGroupManager.cleanupPatchGroups(); - - PatchGroupVoiceChannelGrantUpdate gvcgu = (PatchGroupVoiceChannelGrantUpdate)tsbk; - - channel = gvcgu.getChannel1(); - to = gvcgu.getPatchGroupAddress1(); - - if(hasCallEvent(channel, null, to)) - { - updateCallEvent(channel, null, to); - } - else - { - P25CallEvent event = new P25CallEvent.Builder(CallEventType.PATCH_GROUP_CALL) - .aliasList(getAliasList()) - .channel(channel) - .details((gvcgu.isTDMAChannel1() ? "TDMA " : "") + - (gvcgu.isEncrypted() ? "ENCRYPTED " : "")) - .frequency(gvcgu.getDownlinkFrequency1()) - .to(to) - .build(); - - registerCallEvent(event); - broadcast(event); - } - - if(!gvcgu.isTDMAChannel1()) - { - broadcast(new TrafficChannelAllocationEvent(this, mChannelCallMap.get(channel))); - } - - String channel2 = gvcgu.getChannel2(); - String to2 = gvcgu.getPatchGroupAddress2(); - - if(hasCallEvent(channel2, null, to2)) - { - updateCallEvent(channel2, null, to2); - } - else - { - P25CallEvent event = new P25CallEvent.Builder(CallEventType.PATCH_GROUP_CALL) - .aliasList(getAliasList()) - .channel(channel2) - .details((gvcgu.isTDMAChannel2() ? "TDMA " : "") + - (gvcgu.isEncrypted() ? "ENCRYPTED " : "")) - .frequency(gvcgu.getDownlinkFrequency2()) - .to(to2) - .build(); - - registerCallEvent(event); - broadcast(event); - } - - if(!gvcgu.isTDMAChannel2()) - { - broadcast(new TrafficChannelAllocationEvent(this, mChannelCallMap.get(channel2))); - } - - break; - case PATCH_GROUP_ADD: - PatchGroupAdd pga = (PatchGroupAdd)tsbk; - mPatchGroupManager.updatePatchGroup(pga.getPatchGroupAddress(), pga.getPatchedTalkgroups()); - break; - case PATCH_GROUP_DELETE: - PatchGroupDelete pgd = (PatchGroupDelete)tsbk; - mPatchGroupManager.removePatchGroup(pgd.getPatchGroupAddress()); - break; - case CCH_PLANNED_SHUTDOWN: - if(!mControlChannelShutdownLogged) - { - broadcast(new P25CallEvent.Builder(CallEventType.NOTIFICATION) - .details("PLANNED CONTROL CHANNEL SHUTDOWN") - .build()); - - mControlChannelShutdownLogged = true; - } - break; - } - } - - /** - * Process a traffic channel allocation message - */ - private void processTSBKChannelGrant(TSBKMessage message) - { - //Cleanup patch groups - auto-expire any patch groups before we allocate a channel - mPatchGroupManager.cleanupPatchGroups(); - - String channel = null; - String from = null; - String to = null; - - switch(message.getOpcode()) - { - case GROUP_DATA_CHANNEL_GRANT: - GroupDataChannelGrant gdcg = (GroupDataChannelGrant)message; - - channel = gdcg.getChannel(); - from = gdcg.getSourceAddress(); - to = gdcg.getGroupAddress(); - - if(hasCallEvent(channel, from, to)) - { - updateCallEvent(channel, from, to); - } - else - { - StringBuilder details = new StringBuilder(); - details.append(gdcg.isTDMAChannel() ? "TDMA " : ""); - details.append(gdcg.isEmergency() ? "EMERGENCY " : ""); - details.append("PRI:").append(gdcg.getPriority()).append(" "); - details.append(gdcg.isEncryptedChannel() ? "ENCRYPTED " : ""); - details.append(gdcg.getSessionMode().name()); - - P25CallEvent event = new P25CallEvent.Builder(CallEventType.DATA_CALL) - .aliasList(getAliasList()) - .channel(channel) - .details(details.toString()) - .frequency(gdcg.getDownlinkFrequency()) - .from(from) - .to(to) - .build(); - - registerCallEvent(event); - broadcast(event); - } - - if(!mIgnoreDataCalls && !gdcg.isTDMAChannel()) - { - broadcast(new TrafficChannelAllocationEvent(this, - mChannelCallMap.get(channel))); - } - break; - case GROUP_VOICE_CHANNEL_GRANT: - GroupVoiceChannelGrant gvcg = (GroupVoiceChannelGrant)message; - - channel = gvcg.getChannel(); - from = gvcg.getSourceAddress(); - to = gvcg.getGroupAddress(); - - if(hasCallEvent(channel, from, to)) - { - updateCallEvent(channel, from, to); - } - else - { - StringBuilder details = new StringBuilder(); - details.append(gvcg.isTDMAChannel() ? "TDMA " : ""); - details.append(gvcg.isEmergency() ? "EMERGENCY " : ""); - details.append("PRI:").append(gvcg.getPriority()).append(" "); - details.append(gvcg.isEncryptedChannel() ? "ENCRYPTED " : ""); - details.append(gvcg.getSessionMode().name()); - - P25CallEvent event = new P25CallEvent.Builder(CallEventType.GROUP_CALL) - .aliasList(getAliasList()) - .channel(channel) - .details(details.toString()) - .frequency(gvcg.getDownlinkFrequency()) - .from(from) - .to(to) - .build(); - - registerCallEvent(event); - broadcast(event); - } - - if(!gvcg.isTDMAChannel()) - { - broadcast(new TrafficChannelAllocationEvent(this, mChannelCallMap.get(channel))); - } - break; - case GROUP_VOICE_CHANNEL_GRANT_UPDATE: - GroupVoiceChannelGrantUpdate gvcgu = (GroupVoiceChannelGrantUpdate)message; - - channel = gvcgu.getChannel1(); - from = null; - to = gvcgu.getGroupAddress1(); - - if(hasCallEvent(channel, from, to)) - { - updateCallEvent(channel, from, to); - } - else - { - StringBuilder details = new StringBuilder(); - details.append(gvcgu.isTDMAChannel() ? "TDMA " : ""); - details.append(gvcgu.isEmergency() ? "EMERGENCY " : ""); - details.append("PRI:").append(gvcgu.getPriority()).append(" "); - details.append(gvcgu.isEncryptedChannel() ? "ENCRYPTED " : ""); - details.append(gvcgu.getSessionMode().name()); - - P25CallEvent event = new P25CallEvent.Builder(CallEventType.GROUP_CALL) - .aliasList(getAliasList()) - .channel(gvcgu.getChannel1()) - .details(details.toString()) - .frequency(gvcgu.getDownlinkFrequency1()) - .from(from) - .to(to) - .build(); - - registerCallEvent(event); - broadcast(event); - } - - if(!gvcgu.isTDMAChannel1()) - { - broadcast(new TrafficChannelAllocationEvent(this, mChannelCallMap.get(channel))); - } - - if(gvcgu.hasChannelNumber2()) - { - channel = gvcgu.getChannel2(); - to = gvcgu.getGroupAddress2(); - - if(hasCallEvent(channel, from, to)) - { - updateCallEvent(channel, from, to); - } - else - { - StringBuilder details = new StringBuilder(); - details.append(gvcgu.isTDMAChannel() ? "TDMA " : ""); - details.append(gvcgu.isEmergency() ? "EMERGENCY " : ""); - details.append("PRI:").append(gvcgu.getPriority()).append(" "); - details.append(gvcgu.isEncryptedChannel() ? "ENCRYPTED " : ""); - details.append(gvcgu.getSessionMode().name()); - - P25CallEvent event2 = new P25CallEvent.Builder(CallEventType.GROUP_CALL) - .aliasList(getAliasList()) - .channel(gvcgu.getChannel2()) - .details(details.toString()) - .frequency(gvcgu.getDownlinkFrequency2()) - .to(gvcgu.getGroupAddress2()) - .build(); - - registerCallEvent(event2); - broadcast(event2); - } - - if(!gvcgu.isTDMAChannel2()) - { - broadcast(new TrafficChannelAllocationEvent(this, mChannelCallMap.get(channel))); - } - } - break; - case GROUP_VOICE_CHANNEL_GRANT_UPDATE_EXPLICIT: - GroupVoiceChannelGrantUpdateExplicit gvcgue = - (GroupVoiceChannelGrantUpdateExplicit)message; - - channel = gvcgue.getTransmitChannelIdentifier() + "-" + - gvcgue.getTransmitChannelNumber(); - from = null; - to = gvcgue.getGroupAddress(); - - if(hasCallEvent(channel, from, to)) - { - updateCallEvent(channel, from, to); - } - else - { - StringBuilder details = new StringBuilder(); - details.append(gvcgue.isTDMAChannel() ? "TDMA " : ""); - details.append(gvcgue.isEmergency() ? "EMERGENCY " : ""); - details.append("PRI:").append(gvcgue.getPriority()).append(" "); - details.append(gvcgue.isEncryptedChannel() ? "ENCRYPTED " : ""); - details.append(gvcgue.getSessionMode().name()); - - P25CallEvent event = new P25CallEvent.Builder(CallEventType.GROUP_CALL) - .aliasList(getAliasList()) - .channel(channel) - .details(details.toString()) - .frequency(gvcgue.getDownlinkFrequency()) - .from(from) - .to(to) - .build(); - - registerCallEvent(event); - broadcast(event); - } - - if(!gvcgue.isTDMAChannel()) - { - broadcast(new TrafficChannelAllocationEvent(this, mChannelCallMap.get(channel))); - } - break; - case INDIVIDUAL_DATA_CHANNEL_GRANT: - IndividualDataChannelGrant idcg = (IndividualDataChannelGrant)message; - - channel = idcg.getChannel(); - from = idcg.getSourceAddress(); - to = idcg.getTargetAddress(); - - if(hasCallEvent(channel, from, to)) - { - updateCallEvent(channel, from, to); - } - else - { - StringBuilder details = new StringBuilder(); - details.append(idcg.isTDMAChannel() ? "TDMA " : ""); - details.append(idcg.isEmergency() ? "EMERGENCY " : ""); - details.append("PRI:").append(idcg.getPriority()).append(" "); - details.append(idcg.isEncryptedChannel() ? "ENCRYPTED " : ""); - details.append(idcg.getSessionMode().name()); - - P25CallEvent event = new P25CallEvent.Builder(CallEventType.DATA_CALL) - .aliasList(getAliasList()) - .channel(channel) - .details(details.toString()) - .frequency(idcg.getDownlinkFrequency()) - .from(from) - .to(to) - .build(); - - registerCallEvent(event); - broadcast(event); - } - - if(!mIgnoreDataCalls && !idcg.isTDMAChannel()) - { - broadcast(new TrafficChannelAllocationEvent(this, mChannelCallMap.get(channel))); - } - break; - case SNDCP_DATA_CHANNEL_GRANT: - SNDCPDataChannelGrant sdcg = (SNDCPDataChannelGrant)message; - - channel = sdcg.getTransmitChannel(); - from = null; - to = sdcg.getTargetAddress(); - - if(hasCallEvent(channel, from, to)) - { - updateCallEvent(channel, from, to); - } - else - { - StringBuilder details = new StringBuilder(); - details.append(sdcg.isTDMAChannel() ? "TDMA " : ""); - details.append(sdcg.isEmergency() ? "EMERGENCY " : ""); - details.append(sdcg.isEncryptedChannel() ? "ENCRYPTED " : ""); - details.append(sdcg.getSessionMode().name()); - - P25CallEvent event = new P25CallEvent.Builder(CallEventType.DATA_CALL) - .aliasList(getAliasList()) - .channel(channel) - .details(details.toString()) - .frequency(sdcg.getDownlinkFrequency()) - .from(from) - .to(to) - .build(); - - registerCallEvent(event); - broadcast(event); - } - - if(!mIgnoreDataCalls && !sdcg.isTDMAChannel()) - { - broadcast(new TrafficChannelAllocationEvent(this, mChannelCallMap.get(channel))); - } - break; - case TELEPHONE_INTERCONNECT_VOICE_CHANNEL_GRANT: - TelephoneInterconnectVoiceChannelGrant tivcg = (TelephoneInterconnectVoiceChannelGrant)message; - - channel = tivcg.getChannel(); - from = null; - /* Address is ambiguous and could mean either source or target, - * so we'll place the value in the to field */ - to = tivcg.getAddress(); - - if(hasCallEvent(channel, from, to)) - { - updateCallEvent(channel, from, to); - } - else - { - StringBuilder details = new StringBuilder(); - details.append(tivcg.isTDMAChannel() ? "TDMA " : ""); - details.append(tivcg.isEmergency() ? "EMERGENCY " : ""); - details.append("PRI:").append(tivcg.getPriority()).append(" "); - details.append(tivcg.isEncryptedChannel() ? "ENCRYPTED " : ""); - details.append(tivcg.getSessionMode().name()).append(" "); - details.append("CALL TIMER:").append(tivcg.getCallTimer()); - - P25CallEvent event = new P25CallEvent.Builder( - CallEventType.TELEPHONE_INTERCONNECT) - .aliasList(getAliasList()) - .channel(channel) - .details(details.toString()) - .frequency(tivcg.getDownlinkFrequency()) - .from(from) - .to(to) - .build(); - - registerCallEvent(event); - broadcast(event); - } - - if(!tivcg.isTDMAChannel()) - { - broadcast(new TrafficChannelAllocationEvent(this, mChannelCallMap.get(channel))); - } - break; - case TELEPHONE_INTERCONNECT_VOICE_CHANNEL_GRANT_UPDATE: - TelephoneInterconnectVoiceChannelGrantUpdate tivcgu = (TelephoneInterconnectVoiceChannelGrantUpdate)message; - - channel = tivcgu.getChannelIdentifier() + "-" + tivcgu.getChannelNumber(); - from = null; - - /* Address is ambiguous and could mean either source or target, - * so we'll place the value in the to field */ - to = tivcgu.getAddress(); - - if(hasCallEvent(channel, from, to)) - { - updateCallEvent(channel, from, to); - } - else - { - StringBuilder details = new StringBuilder(); - details.append(tivcgu.isTDMAChannel() ? "TDMA " : ""); - details.append(tivcgu.isEmergency() ? "EMERGENCY " : ""); - details.append("PRI:").append(tivcgu.getPriority()).append(" "); - details.append(tivcgu.isEncryptedChannel() ? "ENCRYPTED " : ""); - details.append(tivcgu.getSessionMode().name()).append(" "); - details.append("CALL TIMER:").append(tivcgu.getCallTimer()); - - P25CallEvent event = new P25CallEvent.Builder( - CallEventType.TELEPHONE_INTERCONNECT) - .aliasList(getAliasList()) - .channel(channel) - .details(details.toString()) - .frequency(tivcgu.getDownlinkFrequency()) - .from(from) - .to(to) - .build(); - - registerCallEvent(event); - broadcast(event); - } - - if(!tivcgu.isTDMAChannel()) - { - broadcast(new TrafficChannelAllocationEvent(this, mChannelCallMap.get(channel))); - } - break; - case UNIT_TO_UNIT_VOICE_CHANNEL_GRANT: - UnitToUnitVoiceChannelGrant uuvcg = (UnitToUnitVoiceChannelGrant)message; - - channel = uuvcg.getChannelIdentifier() + "-" + uuvcg.getChannelNumber(); - from = uuvcg.getSourceAddress(); - to = uuvcg.getTargetAddress(); - - if(hasCallEvent(channel, from, to)) - { - updateCallEvent(channel, from, to); - } - else - { - StringBuilder details = new StringBuilder(); - details.append(uuvcg.isTDMAChannel() ? "TDMA " : ""); - details.append(uuvcg.isEmergency() ? "EMERGENCY " : ""); - details.append("PRI:").append(uuvcg.getPriority()).append(" "); - details.append(uuvcg.isEncryptedChannel() ? " ENCRYPTED " : ""); - details.append(uuvcg.getSessionMode().name()); - - P25CallEvent event = new P25CallEvent.Builder( - CallEventType.UNIT_TO_UNIT_CALL) - .aliasList(getAliasList()) - .channel(channel) - .details(details.toString()) - .frequency(uuvcg.getDownlinkFrequency()) - .from(from) - .to(to) - .build(); - - registerCallEvent(event); - broadcast(event); - } - - if(!uuvcg.isTDMAChannel()) - { - broadcast(new TrafficChannelAllocationEvent(this, mChannelCallMap.get(channel))); - } - break; - case UNIT_TO_UNIT_VOICE_CHANNEL_GRANT_UPDATE: - UnitToUnitVoiceChannelGrantUpdate uuvcgu = (UnitToUnitVoiceChannelGrantUpdate)message; - - channel = uuvcgu.getChannelIdentifier() + "-" + uuvcgu.getChannelNumber(); - from = uuvcgu.getSourceAddress(); - to = uuvcgu.getTargetAddress(); - - if(hasCallEvent(channel, from, to)) - { - updateCallEvent(channel, from, to); - } - else - { - StringBuilder details = new StringBuilder(); - details.append(uuvcgu.isTDMAChannel() ? "TDMA " : ""); - details.append(uuvcgu.isEmergency() ? "EMERGENCY " : ""); - details.append("PRI:").append(uuvcgu.getPriority()).append(" "); - details.append(uuvcgu.isEncryptedChannel() ? "ENCRYPTED " : ""); - details.append(uuvcgu.getSessionMode().name()); - - P25CallEvent event = new P25CallEvent.Builder( - CallEventType.UNIT_TO_UNIT_CALL) - .aliasList(getAliasList()) - .channel(channel) - .details(details.toString()) - .frequency(uuvcgu.getDownlinkFrequency()) - .from(from) - .to(to) - .build(); - - registerCallEvent(event); - broadcast(event); - } - - if(!uuvcgu.isTDMAChannel()) - { - broadcast(new TrafficChannelAllocationEvent(this, mChannelCallMap.get(channel))); - } - break; - default: - break; - } - } - - private void updateSystem(String system) - { - if(mSystem == null || (system != null && !mSystem.contentEquals(system))) - { - mSystem = system; - broadcastSystemAndNACUpdate(); - } - } - - /** - * Broadcasts an update to the NAC - */ - private void updateNAC(String nac) - { - if(mNAC == null || (nac != null && !mNAC.contentEquals(nac))) - { - mNAC = nac; - broadcastSystemAndNACUpdate(); - } - } - - private void broadcastSystemAndNACUpdate() - { - String label = String.format("SYS:%s NAC:%s", mSystem, mNAC); - broadcast(new AttributeChangeRequest(Attribute.NETWORK_ID_1, label)); - } - - /** - * Process a unit paging event message - */ - private void processTSBKPage(TSBKMessage message) - { - switch(message.getOpcode()) - { - case CALL_ALERT: - CallAlert ca = (CallAlert)message; - - broadcast(new P25CallEvent.Builder(CallEventType.PAGE) - .aliasList(getAliasList()) - .from(ca.getSourceID()) - .to(ca.getTargetAddress()) - .build()); - break; - case UNIT_TO_UNIT_ANSWER_REQUEST: - UnitToUnitAnswerRequest utuar = (UnitToUnitAnswerRequest)message; - - if(mLastPageEventID == null || !mLastPageEventID - .contentEquals(utuar.getTargetAddress())) - { - broadcast(new P25CallEvent.Builder(CallEventType.PAGE) - .aliasList(getAliasList()) - .details((utuar.isEmergency() ? "EMERGENCY" : "")) - .from(utuar.getSourceAddress()) - .to(utuar.getTargetAddress()) - .build()); - - mLastPageEventID = utuar.getTargetAddress(); - } - break; - case SNDCP_DATA_PAGE_REQUEST: - SNDCPDataPageRequest sdpr = (SNDCPDataPageRequest)message; - - if(mLastPageEventID == null || !mLastPageEventID - .contentEquals(sdpr.getTargetAddress())) - { - broadcast(new P25CallEvent.Builder(CallEventType.PAGE) - .aliasList(getAliasList()) - .details("SNDCP DATA DAC: " + - sdpr.getDataAccessControl() + - " NSAPI:" + sdpr.getNSAPI()) - .to(sdpr.getTargetAddress()) - .build()); - - mLastPageEventID = sdpr.getTargetAddress(); - } - break; - case TELEPHONE_INTERCONNECT_ANSWER_REQUEST: - TelephoneInterconnectAnswerRequest tiar = - (TelephoneInterconnectAnswerRequest)message; - - if(mLastPageEventID == null || !mLastPageEventID - .contentEquals(tiar.getTargetAddress())) - { - broadcast(new P25CallEvent.Builder(CallEventType.PAGE) - .aliasList(getAliasList()) - .details(("TELEPHONE INTERCONNECT")) - .from(tiar.getTelephoneNumber()) - .to(tiar.getTargetAddress()) - .build()); - - mLastPageEventID = tiar.getTargetAddress(); - } - break; - default: - break; - } - } - - @Override - public String getActivitySummary() - { - StringBuilder sb = new StringBuilder(); - - sb.append("Activity Summary - Decoder:P25 ").append(getModulation().getLabel()).append("\n"); - sb.append(DIVIDER1); - sb.append("SITE "); - - if(mNetworkStatus != null) - { - sb.append("NAC:" + mNetworkStatus.getNAC()); - sb.append("h WACN:" + mNetworkStatus.getWACN()); - sb.append("h SYS:" + mNetworkStatus.getSystemID()); - sb.append("h [").append(mNetworkStatus.getNetworkCallsign()).append("]"); - sb.append(" LRA:" + mNetworkStatus.getLocationRegistrationArea()); - } - else if(mNetworkStatusExtended != null) - { - sb.append("NAC:" + mNetworkStatusExtended.getNAC()); - sb.append("h WACN:" + mNetworkStatusExtended.getWACN()); - sb.append("h SYS:" + mNetworkStatusExtended.getSystemID()); - sb.append("h [").append(mNetworkStatusExtended.getNetworkCallsign()).append("]"); - sb.append(" LRA:" + mNetworkStatusExtended.getLocationRegistrationArea()); - } - - String site = null; - - if(mRFSSStatusMessage != null) - { - site = mRFSSStatusMessage.getRFSubsystemID() + "-" + mRFSSStatusMessage.getSiteID(); - } - else if(mRFSSStatusMessageExtended != null) - { - site = mRFSSStatusMessageExtended.getRFSubsystemID() + "-" + mRFSSStatusMessageExtended.getSiteID(); - } - - sb.append("h RFSS-SITE:").append(site).append("h"); - - if(hasAliasList()) - { - Alias siteAlias = getAliasList().getSiteID(site); - - if(siteAlias != null) - { - sb.append(" " + siteAlias.getName()); - } - } - - sb.append("\n").append(DIVIDER2); - - if(mNetworkStatus != null) - { - sb.append("SERVICES: " + SystemService.toString(mNetworkStatus.getSystemServiceClass())).append("\n"); - sb.append(DIVIDER2); - sb.append("PCCH DOWNLINK ") - .append(mFrequencyFormatter.format((double)mNetworkStatus.getDownlinkFrequency() / 1E6d)) - .append(" [").append(mNetworkStatus.getIdentifier()).append("-").append(mNetworkStatus.getChannel()) - .append("] UPLINK ") - .append(mFrequencyFormatter.format((double)mNetworkStatus.getUplinkFrequency() / 1E6d)) - .append(" [").append(mNetworkStatus.getIdentifier()).append("-").append(mNetworkStatus.getChannel()) - .append("]\n"); - } - else if(mNetworkStatusExtended != null) - { - sb.append("\nSERVICES:").append(SystemService.toString(mNetworkStatusExtended.getSystemServiceClass())) - .append("\n"); - sb.append(DIVIDER2); - sb.append("PCCH DOWNLINK ") - .append(mFrequencyFormatter.format((double)mNetworkStatusExtended.getDownlinkFrequency() / 1E6d)) - .append(" [").append(mNetworkStatusExtended.getTransmitIdentifier()).append("-") - .append(mNetworkStatusExtended.getTransmitChannel()).append("] UPLINK ") - .append(mFrequencyFormatter.format((double)mNetworkStatusExtended.getUplinkFrequency() / 1E6d)) - .append(" [").append(mNetworkStatusExtended.getReceiveIdentifier()).append("-") - .append(mNetworkStatusExtended.getReceiveChannel()).append("]\n"); - } - - if(mSecondaryControlChannels.isEmpty()) - { - sb.append("SCCH: NONE\n"); - } - else - { - for(module.decode.p25.message.tsbk.osp.control.SecondaryControlChannelBroadcast sec : mSecondaryControlChannels) - { - sb.append("SCCH DOWNLINK ") - .append(mFrequencyFormatter.format((double)sec.getDownlinkFrequency1() / 1E6d)) - .append(" [").append(sec.getIdentifier1()).append("-").append(sec.getChannel1()).append("] UPLINK ") - .append(mFrequencyFormatter.format((double)sec.getUplinkFrequency1() / 1E6d)) - .append(" [").append(sec.getIdentifier1()).append("-").append(sec.getChannel1()).append("]"); - - if(sec.hasChannel2()) - { - sb.append(" SCCH 2 DOWNLINK ") - .append(mFrequencyFormatter.format((double)sec.getDownlinkFrequency2() / 1E6d)) - .append(" [").append(sec.getIdentifier2()).append("-").append(sec.getChannel2()) - .append("] UPLINK ").append(mFrequencyFormatter.format((double)sec.getUplinkFrequency2() / 1E6d)) - .append(" [").append(sec.getIdentifier2()).append("-").append(sec.getChannel2()).append("]"); - } - - sb.append("\n"); - } - } - - - if(mSNDCPDataChannel != null) - { - sb.append("SNDCP DOWNLINK ") - .append(mFrequencyFormatter.format((double)mSNDCPDataChannel.getDownlinkFrequency() / 1E6D)) - .append(" [").append(mSNDCPDataChannel.getTransmitChannel()).append("]").append(" UPLINK ") - .append(mFrequencyFormatter.format((double)mSNDCPDataChannel.getUplinkFrequency() / 1E6D)) - .append(" [").append(mSNDCPDataChannel.getReceiveChannel()).append("]\n"); - } - - if(mProtectionParameterBroadcast != null) - { - sb.append(DIVIDER2); - sb.append("ENCRYPTION TYPE:").append(mProtectionParameterBroadcast.getEncryptionType().name()); - sb.append(" ALGORITHM:").append(mProtectionParameterBroadcast.getAlgorithmID()); - sb.append(" KEY:").append(mProtectionParameterBroadcast.getKeyID()); - sb.append(" INBOUND IV:").append(mProtectionParameterBroadcast.getInboundInitializationVector()); - sb.append(" OUTBOUND IV:").append(mProtectionParameterBroadcast.getOutboundInitializationVector()); - sb.append("\n"); - } - - List identifiers = new ArrayList<>(mBands.keySet()); - - Collections.sort(identifiers); - - sb.append(DIVIDER2).append("FREQUENCY BANDS:\n"); - for(Integer id : identifiers) - { - IBandIdentifier band = mBands.get(id); - sb.append(band.toString()).append("\n"); -// sb.append(" ").append(id); -// sb.append(" - BASE: " + mFrequencyFormatter.format( -// (double)band.getBaseFrequency() / 1E6d)); -// sb.append(" CHANNEL SIZE: " + mFrequencyFormatter.format( -// (double)band.getChannelSpacing() / 1E6d)); -// sb.append(" UPLINK OFFSET: " + mFrequencyFormatter.format( -// (double)band.getTransmitOffset() / 1E6D)); -// sb.append("\n"); - } - - sb.append(DIVIDER2).append("NEIGHBOR SITES: "); - - if(mNeighborMap.isEmpty()) - { - sb.append("NONE\n"); - } - else - { - for(IAdjacentSite neighbor : mNeighborMap.values()) - { - sb.append("\n"); - sb.append("NAC:").append(((P25Message)neighbor).getNAC()); - sb.append("h SYSTEM:" + neighbor.getSystemID()); - sb.append("h LRA:" + neighbor.getLRA()); - - String neighborID = neighbor.getRFSS() + "-" + neighbor.getSiteID(); - sb.append("h RFSS-SITE:" + neighborID); - - sb.append("h "); - - if(hasAliasList()) - { - Alias siteAlias = getAliasList().getSiteID(neighborID); - - if(siteAlias != null) - { - sb.append(siteAlias.getName()); - } - } - - sb.append("\n PCCH: DOWNLINK ") - .append(mFrequencyFormatter.format((double)neighbor.getDownlinkFrequency() / 1E6d)) - .append(" [").append(neighbor.getDownlinkChannel()).append("] UPLINK:") - .append(mFrequencyFormatter.format((double)neighbor.getUplinkFrequency() / 1E6d)) - .append(" [").append(neighbor.getDownlinkChannel()).append("] SERVICES: ") - .append(neighbor.getSystemServiceClass()); - } - } - - return sb.toString(); - } - - @Override - public void receiveDecoderStateEvent(DecoderStateEvent event) - { - switch(event.getEvent()) - { - case RESET: - resetState(); - break; - case SOURCE_FREQUENCY: - mCurrentChannelFrequency = event.getFrequency(); - break; - case TRAFFIC_CHANNEL_ALLOCATION: - if(event.getSource() != P25DecoderState.this) - { - if(event instanceof TrafficChannelAllocationEvent) - { - TrafficChannelAllocationEvent allocationEvent = (TrafficChannelAllocationEvent)event; - - mCurrentCallEvent = (P25CallEvent)allocationEvent.getCallEvent(); - - mCurrentChannel = allocationEvent.getCallEvent().getChannel(); - broadcast(new AttributeChangeRequest(Attribute.CHANNEL_FREQUENCY_LABEL, mCurrentChannel)); - - mCurrentChannelFrequency = allocationEvent.getCallEvent().getFrequency(); - broadcast(new AttributeChangeRequest(Attribute.CHANNEL_FREQUENCY, mCurrentChannelFrequency)); - - mFromTalkgroupMonitor.reset(); - mFromTalkgroupMonitor.process(allocationEvent.getCallEvent().getFromID()); - - mToTalkgroupMonitor.reset(); - mToTalkgroupMonitor.process(allocationEvent.getCallEvent().getToID()); - } - } - break; - default: - break; - } - } - - @Override - public void start() - { - //Change the default (45-second) traffic channel timeout to 1 second - if(mChannelType == ChannelType.TRAFFIC) - { - broadcast(new ChangeChannelTimeoutEvent(this, ChannelType.TRAFFIC, 1000)); - } - } -} diff --git a/src/module/decode/p25/P25_C4FMDecoder.java b/src/module/decode/p25/P25_C4FMDecoder.java deleted file mode 100644 index 3dc771fc..00000000 --- a/src/module/decode/p25/P25_C4FMDecoder.java +++ /dev/null @@ -1,225 +0,0 @@ -/******************************************************************************* - * 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 module.decode.p25; - -import instrument.tap.Tap; -import instrument.tap.TapGroup; -import instrument.tap.stream.DibitTap; -import instrument.tap.stream.FloatTap; - -import java.util.ArrayList; -import java.util.List; -import java.util.concurrent.ScheduledExecutorService; - -import org.slf4j.Logger; -import org.slf4j.LoggerFactory; - -import alias.AliasList; -import sample.Listener; -import sample.real.IFilteredRealBufferListener; -import sample.real.RealBuffer; -import source.SourceEvent; - -public class P25_C4FMDecoder extends P25Decoder implements IFilteredRealBufferListener -{ - private final static Logger mLog = LoggerFactory.getLogger( P25_C4FMDecoder.class ); - - /* Instrumentation Taps */ - private static final String INSTRUMENT_C4FM_SYMBOL_FILTER_OUTPUT = "Tap Point: Symbol Filter Output"; - private static final String INSTRUMENT_C4FM_SLICER_OUTPUT = "Tap Point: C4FM Slicer Output"; - - private List mAvailableTaps; - private C4FMSymbolFilter mSymbolFilter; - private C4FMSlicer mC4FMSlicer; - private P25MessageFramer mMessageFramer; - - /** - * P25 Phase 1 C4FM Decoder processes real buffers of un-filtered, - * demodulated audio and produces decoded P25 Phase 1 Messages. - * - * Provides Frequency Control to steer external tuner source and incorporates - * internal gain control. - * - * Note: use the P25AudioModule to convert the decoded messages into audio. - * - * @param aliasList - optional (can be null) list of alias values for network - * infrastructure and subscriber identities that will be included in each - * decoded message - */ - public P25_C4FMDecoder( AliasList aliasList, int frequencyCorrectionMaximum ) - { - super( aliasList ); - - /* Shape gain and frequency offsets to optimize sample stream */ - mSymbolFilter = new C4FMSymbolFilter( frequencyCorrectionMaximum ); - - /* Convert samples to symbols */ - mC4FMSlicer = new C4FMSlicer(); - mSymbolFilter.setListener( mC4FMSlicer ); - - /* Sync pattern detection and message construction */ - mMessageFramer = new P25MessageFramer( aliasList ); - mC4FMSlicer.addListener( mMessageFramer ); - - /* Process and broadcast messages */ - mMessageFramer.setListener( getMessageProcessor() ); - } - - @Override - public void dispose() - { - super.dispose(); - - mSymbolFilter.dispose(); - mSymbolFilter = null; - - mMessageFramer.dispose(); - mMessageFramer = null; - - mC4FMSlicer.dispose(); - mC4FMSlicer = null; - } - - public Modulation getModulation() - { - return Modulation.C4FM; - } - - @Override - public Listener getFilteredRealBufferListener() - { - return mSymbolFilter; - } - - /** - * Instrumentation taps for monitoring internal processing - */ - @Override - public List getTapGroups() - { - if( mAvailableTaps == null ) - { - mAvailableTaps = new ArrayList<>(); - - TapGroup group = new TapGroup( "P25 C4FM Decoder" ); - - group.add( new FloatTap( INSTRUMENT_C4FM_SYMBOL_FILTER_OUTPUT, 0, 0.1f ) ); - group.add( new DibitTap( INSTRUMENT_C4FM_SLICER_OUTPUT, 0, 0.1f ) ); - - mAvailableTaps.add( group ); - - if( mSymbolFilter != null ) - { - mAvailableTaps.addAll( mSymbolFilter.getTapGroups() ); - } - } - - return mAvailableTaps; - } - - /** - * Adds an instrumentation tap to monitor internal processing. - */ - @Override - public void registerTap( Tap tap ) - { - if( mSymbolFilter != null ) - { - mSymbolFilter.registerTap( tap ); - } - - switch( tap.getName() ) - { - case INSTRUMENT_C4FM_SYMBOL_FILTER_OUTPUT: - FloatTap symbolTap = (FloatTap)tap; - mSymbolFilter.setListener( symbolTap ); - symbolTap.setListener( mC4FMSlicer ); - break; - case INSTRUMENT_C4FM_SLICER_OUTPUT: - DibitTap slicerTap = (DibitTap)tap; - - if( mC4FMSlicer != null ) - { - mC4FMSlicer.addListener( slicerTap ); - } - break; - default: - break; - } - } - - /** - * Removes the instrumentation tap. - */ - @Override - public void unregisterTap( Tap tap ) - { - if( mSymbolFilter != null ) - { - mSymbolFilter.unregisterTap( tap ); - } - - switch( tap.getName() ) - { - case INSTRUMENT_C4FM_SYMBOL_FILTER_OUTPUT: - mSymbolFilter.setListener( mC4FMSlicer ); - break; - case INSTRUMENT_C4FM_SLICER_OUTPUT: - DibitTap slicerTap = (DibitTap)tap; - - if( mC4FMSlicer != null ) - { - mC4FMSlicer.removeListener( slicerTap ); - } - break; - default: - throw new IllegalArgumentException( "Unrecognized tap: " + - tap.getName() ); - } - } - - @Override - public void setSourceEventListener(Listener listener ) - { - if( mSymbolFilter != null ) - { - mSymbolFilter.setFrequencyChangeListener( listener ); - } - } - - @Override - public void removeSourceEventListener() - { - if( mSymbolFilter != null ) - { - mSymbolFilter.setFrequencyChangeListener( null ); - } - } - - @Override - public Listener getSourceEventListener() - { - if( mSymbolFilter != null ) - { - return mSymbolFilter.getSourceEventListener(); - } - - return null; - } -} diff --git a/src/module/decode/p25/P25_LSMDecoder.java b/src/module/decode/p25/P25_LSMDecoder.java deleted file mode 100644 index d461cfd0..00000000 --- a/src/module/decode/p25/P25_LSMDecoder.java +++ /dev/null @@ -1,240 +0,0 @@ -/******************************************************************************* - * SDR Trunk - * Copyright (C) 2014,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 - ******************************************************************************/ -package module.decode.p25; - -import instrument.tap.Tap; -import instrument.tap.TapGroup; -import instrument.tap.stream.ComplexSampleTap; -import instrument.tap.stream.ComplexTap; -import instrument.tap.stream.DibitTap; -import instrument.tap.stream.QPSKTap; - -import java.util.ArrayList; -import java.util.List; -import java.util.concurrent.ScheduledExecutorService; - -import org.slf4j.Logger; -import org.slf4j.LoggerFactory; - -import alias.AliasList; -import sample.Listener; -import sample.complex.ComplexBuffer; -import sample.complex.ComplexBufferToStreamConverter; -import sample.complex.IComplexBufferListener; -import dsp.filter.FilterFactory; -import dsp.filter.Window.WindowType; -import dsp.filter.fir.complex.ComplexFIRFilter_CB_CB; -import dsp.gain.ComplexFeedForwardGainControl; -import dsp.psk.LSMDemodulator; -import dsp.psk.QPSKPolarSlicer; -import source.SourceEvent; - -public class P25_LSMDecoder extends P25Decoder implements IComplexBufferListener -{ - private final static Logger mLog = LoggerFactory.getLogger( P25_LSMDecoder.class ); - - /* Instrumentation Taps */ - private static final String INSTRUMENT_BASEBAND_FILTER_OUTPUT = "Tap Point: Baseband Filter Output"; - private static final String INSTRUMENT_AGC_OUTPUT = "Tap Point: AGC Output"; - private static final String INSTRUMENT_LSM_DEMODULATOR_OUTPUT = "Tap Point: LSM Demodulator Output"; - private static final String INSTRUMENT_QPSK_SLICER_OUTPUT = "Tap Point: QPSK Slicer Output"; - - private List mAvailableTaps; - - private ComplexFIRFilter_CB_CB mBasebandFilter; - private ComplexBufferToStreamConverter mStreamConverter = new ComplexBufferToStreamConverter(); - private ComplexFeedForwardGainControl mAGC = - new ComplexFeedForwardGainControl( 32 ); - private LSMDemodulator mLSMDemodulator = new LSMDemodulator(); - private QPSKPolarSlicer mQPSKSlicer = new QPSKPolarSlicer(); - private P25MessageFramer mMessageFramer; - - public P25_LSMDecoder( AliasList aliasList ) - { - super( aliasList ); - - mBasebandFilter = new ComplexFIRFilter_CB_CB( FilterFactory.getLowPass( - 48000, 7250, 8000, 60, WindowType.HANN, true ), 1.0f ); - - mBasebandFilter.setListener( mStreamConverter ); - - mStreamConverter.setListener( mAGC ); - - mAGC.setListener( mLSMDemodulator ); - - mLSMDemodulator.setSymbolListener( mQPSKSlicer ); - - mMessageFramer = new P25MessageFramer( aliasList, mLSMDemodulator ); - mQPSKSlicer.addListener( mMessageFramer ); - - mMessageFramer.setListener( getMessageProcessor() ); - } - - public void dispose() - { - super.dispose(); - - mBasebandFilter.dispose(); - mBasebandFilter = null; - - mStreamConverter.dispose(); - mStreamConverter = null; - - mAGC.dispose(); - mAGC = null; - - mLSMDemodulator.dispose(); - mLSMDemodulator = null; - - mQPSKSlicer.dispose(); - mQPSKSlicer = null; - - mMessageFramer.dispose(); - mMessageFramer = null; - } - - @Override - public void setSourceEventListener(Listener listener) - { - } - - @Override - public void removeSourceEventListener() - { - } - - @Override - public Listener getSourceEventListener() - { - return new Listener() - { - @Override - public void receive(SourceEvent sourceEvent) - { - //Ignored - } - }; - } - - @Override - public Listener getComplexBufferListener() - { - return mBasebandFilter; - } - - public Modulation getModulation() - { - return Modulation.CQPSK; - } - - /** - * Provides a list of instrumentation taps for monitoring internal processing - */ - @Override - public List getTapGroups() - { - if( mAvailableTaps == null ) - { - mAvailableTaps = new ArrayList<>(); - - TapGroup group = new TapGroup( "P25 LSM Decoder" ); - - group.add( new ComplexTap( INSTRUMENT_BASEBAND_FILTER_OUTPUT, 0, 1.0f ) ); - group.add( new ComplexTap( INSTRUMENT_AGC_OUTPUT, 0, 1.0f ) ); - group.add( new QPSKTap( INSTRUMENT_LSM_DEMODULATOR_OUTPUT, 0, 1.0f ) ); - group.add( new DibitTap( INSTRUMENT_QPSK_SLICER_OUTPUT, 0, 0.1f ) ); - - mAvailableTaps.add( group ); - - if( mLSMDemodulator != null ) - { - mAvailableTaps.addAll( mLSMDemodulator.getTapGroups() ); - } - } - - return mAvailableTaps; - } - - /** - * Adds the instrumentation tap - */ - @Override - public void registerTap( Tap tap ) - { - if( mLSMDemodulator != null ) - { - mLSMDemodulator.registerTap( tap ); - } - - switch( tap.getName() ) - { - case INSTRUMENT_BASEBAND_FILTER_OUTPUT: - ComplexSampleTap baseband = (ComplexSampleTap)tap; - mStreamConverter.setListener( baseband ); - baseband.setListener( mAGC ); - break; - case INSTRUMENT_AGC_OUTPUT: - ComplexSampleTap agcSymbol = (ComplexSampleTap)tap; - mAGC.setListener( agcSymbol ); - agcSymbol.setListener( mLSMDemodulator ); - break; - case INSTRUMENT_LSM_DEMODULATOR_OUTPUT: - QPSKTap qpsk = (QPSKTap)tap; - mLSMDemodulator.setSymbolListener( qpsk ); - qpsk.setListener( mQPSKSlicer ); - break; - case INSTRUMENT_QPSK_SLICER_OUTPUT: - mQPSKSlicer.addListener( (DibitTap)tap ); - break; - default: - throw new IllegalArgumentException( "Unrecognized tap: " + - tap.getName() ); - } - } - - /** - * Removes the instrumentation tap - */ - @Override - public void unregisterTap( Tap tap ) - { - if( mLSMDemodulator != null ) - { - mLSMDemodulator.unregisterTap( tap ); - } - - switch( tap.getName() ) - { - case INSTRUMENT_BASEBAND_FILTER_OUTPUT: - mStreamConverter.setListener( mAGC ); - break; - case INSTRUMENT_AGC_OUTPUT: - mAGC.setListener( mLSMDemodulator ); - break; - case INSTRUMENT_LSM_DEMODULATOR_OUTPUT: - mLSMDemodulator.setSymbolListener( mQPSKSlicer ); - break; - case INSTRUMENT_QPSK_SLICER_OUTPUT: - mQPSKSlicer.removeListener( (DibitTap)tap ); - break; - default: - throw new IllegalArgumentException( "Unrecognized tap: " + - tap.getName() ); - } - } -} diff --git a/src/module/decode/p25/audio/P25AudioModule.java b/src/module/decode/p25/audio/P25AudioModule.java deleted file mode 100644 index c49dd2f4..00000000 --- a/src/module/decode/p25/audio/P25AudioModule.java +++ /dev/null @@ -1,269 +0,0 @@ -package module.decode.p25.audio; - -import audio.AudioFormats; -import audio.AudioPacket; -import audio.IAudioPacketProvider; -import audio.squelch.ISquelchStateListener; -import audio.squelch.SquelchState; -import channel.metadata.Metadata; -import dsp.gain.NonClippingGain; -import jmbe.iface.AudioConversionLibrary; -import jmbe.iface.AudioConverter; -import message.IMessageListener; -import message.Message; -import module.Module; -import module.decode.p25.message.hdu.HDUMessage; -import module.decode.p25.message.ldu.LDU1Message; -import module.decode.p25.message.ldu.LDU2Message; -import module.decode.p25.message.ldu.LDUMessage; -import org.slf4j.Logger; -import org.slf4j.LoggerFactory; -import sample.Listener; - -import java.util.concurrent.ScheduledExecutorService; - -public class P25AudioModule extends Module implements Listener, IAudioPacketProvider, IMessageListener, - ISquelchStateListener -{ - private final static Logger mLog = LoggerFactory.getLogger(P25AudioModule.class); - - private static final String IMBE_CODEC = "IMBE"; - private static boolean mLibraryLoadStatusLogged = false; - - private boolean mCanConvertAudio = false; - private boolean mEncryptedCall = false; - private boolean mEncryptedCallStateEstablished = false; - - private AudioConverter mAudioConverter; - private Listener mAudioPacketListener; - private SquelchStateListener mSquelchStateListener = new SquelchStateListener(); - private NonClippingGain mGain = new NonClippingGain(5.0f, 0.95f); - private Metadata mMetadata; - private LDU1Message mCachedLDU1Message = null; - - public P25AudioModule(Metadata metadata) - { - mMetadata = metadata; - loadConverter(); - } - - @Override - public Listener getMessageListener() - { - return this; - } - - @Override - public Listener getSquelchStateListener() - { - return mSquelchStateListener; - } - - @Override - public void dispose() - { - mAudioConverter = null; - } - - @Override - public void reset() - { - } - - @Override - public void start() - { - - } - - @Override - public void stop() - { - - } - - /** - * Processes call header (HDU) and voice frame (LDU1/LDU2) messages to decode audio and to determine the - * encrypted audio status of a call event. Only the HDU and LDU2 messages convey encrypted call status. If an - * LDU1 message is received without a preceding HDU message, then the LDU1 message is cached until the first - * LDU2 message is received and the encryption state can be determined. Both the LDU1 and the LDU2 message are - * then processed for audio if the call is unencrypted. - */ - public void receive(Message message) - { - if(mCanConvertAudio && mAudioPacketListener != null) - { - if(mEncryptedCallStateEstablished) - { - if(message instanceof LDUMessage) - { - processAudio((LDUMessage)message); - } - } - else - { - if(message instanceof HDUMessage) - { - mEncryptedCallStateEstablished = true; - mEncryptedCall = ((HDUMessage)message).isEncryptedAudio(); - } - else if(message instanceof LDU1Message) - { - //When we receive an LDU1 message with first receiving the HDU message, cache the LDU1 Message - //until we can determine the encrypted call state from the next LDU2 message - mCachedLDU1Message = (LDU1Message)message; - } - else if(message instanceof LDU2Message) - { - mEncryptedCallStateEstablished = true; - LDU2Message ldu2 = (LDU2Message)message; - mEncryptedCall = ldu2.isEncryptedAudio(); - - if(mCachedLDU1Message != null) - { - processAudio(mCachedLDU1Message); - mCachedLDU1Message = null; - } - - processAudio(ldu2); - } - } - } - } - - /** - * Processes an audio packet by decoding the IMBE audio frames and rebroadcasting them as PCM audio packets. - */ - private void processAudio(LDUMessage ldu) - { - if(!mEncryptedCall) - { - for(byte[] frame : ldu.getIMBEFrames()) - { - float[] audio = mAudioConverter.decode(frame); - - audio = mGain.apply(audio); - - mAudioPacketListener.receive(new AudioPacket(audio, mMetadata.copyOf())); - } - } - else - { - //Encrypted audio processing not implemented - } - } - - /** - * Loads audio frame processing chain. Constructs an imbe targetdataline - * to receive the raw imbe frames. Adds an IMBE to 8k PCM format conversion - * stream wrapper. Finally, adds an upsampling (8k to 48k) stream wrapper. - */ - private void loadConverter() - { - AudioConversionLibrary library = null; - - try - { - @SuppressWarnings("rawtypes") - Class temp = Class.forName("jmbe.JMBEAudioLibrary"); - - library = (AudioConversionLibrary)temp.newInstance(); - - if((library.getMajorVersion() == 0 && library.getMinorVersion() >= 3 && - library.getBuildVersion() >= 3) || library.getMajorVersion() >= 1) - { - mAudioConverter = library.getAudioConverter(IMBE_CODEC, AudioFormats.PCM_SIGNED_8KHZ_16BITS_MONO); - - if(mAudioConverter != null) - { - mCanConvertAudio = true; - - if(!mLibraryLoadStatusLogged) - { - StringBuilder sb = new StringBuilder(); - sb.append("JMBE audio conversion library ["); - sb.append(library.getVersion()); - sb.append("] successfully loaded - P25 audio will be available"); - - mLog.info(sb.toString()); - - mLibraryLoadStatusLogged = true; - } - } - else - { - if(!mLibraryLoadStatusLogged) - { - mLog.info("JMBE audio conversion library NOT FOUND"); - mLibraryLoadStatusLogged = true; - } - } - } - else - { - mLog.warn("JMBE library version 0.3.3 or higher is required - found: " + library.getVersion()); - } - } - catch(ClassNotFoundException e1) - { - if(!mLibraryLoadStatusLogged) - { - mLog.error("Couldn't find/load JMBE audio conversion library"); - mLibraryLoadStatusLogged = true; - } - } - catch(InstantiationException e1) - { - if(!mLibraryLoadStatusLogged) - { - mLog.error("Couldn't instantiate JMBE audio conversion library class"); - mLibraryLoadStatusLogged = true; - } - } - catch(IllegalAccessException e1) - { - if(!mLibraryLoadStatusLogged) - { - mLog.error("Couldn't load JMBE audio conversion library due to " - + "security restrictions"); - mLibraryLoadStatusLogged = true; - } - } - } - - @Override - public void setAudioPacketListener(Listener listener) - { - mAudioPacketListener = listener; - } - - @Override - public void removeAudioPacketListener() - { - mAudioPacketListener = null; - } - - /** - * Wrapper for squelch state to process end of call actions. At call end the encrypted call state established - * flag is reset so that the encrypted audio state for the next call can be properly detected and we send an - * END audio packet so that downstream processors like the audio recorder can properly close out a call sequence. - */ - public class SquelchStateListener implements Listener - { - @Override - public void receive(SquelchState state) - { - if(state == SquelchState.SQUELCH) - { - if(mAudioPacketListener != null) - { - mAudioPacketListener.receive(new AudioPacket(AudioPacket.Type.END, mMetadata.copyOf())); - } - - mEncryptedCallStateEstablished = false; - mEncryptedCall = false; - mCachedLDU1Message = null; - } - } - } -} diff --git a/src/module/decode/passport/PassportDecoder.java b/src/module/decode/passport/PassportDecoder.java deleted file mode 100644 index 1c298888..00000000 --- a/src/module/decode/passport/PassportDecoder.java +++ /dev/null @@ -1,112 +0,0 @@ -/******************************************************************************* - * SDR Trunk - * Copyright (C) 2014 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 module.decode.passport; - -import instrument.Instrumentable; -import instrument.tap.Tap; -import instrument.tap.TapGroup; - -import java.util.ArrayList; -import java.util.List; -import java.util.concurrent.ScheduledExecutorService; - -import module.decode.Decoder; -import module.decode.DecoderType; -import module.decode.config.DecodeConfiguration; -import sample.Listener; -import sample.real.IUnFilteredRealBufferListener; -import sample.real.RealBuffer; -import alias.AliasList; -import bits.MessageFramer; -import bits.SyncPattern; -import dsp.fsk.LTRFSKDecoder; - -public class PassportDecoder extends Decoder - implements IUnFilteredRealBufferListener, Instrumentable -{ - public static final int PASSPORT_MESSAGE_LENGTH = 68; - public static final int PASSPORT_SYNC_LENGTH = 9; - - private LTRFSKDecoder mPassportFSKDecoder; - private MessageFramer mPassportMessageFramer; - private PassportMessageProcessor mPassportMessageProcessor; - - private List mAvailableTaps; - - /** - * Passport Decoder. Decodes unfiltered (e.g. demodulated but with no DC or - * audio filtering) samples and produces Passport messages. - */ - public PassportDecoder( DecodeConfiguration config, AliasList aliasList ) - { - mPassportFSKDecoder = new LTRFSKDecoder(); - - mPassportMessageFramer = - new MessageFramer( SyncPattern.PASSPORT.getPattern(), - PASSPORT_MESSAGE_LENGTH ); - - mPassportFSKDecoder.addListener( mPassportMessageFramer ); - - mPassportMessageProcessor = new PassportMessageProcessor( aliasList ); - mPassportMessageFramer.addMessageListener( mPassportMessageProcessor ); - mPassportMessageProcessor.setMessageListener( this ); - } - - @Override - public Listener getUnFilteredRealBufferListener() - { - return mPassportFSKDecoder; - } - - @Override - public DecoderType getDecoderType() - { - return DecoderType.PASSPORT; - } - - @Override - public List getTapGroups() - { - if( mAvailableTaps == null ) - { - mAvailableTaps = new ArrayList<>(); - - mAvailableTaps.addAll( mPassportFSKDecoder.getTapGroups() ); - } - - return mAvailableTaps; - } - - @Override - public void registerTap( Tap tap ) - { - mPassportFSKDecoder.registerTap( tap ); - } - - @Override - public void unregisterTap( Tap tap ) - { - mPassportFSKDecoder.unregisterTap( tap ); - } - - @Override - public void reset() - { - mPassportMessageFramer.reset(); - } -} diff --git a/src/module/decode/passport/PassportDecoderState.java b/src/module/decode/passport/PassportDecoderState.java deleted file mode 100644 index c98ef2c6..00000000 --- a/src/module/decode/passport/PassportDecoderState.java +++ /dev/null @@ -1,574 +0,0 @@ -/******************************************************************************* - * 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 module.decode.passport; - -import alias.Alias; -import alias.AliasList; -import alias.id.AliasIDType; -import channel.metadata.AliasedStringAttributeMonitor; -import channel.metadata.Attribute; -import channel.metadata.AttributeChangeRequest; -import channel.state.DecoderState; -import channel.state.DecoderStateEvent; -import channel.state.DecoderStateEvent.Event; -import channel.state.State; -import message.Message; -import module.decode.DecoderType; -import module.decode.event.CallEvent; -import module.decode.event.CallEvent.CallEventType; -import org.slf4j.Logger; -import org.slf4j.LoggerFactory; - -import java.util.ArrayList; -import java.util.Collections; -import java.util.HashMap; -import java.util.HashSet; -import java.util.Iterator; -import java.util.TreeSet; -import java.util.concurrent.ScheduledExecutorService; - -public class PassportDecoderState extends DecoderState -{ - private final static Logger mLog = - LoggerFactory.getLogger(PassportDecoderState.class); - - private HashSet mTalkgroupsFirstHeard = new HashSet(); - private TreeSet mTalkgroups = new TreeSet(); - private TreeSet mMobileIDs = new TreeSet(); - private HashMap mSiteLCNs = new HashMap(); - private HashMap mNeighborLCNs = new HashMap(); - - private AliasedStringAttributeMonitor mFromMobileIDAttribute; - private AliasedStringAttributeMonitor mToTalkgroupAttribute; - private int mChannelNumber; - private int mSiteNumber; - private PassportBand mSiteBand; - private HashMap mActiveCalls = new HashMap(); - private long mFrequency; - - public PassportDecoderState(AliasList aliasList) - { - super(aliasList); - - mFromMobileIDAttribute = new AliasedStringAttributeMonitor(Attribute.PRIMARY_ADDRESS_FROM, - getAttributeChangeRequestListener(), getAliasList(), AliasIDType.MIN); - mToTalkgroupAttribute = new AliasedStringAttributeMonitor(Attribute.PRIMARY_ADDRESS_TO, - getAttributeChangeRequestListener(), getAliasList(), AliasIDType.TALKGROUP); - } - - @Override - public DecoderType getDecoderType() - { - return DecoderType.PASSPORT; - } - - private void logTalkgroup(String talkgroup) - { - if(mTalkgroupsFirstHeard.contains(talkgroup)) - { - mTalkgroups.add(talkgroup); - } - else - { - mTalkgroupsFirstHeard.add(talkgroup); - } - } - - private PassportCallEvent getCurrentCallEvent() - { - return (PassportCallEvent) mCurrentCallEvent; - } - - /** - * Indicates if the talkgroup is different than the talkgroup specified in - * the current call event - */ - private boolean isDifferentTalkgroup(String talkgroup) - { - return talkgroup != null && - mCurrentCallEvent != null && - mCurrentCallEvent.getToID() != null && - !mCurrentCallEvent.getToID().contentEquals(talkgroup); - } - - @Override - public void receive(Message message) - { - if(message instanceof PassportMessage) - { - PassportMessage passport = (PassportMessage) message; - - if(passport.isValid()) - { - switch(passport.getMessageType()) - { - case CA_STRT: - mSiteLCNs.put(passport.getLCN(), - passport.getLCNFrequencyFormatted()); - - String talkgroup = - String.valueOf(passport.getTalkgroupID()); - - logTalkgroup(talkgroup); - - if(mChannelNumber == 0) - { - setChannelNumber(passport.getLCN()); - } - - /* Call on this channel */ - if(passport.getLCN() == mChannelNumber) - { - mToTalkgroupAttribute.process(talkgroup); - - PassportCallEvent current = getCurrentCallEvent(); - - /* If we're already in a call event, add the message - * to the current call event ... if false, then we - * have a different call ... cleanup the old one. */ - if(current != null && isDifferentTalkgroup(talkgroup)) - { - mCurrentCallEvent.end(); - mCurrentCallEvent = null; - } - - if(mCurrentCallEvent == null) - { - mCurrentCallEvent = new PassportCallEvent - .Builder(CallEventType.CALL) - .aliasList(getAliasList()) - .channel(String.valueOf(mChannelNumber)) - .frequency(passport.getLCNFrequency()) - .to(String.valueOf(passport.getTalkgroupID())) - .build(); - - broadcast(mCurrentCallEvent); - - broadcast(new DecoderStateEvent(this, - Event.START, State.CALL)); - } - else - { - broadcast(new DecoderStateEvent(this, - Event.CONTINUATION, State.CALL)); - } - } - else - { - //Call Detection - int lcn = passport.getLCN(); - String tg = String.valueOf(passport.getTalkgroupID()); - - if(!mActiveCalls.containsKey(lcn) || - !mActiveCalls.get(lcn).contentEquals(tg)) - { - mActiveCalls.put(lcn, tg); - - broadcast(new PassportCallEvent - .Builder(CallEventType.CALL_DETECT) - .aliasList(getAliasList()) - .channel(String.valueOf(lcn)) - .details("Site: " + passport.getSite()) - .frequency(passport.getLCNFrequency()) - .to(tg) - .build()); - } - } - break; - case CA_ENDD: - String endTalkgroup = - String.valueOf(passport.getTalkgroupID()); - - logTalkgroup(endTalkgroup); - mToTalkgroupAttribute.process(endTalkgroup); - - broadcast(new DecoderStateEvent(this, - Event.END, State.CALL)); - - if(mCurrentCallEvent != null) - { - mCurrentCallEvent.end(); - broadcast(mCurrentCallEvent); - mCurrentCallEvent = null; - - broadcast(new DecoderStateEvent(this, Event.RESET, State.FADE)); - } - break; - case DA_STRT: - mSiteLCNs.put(passport.getLCN(), - passport.getLCNFrequencyFormatted()); - - String dataTalkgroup = - String.valueOf(passport.getTalkgroupID()); - - logTalkgroup(dataTalkgroup); - - if(mChannelNumber == 0) - { - setChannelNumber(passport.getLCN()); - } - - /* Data call on this channel */ - if(passport.getLCN() == mChannelNumber) - { - mToTalkgroupAttribute.process(dataTalkgroup); - - PassportCallEvent current = getCurrentCallEvent(); - - /* If we're already in a call event, add the message - * to the current call event ... if false, then we - * have a different call ... cleanup the old one. */ - if(current != null && isDifferentTalkgroup(dataTalkgroup)) - { - mCurrentCallEvent.end(); - mCurrentCallEvent = null; - } - - if(mCurrentCallEvent == null) - { - mCurrentCallEvent = new PassportCallEvent - .Builder(CallEventType.DATA_CALL) - .aliasList(getAliasList()) - .channel(String.valueOf(mChannelNumber)) - .frequency(passport.getLCNFrequency()) - .to(String.valueOf(passport.getTalkgroupID())) - .build(); - - broadcast(mCurrentCallEvent); - - broadcast(new DecoderStateEvent(this, - Event.START, State.DATA)); - } - else - { - broadcast(new DecoderStateEvent(this, - Event.CONTINUATION, State.DATA)); - } - } - else - { - //Call Detection - int lcn = passport.getLCN(); - String tg = String.valueOf(passport.getTalkgroupID()); - - if(!mActiveCalls.containsKey(lcn) || - !mActiveCalls.get(lcn).contentEquals(tg)) - { - mActiveCalls.put(lcn, tg); - - broadcast(new PassportCallEvent - .Builder(CallEventType.DATA_CALL) - .aliasList(getAliasList()) - .channel(String.valueOf(lcn)) - .details("Site: " + passport.getSite()) - .frequency(passport.getLCNFrequency()) - .to(tg) - .build()); - } - } - break; - case DA_ENDD: - String dataEndTalkgroup = - String.valueOf(passport.getTalkgroupID()); - - logTalkgroup(dataEndTalkgroup); - mToTalkgroupAttribute.process(dataEndTalkgroup); - - broadcast(new DecoderStateEvent(this, - Event.END, State.DATA)); - - if(mCurrentCallEvent != null) - { - mCurrentCallEvent.end(); - broadcast(mCurrentCallEvent); - mCurrentCallEvent = null; - - broadcast(new DecoderStateEvent(this, Event.RESET, State.FADE)); - } - break; - case ID_RDIO: - String min = passport.getMobileID(); - - if(min != null) - { - mMobileIDs.add(min); - } - - mFromMobileIDAttribute.process(min); - - final CallEvent current = getCurrentCallEvent(); - - if(current != null) - { - current.setFromID(min); - broadcast(current); - - } - break; - case RA_REGI: - if(mCurrentCallEvent == null || - mCurrentCallEvent.getCallEventType() != CallEventType.REGISTER) - { - mCurrentCallEvent = new PassportCallEvent - .Builder(CallEventType.REGISTER) - .aliasList(getAliasList()) - .channel(String.valueOf(passport.getLCN())) - .frequency(passport.getLCNFrequency()) - .to(passport.getToID()) - .build(); - - broadcast(mCurrentCallEvent); - - broadcast(new DecoderStateEvent(this, - Event.START, State.DATA)); - } - else - { - broadcast(new DecoderStateEvent(this, - Event.CONTINUATION, State.DATA)); - } - break; - case SY_IDLE: - if(passport.getFree() != 0) - { - mNeighborLCNs.put(passport.getFree(), - passport.getFreeFrequencyFormatted()); - } - setSiteNumber(passport.getSite()); - - PassportBand band = passport.getSiteBand(); - setSiteBand(band); - setChannelNumber(getSiteBand().getChannel(mFrequency)); - break; - default: - break; - } - } - } - } - - public static String formatTalkgroup(String talkgroup) - { - StringBuilder sb = new StringBuilder(); - - if(talkgroup.length() == 6) - { - sb.append(talkgroup.substring(0, 1)); - sb.append("-"); - sb.append(talkgroup.substring(1, 3)); - sb.append("-"); - sb.append(talkgroup.substring(3, 6)); - - return sb.toString(); - } - else - { - return talkgroup; - } - } - - @Override - public String getActivitySummary() - { - StringBuilder sb = new StringBuilder(); - - sb.append("Activity Summary\n"); - sb.append("Decoder:\tPassport\n\n"); - sb.append("Site Channels\n"); - - if(mSiteLCNs.isEmpty()) - { - sb.append(" None\n"); - } - else - { - ArrayList channels = new ArrayList<>(mSiteLCNs.keySet()); - Collections.sort(channels); - - for(Integer channel : channels) - { - sb.append(" " + channel); - sb.append("\t" + mSiteLCNs.get(channel)); - sb.append("\n"); - } - } - - sb.append("\nNeighbor Channels\n"); - - if(mNeighborLCNs.isEmpty()) - { - sb.append(" None\n"); - } - else - { - ArrayList channels = new ArrayList<>(mNeighborLCNs.keySet()); - Collections.sort(channels); - - for(Integer channel : channels) - { - sb.append(" " + channel); - sb.append("\t" + mNeighborLCNs.get(channel)); - sb.append("\n"); - } - } - - sb.append("\nTalkgroups\n"); - - if(mTalkgroups.isEmpty()) - { - sb.append(" None\n"); - } - else - { - Iterator it = mTalkgroups.iterator(); - - while(it.hasNext()) - { - String tgid = it.next(); - - sb.append(" "); - sb.append(tgid); - sb.append(" "); - - if(hasAliasList()) - { - Alias alias = getAliasList().getTalkgroupAlias(tgid); - - if(alias != null) - { - sb.append(alias.getName()); - } - } - - sb.append("\n"); - } - } - - sb.append("\nMobile ID Numbers\n"); - - if(mMobileIDs.isEmpty()) - { - sb.append(" None\n"); - } - else - { - Iterator it = mMobileIDs.iterator(); - - while(it.hasNext()) - { - String min = it.next(); - - sb.append(" "); - sb.append(min); - sb.append(" "); - - if(hasAliasList()) - { - Alias alias = getAliasList().getMobileIDNumberAlias(min); - - if(alias != null) - { - sb.append(alias.getName()); - } - } - - sb.append("\n"); - } - } - - return sb.toString(); - } - - public void reset() - { - mTalkgroupsFirstHeard.clear(); - mTalkgroups.clear(); - mMobileIDs.clear(); - mSiteLCNs.clear(); - mNeighborLCNs.clear(); - - resetState(); - } - - private void resetState() - { - mToTalkgroupAttribute.reset(); - mFromMobileIDAttribute.reset(); - } - - public int getChannelNumber() - { - return mChannelNumber; - } - - private void setChannelNumber(int channel) - { - if(mChannelNumber != channel) - { - mChannelNumber = channel; - broadcast(new AttributeChangeRequest(Attribute.CHANNEL_FREQUENCY_LABEL, "CHAN:" + mChannelNumber)); - } - } - - public int getSiteNumber() - { - return mSiteNumber; - } - - private void setSiteNumber(int site) - { - if(mSiteNumber != site) - { - mSiteNumber = site; - Alias alias = hasAliasList() ? getAliasList().getSiteID(String.valueOf(getSiteNumber())) : null; - broadcast(new AttributeChangeRequest(Attribute.NETWORK_ID_1, "SITE:" + mSiteNumber, alias)); - } - } - - private PassportBand getSiteBand() - { - return mSiteBand; - } - - private void setSiteBand(PassportBand band) - { - if(mSiteBand == null) - { - mSiteBand = band; - broadcast(new AttributeChangeRequest(Attribute.NETWORK_ID_2, "BAND:" + band.getDescription())); - } - } - - @Override - public void receiveDecoderStateEvent(DecoderStateEvent event) - { - switch(event.getEvent()) - { - case RESET: - resetState(); - break; - case SOURCE_FREQUENCY: - mFrequency = event.getFrequency(); - broadcast(new AttributeChangeRequest(Attribute.CHANNEL_FREQUENCY, mFrequency)); - break; - default: - break; - } - } -} diff --git a/src/module/decode/tait/Tait1200Decoder.java b/src/module/decode/tait/Tait1200Decoder.java deleted file mode 100644 index 917a2192..00000000 --- a/src/module/decode/tait/Tait1200Decoder.java +++ /dev/null @@ -1,193 +0,0 @@ -/******************************************************************************* - * SDR Trunk - * Copyright (C) 2014,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 - ******************************************************************************/ -package module.decode.tait; - -import instrument.Instrumentable; -import instrument.tap.Tap; -import instrument.tap.TapGroup; -import instrument.tap.stream.BinaryTap; -import instrument.tap.stream.FloatBufferTap; -import instrument.tap.stream.FloatTap; - -import java.util.ArrayList; -import java.util.List; -import java.util.concurrent.ScheduledExecutorService; - -import module.decode.Decoder; -import module.decode.DecoderType; -import sample.Broadcaster; -import sample.Listener; -import sample.real.IFilteredRealBufferListener; -import sample.real.RealBuffer; -import alias.AliasList; -import bits.MessageFramer; -import bits.SyncPattern; -import dsp.filter.Filters; -import dsp.filter.fir.real.RealFIRFilter_RB_RB; -import dsp.filter.halfband.real.HalfBandFilter_RB_RB; -import dsp.fsk.FSK2Decoder; -import dsp.fsk.FSK2Decoder.Output; - -/** - * TAIT 1200 - 1200 baud 2FSK decoder - */ -public class Tait1200Decoder extends Decoder implements IFilteredRealBufferListener, - Instrumentable -{ - /* Decimated sample rate ( 48,000 / 2 = 24,000 ) feeding the decoder */ - private static final int DECIMATED_SAMPLE_RATE = 24000; - - /* Baud or Symbol Rate */ - private static final int SYMBOL_RATE = 1200; - - /* Message length ... */ - private static final int MESSAGE_LENGTH = 440; - - /* Instrumentation Taps */ - private ArrayList mAvailableTaps; - private static final String INSTRUMENT_BANDPASS_FILTER_TO_FSK2_DEMOD = - "Tap Point: Bandpass Filter > < FSK2 Decoder"; - private static final String INSTRUMENT_FSK2_DECODER_TO_MESSAGE_FRAMER = - "Tap Point: FSK2 Decoder > < Message Framer"; - - private FSK2Decoder mFSKDecoder; - - private HalfBandFilter_RB_RB mDecimationFilter; - private RealFIRFilter_RB_RB mBandPassFilter; - private MessageFramer mMessageFramerGPS; - private MessageFramer mMessageFramerANI; - private Broadcaster mFSKBroadcaster = new Broadcaster(); - private Tait1200GPSMessageProcessor mMessageAProcessor; - private Tait1200ANIMessageProcessor mMessageBProcessor; - - public Tait1200Decoder( AliasList aliasList ) - { - mDecimationFilter = new HalfBandFilter_RB_RB( - Filters.FIR_HALF_BAND_31T_ONE_EIGHTH_FCO.getCoefficients(), 1.0002f, true ); - - mBandPassFilter = new RealFIRFilter_RB_RB( - Filters.FIRBP_1200FSK_24000FS.getCoefficients(), 1.02f ); - mDecimationFilter.setListener( mBandPassFilter ); - - mFSKDecoder = new FSK2Decoder( DECIMATED_SAMPLE_RATE, - SYMBOL_RATE, Output.INVERTED ); - mBandPassFilter.setListener( mFSKDecoder ); - - - mFSKDecoder.setListener( mFSKBroadcaster ); - - mMessageFramerGPS = new MessageFramer( - SyncPattern.TAIT_CCDI_GPS_MESSAGE.getPattern(), MESSAGE_LENGTH ); - mMessageFramerANI = new MessageFramer( - SyncPattern.TAIT_SELCAL_MESSAGE.getPattern(), MESSAGE_LENGTH ); - - mFSKBroadcaster.addListener( mMessageFramerGPS ); - mFSKBroadcaster.addListener( mMessageFramerANI ); - - mMessageAProcessor = new Tait1200GPSMessageProcessor( aliasList ); - mMessageBProcessor = new Tait1200ANIMessageProcessor( aliasList ); - - mMessageFramerGPS.addMessageListener( mMessageAProcessor ); - mMessageFramerANI.addMessageListener( mMessageBProcessor ); - - mMessageAProcessor.setMessageListener( this ); - mMessageBProcessor.setMessageListener( this ); - } - - public void dispose() - { - super.dispose(); - - mDecimationFilter.dispose(); - mBandPassFilter.dispose(); - mFSKDecoder.dispose(); - mMessageFramerGPS.dispose(); - mMessageFramerANI.dispose(); - mMessageAProcessor.dispose(); - mMessageBProcessor.dispose(); - } - - @Override - public List getTapGroups() - { - if( mAvailableTaps == null ) - { - mAvailableTaps = new ArrayList<>(); - - TapGroup group = new TapGroup( "Tait 1200 Decoder" ); - - group.add( new FloatTap( - INSTRUMENT_BANDPASS_FILTER_TO_FSK2_DEMOD, 0, 0.5f ) ); - group.add( new BinaryTap( - INSTRUMENT_FSK2_DECODER_TO_MESSAGE_FRAMER, 0, 0.025f ) ); - - mAvailableTaps.add( group ); - - mAvailableTaps.addAll( mFSKDecoder.getTapGroups() ); - } - - return mAvailableTaps; - } - - @Override - public void registerTap( Tap tap ) - { - mFSKDecoder.registerTap( tap ); - - switch( tap.getName() ) - { - case INSTRUMENT_BANDPASS_FILTER_TO_FSK2_DEMOD: - FloatBufferTap bpTap = (FloatBufferTap)tap; - mBandPassFilter.setListener( bpTap ); - bpTap.setListener( mFSKDecoder ); - break; - case INSTRUMENT_FSK2_DECODER_TO_MESSAGE_FRAMER: - BinaryTap decoderTap = (BinaryTap)tap; - mFSKBroadcaster.addListener( decoderTap ); - break; - } - } - - @Override - public void unregisterTap( Tap tap ) - { - mFSKDecoder.unregisterTap( tap ); - - switch( tap.getName() ) - { - case INSTRUMENT_BANDPASS_FILTER_TO_FSK2_DEMOD: - mBandPassFilter.setListener( mFSKDecoder ); - break; - case INSTRUMENT_FSK2_DECODER_TO_MESSAGE_FRAMER: - mFSKBroadcaster.removeListener( (BinaryTap)tap ); - break; - } - } - - @Override - public Listener getFilteredRealBufferListener() - { - return mDecimationFilter; - } - - @Override - public DecoderType getDecoderType() - { - return DecoderType.TAIT_1200; - } -} diff --git a/src/module/decode/tait/Tait1200DecoderState.java b/src/module/decode/tait/Tait1200DecoderState.java deleted file mode 100644 index d8750b52..00000000 --- a/src/module/decode/tait/Tait1200DecoderState.java +++ /dev/null @@ -1,212 +0,0 @@ -/******************************************************************************* - * 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 module.decode.tait; - -import alias.Alias; -import alias.AliasList; -import alias.id.AliasIDType; -import channel.metadata.AliasedStringAttributeMonitor; -import channel.metadata.Attribute; -import channel.metadata.AttributeChangeRequest; -import channel.state.DecoderState; -import channel.state.DecoderStateEvent; -import channel.state.DecoderStateEvent.Event; -import channel.state.State; -import message.Message; -import module.decode.DecoderType; -import module.decode.event.CallEvent.CallEventType; -import org.jdesktop.swingx.mapviewer.GeoPosition; -import util.StringUtils; - -import java.util.ArrayList; -import java.util.Collections; -import java.util.List; -import java.util.TreeSet; -import java.util.concurrent.ScheduledExecutorService; - -public class Tait1200DecoderState extends DecoderState -{ - private TreeSet mIdents = new TreeSet(); - - private AliasedStringAttributeMonitor mFromAttribute; - private AliasedStringAttributeMonitor mToAttribute; - private String mMessage; - private String mMessageType; - - public Tait1200DecoderState(AliasList aliasList) - { - super(aliasList); - - mFromAttribute = new AliasedStringAttributeMonitor(Attribute.SECONDARY_ADDRESS_FROM, - getAttributeChangeRequestListener(), getAliasList(), AliasIDType.TALKGROUP); - mToAttribute = new AliasedStringAttributeMonitor(Attribute.SECONDARY_ADDRESS_TO, - getAttributeChangeRequestListener(), getAliasList(), AliasIDType.TALKGROUP); - } - - @Override - public DecoderType getDecoderType() - { - return DecoderType.TAIT_1200; - } - - @Override - public void reset() - { - mIdents.clear(); - - resetState(); - } - - private void resetState() - { - mFromAttribute.reset(); - mToAttribute.reset(); - mMessage = null; - mMessageType = null; - } - - @Override - public void receive(Message message) - { - if(message instanceof Tait1200GPSMessage) - { - Tait1200GPSMessage gps = (Tait1200GPSMessage) message; - - mFromAttribute.process(gps.getFromID()); - mIdents.add(gps.getFromID()); - - mToAttribute.process(gps.getToID()); - mIdents.add(gps.getToID()); - - GeoPosition position = gps.getGPSLocation(); - - if(position != null) - { - setMessage(gps.getGPSLocation().toString().replace("[", "") - .replace("]", "")); - } - - setMessageType("GPS"); - - broadcast(new Tait1200CallEvent(CallEventType.GPS, getAliasList(), - gps.getFromID(), gps.getToID(), gps.getGPSLocation().toString())); - - broadcast(new DecoderStateEvent(this, Event.DECODE, State.DATA)); - } - else if(message instanceof Tait1200ANIMessage) - { - Tait1200ANIMessage ani = (Tait1200ANIMessage) message; - mFromAttribute.process(ani.getFromID()); - mIdents.add(ani.getFromID()); - - mToAttribute.process(ani.getToID()); - mIdents.add(ani.getToID()); - - setMessage(null); - setMessageType("ANI"); - - broadcast(new Tait1200CallEvent(CallEventType.ID_ANI, getAliasList(), - ani.getFromID(), ani.getToID(), "ANI")); - - broadcast(new DecoderStateEvent(this, Event.DECODE, State.CALL)); - } - } - - @Override - public String getActivitySummary() - { - StringBuilder sb = new StringBuilder(); - - sb.append("=============================\n"); - sb.append("Decoder:\tTait-1200I\n\n"); - - if(!mIdents.isEmpty()) - { - sb.append("Radio Identifiers:\n"); - - List idents = new ArrayList(mIdents); - - Collections.sort(idents); - - for(String ident : idents) - { - sb.append("\t"); - sb.append(ident); - - if(hasAliasList()) - { - Alias alias = getAliasList().getTalkgroupAlias(ident); - - if(alias != null) - { - sb.append("\t"); - sb.append(alias.getName()); - } - } - sb.append("\n"); - } - } - - return sb.toString(); - } - - /** - * Responds to reset events issued by the channel state - */ - @Override - public void receiveDecoderStateEvent(DecoderStateEvent event) - { - switch(event.getEvent()) - { - case RESET: - resetState(); - break; - default: - break; - } - } - - public String getMessage() - { - return mMessage; - } - - public void setMessage(String message) - { - if(!StringUtils.isEqual(mMessage, message)) - { - mMessage = message; - broadcast(new AttributeChangeRequest(Attribute.MESSAGE, getMessage())); - } - } - - public String getMessageType() - { - return mMessageType; - } - - public void setMessageType(String type) - { - if(!StringUtils.isEqual(mMessageType, type)) - { - mMessageType = type; - broadcast(new AttributeChangeRequest(Attribute.MESSAGE_TYPE, getMessageType())); - } - } -} diff --git a/src/module/demodulate/am/AMDemodulatorModule.java b/src/module/demodulate/am/AMDemodulatorModule.java deleted file mode 100644 index 9c9f1ace..00000000 --- a/src/module/demodulate/am/AMDemodulatorModule.java +++ /dev/null @@ -1,110 +0,0 @@ -/******************************************************************************* - * SDR Trunk - * Copyright (C) 2014,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 - ******************************************************************************/ -package module.demodulate.am; - -import dsp.am.AMDemodulator_CB; -import dsp.filter.FilterFactory; -import dsp.filter.Window.WindowType; -import dsp.filter.fir.complex.ComplexFIRFilter_CB_CB; -import dsp.filter.fir.real.RealFIRFilter_RB_RB; -import dsp.gain.AutomaticGainControl_RB; -import module.Module; -import sample.Listener; -import sample.complex.ComplexBuffer; -import sample.complex.IComplexBufferListener; -import sample.real.IFilteredRealBufferProvider; -import sample.real.RealBuffer; - -public class AMDemodulatorModule extends Module - implements IComplexBufferListener, IFilteredRealBufferProvider -{ - private static final int SAMPLE_RATE = 48000; - - private ComplexFIRFilter_CB_CB mIQFilter; - private AMDemodulator_CB mDemodulator; - private RealFIRFilter_RB_RB mLowPassFilter; - private AutomaticGainControl_RB mAGC; - - /** - * AM Demodulator Module - */ - public AMDemodulatorModule() - { - mIQFilter = new ComplexFIRFilter_CB_CB(FilterFactory.getLowPass( - SAMPLE_RATE, 5000, 73, WindowType.HAMMING), 1.0f); - - mDemodulator = new AMDemodulator_CB(500.0f); - mIQFilter.setListener(mDemodulator); - - mLowPassFilter = new RealFIRFilter_RB_RB( - FilterFactory.getLowPass(48000, 3000, 31, WindowType.COSINE), 1.0f); - mDemodulator.setListener(mLowPassFilter); - - mAGC = new AutomaticGainControl_RB(); - mLowPassFilter.setListener(mAGC); - } - - @Override - public Listener getComplexBufferListener() - { - return mIQFilter; - } - - @Override - public void dispose() - { - mIQFilter.dispose(); - mIQFilter = null; - - mDemodulator.dispose(); - mDemodulator = null; - - mLowPassFilter.dispose(); - mLowPassFilter = null; - - mAGC.dispose(); - mAGC = null; - } - - @Override - public void reset() - { - } - - @Override - public void setFilteredRealBufferListener(Listener listener) - { - mAGC.setListener(listener); - } - - @Override - public void removeFilteredRealBufferListener() - { - mAGC.removeListener(); - } - - @Override - public void start() - { - } - - @Override - public void stop() - { - } -} diff --git a/src/module/demodulate/audio/DemodulatedAudioFilterModule.java b/src/module/demodulate/audio/DemodulatedAudioFilterModule.java deleted file mode 100644 index fa73f27d..00000000 --- a/src/module/demodulate/audio/DemodulatedAudioFilterModule.java +++ /dev/null @@ -1,121 +0,0 @@ -/******************************************************************************* - * 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 module.demodulate.audio; - -import dsp.filter.FilterFactory; -import dsp.filter.Window.WindowType; -import dsp.filter.dc.DCRemovalFilter_RealBuffer; -import dsp.filter.fir.real.RealFIRFilter_RB_RB; -import module.Module; -import org.slf4j.Logger; -import org.slf4j.LoggerFactory; -import sample.Listener; -import sample.real.IFilteredRealBufferProvider; -import sample.real.IUnFilteredRealBufferListener; -import sample.real.RealBuffer; - -import java.util.concurrent.ScheduledExecutorService; - -public class DemodulatedAudioFilterModule extends Module implements IUnFilteredRealBufferListener, IFilteredRealBufferProvider -{ - private final static Logger mLog = LoggerFactory.getLogger(DemodulatedAudioFilterModule.class); - - private DCRemovalFilter_RealBuffer mDCFilter; - private RealFIRFilter_RB_RB mBandPassFilter; - - /** - * Filters demodulated audio and removes any DC component from the buffer - * - * @param pass - audio pass band pass frequency - * @param stop - audio pass band stop frequency - */ - public DemodulatedAudioFilterModule(int pass, int stop) - { - assert (stop > pass); - - mDCFilter = new DCRemovalFilter_RealBuffer(); - - //TODO: change this to a band pass filter - mBandPassFilter = new RealFIRFilter_RB_RB(FilterFactory.getLowPass(48000, pass, stop, 60, - WindowType.HANN, true), 1.0f); - - mDCFilter.setListener(mBandPassFilter); - } - - /** - * Constructs a demodulated audio filter using the filter taps and gain provided - */ - public DemodulatedAudioFilterModule(float[] taps, float gain) - { - mBandPassFilter = new RealFIRFilter_RB_RB(taps, gain); - } - - @Override - public void dispose() - { - mBandPassFilter.dispose(); - mBandPassFilter = null; - - if(mDCFilter != null) - { - mDCFilter.dispose(); - mDCFilter = null; - } - } - - @Override - public void reset() - { - } - - @Override - public void setFilteredRealBufferListener(Listener listener) - { - mBandPassFilter.setListener(listener); - } - - @Override - public void removeFilteredRealBufferListener() - { - mBandPassFilter.removeListener(); - } - - @Override - public void start() - { - } - - @Override - public void stop() - { - } - - @Override - public Listener getUnFilteredRealBufferListener() - { - if(mDCFilter != null) - { - return mDCFilter; - } - else - { - return mBandPassFilter; - } - } -} diff --git a/src/module/demodulate/fm/FMDemodulatorModule.java b/src/module/demodulate/fm/FMDemodulatorModule.java deleted file mode 100644 index d3278e68..00000000 --- a/src/module/demodulate/fm/FMDemodulatorModule.java +++ /dev/null @@ -1,115 +0,0 @@ -/******************************************************************************* - * SDR Trunk - * Copyright (C) 2014-2016 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 module.demodulate.fm; - -import dsp.filter.FilterFactory; -import dsp.filter.Window.WindowType; -import dsp.filter.fir.complex.ComplexFIRFilter_CB_CB; -import dsp.fm.FMDemodulator_CB; -import module.Module; -import org.slf4j.Logger; -import org.slf4j.LoggerFactory; -import sample.Listener; -import sample.complex.ComplexBuffer; -import sample.complex.IComplexBufferListener; -import sample.real.IUnFilteredRealBufferProvider; -import sample.real.RealBuffer; - -import java.util.concurrent.ScheduledExecutorService; - -public class FMDemodulatorModule extends Module implements IComplexBufferListener, IUnFilteredRealBufferProvider -{ - private final static Logger mLog = LoggerFactory.getLogger(FMDemodulatorModule.class); - - private static final int SAMPLE_RATE = 48000; - - private ComplexFIRFilter_CB_CB mIQFilter; - private FMDemodulator_CB mDemodulator; - - /** - * FM Demodulator with I/Q filter. Demodulated output is unfiltered and - * may contain a DC component. - * - * @param pass - pass frequency for IQ filtering prior to demodulation. This - * frequency should be half of the signal bandwidth since the filter will - * be applied against each of the inphase and quadrature signals and the - * combined pass bandwidth will be twice this value. - * @param stop - stop frequency for IQ filtering prior to demodulation. - */ - public FMDemodulatorModule(int pass, int stop) - { - assert (stop > pass); - - mIQFilter = new ComplexFIRFilter_CB_CB(FilterFactory.getLowPass( - SAMPLE_RATE, pass, stop, 60, WindowType.HAMMING, true), 1.0f); - - mDemodulator = new FMDemodulator_CB(1.0f); - mIQFilter.setListener(mDemodulator); - } - - public FMDemodulatorModule(float[] filter) - { - mIQFilter = new ComplexFIRFilter_CB_CB(filter, 1.0f); - mDemodulator = new FMDemodulator_CB(1.0f); - mIQFilter.setListener(mDemodulator); - } - - @Override - public Listener getComplexBufferListener() - { - return mIQFilter; - } - - @Override - public void dispose() - { - mIQFilter.dispose(); - mIQFilter = null; - - mDemodulator.dispose(); - mDemodulator = null; - } - - @Override - public void reset() - { - mDemodulator.reset(); - } - - @Override - public void setUnFilteredRealBufferListener(Listener listener) - { - mDemodulator.setListener(listener); - } - - @Override - public void removeUnFilteredRealBufferListener() - { - mDemodulator.removeListener(); - } - - @Override - public void start() - { - } - - @Override - public void stop() - { - } -} diff --git a/src/module/log/EventLogger.java b/src/module/log/EventLogger.java deleted file mode 100644 index dcd90caa..00000000 --- a/src/module/log/EventLogger.java +++ /dev/null @@ -1,138 +0,0 @@ -/******************************************************************************* - * 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 module.log; - -import module.Module; -import org.slf4j.Logger; -import org.slf4j.LoggerFactory; -import util.TimeStamp; - -import java.io.File; -import java.io.FileNotFoundException; -import java.io.FileOutputStream; -import java.io.IOException; -import java.io.OutputStreamWriter; -import java.io.Writer; -import java.nio.file.Path; -import java.util.concurrent.ScheduledExecutorService; - -public abstract class EventLogger extends Module -{ - private final static Logger mLog = LoggerFactory.getLogger(EventLogger.class); - - private static final String[] ILLEGAL_FILENAME_CHARACTERS = {"#", "%", "&", "{", "}", "\\", "<", ">", - "*", "?", "/", " ", "$", "!", "'", "\"", ":", "@", "+", "`", "|", "="}; - - private Path mLogDirectory; - private String mFileNameSuffix; - private String mLogFileName; - protected Writer mLogFile; - - public EventLogger(Path logDirectory, String fileNameSuffix) - { - mLogDirectory = logDirectory; - mFileNameSuffix = fileNameSuffix; - } - - public String toString() - { - if(mLogFileName != null) - { - return mLogFileName; - } - else - { - return "Unknown"; - } - } - - public abstract String getHeader(); - - @Override - public void start() - { - if(mLogFile == null) - { - try - { - StringBuilder sb = new StringBuilder(); - sb.append(mLogDirectory); - sb.append(File.separator); - sb.append(TimeStamp.getTimeStamp("_")); - sb.append("_"); - sb.append(replaceIllegalCharacters(mFileNameSuffix)); - - mLogFileName = sb.toString(); - - mLog.info("Creating log file:" + mLogFileName); - - mLogFile = new OutputStreamWriter(new FileOutputStream(mLogFileName)); - - write(getHeader()); - } - catch(FileNotFoundException e) - { - mLog.error("Couldn't create log file in directory:" + mLogDirectory); - } - } - } - - /** - * Replaces any illegal filename characters in the proposed filename - */ - private String replaceIllegalCharacters(String filename) - { - for(String illegalCharacter : ILLEGAL_FILENAME_CHARACTERS) - { - filename = filename.replace(illegalCharacter, "_"); - } - - return filename; - } - - public void stop() - { - if(mLogFile != null) - { - try - { - mLogFile.flush(); - mLogFile.close(); - mLogFile = null; - } - catch(Exception e) - { - mLog.error("Couldn't close log file:" + mFileNameSuffix); - } - } - } - - protected void write(String eventLogEntry) - { - try - { - mLogFile.write(eventLogEntry + "\n"); - mLogFile.flush(); - } - catch(IOException e) - { - mLog.error("Error writing entry to event log file", e); - } - } -} diff --git a/src/record/AudioRecorder.java b/src/record/AudioRecorder.java deleted file mode 100644 index 3c1b7f64..00000000 --- a/src/record/AudioRecorder.java +++ /dev/null @@ -1,336 +0,0 @@ -/******************************************************************************* - * sdrtrunk - * Copyright (C) 2014-2016 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 record; - -import audio.AudioPacket; -import audio.IAudioPacketListener; -import channel.metadata.Metadata; -import module.Module; -import org.slf4j.Logger; -import org.slf4j.LoggerFactory; -import sample.Listener; -import util.ThreadPool; - -import java.io.FileOutputStream; -import java.io.IOException; -import java.io.OutputStream; -import java.nio.file.Path; -import java.util.ArrayList; -import java.util.List; -import java.util.concurrent.LinkedBlockingQueue; -import java.util.concurrent.ScheduledExecutorService; -import java.util.concurrent.ScheduledFuture; -import java.util.concurrent.TimeUnit; -import java.util.concurrent.atomic.AtomicBoolean; - -public abstract class AudioRecorder extends Module implements Listener, IAudioPacketListener -{ - private final static Logger mLog = LoggerFactory.getLogger(AudioRecorder.class); - - private LinkedBlockingQueue mAudioPacketQueue = new LinkedBlockingQueue<>(500); - private List mPacketsToProcess = new ArrayList<>(); - - private FileOutputStream mFileOutputStream; - private AtomicBoolean mRunning = new AtomicBoolean(); - - protected Path mPath; - protected Metadata mMetadata; - protected long mTimeRecordingStart; - protected long mTimeLastPacketReceived; - private BufferProcessor mBufferProcessor; - private ScheduledFuture mProcessorHandle; - private Listener mRecordingClosedListener; - - private long mSampleCount; - - /** - * Abstract audio recorder that implements audio packet queueing and threaded audio conversion/writing to a file - * - * @param path for the output recording - */ - public AudioRecorder(Path path) - { - mPath = path; - } - - /** - * Path for the audio recording file - */ - public Path getPath() - { - return mPath; - } - - /** - * Latest audio metadata received for this recording - */ - public Metadata getMetadata() - { - return mMetadata; - } - - /** - * Timestamp of the last buffer received by this recorder - allows this recorder to be monitored for automatic - * closure after a time period has elapsed. - */ - public long getTimeLastPacketReceived() - { - return mTimeLastPacketReceived; - } - - public long getTimeRecordingStart() - { - return mTimeRecordingStart; - } - - /** - * Recording length in milliseconds - */ - public long getRecordingLength() - { - //Assumes audio sample rate of 8000 samples/second or 8 samples/milli-second - return mSampleCount / 8; - } - - /** - * Implements the IAudioPacketListener interface and simply redirects to the Listener interface. - * This is necessary since you can't have multiple methods with the same erasure (ie Listener) in the - * parent module class. - */ - @Override - public Listener getAudioPacketListener() - { - return this; - } - - /** - * Processes the audio packet and captures the latest Metadata for the recording for easy access. - */ - @Override - public void receive(AudioPacket audioPacket) - { - if(mRunning.get()) - { - mTimeRecordingStart = System.currentTimeMillis(); - mTimeLastPacketReceived = mTimeRecordingStart; - - if(audioPacket.hasMetadata()) - { - mMetadata = audioPacket.getMetadata(); - } - - boolean success = mAudioPacketQueue.offer(audioPacket); - - if(!success) - { - mLog.error("recorder buffer overflow - stopping recorder [" + getPath().toString() + "]"); - stop(); - } - } - } - - /** - * File output stream for the current recording. Intended to allow sub-classes to write binary data to the file. - */ - protected OutputStream getOutputStream() - { - return mFileOutputStream; - } - - /** - * Stops the recorder and flags the recording to be closed. Use this method if you do not need any details about - * the final recording. Otherwise, use the close() method and register a closing listener. - */ - public void stop() - { - close(null); - } - - /** - * Closes the recording file. Upon successful closing of the recording file, the listener is notified that the - * audio recorder is closed. There is potential for the calling thread (here) and the buffer processor thread to - * both inform the recording closed listener that the recording is ended. So, we synchronize on the listener and - * the first thread to get the lock informs the listener and then nullifies the listener pointer so that if the - * second thread attempts a duplicate notification the listener would be null at that point. - */ - public void close(Listener listener) - { - mRecordingClosedListener = listener; - - if(!mRunning.compareAndSet(true, false)) - { - synchronized(mRecordingClosedListener) - { - if(mRecordingClosedListener != null) - { - mRecordingClosedListener.receive(AudioRecorder.this); - mRecordingClosedListener = null; - } - } - } - } - - - /** - * Records the list of audio packets in the sub-class specific audio format. - */ - protected abstract void record(List audioPackets) throws IOException; - - /** - * Starts this recorder as a scheduled thread running under the executor argument - * - * @param executor to use in scheduling audio conversion and file writes. - */ - public void start() - { - if(mRunning.compareAndSet(false, true)) - { - mTimeLastPacketReceived = System.currentTimeMillis(); - - if(mBufferProcessor == null) - { - mBufferProcessor = new BufferProcessor(); - } - - try - { - mFileOutputStream = new FileOutputStream(mPath.toFile()); - - /* Schedule the handler to run every half second */ - mProcessorHandle = ThreadPool.SCHEDULED.scheduleAtFixedRate(mBufferProcessor, 0, 500, TimeUnit.MILLISECONDS); - } - catch(IOException io) - { - mLog.error("Error starting audio recorder [" + getPath().toString() + "]", io); - - mRunning.set(false); - } - } - } - - /** - * Processes the audio packet queue. - */ - private void processAudioPacketQueue() - { - mAudioPacketQueue.drainTo(mPacketsToProcess); - - if(!mPacketsToProcess.isEmpty()) - { - try - { - record(mPacketsToProcess); - - for(AudioPacket packet : mPacketsToProcess) - { - if(packet.getType() == AudioPacket.Type.AUDIO) - { - mSampleCount += packet.getAudioBuffer().getSamples().length; - } - } - } - catch(IOException ioe) - { - mLog.debug("Error while recording audio to [" + getPath().toString() + "] - stopping recorder"); - stop(); - } - - mPacketsToProcess.clear(); - } - } - - /** - * Disposes this audio recorder and prepares it for reclamation - */ - @Override - public void dispose() - { - stop(); - } - - /** - * Not implemented. Recorder modules are not appropriate for reset and reuse. - */ - @Override - public void reset() - { - } - - /** - * Flushes any remaining audio to the output file. This method should be implemented by subclasses where an audio - * converter may contain residual frame data that should be flushed to disk before closing the audio file. - */ - protected void flush() - { - } - - /** - * Drains the audio packet queue and records the audio packets to file - */ - public class BufferProcessor implements Runnable - { - private AtomicBoolean mProcessing = new AtomicBoolean(); - - public void run() - { - if(mProcessing.compareAndSet(false, true)) - { - processAudioPacketQueue(); - - //If we've been stopped or closed, finish the queue, close the recording, notify the listener, and - // cancel the future - if(!mRunning.get()) - { - //Allow sub-classes to flush remaining audio frame data to disk. - flush(); - - if(mFileOutputStream != null) - { - try - { - mFileOutputStream.flush(); - mFileOutputStream.close(); - } - catch(IOException e) - { - mLog.error("Error closing output stream", e); - } - } - - synchronized(mRecordingClosedListener) - { - if(mRecordingClosedListener != null) - { - mRecordingClosedListener.receive(AudioRecorder.this); - mRecordingClosedListener = null; - } - } - - if(mProcessorHandle != null) - { - mProcessorHandle.cancel(false); - mProcessorHandle = null; - } - } - - mProcessing.set(false); - } - } - } -} diff --git a/src/record/RecorderManager.java b/src/record/RecorderManager.java deleted file mode 100644 index 4e054b40..00000000 --- a/src/record/RecorderManager.java +++ /dev/null @@ -1,249 +0,0 @@ -/******************************************************************************* - * SDR Trunk - * 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 record; - -import audio.AudioPacket; -import channel.metadata.Metadata; -import org.slf4j.Logger; -import org.slf4j.LoggerFactory; -import properties.SystemProperties; -import record.wave.ComplexBufferWaveRecorder; -import record.wave.RealBufferWaveRecorder; -import sample.Listener; -import sample.OverflowableTransferQueue; -import sample.real.IOverflowListener; -import util.ThreadPool; - -import java.io.File; -import java.util.ArrayList; -import java.util.HashMap; -import java.util.Iterator; -import java.util.List; -import java.util.Map; -import java.util.concurrent.ScheduledFuture; -import java.util.concurrent.TimeUnit; - -public class RecorderManager implements Listener -{ - private static final Logger mLog = LoggerFactory.getLogger(RecorderManager.class); - - public static final int AUDIO_SAMPLE_RATE = 8000; - public static final long IDLE_RECORDER_REMOVAL_THRESHOLD = 6000; //6 seconds - - private Map mRecorders = new HashMap<>(); - private OverflowableTransferQueue mAudioPacketQueue = new OverflowableTransferQueue<>(1000, 100); - private ScheduledFuture mBufferProcessorFuture; - - private boolean mCanStartNewRecorders = true; - - /** - * Audio recording manager. Monitors stream of audio packets produced by decoding channels and automatically starts - * audio recorders when the channel's metadata designates a call as recordable. Routes call audio to each recorder - * based on audio packet metadata. Recorders are shutdown when the channel sends an end-call audio packet - * indicating that the call is complete. A separate recording monitor periodically checks for idled recorders to - * be stopped for cases when the channel fails to send an end-call audio packet. - */ - public RecorderManager() - { - mAudioPacketQueue.setOverflowListener(new IOverflowListener() - { - @Override - public void sourceOverflow(boolean overflow) - { - if(overflow) - { - mLog.warn("overflow - audio packets will be dropped until recording catches up"); - } - else - { - mLog.info("audio recorder packet processing has returned to normal"); - } - } - }); - - mBufferProcessorFuture = ThreadPool.SCHEDULED.scheduleAtFixedRate(new BufferProcessor(), 0, - 1, TimeUnit.SECONDS); - } - - /** - * Prepares this class for shutdown - */ - public void dispose() - { - if(mBufferProcessorFuture != null) - { - mBufferProcessorFuture.cancel(true); - } - } - - /** - * Primary ingest point for audio packets from all decoding channels - * @param audioPacket to process - */ - @Override - public void receive(AudioPacket audioPacket) - { - if(audioPacket.hasMetadata() && audioPacket.getMetadata().isRecordable()) - { - mAudioPacketQueue.offer(audioPacket); - } - } - - /** - * Process any queued audio buffers and dispatch them to the audio recorders - */ - private void processBuffers() - { - List audioPackets = new ArrayList<>(); - - mAudioPacketQueue.drainTo(audioPackets, 50); - - while(!audioPackets.isEmpty()) - { - for(AudioPacket audioPacket: audioPackets) - { - String identifier = audioPacket.getMetadata().getUniqueIdentifier(); - - if(mRecorders.containsKey(identifier)) - { - RealBufferWaveRecorder recorder = mRecorders.get(identifier); - - if(audioPacket.getType() == AudioPacket.Type.AUDIO) - { - recorder.receive(audioPacket.getAudioBuffer()); - } - else if(audioPacket.getType() == AudioPacket.Type.END) - { - RealBufferWaveRecorder finished = mRecorders.remove(identifier); - finished.stop(); - } - } - else if(audioPacket.getType() == AudioPacket.Type.AUDIO) - { - if(mCanStartNewRecorders) - { - String filePrefix = getFilePrefix(audioPacket); - - RealBufferWaveRecorder recorder = null; - - try - { - recorder = new RealBufferWaveRecorder(AUDIO_SAMPLE_RATE, filePrefix); - - recorder.start(); - - recorder.receive(audioPacket.getAudioBuffer()); - mRecorders.put(identifier, recorder); - } - catch(Exception ioe) - { - mCanStartNewRecorders = false; - - mLog.error("Error attempting to start new audio wave recorder. All (future) audio recording " + - "is disabled", ioe); - - if(recorder != null) - { - recorder.stop(); - } - } - } - } - } - - audioPackets.clear(); - mAudioPacketQueue.drainTo(audioPackets, 50); - } - } - - /** - * Removes recorders that have not received any new audio buffers in the last 6 seconds. - */ - private void removeIdleRecorders() - { - Iterator> it = mRecorders.entrySet().iterator(); - - while(it.hasNext()) - { - Map.Entry entry = it.next(); - - if(entry.getValue().getLastBufferReceived() + IDLE_RECORDER_REMOVAL_THRESHOLD < System.currentTimeMillis()) - { - mLog.info("Removing idle recorder [" + entry.getKey() + "]"); - it.remove(); - entry.getValue().stop(); - } - } - } - - /** - * Constructs a file name and path for an audio recording - */ - private String getFilePrefix(AudioPacket packet) - { - StringBuilder sb = new StringBuilder(); - - sb.append(SystemProperties.getInstance().getApplicationFolder("recordings")); - - sb.append(File.separator); - - Metadata metadata = packet.getMetadata(); - - sb.append(metadata.hasChannelConfigurationSystem() ? metadata.getChannelConfigurationSystem() + "_" : ""); - sb.append(metadata.hasChannelConfigurationSite() ? metadata.getChannelConfigurationSite() + "_" : ""); - sb.append(metadata.hasChannelConfigurationName() ? metadata.getChannelConfigurationName() + "_" : ""); - - if(metadata.getPrimaryAddressTo().hasIdentifier()) - { - sb.append("_TO_").append(metadata.getPrimaryAddressTo().getIdentifier()); - - if(metadata.getPrimaryAddressFrom().hasIdentifier()) - { - sb.append("_FROM_").append(metadata.getPrimaryAddressFrom().getIdentifier()); - } - } - - return sb.toString(); - } - - /** - * Constructs a baseband recorder for use in a processing chain. - */ - public ComplexBufferWaveRecorder getBasebandRecorder(String channelName) - { - StringBuilder sb = new StringBuilder(); - sb.append(SystemProperties.getInstance().getApplicationFolder("recordings")); - sb.append(File.separator).append(channelName).append("_baseband"); - - return new ComplexBufferWaveRecorder(AUDIO_SAMPLE_RATE, sb.toString()); - } - - /** - * Processes queued audio packets and distributes to each of the audio recorders. Removes any idle recorders - * that have not been updated according to an idle threshold period - */ - public class BufferProcessor implements Runnable - { - @Override - public void run() - { - processBuffers(); - removeIdleRecorders(); - } - } -} diff --git a/src/record/wave/ComplexBufferWaveRecorder.java b/src/record/wave/ComplexBufferWaveRecorder.java deleted file mode 100644 index ea369c65..00000000 --- a/src/record/wave/ComplexBufferWaveRecorder.java +++ /dev/null @@ -1,218 +0,0 @@ -/******************************************************************************* - * SDR Trunk - * Copyright (C) 2014,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 - ******************************************************************************/ -package record.wave; - -import module.Module; -import org.slf4j.Logger; -import org.slf4j.LoggerFactory; -import sample.Buffer; -import sample.ConversionUtils; -import sample.Listener; -import sample.complex.ComplexBuffer; -import sample.complex.IComplexBufferListener; -import util.ThreadPool; -import util.TimeStamp; - -import javax.sound.sampled.AudioFormat; -import java.io.IOException; -import java.nio.file.Path; -import java.nio.file.Paths; -import java.util.concurrent.LinkedBlockingQueue; -import java.util.concurrent.ScheduledFuture; -import java.util.concurrent.TimeUnit; -import java.util.concurrent.atomic.AtomicBoolean; - -/** - * WAVE audio recorder module for recording complex (I&Q) samples to a wave file - */ -public class ComplexBufferWaveRecorder extends Module - implements IComplexBufferListener, Listener -{ - private final static Logger mLog = - LoggerFactory.getLogger(ComplexBufferWaveRecorder.class); - - private WaveWriter mWriter; - private String mFilePrefix; - private Path mFile; - private AudioFormat mAudioFormat; - - private BufferProcessor mBufferProcessor; - private ScheduledFuture mProcessorHandle; - private LinkedBlockingQueue mBuffers = new LinkedBlockingQueue<>(500); - - private AtomicBoolean mRunning = new AtomicBoolean(); - - public ComplexBufferWaveRecorder(int sampleRate, String filePrefix) - { - mAudioFormat = new AudioFormat(sampleRate, //SampleRate - 16, //Sample Size - 2, //Channels - true, //Signed - false); //Little Endian - - mFilePrefix = filePrefix; - } - - public Path getFile() - { - return mFile; - } - - public void start() - { - if(mRunning.compareAndSet(false, true)) - { - if(mBufferProcessor == null) - { - mBufferProcessor = new BufferProcessor(); - } - - try - { - StringBuilder sb = new StringBuilder(); - sb.append(mFilePrefix); - sb.append("_"); - sb.append(TimeStamp.getTimeStamp("_")); - sb.append(".wav"); - - mFile = Paths.get(sb.toString()); - - mWriter = new WaveWriter(mAudioFormat, mFile); - - /* Schedule the processor to run every 500 milliseconds */ - mProcessorHandle = ThreadPool.SCHEDULED.scheduleAtFixedRate( - mBufferProcessor, 0, 500, TimeUnit.MILLISECONDS); - } - catch(IOException io) - { - mLog.error("Error starting complex baseband recorder", io); - } - } - } - - public void stop() - { - if(mRunning.compareAndSet(true, false)) - { - receive(new PoisonPill()); - } - } - - @Override - public void receive(ComplexBuffer buffer) - { - if(mRunning.get()) - { - boolean success = mBuffers.offer(buffer); - - if(!success) - { - mLog.error("recorder buffer overflow - purging [" + - mFile.toFile().getAbsolutePath() + "]"); - - mBuffers.clear(); - } - } - } - - @Override - public Listener getComplexBufferListener() - { - return this; - } - - @Override - public void dispose() - { - stop(); - } - - @Override - public void reset() - { - } - - public class BufferProcessor implements Runnable - { - public void run() - { - try - { - Buffer buffer = mBuffers.poll(); - - while(buffer != null) - { - if(buffer instanceof PoisonPill) - { - buffer = null; - - mBuffers.clear(); - - if(mWriter != null) - { - try - { - mWriter.close(); - mWriter = null; - } - catch(IOException io) - { - mLog.error("Error stopping complex wave recorder [" + - getFile() + "]", io); - } - } - - mFile = null; - - if(mProcessorHandle != null) - { - mProcessorHandle.cancel(true); - } - - mProcessorHandle = null; - } - else - { - mWriter.write(ConversionUtils.convertToSigned16BitSamples(buffer)); - buffer = mBuffers.poll(); - } - } - } - catch(IOException ioe) - { - /* Stop this module if/when we get an IO exception */ - mBuffers.clear(); - stop(); - - mLog.error("IOException while trying to write to the wave " - + "writer", ioe); - } - } - } - - /** - * This is used as a sentinel value to signal the buffer processor to end - */ - public class PoisonPill extends ComplexBuffer - { - public PoisonPill() - { - super(new float[1]); - } - } -} diff --git a/src/record/wave/RealBufferWaveRecorder.java b/src/record/wave/RealBufferWaveRecorder.java deleted file mode 100644 index d006fe4a..00000000 --- a/src/record/wave/RealBufferWaveRecorder.java +++ /dev/null @@ -1,212 +0,0 @@ -/******************************************************************************* - * 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 record.wave; - -import module.Module; -import org.slf4j.Logger; -import org.slf4j.LoggerFactory; -import sample.ConversionUtils; -import sample.Listener; -import sample.real.IFilteredRealBufferListener; -import sample.real.RealBuffer; -import util.ThreadPool; -import util.TimeStamp; - -import javax.sound.sampled.AudioFormat; -import java.io.IOException; -import java.nio.file.Path; -import java.nio.file.Paths; -import java.util.concurrent.LinkedBlockingQueue; -import java.util.concurrent.ScheduledExecutorService; -import java.util.concurrent.ScheduledFuture; -import java.util.concurrent.TimeUnit; -import java.util.concurrent.atomic.AtomicBoolean; - -/** - * WAVE audio recorder module for recording real sample buffers to a wave file - */ -public class RealBufferWaveRecorder extends Module - implements IFilteredRealBufferListener, Listener -{ - private final static Logger mLog = LoggerFactory.getLogger(RealBufferWaveRecorder.class); - - private WaveWriter mWriter; - private String mFilePrefix; - private Path mFile; - private AudioFormat mAudioFormat; - - private BufferProcessor mBufferProcessor; - private ScheduledFuture mProcessorHandle; - private LinkedBlockingQueue mBuffers = new LinkedBlockingQueue<>(500); - private long mLastBufferReceived; - - private AtomicBoolean mRunning = new AtomicBoolean(); - - public RealBufferWaveRecorder(int sampleRate, String filePrefix) - { - mAudioFormat = new AudioFormat(sampleRate, //SampleRate - 16, //Sample Size - 1, //Channels - true, //Signed - false); //Little Endian - - mFilePrefix = filePrefix; - } - - /** - * Indicates if the recorder is currently running. - */ - public boolean isRunning() - { - return mRunning.get(); - } - - /** - * Timestamp of when the latest buffer was received by this recorder - */ - public long getLastBufferReceived() - { - return mLastBufferReceived; - } - - public Path getFile() - { - return mFile; - } - - public void start() - { - if(mRunning.compareAndSet(false, true)) - { - if(mBufferProcessor == null) - { - mBufferProcessor = new BufferProcessor(); - } - - try - { - StringBuilder sb = new StringBuilder(); - sb.append(mFilePrefix); - sb.append("_"); - sb.append(TimeStamp.getLongTimeStamp("_")); - sb.append(".wav"); - - mFile = Paths.get(sb.toString()); - - mWriter = new WaveWriter(mAudioFormat, mFile); - - /* Schedule the processor to run every 500 milliseconds */ - mProcessorHandle = ThreadPool.SCHEDULED.scheduleAtFixedRate(mBufferProcessor, 0, 500, TimeUnit.MILLISECONDS); - } - catch(IOException io) - { - mLog.error("Error starting real buffer recorder", io); - } - } - } - - public void stop() - { - if(mRunning.compareAndSet(true, false)) - { - try - { - write(); - - if(mWriter != null) - { - mWriter.close(); - mWriter = null; - } - } - catch(IOException ioe) - { - mLog.error("Error writing final audio buffers to recording during shutdown", ioe); - } - } - } - - @Override - public void receive(RealBuffer buffer) - { - if(mRunning.get()) - { - boolean success = mBuffers.offer(buffer); - - if(!success) - { - mLog.error("recorder buffer overflow - purging [" + mFile.toFile().getAbsolutePath() + "]"); - mBuffers.clear(); - } - - mLastBufferReceived = System.currentTimeMillis(); - } - } - - @Override - public Listener getFilteredRealBufferListener() - { - return this; - } - - @Override - public void dispose() - { - stop(); - } - - @Override - public void reset() - { - } - - /** - * Writes all audio currently in the queue to the file - * @throws IOException if there are any errors writing the audio - */ - private void write() throws IOException - { - RealBuffer buffer = mBuffers.poll(); - - while(buffer != null) - { - mWriter.write(ConversionUtils.convertToSigned16BitSamples(buffer)); - buffer = mBuffers.poll(); - } - } - - public class BufferProcessor implements Runnable - { - public void run() - { - try - { - write(); - } - catch(IOException ioe) - { - /* Stop this module if/when we get an IO exception */ - mBuffers.clear(); - stop(); - - mLog.error("IO Exception while trying to write to the wave writer", ioe); - } - } - } -} diff --git a/src/sample/Broadcaster.java b/src/sample/Broadcaster.java deleted file mode 100644 index b3bf951a..00000000 --- a/src/sample/Broadcaster.java +++ /dev/null @@ -1,124 +0,0 @@ -/******************************************************************************* - * SDR Trunk - * Copyright (C) 2014 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 sample; - -import java.util.List; -import java.util.concurrent.CopyOnWriteArrayList; - -/** - * Broadcasts an item to multiple listeners - */ -public class Broadcaster implements Listener -{ - private List> mListeners = new CopyOnWriteArrayList<>(); - - /** - * Implements the Listener interface to receive an element and broadcast that element to all registered - * listeners. - * - * @param t element to broadcast - */ - @Override - public void receive(T t) - { - broadcast(t); - } - - /** - * Clear listeners to prepare for garbage collection - */ - public void dispose() - { - mListeners.clear(); - } - - /** - * Indicates if this broadcaster has any listeners registered - */ - public boolean hasListeners() - { - return !mListeners.isEmpty(); - } - - /** - * Indicates if the listener is currently registered with this broadcaster - */ - public boolean hasListener(Listener listener) - { - return listener != null && mListeners.contains(listener); - } - - /** - * The count of listeners currently registered with this broadcaster - */ - public int getListenerCount() - { - return mListeners.size(); - } - - /** - * The list of listeners currently registered with this broadcaster - */ - public List> getListeners() - { - return mListeners; - } - - /** - * Registers the listener to receive elements from this broadcaster - * - * @param listener - */ - public void addListener(Listener listener) - { - if(listener != null && !mListeners.contains(listener)) - { - mListeners.add(listener); - } - } - - /** - * Deregisters the listener from receiving elements from this broadcaster - */ - public void removeListener(Listener listener) - { - if(listener != null && mListeners.contains(listener)) - { - mListeners.remove(listener); - } - } - - /** - * Deregisters all listeners from this broadcaster - */ - public void clear() - { - mListeners.clear(); - } - - /** - * Broadcasts the element to all registered listeners - */ - public void broadcast(T t) - { - for(Listener listener : mListeners) - { - listener.receive(t); - } - } -} diff --git a/src/sample/OverflowableTransferQueue.java b/src/sample/OverflowableTransferQueue.java deleted file mode 100644 index e532bc61..00000000 --- a/src/sample/OverflowableTransferQueue.java +++ /dev/null @@ -1,151 +0,0 @@ -/******************************************************************************* - * 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 sample; - -import org.slf4j.Logger; -import org.slf4j.LoggerFactory; -import sample.real.IOverflowListener; -import source.Source; - -import java.util.Collection; -import java.util.concurrent.LinkedTransferQueue; -import java.util.concurrent.atomic.AtomicBoolean; -import java.util.concurrent.atomic.AtomicInteger; - -public class OverflowableTransferQueue -{ - private final static Logger mLog = LoggerFactory.getLogger(OverflowableTransferQueue.class); - - public enum State {NORMAL, OVERFLOW}; - private IOverflowListener mOverflowListener; - private Source mSourceOverflowListener; - - private LinkedTransferQueue mQueue = new LinkedTransferQueue(); - private AtomicInteger mCounter = new AtomicInteger(); - private AtomicBoolean mOverflow = new AtomicBoolean(); - private int mMaximumSize; - private int mResetThreshold; - - /** - * Concurrent transfer queue that couples a higher-throughput linked transfer queue with an atomic integer for - * monitoring queue size. When the queue size exceeds maximum size (overflow), all inbound elements are ignored - * until the queue size is reduced to or below the reset threshold. - * - * @param maximumSize of the queue. Overflow state will occur once queue size exceeds this value. - * @param resetThreshold for resetting overflow state to normal, once queue size is at or below this value. - */ - public OverflowableTransferQueue(int maximumSize, int resetThreshold) - { - mMaximumSize = maximumSize; - mResetThreshold = resetThreshold; - } - - /** - * Adds the element to the queue if able to do so without exceeding maximum queue size. Otherwise, ignores - * the element. - */ - public void offer(E e) - { - if(!mOverflow.get()) - { - mQueue.offer(e); - - int size = mCounter.incrementAndGet(); - - if(size > mMaximumSize) - { - setOverflow(true); - } - } - } - - /** - * Removes and returns a single element from the head of the queue or null if the queue is empty - */ - public E poll() - { - return mQueue.poll(); - } - - /** - * Retrieves elements from the queue into the collection up to the maximum number of elements specified - */ - public int drainTo(Collection collection, int maxElements) - { - int drainCount = mQueue.drainTo(collection, maxElements); - - int size = mCounter.addAndGet(-drainCount); - - if(mOverflow.get() && size <= mResetThreshold) - { - setOverflow(false); - } - - return drainCount; - } - - /** - * Sets a listener to receive overflow state change events. - */ - public void setOverflowListener(IOverflowListener listener) - { - mOverflowListener = listener; - } - - /** - * Sets the source to receive overflow state change events (in addition to an IOverflow listener) - */ - public void setSourceOverflowListener(Source source) - { - mSourceOverflowListener = source; - } - - - /** - * Toggles the overflow state and broadcast state change to listener - */ - private void setOverflow(boolean overflow) - { - if(mOverflow.compareAndSet(!overflow, overflow)) - { - if(mOverflowListener != null) - { - mOverflowListener.sourceOverflow(overflow); - } - - if(mSourceOverflowListener != null) - { - mSourceOverflowListener.broadcastOverflowState(overflow); - } - } - } - - /** - * Clears all elements from the queue and resets the internal counter to 0 - */ - public void clear() - { - synchronized(mQueue) - { - mQueue.clear(); - mCounter.set(0); - mOverflow.set(false); - } - } -} diff --git a/src/sample/SampleType.java b/src/sample/SampleType.java deleted file mode 100644 index 8aedd8e2..00000000 --- a/src/sample/SampleType.java +++ /dev/null @@ -1,7 +0,0 @@ -package sample; - -public enum SampleType -{ - COMPLEX, // I/Q - REAL; // Real -} diff --git a/src/source/ComplexSource.java b/src/source/ComplexSource.java deleted file mode 100644 index b864aae8..00000000 --- a/src/source/ComplexSource.java +++ /dev/null @@ -1,32 +0,0 @@ -/******************************************************************************* - * 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 source; - -import sample.Provider; -import sample.SampleType; -import sample.complex.ComplexBuffer; - -public abstract class ComplexSource extends Source implements Provider -{ - @Override - public SampleType getSampleType() - { - return SampleType.COMPLEX; - } -} diff --git a/src/source/InvalidFrequencyException.java b/src/source/InvalidFrequencyException.java deleted file mode 100644 index e60a8843..00000000 --- a/src/source/InvalidFrequencyException.java +++ /dev/null @@ -1,64 +0,0 @@ -/******************************************************************************* - * sdrtrunk - * Copyright (C) 2014-2016 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 source; - -public class InvalidFrequencyException extends SourceException -{ - private long mInvalidFrequency; - private long mValidFrequency; - - /** - * Thrown when the user or the application requests to tune to an invalid frequency. Invalid means that the - * frequency is outside of the tunable range of the current tuner device. This class provides both the requested - * invalid frequency and the suggested valid frequency to use in lieu of the requested invalid frequency. - * @param message optional text - * @param invalidFrequency that was requested - * @param validFrequency that should be used instead - */ - public InvalidFrequencyException( String message, long invalidFrequency, long validFrequency ) - { - super( message ); - - mInvalidFrequency = invalidFrequency; - mValidFrequency = validFrequency; - } - - /** - * The invalid requested frequency - */ - public long getInvalidFrequency() - { - return mInvalidFrequency; - } - - /** - * The valid frequency that should be used - */ - public long getValidFrequency() - { - return mValidFrequency; - } - - @Override - public String getMessage() - { - return super.getMessage() + " Invalid Frequency: " + getInvalidFrequency() + - " - current valid frequency:" + getValidFrequency(); - } -} diff --git a/src/source/RealSource.java b/src/source/RealSource.java deleted file mode 100644 index 8b08ebca..00000000 --- a/src/source/RealSource.java +++ /dev/null @@ -1,32 +0,0 @@ -/******************************************************************************* - * 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 source; - -import sample.Provider; -import sample.SampleType; -import sample.real.RealBuffer; - -public abstract class RealSource extends Source implements Provider -{ - @Override - public SampleType getSampleType() - { - return SampleType.REAL; - } -} diff --git a/src/source/Source.java b/src/source/Source.java deleted file mode 100644 index 16a8caeb..00000000 --- a/src/source/Source.java +++ /dev/null @@ -1,89 +0,0 @@ -/******************************************************************************* - * 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 source; - -import source.heartbeat.HeartbeatManager; -import source.heartbeat.IHeartbeatProvider; -import module.Module; -import sample.SampleType; -import sample.real.IOverflowListener; - -/** - * Abstract class to define the minimum functionality of a sample data provider. - */ -public abstract class Source extends Module implements ISourceEventListener, ISourceEventProvider, IHeartbeatProvider -{ - private HeartbeatManager mHeartbeatManager = new HeartbeatManager(); - - /** - * Heartbeat manager for this source - */ - public HeartbeatManager getHeartbeatManager() - { - return mHeartbeatManager; - } - - - protected IOverflowListener mOverflowListener; - - /** - * Indicates the type of samples provided by this source: real or complex - */ - public abstract SampleType getSampleType(); - - /** - * Sample rate provided by this source - * - * @throws SourceException if there is an issue determining the sample rate for this source - */ - public abstract double getSampleRate(); - - /** - * Center frequency for this source in Hertz - * - * @throws SourceException if there is an issue in determining the center frequency for this source - */ - public abstract long getFrequency(); - - /** - * Process any cleanup actions to prepare for garbage collection of this source - */ - public abstract void dispose(); - - /** - * Registers the listener to receive overflow state changes. Use null argument to clear the listener - */ - public void setOverflowListener(IOverflowListener listener) - { - mOverflowListener = listener; - } - - /** - * Broadcasts an overflow state - * - * @param overflow true if overflow, false if normal - */ - public void broadcastOverflowState(boolean overflow) - { - if(mOverflowListener != null) - { - mOverflowListener.sourceOverflow(overflow); - } - } -} diff --git a/src/source/config/SourceConfigRecording.java b/src/source/config/SourceConfigRecording.java deleted file mode 100644 index d92b4fe5..00000000 --- a/src/source/config/SourceConfigRecording.java +++ /dev/null @@ -1,70 +0,0 @@ -/******************************************************************************* - * 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 source.config; - -import source.SourceType; -import source.tuner.channel.TunerChannel; - -import javax.xml.bind.annotation.XmlAttribute; - -public class SourceConfigRecording extends SourceConfiguration -{ - private String mRecordingAlias; - private long mFrequency; - - public SourceConfigRecording() - { - super(SourceType.RECORDING); - } - - @XmlAttribute(name = "recording_alias") - public String getRecordingAlias() - { - return mRecordingAlias; - } - - public void setRecordingAlias(String alias) - { - mRecordingAlias = alias; - } - - @XmlAttribute(name = "frequency") - public long getFrequency() - { - return mFrequency; - } - - public void setFrequency(long frequency) - { - mFrequency = frequency; - } - - @Override - public String getDescription() - { - return getRecordingAlias(); - } - - public TunerChannel getTunerChannel() - { - /* Create a tuner channel with a default bandwidth of 12.5 kHz */ - return new TunerChannel(mFrequency, 12500); - } - -} diff --git a/src/source/config/SourceConfigTuner.java b/src/source/config/SourceConfigTuner.java deleted file mode 100644 index 8fe54732..00000000 --- a/src/source/config/SourceConfigTuner.java +++ /dev/null @@ -1,68 +0,0 @@ -/******************************************************************************* - * 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 source.config; - -import source.SourceType; -import source.tuner.channel.TunerChannel; - -import javax.xml.bind.annotation.XmlAttribute; -import java.text.DecimalFormat; - -public class SourceConfigTuner extends SourceConfiguration -{ - private static DecimalFormat FREQUENCY_FORMAT = new DecimalFormat("0.00000"); - - private long mFrequency = 0; - private int mBandwidth = 12500; - - public SourceConfigTuner() - { - super(SourceType.TUNER); - } - - public SourceConfigTuner(TunerChannel tunerChannel) - { - this(); - - mFrequency = tunerChannel.getFrequency(); - mBandwidth = tunerChannel.getBandwidth(); - } - - @XmlAttribute(name = "frequency") - public long getFrequency() - { - return mFrequency; - } - - public void setFrequency(long frequency) - { - mFrequency = frequency; - } - - @Override - public String getDescription() - { - return FREQUENCY_FORMAT.format((double)mFrequency / 1000000.0d) + " MHz"; - } - - public TunerChannel getTunerChannel() - { - return new TunerChannel(mFrequency, mBandwidth); - } -} diff --git a/src/source/heartbeat/IHeartbeatListener.java b/src/source/heartbeat/IHeartbeatListener.java deleted file mode 100644 index 296860a7..00000000 --- a/src/source/heartbeat/IHeartbeatListener.java +++ /dev/null @@ -1,26 +0,0 @@ -/******************************************************************************* - * 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 source.heartbeat; - -import sample.Listener; - -public interface IHeartbeatListener -{ - public Listener getHeartbeatListener(); -} diff --git a/src/source/mixer/RealMixerSource.java b/src/source/mixer/RealMixerSource.java deleted file mode 100644 index 83d72d3d..00000000 --- a/src/source/mixer/RealMixerSource.java +++ /dev/null @@ -1,306 +0,0 @@ -/******************************************************************************* - * SDR Trunk - * Copyright (C) 2014 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 source.mixer; - -import source.heartbeat.Heartbeat; -import org.slf4j.Logger; -import org.slf4j.LoggerFactory; -import sample.Listener; -import sample.adapter.ISampleAdapter; -import sample.real.RealBuffer; -import source.RealSource; -import source.SourceException; -import source.SourceEvent; - -import javax.sound.sampled.AudioFormat; -import javax.sound.sampled.AudioSystem; -import javax.sound.sampled.LineUnavailableException; -import javax.sound.sampled.TargetDataLine; -import java.util.Iterator; -import java.util.concurrent.CopyOnWriteArrayList; -import java.util.concurrent.ScheduledExecutorService; -import java.util.concurrent.atomic.AtomicBoolean; - -public class RealMixerSource extends RealSource -{ - private final static Logger mLog = LoggerFactory.getLogger(RealMixerSource.class); - - private long mFrequency = 0; - private int mBufferSize = 16384; - - private BufferReader mBufferReader = new BufferReader(); - private TargetDataLine mTargetDataLine; - private AudioFormat mAudioFormat; - private int mBytesPerFrame = 0; - private ISampleAdapter mSampleAdapter; - private Listener mHeartbeatListener; - private static final Heartbeat HEARTBEAT = new Heartbeat(); - - private CopyOnWriteArrayList> mSampleListeners = new CopyOnWriteArrayList>(); - - /** - * Real Mixer Source - constructs a reader on the mixer/sound card target - * data line using the specified audio format (sample size, sample rate ) - * and broadcasts real buffers (float arrays) to all registered listeners. - * Reads buffers sized to 10% of the sample rate specified in audio format. - * - * @param targetDataLine - mixer or sound card to be used - * @param format - audio format - * @param sampleAdapter - adapter to convert byte array data read from the - * mixer into float array data. Can optionally invert the channel data if - * the left/right stereo channels are inverted. - */ - public RealMixerSource(TargetDataLine targetDataLine, - AudioFormat format, - ISampleAdapter sampleAdapter) - { - mTargetDataLine = targetDataLine; - mAudioFormat = format; - mSampleAdapter = sampleAdapter; - } - - @Override - public void setSourceEventListener(Listener listener) - { - //Not implemented - } - - @Override - public void removeSourceEventListener() - { - //Not implemented - } - - @Override - public Listener getSourceEventListener() - { - //Not implemented - return null; - } - - @Override - public void reset() - { - } - - @Override - public void start() - { - } - - @Override - public void stop() - { - } - - public void setListener(Listener listener) - { - mSampleListeners.add(listener); - - /* If this is the first listener, start the reader thread */ - if(!mBufferReader.isRunning()) - { - Thread thread = new Thread(mBufferReader); - thread.setDaemon(true); - thread.setName("Sample Reader"); - thread.start(); - } - } - - public void removeListener(Listener listener) - { - mSampleListeners.remove(listener); - - /* If this is the laster listener, stop the reader thread */ - if(mSampleListeners.isEmpty()) - { - mBufferReader.stop(); - } - } - - public void broadcast(RealBuffer samples) - { - Iterator> it = mSampleListeners.iterator(); - - while(it.hasNext()) - { - Listener next = it.next(); - - /* if this is the last (or only) listener, send him the original - * buffer, otherwise send him a copy of the buffer */ - if(it.hasNext()) - { - next.receive(samples.copyOf()); - } - else - { - next.receive(samples); - } - } - } - - public double getSampleRate() - { - if(mTargetDataLine != null) - { - return mTargetDataLine.getFormat().getSampleRate(); - } - else - { - return 0; - } - } - - /** - * Returns the frequency of this source. Default is 0. - */ - public long getFrequency() - { - return mFrequency; - } - - /** - * Specify the frequency that will be returned from this source. This may - * be useful if you are streaming an external audio source in through the - * sound card and you want to specify a frequency for that source - */ - public void setFrequency(long frequency) - { - mFrequency = frequency; - } - - /** - * Reader thread. Performs blocking read against the mixer target data - * line, converts the samples to an array of floats using the specified - * adapter. Dispatches float arrays to all registered listeners. - */ - public class BufferReader implements Runnable - { - private AtomicBoolean mRunning = new AtomicBoolean(); - - @Override - public void run() - { - if(mRunning.compareAndSet(false, true)) - { - //Send a heartbeat every time we run - if(mHeartbeatListener != null) - { - mHeartbeatListener.receive(HEARTBEAT); - } - - if(mTargetDataLine == null) - { - mRunning.set(false); - - mLog.error("ComplexMixerSource - mixer target data line" - + " is null"); - } - else - { - mBytesPerFrame = mAudioFormat.getSampleSizeInBits() / 8; - - if(mBytesPerFrame == AudioSystem.NOT_SPECIFIED) - { - mBytesPerFrame = 2; - } - - /* Set buffer size to 1/10 second of samples */ - mBufferSize = (int)(mAudioFormat.getSampleRate() - * 0.05) * mBytesPerFrame; - - /* We'll reuse the same buffer for each read */ - byte[] buffer = new byte[mBufferSize]; - - try - { - mTargetDataLine.open(mAudioFormat); - - mTargetDataLine.start(); - } - catch(LineUnavailableException e) - { - mLog.error("ComplexMixerSource - mixer target data line" - + "not available to read data from", e); - - mRunning.set(false); - } - - while(mRunning.get()) - { - try - { - /* Blocking read - waits until the buffer fills */ - mTargetDataLine.read(buffer, 0, buffer.length); - - /* Convert samples to float array */ - float[] samples = - mSampleAdapter.convert(buffer); - - /* Dispatch samples to registered listeners */ - broadcast(new RealBuffer(samples)); - } - catch(Exception e) - { - mLog.error("ComplexMixerSource - error while reading" - + "from the mixer target data line", e); - - mRunning.set(false); - } - } - } - - /* Close the data line if it is still open */ - if(mTargetDataLine != null && mTargetDataLine.isOpen()) - { - mTargetDataLine.close(); - } - } - } - - /** - * Stops the reader thread - */ - public void stop() - { - if(mRunning.compareAndSet(true, false)) - { - mTargetDataLine.stop(); - mTargetDataLine.close(); - } - } - - /** - * Indicates if the reader thread is running - */ - public boolean isRunning() - { - return mRunning.get(); - } - } - - @Override - public void dispose() - { - if(mBufferReader != null) - { - mBufferReader.stop(); - mSampleListeners.clear(); - } - } -} diff --git a/src/source/recording/Recording.java b/src/source/recording/Recording.java deleted file mode 100644 index 938dc6a6..00000000 --- a/src/source/recording/Recording.java +++ /dev/null @@ -1,117 +0,0 @@ -package source.recording; - -import java.io.File; -import java.util.ArrayList; -import java.util.concurrent.RejectedExecutionException; - -import settings.SettingsManager; -import source.SourceException; -import source.tuner.ITunerChannelProvider; -import source.tuner.channel.TunerChannel; -import source.tuner.channel.TunerChannelSource; -import source.SourceEvent; -import source.SourceEvent.Event; -import source.ISourceEventProcessor; - -public class Recording implements Comparable, - ISourceEventProcessor, - ITunerChannelProvider -{ - private RecordingConfiguration mConfiguration; - - private ArrayList mTunerChannels = - new ArrayList(); - - @SuppressWarnings( "unused" ) - private long mCenterFrequency; - - @SuppressWarnings( "unused" ) - private SettingsManager mSettingsManager; - - public Recording( SettingsManager settingsManager, - RecordingConfiguration configuration ) - { - mSettingsManager = settingsManager; - mConfiguration = configuration; - mCenterFrequency = mConfiguration.getCenterFrequency(); - } - - public void setAlias( String alias ) - { - mConfiguration.setAlias( alias ); -// mSettingsManager.save(); - } - - public void setRecordingFile( File file ) throws SourceException - { - if( hasChannels() ) - { - throw new SourceException( "Recording source - can't change " - + "recording file while channels are enabled against the " - + "current recording." ); - } - - mConfiguration.setFilePath( file.getAbsolutePath() ); -// mSettingsManager.save(); - } - - /** - * Indicates if this recording is currently playing/providing samples to - * tuner channel sources - */ - public boolean hasChannels() - { - return mTunerChannels.size() > 0; - } - - @Override - public TunerChannelSource getChannel( TunerChannel channel ) - throws RejectedExecutionException, SourceException - { - // TODO Auto-generated method stub - return null; - } - - @Override - public void releaseChannel( TunerChannelSource source ) - { - // TODO Auto-generated method stub - } - - public RecordingConfiguration getRecordingConfiguration() - { - return mConfiguration; - } - - public String toString() - { - return mConfiguration.getAlias(); - } - - @Override - public void process(SourceEvent event ) throws SourceException - { - if( event.getEvent() == Event.NOTIFICATION_FREQUENCY_CHANGE ) - { - long frequency = event.getValue().longValue(); - - mConfiguration.setCenterFrequency( frequency ); - -// mSettingsManager.save(); - - mCenterFrequency = frequency; - - for( TunerChannelSource channel: mTunerChannels ) - { - channel.process( event ); - } - } - } - - @Override - public int compareTo( Recording other ) - { - return getRecordingConfiguration().getAlias() - .compareTo( other.getRecordingConfiguration().getAlias() ); - } -} diff --git a/src/source/recording/RecordingSourceManager.java b/src/source/recording/RecordingSourceManager.java deleted file mode 100644 index d5fb4a69..00000000 --- a/src/source/recording/RecordingSourceManager.java +++ /dev/null @@ -1,146 +0,0 @@ -/******************************************************************************* - * SDR Trunk - * Copyright (C) 2014 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 source.recording; - -import java.util.ArrayList; -import java.util.Collections; -import java.util.List; - -import org.slf4j.Logger; -import org.slf4j.LoggerFactory; - -import settings.SettingsManager; -import source.Source; -import source.SourceException; -import source.config.SourceConfigRecording; -import source.config.SourceConfiguration; -import source.tuner.channel.TunerChannel; -import source.tuner.channel.TunerChannelSource; - -public class RecordingSourceManager -{ - private final static Logger mLog = - LoggerFactory.getLogger( RecordingSourceManager.class ); - - private ArrayList mRecordings = new ArrayList(); - - private SettingsManager mSettingsManager; - - public RecordingSourceManager( SettingsManager settingsManager ) - { - mSettingsManager = settingsManager; - loadRecordings(); - } - - /** - * Iterates current recordings to get a tuner channel source for the frequency - * specified in the channel config's source config object - */ - public Source getSource( SourceConfiguration config, int bandwidth ) - throws SourceException - { - TunerChannelSource retVal = null; - - if( config instanceof SourceConfigRecording ) - { - SourceConfigRecording configRecording = (SourceConfigRecording)config; - - TunerChannel tunerChannel = configRecording.getTunerChannel(); - - tunerChannel.setBandwidth( bandwidth ); - - Recording recording = getRecordingFromAlias( - configRecording.getRecordingAlias() ); - - if( recording != null ) - { - retVal = recording.getChannel( tunerChannel ); - } - else - { - throw new SourceException( "Recording with alias name [" + - configRecording.getRecordingAlias() + "] is not currently available" ); - } - } - - return retVal; - } - - public Recording getRecordingFromAlias( String alias ) - { - for( Recording recording: mRecordings ) - { - if( recording.getRecordingConfiguration() - .getAlias().contentEquals( alias ) ) - { - return recording; - } - } - - return null; - } - - /** - * Get list of loaded IQ files - */ - public List getRecordings() - { - Collections.sort( mRecordings ); - - return mRecordings; - } - - public void addRecording( Recording recording ) - { - mRecordings.add( recording ); - } - - public Recording addRecording( RecordingConfiguration config ) - { - Recording recording = new Recording( mSettingsManager, config ); - - mRecordings.add( recording ); - - return recording; - } - - public void removeRecording( Recording recording ) - { - /* Remove the config from the settings manager */ - mSettingsManager.removeRecordingConfiguration( - recording.getRecordingConfiguration() ); - - mRecordings.remove( recording ); - } - - private void loadRecordings() - { - List recordingConfigurations = - mSettingsManager.getRecordingConfigurations(); - - mLog.info( "RecordingSourceManager - discovered [" + - recordingConfigurations.size() + "] recording configurations" ); - - for( RecordingConfiguration config: recordingConfigurations ) - { - Recording recording = new Recording( mSettingsManager, config ); - - mRecordings.add( recording ); - } - } -} diff --git a/src/source/tuner/ITunerChannelProvider.java b/src/source/tuner/ITunerChannelProvider.java deleted file mode 100644 index 17558dbc..00000000 --- a/src/source/tuner/ITunerChannelProvider.java +++ /dev/null @@ -1,46 +0,0 @@ -/******************************************************************************* - * SDR Trunk - * Copyright (C) 2014-2016 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 source.tuner; - -import java.util.concurrent.RejectedExecutionException; - -import source.SourceException; -import source.tuner.channel.TunerChannel; -import source.tuner.channel.TunerChannelSource; - -public interface ITunerChannelProvider -{ - /** - * Returns a tuner frequency channel source, tuned to the correct frequency - * - * @param frequency - desired frequency - * - * @return - source for 48k sample rate - */ - public abstract TunerChannelSource getChannel(TunerChannel channel ) - throws RejectedExecutionException, SourceException; - - /** - * Releases the tuned channel resources - * - * @param channel - previously obtained tuner channel - */ - public abstract void releaseChannel( TunerChannelSource source ); - - -} diff --git a/src/source/tuner/Tuner.java b/src/source/tuner/Tuner.java deleted file mode 100644 index ee475f4e..00000000 --- a/src/source/tuner/Tuner.java +++ /dev/null @@ -1,148 +0,0 @@ -/******************************************************************************* - * 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 source.tuner; - -import org.slf4j.Logger; -import org.slf4j.LoggerFactory; -import sample.Broadcaster; -import sample.Listener; -import source.ISourceEventProcessor; -import source.SourceEvent; - -/** - * Tuner - provides tuner channel sources, representing a channel frequency - */ -public abstract class Tuner -{ - private final static Logger mLog = LoggerFactory.getLogger(Tuner.class); - - protected Broadcaster mTunerEventBroadcaster = new Broadcaster<>(); - private TunerController mTunerController; - private String mName; - - /** - * Abstract tuner class. - * @param name of the tuner - * @param tunerController for the tuner - */ - public Tuner(String name, TunerController tunerController) - { - mName = name; - mTunerController = tunerController; - - //Rebroadcast frequency and sample rate change events as tuner events - mTunerController.addListener(new ISourceEventProcessor() - { - @Override - public void process(SourceEvent event) - { - switch(event.getEvent()) - { - case NOTIFICATION_FREQUENCY_CHANGE: - broadcast(new TunerEvent(Tuner.this, source.tuner.TunerEvent.Event.FREQUENCY)); - break; - case NOTIFICATION_SAMPLE_RATE_CHANGE: - broadcast(new TunerEvent(Tuner.this, source.tuner.TunerEvent.Event.SAMPLE_RATE)); - break; - default: - break; - } - } - }); - } - - public TunerController getTunerController() - { - return mTunerController; - } - - public String toString() - { - return mName; - } - - public void dispose() - { - } - - public void setName(String name) - { - mName = name; - } - - /** - * Unique identifier for this tuner, used to lookup a tuner configuration - * from the settings manager. - * - * @return - unique identifier like a serial number, or a usb bus location - * or ip address and port. Return some form of unique identification that - * allows this tuner to be identified from among several of the same types - * of tuners. - */ - public abstract String getUniqueID(); - - /** - * @return - tuner class enum entry - */ - public abstract TunerClass getTunerClass(); - - /** - * @return - tuner type enum entry - */ - public abstract TunerType getTunerType(); - - /** - * Name of this tuner object - * - * @return - string name of this tuner object - */ - public String getName() - { - return mName; - } - - /** - * Sample size in bits - */ - public abstract double getSampleSize(); - - /** - * Registers the listener - */ - public void addTunerChangeListener(Listener listener) - { - mTunerEventBroadcaster.addListener(listener); - } - - /** - * Removes the registered listener - */ - public void removeTunerChangeListener(Listener listener) - { - mTunerEventBroadcaster.removeListener(listener); - } - - /** - * Broadcasts the tuner change event - */ - public void broadcast(TunerEvent tunerEvent) - { - mTunerEventBroadcaster.broadcast(tunerEvent); - } -} diff --git a/src/source/tuner/TunerClass.java b/src/source/tuner/TunerClass.java deleted file mode 100644 index 9d898cda..00000000 --- a/src/source/tuner/TunerClass.java +++ /dev/null @@ -1,278 +0,0 @@ -/******************************************************************************* - * SDR Trunk - * Copyright (C) 2014 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 source.tuner; - -import javax.usb.UsbDeviceDescriptor; - -public enum TunerClass -{ - AIRSPY( TunerType.AIRSPY_R820T, "1D50", "60A1", "Airspy", "Airspy" ), - GENERIC_2832( TunerType.RTL2832_VARIOUS, "0BDA", "2832", "RTL2832", "SDR" ), - GENERIC_2838( TunerType.RTL2832_VARIOUS, "0BDA", "2838", "RTL2832", "SDR" ), - COMPRO_VIDEOMATE_U620F( TunerType.ELONICS_E4000, "185B", "0620", "Compro", "Videomate U620F" ), - COMPRO_VIDEOMATE_U650F( TunerType.ELONICS_E4000, "185B", "0650", "Compro", "Videomate U620F" ), - COMPRO_VIDEOMATE_U680F( TunerType.ELONICS_E4000, "185B", "0680", "Compro", "Videomate U620F" ), - DEXATEK_LOGILINK_VG002A( TunerType.FCI_FC2580, "1D19", "1101", "Dexatek", "Logilink VG0002A" ), - DEXATEK_DIGIVOX_MINI_II_REV3( TunerType.FCI_FC2580, "1D19", "1102", "Dexatek", "MSI Digivox Mini II v3.0" ), - DEXATEK_5217_DVBT( TunerType.FCI_FC2580, "1D19", "1103", "Dexatek", "5217 DVB-T" ), - ETTUS_USRP_B100( TunerType.ETTUS_VARIOUS, "2500", "0002", "Ettus Research", "USRP B100" ), - FUNCUBE_DONGLE_PRO( TunerType.FUNCUBE_DONGLE_PRO, "04D8", "FB56", "Hamlincrest", "Funcube Dongle Pro" ), - FUNCUBE_DONGLE_PRO_PLUS( TunerType.FUNCUBE_DONGLE_PRO_PLUS, "04D8", "FB31", "Hamlincrest", "Funcube Dongle Pro Plus" ), - GIGABYTE_GTU7300( TunerType.FITIPOWER_FC0012, "1B80", "D393", "Gigabyte", "GT-U7300" ), - GTEK_T803( TunerType.FITIPOWER_FC0012, "1F4D", "B803", "GTek", "T803" ), - HACKRF_ONE( TunerType.HACKRF, "1D50", "6089", "Great Scott Gadgets", "HackRF One" ), - RAD1O( TunerType.HACKRF, "1D50", "CC15", "Munich hackerspace", "Rad1o" ), - LIFEVIEW_LV5T_DELUXE( TunerType.FITIPOWER_FC0012, "1F4D", "C803", "Liveview", "LV5T Deluxe" ), - MYGICA_TD312( TunerType.FITIPOWER_FC0012, "1F4D", "D286", "MyGica", "TD312" ), - PEAK_102569AGPK( TunerType.FITIPOWER_FC0012, "1B80", "D395", "Peak", "102569AGPK" ), - PROLECTRIX_DV107669( TunerType.FITIPOWER_FC0012, "1F4D", "D803", "Prolectrix", "DV107669" ), - SVEON_STV20( TunerType.FITIPOWER_FC0012, "1B80", "D39D", "Sveon", "STV20 DVB-T USB & FM" ), - TERRATEC_CINERGY_T_REV1( TunerType.FITIPOWER_FC0012, "0CCD", "00A9", "Terratec", "Cinergy T R1" ), - TERRATEC_CINERGY_T_REV3( TunerType.ELONICS_E4000, "0CCD", "00D3", "Terratec", "Cinergy T R3" ), - TERRATEC_NOXON_REV1_B3( TunerType.FITIPOWER_FC0013, "0CCD", "00B3", "Terratec", "NOXON R1 (B3)" ), - TERRATEC_NOXON_REV1_B4( TunerType.FITIPOWER_FC0013, "0CCD", "00B4", "Terratec", "NOXON R1 (B4)" ), - TERRATEC_NOXON_REV1_B7( TunerType.FITIPOWER_FC0013, "0CCD", "00B7", "Terratec", "NOXON R1 (B7)" ), - TERRATEC_NOXON_REV1_C6( TunerType.FITIPOWER_FC0013, "0CCD", "00C6", "Terratec", "NOXON R1 (C6)" ), - TERRATEC_NOXON_REV2( TunerType.ELONICS_E4000, "0CCD", "00E0", "Terratec", "NOXON R2" ), - TERRATEC_T_STICK_PLUS( TunerType.ELONICS_E4000, "0CCD", "00D7", "Terratec", "T Stick Plus" ), - TWINTECH_UT40( TunerType.FITIPOWER_FC0013, "1B80", "D3A4", "Twintech", "UT-40" ), - ZAAPA_ZTMINDVBZP( TunerType.FITIPOWER_FC0012, "1B80", "D398", "Zaapa", "ZT-MINDVBZP" ), - TEST_TUNER(TunerType.TEST, "0", "0", "ABC Tuners Inc.", "Model XYZ"), - UNKNOWN( TunerType.UNKNOWN, "0", "0", "Unknown Manufacturer", "Unknown Device" ); - - private TunerType mTunerType; - private String mVendorID; - private String mDeviceID; - private String mVendorDescription; - private String mDeviceDescription; - - private TunerClass( TunerType tunerType, - String vendorID, - String deviceID, - String vendorDescription, - String deviceDescription ) - { - mTunerType = tunerType; - mVendorID = vendorID; - mDeviceID = deviceID; - mVendorDescription = vendorDescription; - mDeviceDescription = deviceDescription; - } - - public String toString() - { - return "USB" + - " Tuner:" + mTunerType.toString() + - " Vendor:" + mVendorDescription + - " Device:" + mDeviceDescription + - " Address:" + mVendorID + ":" + mDeviceID; - } - - public String getVendorDeviceLabel() - { - return mVendorDescription + " " + mDeviceDescription; - } - - public TunerType getTunerType() - { - return mTunerType; - } - - public static TunerClass valueOf( UsbDeviceDescriptor descriptor ) - { - return valueOf( descriptor.idVendor(), descriptor.idProduct() ); - } - - public static TunerClass valueOf( short vendor, short product ) - { - TunerClass retVal = TunerClass.UNKNOWN; - - //Cast the short to integer so that we can switch on unsigned numbers - int vendorID = vendor & 0xFFFF; - int productID = product & 0xFFFF; - - switch( vendorID ) - { - case 1240: //04DA - if( productID == 64305 ) //FB31 - { - retVal = FUNCUBE_DONGLE_PRO_PLUS; - } - else if( productID == 64342 ) //FB56 - { - retVal = FUNCUBE_DONGLE_PRO; - } - break; - case 3034: //0BDA - if( productID == 10290 ) //2832 - { - retVal = GENERIC_2832; - } - else if( productID == 10296 ) //2838 - { - retVal = GENERIC_2838; - } - break; - case 6235: //185B - if( productID == 1568 ) //0620 - { - retVal = COMPRO_VIDEOMATE_U620F; - } - else if( productID == 1616 ) //0650 - { - retVal = COMPRO_VIDEOMATE_U650F; - } - else if( productID == 1664 ) //0680 - { - retVal = COMPRO_VIDEOMATE_U680F; - } - break; - case 7040: //1B80 - if( productID == 54163 ) //D393 - { - retVal = GIGABYTE_GTU7300; - } - else if( productID == 54165 ) //D395 - { - retVal = PEAK_102569AGPK; - } - else if( productID == 54168 ) //D398 - { - retVal = ZAAPA_ZTMINDVBZP; - } - else if( productID == 54173 ) //D39D - { - retVal = SVEON_STV20; - } - else if( productID == 54180 ) //D3A4 - { - retVal = TWINTECH_UT40; - } - break; - case 7449: //1D19 - if( productID == 4353 ) //1101 - { - retVal = DEXATEK_LOGILINK_VG002A; - } - else if( productID == 4354 ) //1102 - { - retVal = DEXATEK_DIGIVOX_MINI_II_REV3; - } - else if( productID == 4355 ) //1103 - { - retVal = DEXATEK_5217_DVBT; - } - break; - case 7504: //1D50 - if( productID == 24713 ) //6089 - { - retVal = HACKRF_ONE; - } - else if( productID == 24737 ) //60A1 - { - retVal = AIRSPY; - } - else if( productID == 52245 ) //CC15 - { - retVal = HACKRF_ONE; - } - break; - case 8013: //1F4D - if( productID == 47107 ) //B803 - { - retVal = GTEK_T803; - } - else if( productID == 51203 ) //C803 - { - retVal = LIFEVIEW_LV5T_DELUXE; - } - else if( productID == 53894 ) //D286 - { - retVal = MYGICA_TD312; - } - else if( productID == 55299 ) //D803 - { - retVal = PROLECTRIX_DV107669; - } - break; - case 3277: //0CCD - if( productID == 169 ) //00A9 - { - retVal = TERRATEC_CINERGY_T_REV1; - } - else if( productID == 179 ) //00B3 - { - retVal = TERRATEC_NOXON_REV1_B3; - } - else if( productID == 180 ) //00B4 - { - retVal = TERRATEC_NOXON_REV1_B4; - } - else if( productID == 181 ) //00B5 - { - retVal = TERRATEC_NOXON_REV1_B7; - } - else if( productID == 198 ) //00C6 - { - retVal = TERRATEC_NOXON_REV1_C6; - } - else if( productID == 211 ) //00D3 - { - retVal = TERRATEC_CINERGY_T_REV3; - } - else if( productID == 215 ) //00D7 - { - retVal = TERRATEC_T_STICK_PLUS; - } - else if( productID == 224) //00E0 - { - retVal = TERRATEC_NOXON_REV2; - } - break; - case 9472: //2500 - if( productID == 2 ) //0002 - { - retVal = ETTUS_USRP_B100; - } - break; - default: - } - - return retVal; - } - - public String getVendorID() - { - return mVendorID; - } - - public String getDeviceID() - { - return mDeviceID; - } - - public String getVendorDescription() - { - return mVendorDescription; - } - - public String getDeviceDescription() - { - return mDeviceDescription; - } -} diff --git a/src/source/tuner/TunerController.java b/src/source/tuner/TunerController.java deleted file mode 100644 index 24043844..00000000 --- a/src/source/tuner/TunerController.java +++ /dev/null @@ -1,202 +0,0 @@ -/******************************************************************************* - * SDR Trunk - * 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 source.tuner; - -import org.slf4j.Logger; -import org.slf4j.LoggerFactory; -import sample.Broadcaster; -import sample.Listener; -import sample.complex.ComplexBuffer; -import sample.complex.IComplexBufferProvider; -import source.ISourceEventListener; -import source.ISourceEventProcessor; -import source.SourceEvent; -import source.SourceEventListenerToProcessorAdapter; -import source.SourceException; -import source.tuner.channel.ChannelManager; -import source.tuner.channel.cic.CICTunerChannelSource; -import source.tuner.channel.TunerChannel; -import source.tuner.channel.polyphase.PolyphaseChannelManager; -import source.tuner.configuration.TunerConfiguration; -import source.tuner.channel.FrequencyController; -import source.tuner.channel.FrequencyController.Tunable; - -import java.util.SortedSet; -import java.util.concurrent.ConcurrentSkipListSet; - -public abstract class TunerController implements Tunable, ISourceEventProcessor, ISourceEventListener, - IComplexBufferProvider, Listener -{ - private final static Logger mLog = LoggerFactory.getLogger(TunerController.class); - protected Broadcaster mSampleBroadcaster = new Broadcaster<>(); - - /* List of currently tuned channels being served to demod channels */ - private SortedSet mTunedChannels = new ConcurrentSkipListSet<>(); - private FrequencyController mFrequencyController; - private ChannelManager mChannelManager; - private Listener mSourceEventListener; - - /** - * Abstract tuner controller class. The tuner controller manages frequency bandwidth and currently tuned channels - * that are being fed samples from the tuner. - * - * @param minimumFrequency minimum uncorrected tunable frequency - * @param maximumFrequency maximum uncorrected tunable frequency - * @param middleUnusable is the +/- value center DC spike to avoid for channels - * @param usableBandwidth percentage of usable bandwidth relative to space at the extreme ends of the spectrum - * @throws SourceException - for any issues related to constructing the - * class, tuning a frequency, or setting the bandwidth - */ - public TunerController(long minimumFrequency, long maximumFrequency, long middleUnusable, double usableBandwidth) - { - mFrequencyController = new FrequencyController(this, minimumFrequency, maximumFrequency, - 0.0d, middleUnusable, usableBandwidth); - - mChannelManager = new PolyphaseChannelManager(mFrequencyController, this); - - mSourceEventListener = new SourceEventListenerToProcessorAdapter(this); - } - - /** - * Frequency controller for managing frequency and sample rate. - */ - public FrequencyController getFrequencyController() - { - return mFrequencyController; - } - - /** - * Channel manager for managing Digital Drop Chanel (DDC) tuner channel sources - */ - public ChannelManager getChannelManager() - { - return mChannelManager; - } - - /** - * Implements the ISourceEventListener interface to receive requests from sample consumers - */ - @Override - public Listener getSourceEventListener() - { - return mSourceEventListener; - } - - /** - * Applies the settings in the tuner configuration - */ - public abstract void apply(TunerConfiguration config) throws SourceException; - - /** - * Responds to requests to set the frequency - */ - @Override - public void process(SourceEvent sourceEvent ) throws SourceException - { - switch(sourceEvent.getEvent()) - { - case REQUEST_FREQUENCY_CHANGE: - getFrequencyController().setFrequency( sourceEvent.getValue().longValue() ); - break; - case REQUEST_START_SAMPLE_STREAM: - if(sourceEvent.getSource() instanceof CICTunerChannelSource) - { - addComplexBufferListener((CICTunerChannelSource)sourceEvent.getSource()); - } - break; - case REQUEST_STOP_SAMPLE_STREAM: - if(sourceEvent.getSource() instanceof CICTunerChannelSource) - { - removeComplexBufferListener((CICTunerChannelSource)sourceEvent.getSource()); - } - break; - default: - mLog.error("Ignoring unrecognized source event: " + sourceEvent.getEvent().name() + " from [" + - sourceEvent.getSource().getClass() + "]" ); - } - } - @Override - public boolean canTune(long frequency) - { - return getFrequencyController().canTune(frequency); - } - - /** - * Sets the listener to be notified any time that the tuner changes frequency - * or bandwidth/sample rate. - * - * Note: this is normally used by the Tuner. Any additional listeners can - * be registered on the tuner. - */ - public void addListener( ISourceEventProcessor processor ) - { - mFrequencyController.addListener(processor); - } - - /** - * Removes the frequency change listener - */ - public void removeListener( ISourceEventProcessor processor ) - { - mFrequencyController.removeFrequencyChangeProcessor(processor); - } - - /** - * Adds the listener to receive complex buffer samples - */ - @Override - public void addComplexBufferListener(Listener listener) - { - mSampleBroadcaster.addListener(listener); - } - - /** - * Removes the listener from receiving complex buffer samples - */ - @Override - public void removeComplexBufferListener(Listener listener) - { - mSampleBroadcaster.removeListener(listener); - } - - /** - * Indicates if there are any complex buffer listeners registered on this controller - */ - @Override - public boolean hasComplexBufferListeners() - { - return mSampleBroadcaster.hasListeners(); - } - - /** - * Broadcasts the buffer to any registered listeners - */ - protected void broadcast(ComplexBuffer complexBuffer) - { - mSampleBroadcaster.broadcast(complexBuffer); - } - - /** - * Implements the Listener interface to receive and distribute complex buffers from subclass implementations - */ - @Override - public void receive(ComplexBuffer complexBuffer) - { - broadcast(complexBuffer); - } -} diff --git a/src/source/tuner/TunerEditor.java b/src/source/tuner/TunerEditor.java deleted file mode 100644 index f1ffa2d9..00000000 --- a/src/source/tuner/TunerEditor.java +++ /dev/null @@ -1,356 +0,0 @@ -/******************************************************************************* - * SDR Trunk - * Copyright (C) 2014-2016 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 source.tuner; - -import gui.control.JFrequencyControl; -import gui.editor.Editor; -import gui.editor.EmptyEditor; - -import java.awt.Dimension; -import java.awt.event.ActionEvent; -import java.awt.event.ActionListener; - -import javax.swing.JButton; -import javax.swing.JLabel; -import javax.swing.JOptionPane; -import javax.swing.JPanel; -import javax.swing.JScrollPane; -import javax.swing.JTable; -import javax.swing.RowFilter; -import javax.swing.ScrollPaneConstants; -import javax.swing.event.ListSelectionEvent; -import javax.swing.event.ListSelectionListener; -import javax.swing.table.TableRowSorter; - -import net.miginfocom.swing.MigLayout; - -import org.slf4j.Logger; -import org.slf4j.LoggerFactory; - -import source.tuner.configuration.TunerConfiguration; -import source.tuner.configuration.TunerConfigurationFactory; -import source.tuner.configuration.TunerConfigurationModel; - -import com.jidesoft.swing.JideSplitPane; - -public class TunerEditor extends Editor -{ - private static final long serialVersionUID = 1L; - - private final static Logger mLog = LoggerFactory.getLogger( TunerEditor.class ); - - private TunerConfigurationModel mTunerConfigurationModel; - private JTable mTunerConfigurationTable; - private TableRowSorter mRowSorter; - private JFrequencyControl mFrequencyControl = new JFrequencyControl(); - private JLabel mSelectedTunerType = new JLabel( "No Tuner Selected" ); - private JButton mNewConfigurationButton = new JButton( "New" ); - private JButton mDeleteConfigurationButton = new JButton( "Delete" ); - private JButton mAssignConfigurationButton = new JButton( "Assign" ); - private JScrollPane mEditorScroller; - private Editor mEditor = new EmptyEditor<>( "a tuner" ); - private JideSplitPane mEditorSplitPane = new JideSplitPane(); - - public TunerEditor( TunerConfigurationModel tunerConfigurationModel ) - { - mTunerConfigurationModel = tunerConfigurationModel; - init(); - } - - public void init() - { - setLayout( new MigLayout( "insets 0 0 0 0", "[grow,fill]", "[grow,fill]" ) ); - - JPanel listPanel = new JPanel(); - listPanel.setLayout( new MigLayout( "fill,wrap 3", "[grow,fill]", "[][][][grow,fill][]" ) ); - - listPanel.add( mSelectedTunerType, "span" ); - - mFrequencyControl.setEnabled( false ); - listPanel.add( mFrequencyControl, "span" ); - - mRowSorter = new TableRowSorter<>( mTunerConfigurationModel ); - mTunerConfigurationTable = new JTable( mTunerConfigurationModel ); - mTunerConfigurationTable.setRowSorter( mRowSorter ); - mTunerConfigurationTable.setEnabled( false ); - mTunerConfigurationTable.getSelectionModel().addListSelectionListener( new ListSelectionListener() - { - @Override - public void valueChanged( ListSelectionEvent event ) - { - if( !event.getValueIsAdjusting() ) - { - int row = mTunerConfigurationTable.getSelectedRow(); - - if( row >= 0 ) - { - int modelRow = mTunerConfigurationTable.convertRowIndexToModel( row ); - - if( modelRow >= 0 ) - { - setTunerConfiguration( mTunerConfigurationModel.getTunerConfiguration( modelRow ) ); - } - } - } - } - } ); - - JScrollPane tableScroller = new JScrollPane( mTunerConfigurationTable ); - tableScroller.setHorizontalScrollBarPolicy( - ScrollPaneConstants.HORIZONTAL_SCROLLBAR_NEVER ); - listPanel.add( new JLabel( "Tuner Configurations" ), "span" ); - listPanel.add( tableScroller, "span" ); - - mNewConfigurationButton.setEnabled( false ); - mNewConfigurationButton.setToolTipText( "Create a new configuration for the currently selected tuner" ); - mNewConfigurationButton.addActionListener( new ActionListener() - { - @Override - public void actionPerformed( ActionEvent e ) - { - Tuner tuner = getItem(); - - String name = mTunerConfigurationModel - .getDistinctName( tuner.getTunerType(), tuner.getUniqueID() ); - - TunerConfiguration config = TunerConfigurationFactory - .getTunerConfiguration( tuner.getTunerType(), - tuner.getUniqueID(), name ); - - mTunerConfigurationModel.addTunerConfiguration( config ); - } - } ); - listPanel.add( mNewConfigurationButton ); - - mDeleteConfigurationButton.setEnabled( false ); - mDeleteConfigurationButton.setToolTipText( "Deletes the currently selected tuner" ); - mDeleteConfigurationButton.addActionListener( new ActionListener() - { - @Override - public void actionPerformed( ActionEvent e ) - { - int choice = JOptionPane.showConfirmDialog( TunerEditor.this, - "Do you want to delete this tuner configuration?", - "Delete Tuner Configuration?", JOptionPane.YES_NO_OPTION ); - - if( choice == JOptionPane.YES_OPTION ) - { - TunerConfiguration selected = getSelectedTunerConfiguration(); - - mTunerConfigurationModel.removeTunerConfiguration( selected ); - mTunerConfigurationTable.setRowSelectionInterval( 0, 0 ); - } - } - } ); - listPanel.add( mDeleteConfigurationButton ); - - mAssignConfigurationButton.setEnabled( false ); - mAssignConfigurationButton.setToolTipText( "Assigns to the currently selected tuner configuration to the selected tuner" ); - mAssignConfigurationButton.addActionListener( new ActionListener() - { - @Override - public void actionPerformed( ActionEvent e ) - { - TunerConfiguration selected = getSelectedTunerConfiguration(); - - mTunerConfigurationModel.assignTunerConfiguration( selected ); - setTunerConfiguration( getSelectedTunerConfiguration() ); - } - } ); - listPanel.add( mAssignConfigurationButton ); - - //Set preferred size on both scrollers to same width, so they split the space - listPanel.setPreferredSize( new Dimension( 100, 80 ) ); - JScrollPane listScroller = new JScrollPane( listPanel ); - listScroller.setPreferredSize( new Dimension( 100, 80 ) ); - - mEditorScroller = new JScrollPane( mEditor ); - mEditorScroller.setPreferredSize( new Dimension( 100, 80 ) ); - - mEditorSplitPane = new JideSplitPane( JideSplitPane.HORIZONTAL_SPLIT ); - mEditorSplitPane.add( listScroller ); - mEditorSplitPane.add( mEditorScroller ); - - add( mEditorSplitPane ); - } - - private TunerConfiguration getSelectedTunerConfiguration() - { - int tableRow = mTunerConfigurationTable.getSelectedRow(); - - if( tableRow >= 0 ) - { - int modelRow = mTunerConfigurationTable.convertRowIndexToModel( tableRow ); - - if( modelRow >= 0 ) - { - return mTunerConfigurationModel.getTunerConfiguration( modelRow ); - } - } - - return null; - } - - @Override - public void save() - { - //Unused - } - - @Override - public void setItem( Tuner tuner ) - { - //Cleanup previously selected tuner - if( hasItem() ) - { - Tuner previous = getItem(); - previous.getTunerController().removeListener( mFrequencyControl ); - mFrequencyControl.removeListener( previous.getTunerController() ); - } - - super.setItem( tuner ); - - if( hasItem() ) - { - mSelectedTunerType.setText( tuner.getName() ); - mFrequencyControl.setEnabled( true ); - mTunerConfigurationTable.setEnabled( true ); - mNewConfigurationButton.setEnabled( true ); - - //Change to an editor for this config's tuner type - mEditor = TunerConfigurationFactory.getEditor( getItem(), mTunerConfigurationModel ); - - mRowSorter.setRowFilter( new ConfigurationRowFilter( tuner.getTunerType(), - tuner.getUniqueID() ) ); - - TunerConfiguration assigned = mTunerConfigurationModel - .getTunerConfiguration( tuner.getTunerType(), tuner.getUniqueID() ); - - int modelIndex = mTunerConfigurationModel.getRowIndex( assigned ); - - if( modelIndex >= 0 ) - { - int viewIndex = mTunerConfigurationTable - .convertRowIndexToView( modelIndex ); - - if( viewIndex >= 0 ) - { - mTunerConfigurationTable - .setRowSelectionInterval( viewIndex, viewIndex ); - } - } - - //Link frequency control to the tuner - mFrequencyControl.addListener( tuner.getTunerController() ); - - //Link tuner to frequency control - tuner.getTunerController().addListener( mFrequencyControl ); - - //Set the displayed frequency without adjusting the tuner's frequency - mFrequencyControl.setFrequency( - tuner.getTunerController().getFrequencyController().getFrequency(), false ); - } - else - { - mSelectedTunerType.setText( "No Tuner Selected" ); - mFrequencyControl.setEnabled( false ); - mTunerConfigurationTable.setEnabled( false ); - mNewConfigurationButton.setEnabled( false ); - mRowSorter.setRowFilter( null ); - mEditor = new EmptyEditor(); - } - - //Swap out the editor - int split = mEditorSplitPane.getDividerLocation( 0 ); - mEditorSplitPane.remove( mEditorScroller ); - mEditorScroller = new JScrollPane( mEditor ); - mEditorSplitPane.add( mEditorScroller ); - mEditorSplitPane.setDividerLocation( 0, split ); - } - - private void setTunerConfiguration( TunerConfiguration config ) - { - if( config != null ) - { - if( !config.isAssigned() ) - { - if( !mAssignConfigurationButton.isEnabled() ) - { - mAssignConfigurationButton.setEnabled( true ); - } - if( !mDeleteConfigurationButton.isEnabled() ) - { - mDeleteConfigurationButton.setEnabled( true ); - } - } - else - { - if( mAssignConfigurationButton.isEnabled() ) - { - mAssignConfigurationButton.setEnabled( false ); - } - - if( mDeleteConfigurationButton.isEnabled() ) - { - mDeleteConfigurationButton.setEnabled( false ); - } - } - } - else - { - if( mDeleteConfigurationButton.isEnabled() ) - { - mDeleteConfigurationButton.setEnabled( false ); - } - - if( mAssignConfigurationButton.isEnabled() ) - { - mAssignConfigurationButton.setEnabled( false ); - } - } - - mEditor.setItem( config ); - } - - public class ConfigurationRowFilter extends RowFilter - { - private TunerType mTunerType; - private String mUniqueID; - - public ConfigurationRowFilter( TunerType type, String uniqueID ) - { - mTunerType = type; - mUniqueID = uniqueID; - } - - @Override - public boolean include( javax.swing.RowFilter.Entry entry ) - { - TunerConfiguration config = entry.getModel() - .getTunerConfiguration( entry.getIdentifier() ); - - return config != null && - config.getTunerType() == mTunerType && - config.getUniqueID() != null && - config.getUniqueID().contentEquals( mUniqueID ); - } - } - -} diff --git a/src/source/tuner/TunerManager.java b/src/source/tuner/TunerManager.java deleted file mode 100644 index 12d228aa..00000000 --- a/src/source/tuner/TunerManager.java +++ /dev/null @@ -1,463 +0,0 @@ -/******************************************************************************* - * 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 source.tuner; - -import org.slf4j.Logger; -import org.slf4j.LoggerFactory; -import org.usb4java.Device; -import org.usb4java.DeviceDescriptor; -import org.usb4java.DeviceList; -import org.usb4java.LibUsb; -import source.SourceException; -import source.mixer.MixerManager; -import source.tuner.airspy.AirspyTuner; -import source.tuner.airspy.AirspyTunerController; -import source.tuner.fcd.FCDTuner; -import source.tuner.fcd.proV1.FCD1TunerController; -import source.tuner.fcd.proplusV2.FCD2TunerController; -import source.tuner.hackrf.HackRFTuner; -import source.tuner.hackrf.HackRFTunerController; -import source.tuner.rtl.RTL2832Tuner; -import source.tuner.rtl.RTL2832TunerController; -import source.tuner.rtl.e4k.E4KTunerController; -import source.tuner.rtl.r820t.R820TTunerController; - -import java.util.Collection; - -public class TunerManager -{ - private final static Logger mLog = LoggerFactory.getLogger(TunerManager.class); - - private MixerManager mMixerManager; - private TunerModel mTunerModel; - - - /** - * Application-wide LibUSB timeout processor for transfer buffers. All classes that need to use USB transfer - * buffers can register with this processor and the processor will auto-start and auto-stop while USB transfer - * processors are registered. - */ - public static final LibUSBTransferProcessor LIBUSB_TRANSFER_PROCESSOR; - - static - { - LIBUSB_TRANSFER_PROCESSOR = new LibUSBTransferProcessor(); - } - - public TunerManager(MixerManager mixerManager, TunerModel tunerModel) - { - mMixerManager = mixerManager; - mTunerModel = tunerModel; - - initTuners(); - } - - /** - * Performs cleanup of USB related issues - */ - public void dispose() - { - LibUsb.exit(null); - } - - /** - * Loads all USB tuners and USB/Mixer tuner devices - */ - private void initTuners() - { - DeviceList deviceList = new DeviceList(); - - int result = LibUsb.init(null); - - if(result != LibUsb.SUCCESS) - { - mLog.error("unable to initialize libusb [" + - LibUsb.errorName(result) + "]"); - } - else - { - mLog.info("LibUSB API Version: " + LibUsb.getApiVersion()); - mLog.info("LibUSB Version: " + LibUsb.getVersion()); - - result = LibUsb.getDeviceList(null, deviceList); - - if(result < 0) - { - mLog.error("unable to get device list from libusb [" + result + " / " + - LibUsb.errorName(result) + "]"); - } - else - { - mLog.info("discovered [" + result + "] attached USB devices"); - } - } - - for(Device device : deviceList) - { - DeviceDescriptor descriptor = new DeviceDescriptor(); - - result = LibUsb.getDeviceDescriptor(device, descriptor); - - if(result != LibUsb.SUCCESS) - { - mLog.error("unable to read device descriptor [" + - LibUsb.errorName(result) + "]"); - } - else - { - TunerInitStatus status = initTuner(device, descriptor); - - StringBuilder sb = new StringBuilder(); - - sb.append("usb device ["); - sb.append(String.format("%04X", descriptor.idVendor())); - sb.append(":"); - sb.append(String.format("%04X", descriptor.idProduct())); - - if(status.isLoaded()) - { - Tuner tuner = status.getTuner(); - - try - { - mTunerModel.addTuner(tuner); - sb.append("] LOADED: "); - sb.append(tuner.toString()); - } - catch(Exception e) - { - sb.append("] NOT LOADED: "); - sb.append(status.getInfo()); - sb.append(" Error:" + e.getMessage()); - } - } - else - { - sb.append("] NOT LOADED: "); - sb.append(status.getInfo()); - } - - mLog.info(sb.toString()); - } - } - - LibUsb.freeDeviceList(deviceList, true); - } - - private TunerInitStatus initTuner(Device device, - DeviceDescriptor descriptor) - { - if(device != null && descriptor != null) - { - TunerClass tunerClass = TunerClass.valueOf(descriptor.idVendor(), - descriptor.idProduct()); - - switch(tunerClass) - { - case AIRSPY: - return initAirspyTuner(device, descriptor); - case ETTUS_USRP_B100: - return initEttusB100Tuner(device, descriptor); - case FUNCUBE_DONGLE_PRO: - return initFuncubeProTuner(device, descriptor); - case FUNCUBE_DONGLE_PRO_PLUS: - return initFuncubeProPlusTuner(device, descriptor); - case HACKRF_ONE: - case RAD1O: - return initHackRFTuner(device, descriptor); - case COMPRO_VIDEOMATE_U620F: - case COMPRO_VIDEOMATE_U650F: - case COMPRO_VIDEOMATE_U680F: - case GENERIC_2832: - case GENERIC_2838: - case DEXATEK_5217_DVBT: - case DEXATEK_DIGIVOX_MINI_II_REV3: - case DEXATEK_LOGILINK_VG002A: - case GIGABYTE_GTU7300: - case GTEK_T803: - case LIFEVIEW_LV5T_DELUXE: - case MYGICA_TD312: - case PEAK_102569AGPK: - case PROLECTRIX_DV107669: - case SVEON_STV20: - case TERRATEC_CINERGY_T_REV1: - case TERRATEC_CINERGY_T_REV3: - case TERRATEC_NOXON_REV1_B3: - case TERRATEC_NOXON_REV1_B4: - case TERRATEC_NOXON_REV1_B7: - case TERRATEC_NOXON_REV1_C6: - case TERRATEC_NOXON_REV2: - case TERRATEC_T_STICK_PLUS: - case TWINTECH_UT40: - case ZAAPA_ZTMINDVBZP: - return initRTL2832Tuner(tunerClass, device, descriptor); - case UNKNOWN: - default: - break; - } - } - - return new TunerInitStatus(null, "Unknown Device"); - } - - private TunerInitStatus initAirspyTuner(Device device, - DeviceDescriptor descriptor) - { - try - { - AirspyTunerController airspyController = new AirspyTunerController(device); - - airspyController.init(); - - AirspyTuner tuner = new AirspyTuner(airspyController); - - return new TunerInitStatus(tuner, "LOADED"); - } - catch(SourceException se) - { - mLog.error("couldn't construct Airspy controller/tuner", se); - - return new TunerInitStatus(null, - "error constructing Airspy tuner controller"); - } - } - - - private TunerInitStatus initEttusB100Tuner(Device device, - DeviceDescriptor descriptor) - { - return new TunerInitStatus(null, "Ettus B100 tuner not currently " - + "supported"); - } - - private TunerInitStatus initFuncubeProTuner(Device device, DeviceDescriptor descriptor) - { - String reason = "NOT LOADED"; - - MixerTunerDataLine dataline = getMixerTunerDataLine(TunerClass.FUNCUBE_DONGLE_PRO.getTunerType()); - - if(dataline != null) - { - FCD1TunerController controller = new FCD1TunerController(dataline, device, descriptor); - - try - { - controller.init(); - - FCDTuner tuner = new FCDTuner(controller); - - return new TunerInitStatus(tuner, "LOADED"); - } - catch(SourceException e) - { - mLog.error("couldn't load funcube dongle pro tuner", e); - - reason = "error during initialization - " + e.getLocalizedMessage(); - } - } - else - { - reason = "couldn't find matching mixer dataline"; - } - - return new TunerInitStatus(null, "Funcube Dongle Pro tuner not " - + "loaded - " + reason); - } - - private TunerInitStatus initFuncubeProPlusTuner(Device device, DeviceDescriptor descriptor) - { - String reason = "NOT LOADED"; - - MixerTunerDataLine dataline = getMixerTunerDataLine(TunerClass.FUNCUBE_DONGLE_PRO_PLUS.getTunerType()); - - if(dataline != null) - { - FCD2TunerController controller = new FCD2TunerController(dataline, device, descriptor); - - try - { - controller.init(); - - FCDTuner tuner = new FCDTuner(controller); - - return new TunerInitStatus(tuner, "LOADED"); - } - catch(SourceException e) - { - mLog.error("couldn't load funcube dongle pro plus tuner", e); - - reason = "error during initialization - " + - e.getLocalizedMessage(); - } - } - else - { - reason = "couldn't find matching mixer dataline"; - } - - return new TunerInitStatus(null, "Funcube Dongle Pro tuner not " - + "loaded - " + reason); - } - - private TunerInitStatus initHackRFTuner(Device device, DeviceDescriptor descriptor) - { - try - { - HackRFTunerController hackRFController = new HackRFTunerController(device, descriptor); - - hackRFController.init(); - - HackRFTuner tuner = new HackRFTuner(hackRFController); - - return new TunerInitStatus(tuner, "LOADED"); - } - catch(SourceException se) - { - mLog.error("couldn't construct HackRF controller/tuner", se); - - return new TunerInitStatus(null, - "error constructing HackRF tuner controller"); - } - } - - private TunerInitStatus initRTL2832Tuner(TunerClass tunerClass, - Device device, - DeviceDescriptor deviceDescriptor) - { - String reason = "NOT LOADED"; - - TunerType tunerType = tunerClass.getTunerType(); - - if(tunerType == TunerType.RTL2832_VARIOUS) - { - try - { - tunerType = RTL2832TunerController.identifyTunerType(device); - } - catch(SourceException e) - { - mLog.error("couldn't determine RTL2832 tuner type", e); - tunerType = TunerType.UNKNOWN; - } - } - - switch(tunerType) - { - case ELONICS_E4000: - try - { - E4KTunerController controller = new E4KTunerController(device, deviceDescriptor); - - controller.init(); - - RTL2832Tuner rtlTuner = new RTL2832Tuner(tunerClass, controller); - - return new TunerInitStatus(rtlTuner, "LOADED"); - } - catch(SourceException se) - { - return new TunerInitStatus(null, "Error constructing E4K tuner controller - " + - se.getLocalizedMessage()); - } - case RAFAELMICRO_R820T: - try - { - R820TTunerController controller = new R820TTunerController(device, deviceDescriptor); - - controller.init(); - - RTL2832Tuner rtlTuner = new RTL2832Tuner(tunerClass, controller); - - return new TunerInitStatus(rtlTuner, "LOADED"); - } - catch(SourceException se) - { - mLog.error("error constructing tuner", se); - - return new TunerInitStatus(null, "Error constructing R820T " - + "tuner controller - " + se.getLocalizedMessage()); - } - case FITIPOWER_FC0012: - case FITIPOWER_FC0013: - case RAFAELMICRO_R828D: - case UNKNOWN: - default: - reason = "SDRTRunk doesn't currently support RTL2832 " - + "Dongle with [" + tunerType.toString() + - "] tuner for tuner class[" + tunerClass.toString() + "]"; - break; - } - - return new TunerInitStatus(null, reason); - } - - /** - * Gets the first tuner mixer dataline that corresponds to the tuner class. - * - * Note: this method is not currently able to align multiple tuner mixer - * data lines of the same tuner type. If you have multiple Funcube Dongle - * tuners of the same TYPE, there is no guarantee that you will get the - * correct mixer. - * - * @param tunerClass - * @return - */ - private MixerTunerDataLine getMixerTunerDataLine(TunerType tunerClass) - { - Collection datalines = - mMixerManager.getMixerTunerDataLines(); - - for(MixerTunerDataLine mixerTDL : datalines) - { - if(mixerTDL.getMixerTunerType().getTunerClass() == tunerClass) - { - return mixerTDL; - } - } - - return null; - } - - public class TunerInitStatus - { - private Tuner mTuner; - private String mInfo; - - public TunerInitStatus(Tuner tuner, String info) - { - mTuner = tuner; - mInfo = info; - } - - public Tuner getTuner() - { - return mTuner; - } - - public String getInfo() - { - return mInfo; - } - - public boolean isLoaded() - { - return mTuner != null; - } - } -} diff --git a/src/source/tuner/TunerModel.java b/src/source/tuner/TunerModel.java deleted file mode 100644 index 426ad719..00000000 --- a/src/source/tuner/TunerModel.java +++ /dev/null @@ -1,292 +0,0 @@ -package source.tuner; - -import org.slf4j.Logger; -import org.slf4j.LoggerFactory; -import sample.Listener; -import source.Source; -import source.SourceException; -import source.config.SourceConfigTuner; -import source.tuner.TunerEvent.Event; -import source.tuner.channel.TunerChannel; -import source.tuner.channel.TunerChannelSource; -import source.tuner.configuration.TunerConfiguration; -import source.tuner.configuration.TunerConfigurationModel; - -import javax.swing.table.AbstractTableModel; -import java.text.DecimalFormat; -import java.util.ArrayList; -import java.util.Iterator; -import java.util.List; - -public class TunerModel extends AbstractTableModel implements Listener -{ - private static final long serialVersionUID = 1L; - - private final static Logger mLog = LoggerFactory.getLogger(TunerModel.class); - - public static final int TUNER_TYPE = 0; - public static final int TUNER_ID = 1; - public static final int SAMPLE_RATE = 2; - public static final int FREQUENCY = 3; - public static final int CHANNEL_COUNT = 4; - public static final int SPECTRAL_DISPLAY_MAIN = 5; - public static final int SPECTRAL_DISPLAY_NEW = 6; - - private static final String MHZ = " MHz"; - private static final String[] COLUMNS = - {"Tuner", "ID", "Sample Rate", "Frequency", "Channels", "Spectral", "Display"}; - - private List mTuners = new ArrayList<>(); - private List> mTunerEventListeners = new ArrayList<>(); - private DecimalFormat mFrequencyFormat = new DecimalFormat("0.00000"); - private DecimalFormat mSampleRateFormat = new DecimalFormat("0.000"); - - private TunerConfigurationModel mTunerConfigurationModel; - - public TunerModel(TunerConfigurationModel tunerConfigurationModel) - { - mTunerConfigurationModel = tunerConfigurationModel; - } - - public TunerConfigurationModel getTunerConfigurationModel() - { - return mTunerConfigurationModel; - } - - /** - * List of Tuners currently in the model - */ - public List getTuners() - { - return mTuners; - } - - public Tuner getTuner(int index) - { - if(index < mTuners.size()) - { - return mTuners.get(index); - } - - return null; - } - - public void addTuners(List Tuners) - { - for(Tuner tuner : Tuners) - { - addTuner(tuner); - } - } - - /** - * Adds the Tuner to this model - */ - public void addTuner(Tuner tuner) - { - if(!mTuners.contains(tuner)) - { - //Get the tuner configuration and apply it to the tuner - this - //call should always produce a tuner configuration - TunerConfiguration config = mTunerConfigurationModel - .getTunerConfiguration(tuner.getTunerType(), tuner.getUniqueID()); - - try - { - tuner.getTunerController().apply(config); - - mTuners.add(tuner); - - int index = mTuners.indexOf(tuner); - - fireTableRowsInserted(index, index); - - tuner.addTunerChangeListener(this); - } - catch(SourceException se) - { - mLog.error("Couldn't apply tuner configuration to tuner - [" - + tuner.getTunerType().name() + "] with id [" - + tuner.getUniqueID() + "] - tuner will not be included"); - } - - } - } - - /** - * Removes the Tuner from this model - */ - public void removeTuner(Tuner tuner) - { - if(mTuners.contains(tuner)) - { - tuner.removeTunerChangeListener(this); - - int index = mTuners.indexOf(tuner); - - mTuners.remove(tuner); - - fireTableRowsDeleted(index, index); - } - } - - public void addListener(Listener listener) - { - mTunerEventListeners.add(listener); - } - - public void removeListener(Listener listener) - { - mTunerEventListeners.remove(listener); - } - - public void broadcast(TunerEvent event) - { - for(Listener listener : mTunerEventListeners) - { - listener.receive(event); - } - } - - /** - * Requests to display the first tuner in this model. Invoke this method - * after all listeners have registered and tuners have been added to this - * model, in order to inform the primary display to use the first tuner. - */ - public void requestFirstTunerDisplay() - { - if(mTuners.size() > 0) - { - broadcast(new TunerEvent(mTuners.get(0), - Event.REQUEST_MAIN_SPECTRAL_DISPLAY)); - } - } - - @Override - public void receive(TunerEvent event) - { - if(event.getTuner() != null) - { - int index = mTuners.indexOf(event.getTuner()); - - if(index >= 0) - { - switch(event.getEvent()) - { - case CHANNEL_COUNT: - fireTableCellUpdated(index, CHANNEL_COUNT); - break; - case FREQUENCY: - fireTableCellUpdated(index, FREQUENCY); - mTunerConfigurationModel.tunerFrequencyChanged(event.getTuner()); - break; - case SAMPLE_RATE: - fireTableCellUpdated(index, SAMPLE_RATE); - break; - case REQUEST_MAIN_SPECTRAL_DISPLAY: - fireTableCellUpdated(index, SPECTRAL_DISPLAY_MAIN); - default: - break; - } - } - } - - broadcast(event); - } - - @Override - public int getRowCount() - { - return mTuners.size(); - } - - @Override - public int getColumnCount() - { - return COLUMNS.length; - } - - @Override - public Object getValueAt(int rowIndex, int columnIndex) - { - if(rowIndex < mTuners.size()) - { - Tuner tuner = mTuners.get(rowIndex); - - switch(columnIndex) - { - case TUNER_TYPE: - return tuner.getTunerType().getLabel(); - case TUNER_ID: - return tuner.getUniqueID(); - case SAMPLE_RATE: - double sampleRate = tuner.getTunerController().getFrequencyController().getSampleRate(); - - return mSampleRateFormat.format(sampleRate / 1E6D) + MHZ; - case FREQUENCY: - try - { - long frequency = tuner.getTunerController().getFrequencyController().getFrequency(); - - return mFrequencyFormat.format(frequency / 1E6D) + MHZ; - } - catch(Exception e) - { - return 0; - } - case CHANNEL_COUNT: - return tuner.getTunerController().getChannelCount(); - case SPECTRAL_DISPLAY_MAIN: - return "Main"; - case SPECTRAL_DISPLAY_NEW: - return "New"; - default: - break; - } - } - - return null; - } - - @Override - public String getColumnName(int columnIndex) - { - return COLUMNS[columnIndex]; - } - - /** - * Iterates current tuners to get a tuner channel source for the frequency - * specified in the channel config's source config object. - * - * Returns null if no tuner can source the channel - */ - public Source getSource(SourceConfigTuner config, int bandwidth) - { - TunerChannelSource retVal = null; - - TunerChannel tunerChannel = config.getTunerChannel(); - - tunerChannel.setBandwidth(bandwidth); - - Iterator it = mTuners.iterator(); - - Tuner tuner; - - while(it.hasNext() && retVal == null) - { - tuner = it.next(); - - try - { - retVal = tuner.getSourceManager().getSource(tunerChannel); - } - catch(Exception e) - { - mLog.error("Error obtaining channel source from tuner [" + tuner.getName() + "]", e); - } - } - - return retVal; - } - -} diff --git a/src/source/tuner/TunerSourceEditor.java b/src/source/tuner/TunerSourceEditor.java deleted file mode 100644 index 8d92360f..00000000 --- a/src/source/tuner/TunerSourceEditor.java +++ /dev/null @@ -1,95 +0,0 @@ -/******************************************************************************* - * SDR Trunk - * Copyright (C) 2014 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 source.tuner; - -import gui.control.JFrequencyControl; -import gui.editor.Editor; -import net.miginfocom.swing.MigLayout; -import source.config.SourceConfigTuner; -import source.config.SourceConfiguration; -import source.SourceEvent; -import source.ISourceEventProcessor; -import controller.channel.Channel; - -public class TunerSourceEditor extends Editor -{ - private static final long serialVersionUID = 1L; - private JFrequencyControl mFrequencyControl; - - public TunerSourceEditor() - { - init(); - } - - private void init() - { - setLayout( new MigLayout( "insets 0 0 0 0", "[left]", "" ) ); - mFrequencyControl = new JFrequencyControl(); - mFrequencyControl.setEnabled( false ); - mFrequencyControl.addListener( new ISourceEventProcessor() - { - @Override - public void process(SourceEvent event ) - { - setModified( true ); - } - } ); - add( mFrequencyControl ); - } - - public void save() - { - if( hasItem() && isModified() ) - { - SourceConfigTuner config = new SourceConfigTuner(); - config.setFrequency( mFrequencyControl.getFrequency() ); - getItem().setSourceConfiguration( config ); - } - - setModified( false ); - } - - @Override - public void setItem( Channel item ) - { - super.setItem( item ); - - if( hasItem() ) - { - SourceConfiguration config = getItem().getSourceConfiguration(); - - mFrequencyControl.setEnabled( true ); - - if( config instanceof SourceConfigTuner ) - { - mFrequencyControl.setFrequency( ((SourceConfigTuner)config).getFrequency(), false ); - setModified( false ); - } - else - { - mFrequencyControl.setFrequency( 101000000, false ); - setModified( true ); - } - } - else - { - mFrequencyControl.setEnabled( false ); - setModified( false ); - } - } -} diff --git a/src/source/tuner/TunerType.java b/src/source/tuner/TunerType.java deleted file mode 100644 index 156c6dd6..00000000 --- a/src/source/tuner/TunerType.java +++ /dev/null @@ -1,49 +0,0 @@ -/******************************************************************************* - * SDR Trunk - * Copyright (C) 2014 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 source.tuner; - -public enum TunerType -{ - AIRSPY_R820T( "Airspy R820T" ), - ELONICS_E4000( "E4000" ), - ETTUS_WBX( "WBX" ), - ETTUS_VARIOUS( "Ettus Tuner" ), - FCI_FC2580( "FC2580" ), - FITIPOWER_FC0012( "FC0012" ), - FITIPOWER_FC0013( "FC0013" ), - FUNCUBE_DONGLE_PRO( "Funcube Dongle Pro" ), - FUNCUBE_DONGLE_PRO_PLUS( "Funcube Dongle Pro Plus" ), - HACKRF( "HackRF" ), - RAFAELMICRO_R820T( "R820T" ), - RAFAELMICRO_R828D( "R828D" ), - RTL2832_VARIOUS( "Generic" ), - TEST("Test"), - UNKNOWN( "Unknown" ); - - private String mLabel; - - private TunerType( String label ) - { - mLabel = label; - } - - public String getLabel() - { - return mLabel; - } -} diff --git a/src/source/tuner/airspy/AirspyTuner.java b/src/source/tuner/airspy/AirspyTuner.java deleted file mode 100644 index 649a4eae..00000000 --- a/src/source/tuner/airspy/AirspyTuner.java +++ /dev/null @@ -1,74 +0,0 @@ -/******************************************************************************* - * 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 - ******************************************************************************/ -package source.tuner.airspy; - -import org.slf4j.Logger; -import org.slf4j.LoggerFactory; - -import source.tuner.Tuner; -import source.tuner.TunerClass; -import source.tuner.TunerType; -import source.tuner.airspy.AirspyTunerController.BoardID; - -public class AirspyTuner extends Tuner -{ - private final static Logger mLog = LoggerFactory.getLogger( AirspyTuner.class ); - - public AirspyTuner( AirspyTunerController controller ) - { - super( "Airspy " + controller.getDeviceInfo().getSerialNumber(), controller ); - } - - public AirspyTunerController getController() - { - return (AirspyTunerController)getTunerController(); - } - - @Override - public String getUniqueID() - { - try - { - return getController().getDeviceInfo().getSerialNumber(); - } - catch( Exception e ) - { - mLog.error( "error getting serial number", e ); - } - - return BoardID.AIRSPY.getLabel(); - } - - @Override - public TunerClass getTunerClass() - { - return TunerClass.AIRSPY; - } - - @Override - public TunerType getTunerType() - { - return TunerType.AIRSPY_R820T; - } - - @Override - public double getSampleSize() - { - return 13.0; - } -} diff --git a/src/source/tuner/airspy/AirspyTunerController.java b/src/source/tuner/airspy/AirspyTunerController.java deleted file mode 100644 index 126321e7..00000000 --- a/src/source/tuner/airspy/AirspyTunerController.java +++ /dev/null @@ -1,1191 +0,0 @@ -package source.tuner.airspy; - -import org.apache.commons.io.EndianUtils; -import org.slf4j.Logger; -import org.slf4j.LoggerFactory; -import org.usb4java.Device; -import org.usb4java.DeviceHandle; -import org.usb4java.LibUsb; -import org.usb4java.LibUsbException; -import source.SourceException; -import source.tuner.configuration.TunerConfiguration; -import source.tuner.usb.USBTransferProcessor; -import source.tuner.usb.USBTunerController; - -import javax.usb.UsbException; -import java.nio.ByteBuffer; -import java.nio.ByteOrder; -import java.text.DecimalFormat; -import java.util.ArrayList; -import java.util.EnumSet; -import java.util.List; - -/** - * SDR Trunk - * Copyright (C) 2015-2017 Dennis Sheirer - * - * Ported from libairspy at: - * https://github.com/airspy/host/tree/master/libairspy - * ----------------------------------------------------------------------------- - * Copyright (c) 2013, Michael Ossmann - * Copyright (c) 2012, Jared Boone - * Copyright (c) 2014, Youssef Touil - * Copyright (c) 2014, Benjamin Vernoux - * Copyright (c) 2015, Ian Gilmour - * - * All rights reserved. - * - * Redistribution and use in source and binary forms, with or without - * modification, are permitted provided that the following conditions are met: - * - * Redistributions of source code must retain the above copyright notice, this - * list of conditions and the following disclaimer. - * Redistributions in binary form must reproduce the above copyright notice, - * this list of conditions and the following disclaimer in the documentation - * and/or other materials provided with the distribution. - * Neither the name of AirSpy nor the names of its contributors may be used to - * endorse or promote products derived from this software without specific prior - * written permission. - * - * THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS" - * AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE - * IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE - * ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT HOLDER OR CONTRIBUTORS BE - * LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR - * CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF - * SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS - * INTERRUPTION) HOWEVER CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN - * CONTRACT, STRICT LIABILITY, OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) - * ARISING IN ANY WAY OUT OF THE USE OF THIS SOFTWARE, EVEN IF ADVISED OF THE - * POSSIBILITY OF SUCH DAMAGE. - */ - -public class AirspyTunerController extends USBTunerController -{ - public static final Gain LINEARITY_GAIN_DEFAULT = Gain.LINEARITY_14; - public static final Gain SENSITIVITY_GAIN_DEFAULT = Gain.SENSITIVITY_10; - public static final int GAIN_MIN = 1; - public static final int GAIN_MAX = 22; - public static final int LNA_GAIN_MIN = 0; - public static final int LNA_GAIN_MAX = 14; - public static final int LNA_GAIN_DEFAULT = 7; - public static final int MIXER_GAIN_MIN = 0; - public static final int MIXER_GAIN_MAX = 15; - public static final int MIXER_GAIN_DEFAULT = 9; - public static final int IF_GAIN_MIN = 0; - public static final int IF_GAIN_MAX = 15; - public static final int IF_GAIN_DEFAULT = 9; - public static final long FREQUENCY_MIN = 24000000l; - public static final long FREQUENCY_MAX = 1800000000l; - public static final long FREQUENCY_DEFAULT = 101100000; - public static final double USABLE_BANDWIDTH_PERCENT = 0.90; - public static final AirspySampleRate DEFAULT_SAMPLE_RATE = - new AirspySampleRate(0, 10000000, "10.00 MHz"); - - private static final long USB_TIMEOUT_MS = 2000l; //milliseconds - private static final byte USB_INTERFACE = (byte)0x0; - private static final int USB_TRANSFER_BUFFER_SIZE = 262144; - private static final byte USB_REQUEST_IN = (byte)(LibUsb.ENDPOINT_IN | LibUsb.REQUEST_TYPE_VENDOR | LibUsb.RECIPIENT_DEVICE); - private static final byte USB_REQUEST_OUT = (byte)(LibUsb.ENDPOINT_OUT | LibUsb.REQUEST_TYPE_VENDOR | LibUsb.RECIPIENT_DEVICE); - public static final DecimalFormat MHZ_FORMATTER = new DecimalFormat("#.00 MHz"); - - private final static Logger mLog = LoggerFactory.getLogger(AirspyTunerController.class); - - private Device mDevice; - private DeviceHandle mDeviceHandle; - - private AirspySampleAdapter mSampleAdapter = new AirspySampleAdapter(); - private AirspyDeviceInformation mDeviceInfo; - private List mSampleRates = new ArrayList<>(); - private int mSampleRate = 0; - private USBTransferProcessor mUSBTransferProcessor; - - public AirspyTunerController(Device device) throws SourceException - { - super(FREQUENCY_MIN, FREQUENCY_MAX, 0, USABLE_BANDWIDTH_PERCENT); - - mDevice = device; - } - - public void init() throws SourceException - { - mDeviceHandle = new DeviceHandle(); - - int result = LibUsb.open(mDevice, mDeviceHandle); - - if(result != LibUsb.SUCCESS) - { - if(result == LibUsb.ERROR_ACCESS) - { - mLog.error("Unable to access Airspy - insufficient permissions." - + " If you are running a Linux OS, have you installed the " - + "airspy rules file in \\etc\\udev\\rules.d ??"); - } - - throw new SourceException("Couldn't open airspy device - " + - LibUsb.strError(result)); - } - - try - { - claimInterface(); - } - catch(Exception e) - { - throw new SourceException("Airspy Tuner Controller - error while " - + "setting USB configuration, claiming USB interface or " - + "reset the kernel mode driver", e); - } - - try - { - setSamplePacking(false); - } - catch(LibUsbException | UsbException | UnsupportedOperationException e) - { - mLog.info("Sample packing is not supported by airspy firmware"); - } - - try - { - setReceiverMode(true); - } - catch(Exception e) - { - mLog.error("Couldn't enable airspy receiver mode", e); - } - - getFrequencyController().setFrequency(FREQUENCY_DEFAULT); - - try - { - determineAvailableSampleRates(); - } - catch(LibUsbException | UsbException e) - { - mLog.error("Error identifying available samples rates", e); - } - - try - { - setSampleRate(DEFAULT_SAMPLE_RATE); - } - catch(IllegalArgumentException | LibUsbException | UsbException e) - { - mLog.error("Setting sample rate is not supported by firmware", e); - } - - String deviceName = "Airspy " + getDeviceInfo().getSerialNumber(); - - mUSBTransferProcessor = new USBTransferProcessor(deviceName, mDeviceHandle, mSampleAdapter, - USB_TRANSFER_BUFFER_SIZE); - } - - - @Override - protected USBTransferProcessor getUSBTransferProcessor() - { - return mUSBTransferProcessor; - } - - - /** - * Claims the USB interface. If another application currently has - * the interface claimed, the USB_FORCE_CLAIM_INTERFACE setting - * will dictate if the interface is forcibly claimed from the other - * application - */ - private void claimInterface() throws SourceException - { - if(mDeviceHandle != null) - { - int result = LibUsb.kernelDriverActive(mDeviceHandle, USB_INTERFACE); - - if(result == 1) - { - - result = LibUsb.detachKernelDriver(mDeviceHandle, USB_INTERFACE); - - if(result != LibUsb.SUCCESS) - { - mLog.error("failed attempt to detach kernel driver [" + - LibUsb.errorName(result) + "]"); - - throw new SourceException("couldn't detach kernel driver " - + "from device"); - } - } - - result = LibUsb.setConfiguration(mDeviceHandle, 1); - - if(result != LibUsb.SUCCESS) - { - throw new SourceException("couldn't set USB configuration 1 [" + - LibUsb.errorName(result) + "]"); - } - - result = LibUsb.claimInterface(mDeviceHandle, USB_INTERFACE); - - if(result != LibUsb.SUCCESS) - { - throw new SourceException("couldn't claim usb interface [" + - LibUsb.errorName(result) + "]"); - } - } - else - { - throw new SourceException("couldn't claim usb interface - no " - + "device handle"); - } - } - - public static String getTransferStatus(int status) - { - switch(status) - { - case 0: - return "TRANSFER COMPLETED (0)"; - case 1: - return "TRANSFER ERROR (1)"; - case 2: - return "TRANSFER TIMED OUT (2)"; - case 3: - return "TRANSFER CANCELLED (3)"; - case 4: - return "TRANSFER STALL (4)"; - case 5: - return "TRANSFER NO DEVICE (5)"; - case 6: - return "TRANSFER OVERFLOW (6)"; - default: - return "UNKNOWN TRANSFER STATUS (" + status + ")"; - } - } - - - @Override - public void apply(TunerConfiguration config) throws SourceException - { - if(config instanceof AirspyTunerConfiguration) - { - AirspyTunerConfiguration airspy = (AirspyTunerConfiguration)config; - - int sampleRate = airspy.getSampleRate(); - - AirspySampleRate rate = getSampleRate(sampleRate); - - if(rate == null) - { - if(!mSampleRates.isEmpty()) - { - rate = mSampleRates.get(0); - } - else - { - rate = DEFAULT_SAMPLE_RATE; - } - } - - try - { - setSampleRate(rate); - } - catch(UsbException e) - { - throw new SourceException("Couldn't set sample rate [" + - rate.toString() + "]", e); - } - - try - { - setIFGain(airspy.getIFGain()); - setMixerGain(airspy.getMixerGain()); - setLNAGain(airspy.getLNAGain()); - - setMixerAGC(airspy.isMixerAGC()); - setLNAAGC(airspy.isLNAAGC()); - - //Set the gain mode last, so custom values are already set, and - //linearity and sensitivity modes will automatically override - //the custom values. - setGain(airspy.getGain()); - - getFrequencyController().setFrequencyCorrection(airspy.getFrequencyCorrection()); - } - catch(Exception e) - { - throw new SourceException("Couldn't apply gain settings from " - + "airspy config", e); - } - - try - { - getFrequencyController().setFrequency(airspy.getFrequency()); - } - catch(SourceException se) - { - //Do nothing, we couldn't set the frequency - } - - } - else - { - throw new IllegalArgumentException("Invalid tuner config:" + - config.getClass()); - } - } - - @Override - public long getTunedFrequency() throws SourceException - { - return getFrequencyController().getTunedFrequency(); - } - - @Override - public void setTunedFrequency(long frequency) throws SourceException - { - if(FREQUENCY_MIN <= frequency && frequency <= FREQUENCY_MAX) - { - ByteBuffer buffer = ByteBuffer.allocateDirect(4); - - buffer.order(ByteOrder.LITTLE_ENDIAN); - - buffer.putInt((int)frequency); - - buffer.rewind(); - - try - { - write(Command.SET_FREQUENCY, 0, 0, buffer); - } - catch(UsbException e) - { - mLog.error("error setting frequency [" + frequency + "]", e); - - throw new SourceException("error setting frequency [" + - frequency + "]", e); - } - } - else - { - throw new SourceException("Frequency [" + frequency + "] outside " - + "of tunable range " + FREQUENCY_MIN + "-" + FREQUENCY_MAX); - } - } - - @Override - public double getCurrentSampleRate() - { - return mSampleRate; - } - - /** - * Sets the sample rate to the rate specified by the index value in the - * available sample rates map - * - * @param rate to a sample rate in the available samples rates map. - * @throws IllegalArgumentException if index is not a valid rate index - * @throws LibUsbException if there was a read error or if this operation - * is not supported by the current firmware - * @throws UsbException if there was a USB error - */ - public void setSampleRate(AirspySampleRate rate) throws LibUsbException, UsbException, SourceException - { - if(rate.getRate() != mSampleRate) - { - int result = readByte(Command.SET_SAMPLE_RATE, 0, rate.getIndex(), true); - - if(result != 1) - { - throw new UsbException("Error setting sample rate [" + - rate + "] rate - return value [" + result + "]"); - } - else - { - mSampleRate = rate.getRate(); - getFrequencyController().setSampleRate(mSampleRate); - } - } - } - - /** - * Returns a list of sample rates supported by the firmware version - */ - public List getSampleRates() - { - return mSampleRates; - } - - /** - * Airspy sample rate object that matches the current sample rate setting. - */ - public AirspySampleRate getAirspySampleRate() - { - return getSampleRate(mSampleRate); - } - - /** - * Airspy sample rate object that matches the specified rate in hertz, or - * null if there are no available sample rates for the tuner that match the - * argument value. - */ - public AirspySampleRate getSampleRate(int rate) - { - for(AirspySampleRate sampleRate : mSampleRates) - { - if(sampleRate.getRate() == rate) - { - return sampleRate; - } - } - - //We should never get to here ... - return null; - } - - /** - * Enables/Disables sample packing to allow two 12-bit samples to be packed - * into 3 bytes (enabled) or 4 bytes (disabled). - * - * @param enabled - * @throws UsbException if sample packing is not supported by the current - * device firmware or if there were usb communication issues - */ - public void setSamplePacking(boolean enabled) - throws LibUsbException, UsbException - { - int result = readByte(Command.SET_PACKING, 0, (enabled ? 1 : 0), true); - - if(result != 1) - { - throw new UsbException("Couldnt set sample packing enabled: " + enabled); - } - - /* If we didn't throw an exception above, then update the sample adapter - * to process samples accordingly */ - mSampleAdapter.setSamplePacking(enabled); - } - - /** - * Enables/disables the mixer automatic gain setting - * - * @param enabled - * @throws LibUsbException on unsuccessful read operation - * @throws UsbException on USB error - */ - public void setMixerAGC(boolean enabled) - throws LibUsbException, UsbException - { - int result = readByte(Command.SET_MIXER_AGC, 0, (enabled ? 1 : 0), true); - - if(result != LibUsb.SUCCESS) - { - throw new UsbException("Couldnt set mixer AGC enabled: " + enabled); - } - } - - /** - * Enables/disables the low noise amplifier automatic gain setting - * - * @param enabled - * @throws LibUsbException on unsuccessful read operation - * @throws UsbException on USB error - */ - public void setLNAAGC(boolean enabled) throws LibUsbException, UsbException - { - int result = readByte(Command.SET_LNA_AGC, 0, (enabled ? 1 : 0), true); - - if(result != LibUsb.SUCCESS) - { - throw new UsbException("Couldnt set LNA AGC enabled: " + enabled); - } - } - - public void setGain(Gain gain) throws UsbException - { - if(gain != Gain.CUSTOM) - { - setMixerAGC(false); - setLNAAGC(false); - setLNAGain(gain.getLNA()); - setMixerGain(gain.getMixer()); - setIFGain(gain.getIF()); - } - } - - /** - * Sets LNA gain - * - * @param gain - value within range of LNA_GAIN_MIN to LNA_GAIN_MAX - * @throws LibUsbException on error in java USB wrapper - * @throws UsbException on error in USB transfer - * @throws IllegalArgumentException if gain value is invalid - */ - public void setLNAGain(int gain) - throws LibUsbException, UsbException, IllegalArgumentException - { - if(LNA_GAIN_MIN <= gain && gain <= LNA_GAIN_MAX) - { - int result = readByte(Command.SET_LNA_GAIN, 0, gain, true); - - if(result != LibUsb.SUCCESS) - { - throw new UsbException("Couldnt set LNA gain to: " + gain); - } - } - else - { - throw new IllegalArgumentException("LNA gain value [" + gain + - "] is outside value range: " + LNA_GAIN_MIN + "-" + LNA_GAIN_MAX); - } - } - - /** - * Sets Mixer gain - * - * @param gain - value within range of MIXER_GAIN_MIN to MIXER_GAIN_MAX - * @throws LibUsbException on error in java USB wrapper - * @throws UsbException on error in USB transfer - * @throws IllegalArgumentException if gain value is invalid - */ - public void setMixerGain(int gain) - throws LibUsbException, UsbException, IllegalArgumentException - { - if(MIXER_GAIN_MIN <= gain && gain <= MIXER_GAIN_MAX) - { - int result = readByte(Command.SET_MIXER_GAIN, 0, gain, true); - - if(result != LibUsb.SUCCESS) - { - throw new UsbException("Couldnt set mixer gain to: " + gain); - } - } - else - { - throw new IllegalArgumentException("Mixer gain value [" + gain + - "] is outside value range: " + MIXER_GAIN_MIN + "-" + MIXER_GAIN_MAX); - } - } - - /** - * Sets IF (VGA) gain - * - * @param gain - value within range of VGA_GAIN_MIN to VGA_GAIN_MAX - * @throws LibUsbException on error in java USB wrapper - * @throws UsbException on error in USB transfer - * @throws IllegalArgumentException if gain value is invalid - */ - public void setIFGain(int gain) - throws LibUsbException, UsbException, IllegalArgumentException - { - if(IF_GAIN_MIN <= gain && gain <= IF_GAIN_MAX) - { - int result = readByte(Command.SET_VGA_GAIN, 0, gain, true); - - if(result != LibUsb.SUCCESS) - { - throw new UsbException("Couldnt set VGA gain to: " + gain); - } - } - else - { - throw new IllegalArgumentException("VGA gain value [" + gain + - "] is outside value range: " + IF_GAIN_MIN + "-" + IF_GAIN_MAX); - } - } - - public void setReceiverMode(boolean enabled) throws LibUsbException, UsbException - { - //Empty buffer to throw away - ByteBuffer buffer = ByteBuffer.allocateDirect(0); - - write(Command.RECEIVER_MODE, (enabled ? 1 : 0), 0, buffer); - } - - /** - * Queries the device for available sample rates. Will always provide at - * least the default 10 MHz sample rate. - */ - private void determineAvailableSampleRates() - throws LibUsbException, UsbException - { - mSampleRates.clear(); - - //Get a count of available sample rates. If we get an exception, then - //we're using an older firmware revision and only the default 10 MHz - //rate is supported - try - { - byte[] rawCount = readArray(Command.GET_SAMPLE_RATES, 0, 0, 4); - - if(rawCount != null) - { - int count = EndianUtils.readSwappedInteger(rawCount, 0); - - byte[] rawRates = readArray(Command.GET_SAMPLE_RATES, 0, - count, (count * 4)); - - for(int x = 0; x < count; x++) - { - int rate = EndianUtils.readSwappedInteger(rawRates, (x * 4)); - - mSampleRates.add(new AirspySampleRate(x, rate, formatSampleRate(rate))); - } - } - } - catch(LibUsbException e) - { - //Press on, nothing else to do here .. - } - - if(mSampleRates.isEmpty()) - { - mSampleRates.add(DEFAULT_SAMPLE_RATE); - } - } - - /** - * Formats the rate in hertz for display as megahertz - */ - private static String formatSampleRate(int rate) - { - return MHZ_FORMATTER.format((double)rate / 1E6d); - } - - /** - * Device information - */ - public AirspyDeviceInformation getDeviceInfo() - { - //Lazy initialization - if(mDeviceInfo == null) - { - readDeviceInfo(); - } - - return mDeviceInfo; - } - - /** - * Reads version information from the device and populates the info object - */ - private void readDeviceInfo() - { - if(mDeviceInfo == null) - { - mDeviceInfo = new AirspyDeviceInformation(); - } - - /* Board ID */ - try - { - int boardID = readByte(Command.BOARD_ID_READ, 0, 0, true); - - mDeviceInfo.setBoardID(boardID); - } - catch(LibUsbException | UsbException e) - { - mLog.error("Error reading airspy board ID", e); - } - - /* Version String */ - try - { - //NOTE: libairspy is internally reading 127 bytes, however airspy_info - //script is telling it to read 255 bytes ... things that make you go hmmmm - byte[] version = readArray(Command.VERSION_STRING_READ, 0, 0, 127); - - mDeviceInfo.setVersion(version); - } - catch(LibUsbException | UsbException e) - { - mLog.error("Error reading airspy version string", e); - } - - /* Part ID and Serial Number */ - try - { - //Read 6 x 32-bit integers = 24 bytes - byte[] serial = readArray( - Command.BOARD_PART_ID_SERIAL_NUMBER_READ, 0, 0, 24); - - mDeviceInfo.setPartAndSerialNumber(serial); - } - catch(LibUsbException | UsbException e) - { - mLog.error("Error reading airspy version string", e); - } - } - - /** - * Reads a single byte value from the device. - * - * @param command - airspy command - * @param value - value field for usb setup packet - * @param index - index field for usb setup packet - * @return - byte value as an integer - * @throws LibUsbException if the operation is unsuccesful - * @throws UsbException on any usb errors - */ - private int readByte(Command command, int value, int index, boolean signed) - throws LibUsbException, UsbException - { - if(mDeviceHandle != null) - { - ByteBuffer buffer = ByteBuffer.allocateDirect(1); - - int transferred = LibUsb.controlTransfer(mDeviceHandle, - USB_REQUEST_IN, - command.getValue(), - (short)value, - (short)index, - buffer, - USB_TIMEOUT_MS); - - if(transferred < 0) - { - throw new LibUsbException("read error", transferred); - } - - byte result = buffer.get(0); - - if(signed) - { - return (result & 0xFF); - } - else - { - return result; - } - } - else - { - throw new LibUsbException("device handle is null", - LibUsb.ERROR_NO_DEVICE); - } - } - - /** - * Reads a multi-byte value from the device - * - * @param command - airspy command - * @param value - usb packet value - * @param index - usb packet index - * @param length - number of bytes to read - * @return - bytes read from the device - * @throws LibUsbException if quantity of bytes read doesn't equal the - * requested number of bytes - * @throws UsbException on error communicating with the device - */ - private byte[] readArray(Command command, int value, int index, int length) - throws LibUsbException, UsbException - { - if(mDeviceHandle != null) - { - ByteBuffer buffer = ByteBuffer.allocateDirect(length); - - int transferred = LibUsb.controlTransfer(mDeviceHandle, - USB_REQUEST_IN, - command.getValue(), - (short)value, - (short)index, - buffer, - USB_TIMEOUT_MS); - - if(transferred < 0) - { - throw new LibUsbException("read error", transferred); - } - - byte[] results = new byte[transferred]; - - buffer.get(results); - - return results; - } - else - { - throw new LibUsbException("device handle is null", - LibUsb.ERROR_NO_DEVICE); - } - } - - /** - * Writes the buffer contents to the device - * - * @param command - airspy command - * @param value - usb packet value - * @param index - usb packet index - * @param buffer - data to write to the device - * @throws UsbException on error - */ - public void write(Command command, int value, int index, ByteBuffer buffer) - throws UsbException - { - if(mDeviceHandle != null) - { - int transferred = LibUsb.controlTransfer(mDeviceHandle, - USB_REQUEST_OUT, - command.getValue(), - (short)value, - (short)index, - buffer, - USB_TIMEOUT_MS); - - if(transferred < 0) - { - throw new LibUsbException("error writing byte buffer", - transferred); - } - else if(transferred != buffer.capacity()) - { - throw new LibUsbException("transferred bytes [" + - transferred + "] is not what was expected [" + - buffer.capacity() + "]", transferred); - } - } - else - { - throw new LibUsbException("device handle is null", - LibUsb.ERROR_NO_DEVICE); - } - } - - public enum GainMode - { - LINEARITY, - SENSITIVITY, - CUSTOM; - } - - public enum Gain - { - LINEARITY_1(1, 4, 0, 0), - LINEARITY_2(2, 5, 0, 0), - LINEARITY_3(3, 6, 1, 0), - LINEARITY_4(4, 7, 1, 0), - LINEARITY_5(5, 8, 1, 0), - LINEARITY_6(6, 9, 1, 0), - LINEARITY_7(7, 10, 2, 0), - LINEARITY_8(8, 10, 2, 1), - LINEARITY_9(9, 10, 0, 3), - LINEARITY_10(10, 10, 0, 5), - LINEARITY_11(11, 10, 1, 6), - LINEARITY_12(12, 10, 0, 8), - LINEARITY_13(13, 10, 0, 9), - LINEARITY_14(14, 10, 5, 8), - LINEARITY_15(15, 10, 6, 9), - LINEARITY_16(16, 11, 6, 9), - LINEARITY_17(17, 11, 7, 10), - LINEARITY_18(18, 11, 8, 12), - LINEARITY_19(19, 11, 9, 13), - LINEARITY_20(20, 11, 11, 14), - LINEARITY_21(21, 12, 12, 14), - LINEARITY_22(22, 13, 12, 14), - SENSITIVITY_1(1, 4, 0, 0), - SENSITIVITY_2(2, 4, 0, 1), - SENSITIVITY_3(3, 4, 0, 2), - SENSITIVITY_4(4, 4, 0, 3), - SENSITIVITY_5(5, 4, 1, 5), - SENSITIVITY_6(6, 4, 2, 6), - SENSITIVITY_7(7, 4, 2, 7), - SENSITIVITY_8(8, 4, 3, 8), - SENSITIVITY_9(9, 4, 4, 9), - SENSITIVITY_10(10, 5, 4, 9), - SENSITIVITY_11(11, 5, 4, 12), - SENSITIVITY_12(12, 5, 7, 12), - SENSITIVITY_13(13, 5, 8, 13), - SENSITIVITY_14(14, 5, 9, 14), - SENSITIVITY_15(15, 6, 9, 14), - SENSITIVITY_16(16, 7, 10, 14), - SENSITIVITY_17(17, 8, 10, 14), - SENSITIVITY_18(18, 9, 11, 14), - SENSITIVITY_19(19, 10, 12, 14), - SENSITIVITY_20(20, 11, 12, 14), - SENSITIVITY_21(21, 12, 12, 14), - SENSITIVITY_22(22, 13, 12, 14), - CUSTOM(1, 0, 0, 0); - - private int mValue; - private int mIF; - private int mMixer; - private int mLNA; - - private Gain(int value, int ifGain, int mixer, int lna) - { - mValue = value; - mIF = ifGain; - mMixer = mixer; - mLNA = lna; - } - - public int getValue() - { - return mValue; - } - - public int getIF() - { - return mIF; - } - - public int getMixer() - { - return mMixer; - } - - public int getLNA() - { - return mLNA; - } - - public static Gain getGain(GainMode mode, int value) - { - assert (GAIN_MIN <= value && value <= GAIN_MAX); - - switch(mode) - { - case LINEARITY: - for(Gain gain : getLinearityGains()) - { - if(gain.getValue() == value) - { - return gain; - } - } - return LINEARITY_GAIN_DEFAULT; - case SENSITIVITY: - for(Gain gain : getSensitivityGains()) - { - if(gain.getValue() == value) - { - return gain; - } - } - return SENSITIVITY_GAIN_DEFAULT; - case CUSTOM: - default: - return Gain.CUSTOM; - } - } - - public static GainMode getGainMode(Gain gain) - { - if(gain == CUSTOM) - { - return GainMode.CUSTOM; - } - else if(getLinearityGains().contains(gain)) - { - return GainMode.LINEARITY; - } - else if(getSensitivityGains().contains(gain)) - { - return GainMode.SENSITIVITY; - } - - return GainMode.CUSTOM; - } - - public static EnumSet getLinearityGains() - { - return EnumSet.range(LINEARITY_1, LINEARITY_22); - } - - public static EnumSet getSensitivityGains() - { - return EnumSet.range(SENSITIVITY_1, SENSITIVITY_22); - } - } - - - /** - * Airspy Board Identifier - */ - public enum BoardID - { - AIRSPY(0, "Airspy"), - UNKNOWN(-1, "Unknown"); - - private int mValue; - private String mLabel; - - private BoardID(int value, String label) - { - mValue = value; - mLabel = label; - } - - public int getValue() - { - return mValue; - } - - public String getLabel() - { - return mLabel; - } - - public static BoardID fromValue(int value) - { - if(value == 0) - { - return AIRSPY; - } - - return UNKNOWN; - } - } - - /** - * Airspy Commands - */ - public enum Command - { - INVALID(0), - RECEIVER_MODE(1), - SI5351C_WRITE(2), - SI5351C_READ(3), - R820T_WRITE(4), - R820T_READ(5), - SPIFLASH_ERASE(6), - SPIFLASH_WRITE(7), - SPIFLASH_READ(8), - BOARD_ID_READ(9), - VERSION_STRING_READ(10), - BOARD_PART_ID_SERIAL_NUMBER_READ(11), - SET_SAMPLE_RATE(12), - SET_FREQUENCY(13), - SET_LNA_GAIN(14), - SET_MIXER_GAIN(15), - SET_VGA_GAIN(16), - SET_LNA_AGC(17), - SET_MIXER_AGC(18), - MS_VENDOR_COMMAND(19), - SET_RF_BIAS_COMMAND(20), - GPIO_WRITE(21), - GPIO_READ(22), - GPIO_DIR__WRITE(23), - GPIO_DIR_READ(24), - GET_SAMPLE_RATES(25), - SET_PACKING(26); - - private int mValue; - - private Command(int value) - { - mValue = value; - } - - public byte getValue() - { - return (byte)mValue; - } - - public static Command fromValue(int value) - { - if(0 <= value && value <= 25) - { - return Command.values()[value]; - } - - return INVALID; - } - } - - public enum ReceiverMode - { - OFF(0), - ON(1); - - private int mValue; - - private ReceiverMode(int value) - { - mValue = value; - } - - public int getValue() - { - return mValue; - } - } - - /** - * General Purpose Input/Output Ports (accessible on the airspy board) - */ - public enum GPIOPort - { - PORT_0(0), - PORT_1(1), - PORT_2(2), - PORT_3(3), - PORT_4(4), - PORT_5(5), - PORT_6(6), - PORT_7(7); - - private int mValue; - - private GPIOPort(int value) - { - mValue = value; - } - - public int getValue() - { - return mValue; - } - } - - /** - * General Purpose Input/Output Pins (accessible on the airspy board) - */ - public enum GPIOPin - { - PIN_0(0), - PIN_1(1), - PIN_2(2), - PIN_3(3), - PIN_4(4), - PIN_5(5), - PIN_6(6), - PIN_7(7), - PIN_8(8), - PIN_9(9), - PIN_10(10), - PIN_11(11), - PIN_12(12), - PIN_13(13), - PIN_14(14), - PIN_15(15), - PIN_16(16), - PIN_17(17), - PIN_18(18), - PIN_19(19), - PIN_20(20), - PIN_21(21), - PIN_22(22), - PIN_23(23), - PIN_24(24), - PIN_25(25), - PIN_26(26), - PIN_27(27), - PIN_28(28), - PIN_29(29), - PIN_30(30), - PIN_31(31); - - private int mValue; - - private GPIOPin(int value) - { - mValue = value; - } - - public int getValue() - { - return mValue; - } - } -} diff --git a/src/source/tuner/airspy/AirspyTunerEditor.java b/src/source/tuner/airspy/AirspyTunerEditor.java deleted file mode 100644 index d5dce404..00000000 --- a/src/source/tuner/airspy/AirspyTunerEditor.java +++ /dev/null @@ -1,770 +0,0 @@ -/******************************************************************************* - * SDR Trunk - * Copyright (C) 2015-2016 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 source.tuner.airspy; - -import java.awt.event.ActionEvent; -import java.awt.event.ActionListener; -import java.awt.event.FocusEvent; -import java.awt.event.FocusListener; -import java.text.DecimalFormat; -import java.util.List; - -import javax.swing.DefaultComboBoxModel; -import javax.swing.JButton; -import javax.swing.JCheckBox; -import javax.swing.JComboBox; -import javax.swing.JLabel; -import javax.swing.JOptionPane; -import javax.swing.JSeparator; -import javax.swing.JSlider; -import javax.swing.JSpinner; -import javax.swing.JTextField; -import javax.swing.SpinnerModel; -import javax.swing.SpinnerNumberModel; -import javax.swing.SwingConstants; -import javax.swing.event.ChangeEvent; -import javax.swing.event.ChangeListener; -import javax.usb.UsbException; - -import net.miginfocom.swing.MigLayout; - -import org.slf4j.Logger; -import org.slf4j.LoggerFactory; -import org.usb4java.LibUsbException; - -import source.SourceException; -import source.tuner.airspy.AirspyTunerController.Gain; -import source.tuner.airspy.AirspyTunerController.GainMode; -import source.tuner.configuration.TunerConfiguration; -import source.tuner.configuration.TunerConfigurationEditor; -import source.tuner.configuration.TunerConfigurationEvent; -import source.tuner.configuration.TunerConfigurationModel; -import source.tuner.configuration.TunerConfigurationEvent.Event; - -public class AirspyTunerEditor extends TunerConfigurationEditor -{ - private static final long serialVersionUID = 1L; - - private final static Logger mLog = - LoggerFactory.getLogger( AirspyTunerEditor.class ); - - private JTextField mConfigurationName; - private JButton mTunerInfo; - - private JComboBox mSampleRateCombo; - private JSpinner mFrequencyCorrection; - - private JComboBox mGainModeCombo; - - private JSlider mMasterGain; - private JLabel mMasterGainValueLabel; - - private JSlider mIFGain; - private JLabel mIFGainValueLabel; - - private JSlider mLNAGain; - private JLabel mLNAGainValueLabel; - - private JSlider mMixerGain; - private JLabel mMixerGainValueLabel; - - private JCheckBox mLNAAGC; - private JCheckBox mMixerAGC; - - private AirspyTunerController mController; - private boolean mLoading; - - public AirspyTunerEditor( TunerConfigurationModel tunerConfigurationModel, - AirspyTuner tuner ) - { - super( tunerConfigurationModel ); - mController = tuner.getController(); - - init(); - } - - private AirspyTunerConfiguration getConfiguration() - { - if( hasItem() ) - { - return (AirspyTunerConfiguration)getItem(); - } - - return null; - } - - private void init() - { - setLayout( new MigLayout( "fill,wrap 4", "[right][grow,fill][right][grow,fill]", - "[][][][][][grow]" ) ); - - add( new JLabel( "Airspy Tuner Configuration" ), "span,align center" ); - - mConfigurationName = new JTextField(); - mConfigurationName.setEnabled( false ); - mConfigurationName.addFocusListener( new FocusListener() - { - @Override - public void focusLost( FocusEvent e ) - { - save(); - } - @Override - public void focusGained( FocusEvent e ) {} - } ); - - add( new JLabel( "Name:" ) ); - add( mConfigurationName, "span 2" ); - - mTunerInfo = new JButton( "Tuner Info" ); - mTunerInfo.setEnabled( false ); - mTunerInfo.addActionListener( new ActionListener() - { - @Override - public void actionPerformed( ActionEvent e ) - { - JOptionPane.showMessageDialog( AirspyTunerEditor.this, getTunerInfo(), - "Tuner Info", JOptionPane.INFORMATION_MESSAGE ); - } - } ); - add( mTunerInfo ); - - /** - * Sample Rate - */ - add( new JLabel( "Sample Rate:" ) ); - - List rates = mController.getSampleRates(); - - mSampleRateCombo = new JComboBox( - new DefaultComboBoxModel( rates.toArray( - new AirspySampleRate[ rates.size() ] ) ) ); - mSampleRateCombo.setEnabled( false ); - mSampleRateCombo.addActionListener( new ActionListener() - { - @Override - public void actionPerformed( ActionEvent e ) - { - AirspySampleRate rate = (AirspySampleRate)mSampleRateCombo.getSelectedItem(); - - try - { - mController.setSampleRate( rate ); - save(); - } - catch ( LibUsbException | UsbException | SourceException e1 ) - { - JOptionPane.showMessageDialog( AirspyTunerEditor.this, - "Couldn't set sample rate to " + rate.getLabel() ); - - mLog.error( "Error setting airspy sample rate", e1 ); - } - } - } ); - - add( mSampleRateCombo ); - - /** - * Frequency Correction - */ - SpinnerModel model = - new SpinnerNumberModel( 0.0, //initial value - -1000.0, //min - 1000.0, //max - 0.1 ); //step - - mFrequencyCorrection = new JSpinner( model ); - mFrequencyCorrection.setEnabled( false ); - - JSpinner.NumberEditor editor = - (JSpinner.NumberEditor)mFrequencyCorrection.getEditor(); - - DecimalFormat format = editor.getFormat(); - format.setMinimumFractionDigits( 1 ); - editor.getTextField().setHorizontalAlignment( SwingConstants.CENTER ); - - mFrequencyCorrection.addChangeListener( new ChangeListener() - { - @Override - public void stateChanged( ChangeEvent e ) - { - final double value = ((SpinnerNumberModel)mFrequencyCorrection - .getModel()).getNumber().doubleValue(); - - try - { - mController.getFrequencyController().setFrequencyCorrection( value ); - } - catch ( SourceException e1 ) - { - mLog.error( "Error setting frequency correction value", e1 ); - } - - save(); - } - } ); - - add( new JLabel( "PPM:" ) ); - add( mFrequencyCorrection ); - - add( new JSeparator(), "span,growx,push" ); - - /** - * Gain Mode - */ - add( new JLabel( "Gain Mode:" ) ); - mGainModeCombo = new JComboBox( GainMode.values() ); - mGainModeCombo.setEnabled( false ); - mGainModeCombo.addActionListener( new ActionListener() - { - @Override - public void actionPerformed( ActionEvent e ) - { - GainMode mode = (GainMode)mGainModeCombo.getSelectedItem(); - - updateGainComponents( mode ); - - if( hasItem() ) - { - AirspyTunerConfiguration airspy = getConfiguration(); - - Gain gain = airspy.getGain(); - - if( mode == GainMode.CUSTOM ) - { - mIFGain.setValue( airspy.getIFGain() ); - mMixerGain.setValue( airspy.getMixerGain() ); - mMixerAGC.setSelected( airspy.isMixerAGC() ); - mLNAGain.setValue( airspy.getLNAGain() ); - mLNAAGC.setSelected( airspy.isLNAAGC() ); - } - else - { - mMixerAGC.setSelected( false ); - mLNAAGC.setSelected( false ); - mMasterGain.setValue( gain.getValue() ); - mIFGain.setValue( gain.getIF() ); - mMixerGain.setValue( gain.getMixer() ); - mLNAGain.setValue( gain.getLNA() ); - } - } - - save(); - } - } ); - - add( mGainModeCombo, "wrap" ); - - /** - * Gain - */ - add( new JLabel( "Master:" ) ); - - mMasterGain = new JSlider( JSlider.HORIZONTAL, - AirspyTunerController.GAIN_MIN, - AirspyTunerController.GAIN_MAX, - AirspyTunerController.GAIN_MIN ); - mMasterGain.setMajorTickSpacing( 1 ); - mMasterGain.setPaintTicks( true ); - - mMasterGain.addChangeListener( new ChangeListener() - { - @Override - public void stateChanged( ChangeEvent event ) - { - GainMode mode = (GainMode)mGainModeCombo.getSelectedItem(); - int value = mMasterGain.getValue(); - Gain gain = Gain.getGain( mode, value ); - - try - { - mController.setGain( gain ); - save(); - mMasterGainValueLabel.setText( String.valueOf( gain.getValue() ) ); - } - catch ( Exception e ) - { - mLog.error( "Couldn't set airspy gain to:" + gain.name(), e ); - JOptionPane.showMessageDialog( mMasterGain, "Couldn't set gain value to " + - gain.getValue() ); - } - } - } ); - - add( mMasterGain, "span 2" ); - - mMasterGainValueLabel = new JLabel( "0" ); - add( mMasterGainValueLabel ); - - add( new JLabel( "IF:" ) ); - - mIFGain = new JSlider( JSlider.HORIZONTAL, - AirspyTunerController.IF_GAIN_MIN, - AirspyTunerController.IF_GAIN_MAX, - AirspyTunerController.IF_GAIN_MIN ); - - mIFGain.setMajorTickSpacing( 1 ); - mIFGain.setPaintTicks( true ); - - mIFGain.addChangeListener( new ChangeListener() - { - @Override - public void stateChanged( ChangeEvent event ) - { - int gain = mIFGain.getValue(); - - try - { - mController.setIFGain( gain ); - save(); - mIFGainValueLabel.setText( String.valueOf( gain ) ); - } - catch ( Exception e ) - { - mLog.error( "Couldn't set airspy IF gain to:" + gain, e ); - - JOptionPane.showMessageDialog( mIFGain, "Couldn't set IF gain value to " + gain ); - } - } - } ); - - add( mIFGain, "span 2" ); - - mIFGainValueLabel = new JLabel( "0" ); - add( mIFGainValueLabel ); - - /** - * Mixer Gain - */ - add( new JLabel( "Mixer:" ) ); - - mMixerGain = new JSlider( JSlider.HORIZONTAL, - AirspyTunerController.MIXER_GAIN_MIN, - AirspyTunerController.MIXER_GAIN_MAX, - AirspyTunerController.MIXER_GAIN_MIN ); - - mMixerGain.setMajorTickSpacing( 1 ); - mMixerGain.setPaintTicks( true ); - - mMixerGain.addChangeListener( new ChangeListener() - { - @Override - public void stateChanged( ChangeEvent event ) - { - int gain = mMixerGain.getValue(); - - try - { - mController.setMixerGain( gain ); - save(); - mMixerGainValueLabel.setText( String.valueOf( gain ) ); - } - catch ( Exception e ) - { - mLog.error( "Couldn't set airspy Mixer gain to:" + gain, e ); - JOptionPane.showMessageDialog( mIFGain, "Couldn't set Mixer gain value to " + gain ); - } - } - } ); - - add( mMixerGain, "span 2" ); - - mMixerGainValueLabel = new JLabel( "0" ); - add( mMixerGainValueLabel ); - - add( new JLabel( "LNA:" ) ); - - mLNAGain = new JSlider( JSlider.HORIZONTAL, - AirspyTunerController.LNA_GAIN_MIN, - AirspyTunerController.LNA_GAIN_MAX, - AirspyTunerController.LNA_GAIN_MIN ); - - mLNAGain.setMajorTickSpacing( 1 ); - mLNAGain.setPaintTicks( true ); - - mLNAGain.addChangeListener( new ChangeListener() - { - @Override - public void stateChanged( ChangeEvent event ) - { - int gain = mLNAGain.getValue(); - - try - { - mController.setLNAGain( gain ); - save(); - mLNAGainValueLabel.setText( String.valueOf( gain ) ); - } - catch ( Exception e ) - { - mLog.error( "Couldn't set airspy LNA gain to:" + gain, e ); - JOptionPane.showMessageDialog( mIFGain, "Couldn't set LNA gain value to " + gain ); - } - } - } ); - - add( mLNAGain, "span 2" ); - - mLNAGainValueLabel = new JLabel( "0" ); - add( mLNAGainValueLabel ); - - mMixerAGC = new JCheckBox( "Mixer AGC" ); - mMixerAGC.addActionListener( new ActionListener() - { - @Override - public void actionPerformed( ActionEvent e ) - { - try - { - mController.setMixerAGC( mMixerAGC.isSelected() ); - mMixerGain.setEnabled( !mMixerAGC.isSelected() ); - - save(); - } - catch ( Exception e1 ) - { - mLog.error( "Error setting Mixer AGC Enabled" ); - } - } - } ); - - add( mMixerAGC, "span 2,center" ); - - mLNAAGC = new JCheckBox( "LNA AGC" ); - mLNAAGC.addActionListener( new ActionListener() - { - @Override - public void actionPerformed( ActionEvent e ) - { - try - { - mController.setLNAAGC( mLNAAGC.isSelected() ); - mLNAGain.setEnabled( !mLNAAGC.isSelected() ); - - save(); - } - catch ( Exception e1 ) - { - mLog.error( "Error setting LNA AGC Enabled" ); - } - - } - } ); - - add( mLNAAGC, "span 2,center" ); - } - - /** - * Updates the enabled state of each of the gain controls according to the - * specified gain mode. The master gain control is enabled for linearity - * and sensitivity and the individual gain controls are disabled, and - * vice-versa for custom mode. - */ - private void updateGainComponents( GainMode mode ) - { - switch( mode ) - { - case LINEARITY: - if( !mMasterGain.isEnabled() ) - { - mMasterGain.setEnabled( true ); - mMasterGainValueLabel.setEnabled( true ); - } - if( mIFGain.isEnabled() ) - { - mIFGain.setEnabled( false ); - mIFGainValueLabel.setEnabled( false ); - } - if( mLNAGain.isEnabled() ) - { - mLNAAGC.setEnabled( false ); - mLNAGainValueLabel.setEnabled( false ); - mLNAGain.setEnabled( false ); - } - if( mMixerGain.isEnabled() ) - { - mMixerGainValueLabel.setEnabled( false ); - mMixerAGC.setEnabled( false ); - mMixerGain.setEnabled( false ); - } - break; - case SENSITIVITY: - if( !mMasterGain.isEnabled() ) - { - mMasterGain.setEnabled( true ); - mMasterGainValueLabel.setEnabled( true ); - } - if( mIFGain.isEnabled() ) - { - mIFGain.setEnabled( false ); - mIFGainValueLabel.setEnabled( false ); - } - if( mLNAGain.isEnabled() ) - { - mLNAAGC.setEnabled( false ); - mLNAGainValueLabel.setEnabled( false ); - mLNAGain.setEnabled( false ); - } - if( mMixerGain.isEnabled() ) - { - mMixerGainValueLabel.setEnabled( false ); - mMixerAGC.setEnabled( false ); - mMixerGain.setEnabled( false ); - } - break; - case CUSTOM: - if( mMasterGain.isEnabled() ) - { - mMasterGain.setEnabled( false ); - mMasterGainValueLabel.setEnabled( false ); - } - if( !mIFGain.isEnabled() ) - { - mIFGain.setEnabled( true ); - mIFGainValueLabel.setEnabled( true ); - } - if( !mLNAGain.isEnabled() ) - { - mLNAAGC.setEnabled( true ); - mLNAGainValueLabel.setEnabled( true ); - mLNAGain.setEnabled( true ); - } - if( !mMixerGain.isEnabled() ) - { - mMixerGainValueLabel.setEnabled( true ); - mMixerAGC.setEnabled( true ); - mMixerGain.setEnabled( true ); - } - default: - break; - } - } - - @Override - public void save() - { - if( hasItem() && !mLoading ) - { - AirspyTunerConfiguration config = getConfiguration(); - config.setName( mConfigurationName.getText() ); - config.setSampleRate( ((AirspySampleRate)mSampleRateCombo.getSelectedItem()).getRate() ); - - double value = ((SpinnerNumberModel)mFrequencyCorrection.getModel()).getNumber().doubleValue(); - config.setFrequencyCorrection( value ); - - Gain gain = Gain.getGain( (GainMode)mGainModeCombo.getSelectedItem(), mMasterGain.getValue() ); - - config.setGain( gain ); - config.setIFGain( mIFGain.getValue() ); - config.setMixerGain( mMixerGain.getValue() ); - config.setLNAGain( mLNAGain.getValue() ); - config.setMixerAGC( mMixerAGC.isSelected() ); - config.setLNAAGC( mLNAAGC.isSelected() ); - - getTunerConfigurationModel().broadcast( new TunerConfigurationEvent( config, Event.CHANGE ) ); - } - } - - private AirspySampleRate fromValue( int value ) - { - List rates = mController.getSampleRates(); - - for( AirspySampleRate rate: rates ) - { - if( rate.getRate() == value ) - { - return rate; - } - } - - if( rates.size() > 0 ) - { - return rates.get( 0 ); - } - - return null; - } - - /** - * Sets all controls to the argument enabled state - */ - private void setControlsEnabled( boolean enabled ) - { - if( mConfigurationName.isEnabled() != enabled ) - { - mConfigurationName.setEnabled( enabled ); - } - - if( mTunerInfo.isEnabled() != enabled ) - { - mTunerInfo.setEnabled( enabled ); - } - - if( mSampleRateCombo.isEnabled() != enabled ) - { - mSampleRateCombo.setEnabled( enabled ); - } - - if( mFrequencyCorrection.isEnabled() != enabled ) - { - mFrequencyCorrection.setEnabled( enabled ); - } - - if( mGainModeCombo.isEnabled() != enabled ) - { - mGainModeCombo.setEnabled( enabled ); - } - - if( mMasterGain.isEnabled() != enabled ) - { - mMasterGain.setEnabled( enabled ); - } - - if( mIFGain.isEnabled() != enabled ) - { - mIFGain.setEnabled( enabled ); - } - - if( mMixerGain.isEnabled() != enabled ) - { - mMixerGain.setEnabled( enabled ); - } - - if( mMixerAGC.isEnabled() != enabled ) - { - mMixerAGC.setEnabled( enabled ); - } - - if( mLNAGain.isEnabled() != enabled ) - { - mLNAGain.setEnabled( enabled ); - } - - if( mLNAAGC.isEnabled() != enabled ) - { - mLNAAGC.setEnabled( enabled ); - } - } - - /** - * Applies the tuner configuration to the editor. If the configuration is assigned to the - * currently selected tuner, then the settings are applied. - */ - @Override - public void setItem( TunerConfiguration config ) - { - super.setItem( config ); - - mLoading = true; - - if( hasItem() && getItem().isAssigned() ) - { - AirspyTunerConfiguration airspy = getConfiguration(); - - setControlsEnabled( true ); - - mConfigurationName.setText( airspy.getName() ); - - AirspySampleRate rate = fromValue( airspy.getSampleRate() ); - - if( rate != null ) - { - mSampleRateCombo.setSelectedItem( rate ); - } - - mFrequencyCorrection.setValue( airspy.getFrequencyCorrection() ); - - GainMode mode = Gain.getGainMode( airspy.getGain() ); - - //This will set the master, IF, Mixer, LNA and AGC controls - mGainModeCombo.setSelectedItem( mode ); - - if( mode == GainMode.CUSTOM ) - { - mMasterGain.setValue( airspy.getGain().getValue() ); - mIFGain.setValue( airspy.getIFGain() ); - - if( airspy.isMixerAGC() ) - { - mMixerAGC.setSelected( airspy.isMixerAGC() ); - mMixerGain.setEnabled( false ); - } - else - { - mMixerGain.setValue( airspy.getMixerGain() ); - } - - if( airspy.isLNAAGC() ) - { - mLNAAGC.setSelected( airspy.isLNAAGC() ); - mLNAGain.setEnabled( false ); - } - else - { - mLNAGain.setValue( airspy.getLNAGain() ); - } - } - else - { - mMixerAGC.setSelected( false ); - mLNAAGC.setSelected( false ); - - Gain gain = airspy.getGain(); - - mMasterGain.setValue( gain.getValue() ); - mIFGain.setValue( gain.getIF() ); - mMixerGain.setValue( gain.getMixer() ); - mLNAGain.setValue( gain.getLNA() ); - } - } - else - { - setControlsEnabled( false ); - mConfigurationName.setText( hasItem() ? getItem().getName() : "" ); - } - - mLoading = false; - } - - private String getTunerInfo() - { - StringBuilder sb = new StringBuilder(); - - AirspyDeviceInformation info = mController.getDeviceInfo(); - - sb.append( "

Airspy Tuner

" ); - sb.append( "Serial: " ); - sb.append( info.getSerialNumber() ); - sb.append( "
" ); - - sb.append( "Firmware: " ); - String[] firmware = info.getVersion().split( " " ); - sb.append( firmware.length > 1 ? firmware[ 0 ] : info.getVersion() ); - sb.append( "
" ); - - sb.append( "Part: " ); - sb.append( info.getPartNumber() ); - sb.append( "
" ); - - sb.append( "Board ID: " ); - sb.append( info.getBoardID().getLabel() ); - sb.append( "
" ); - - return sb.toString(); - } -} diff --git a/src/source/tuner/channel/ChannelManager.java b/src/source/tuner/channel/ChannelManager.java deleted file mode 100644 index 264b9258..00000000 --- a/src/source/tuner/channel/ChannelManager.java +++ /dev/null @@ -1,337 +0,0 @@ -/******************************************************************************* - * 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 source.tuner.channel; - -import org.slf4j.Logger; -import org.slf4j.LoggerFactory; -import sample.Listener; -import source.Source; -import source.SourceEvent; -import source.SourceException; -import source.tuner.Tuner; -import source.tuner.channel.cic.CICTunerChannelSource; - -import java.util.SortedSet; -import java.util.concurrent.ConcurrentSkipListSet; -import java.util.concurrent.RejectedExecutionException; - -public abstract class ChannelManager implements Listener -{ - private final static Logger mLog = LoggerFactory.getLogger(ChannelManager.class); - - private FrequencyController mFrequencyController; - private SortedSet mTunedChannels = new ConcurrentSkipListSet<>(); - - /** - * Channel manager provides access to Digital Drop Channel (DDC) tuner channel sources and automatic updating of - * a frequency controller to ensure that the center tuned frequency is correct for the provided DDC channels. - * - * @param frequencyController with a center tuned frequency that can be adjusted - */ - public ChannelManager(FrequencyController frequencyController) - { - mFrequencyController = frequencyController; - } - - protected FrequencyController getFrequencyController() - { - return mFrequencyController; - } - - /** - * Creates a tuner channel source for the tuner channel if possible. - * - * @param tunerChannel to source - * @return tuner channel source or null if the tuner channel cannot be sourced by this channel manager - */ - public TunerChannelSource getSource(TunerChannel tunerChannel) - { - TunerChannelSource tunerChannelSource = null; - - if(canTune(tunerChannel)) - { - //Add the channel so that we can calculate the new center frequency - mTunedChannels.add(tunerChannel); - - long centerFrequency = calculateCenterFrequency(); - - if(centerFrequency != getFrequencyController().getFrequency()) - { - try - { - getFrequencyController().setLockedFrequency(centerFrequency); - tunerChannelSource = getTunerChannelSource(tunerChannel); - } - catch(SourceException se) - { - mLog.error("Error setting tuner frequency while creating new tuner channel source for channel: " + - tunerChannel, se); - } - } - } - - //If we're not successful, remove the tuner channel from the sorted set - if(tunerChannelSource == null && mTunedChannels.contains(tunerChannel)) - { - mTunedChannels.remove(tunerChannel); - } - - return tunerChannelSource; - } - - /** - * Releases the source and performs any cleanup. - * @param source to release that was previously provided by this channel manager - */ - public void releaseSource(TunerChannelSource source) - { - mTunedChannels.remove(source.getTunerChannel()); - } - - /** - * Determine the center frequency to use for the current set of tuner channels. - */ - protected abstract long calculateCenterFrequency(); - - /** - * Constructs the actual tuner channel source for the tuner channel. This method assumes that the tuner channel - * can be tuned and that the tuner is correctly center tuned to source the channel. - * @param tunerChannel to source - * @return tuner channel source or null if there are any errors in creating the tuner channel source - */ - protected abstract TunerChannelSource getTunerChannelSource(TunerChannel tunerChannel); - - /** - * Indicates if channel along with all of the other currently sourced channels can fit within the tunable bandwidth. - */ - private boolean canTune(TunerChannel channel) - { - //Make sure we're within the tunable frequency range of this tuner - if(getFrequencyController().getMinimumFrequency() < channel.getMinFrequency() && - getFrequencyController().getMaximumFrequency() > channel.getMaxFrequency()) - { - //If this is the first lock, then we're good - if(mTunedChannels.isEmpty()) - { - return true; - } - else - { - int usableBandwidth = getFrequencyController().getUsableBandwidth(); - long minLockedFrequency = mTunedChannels.first().getMinFrequency(); - long maxLockedFrequency = mTunedChannels.last().getMaxFrequency(); - - //Requested channel is within current locked channel frequency range - if(minLockedFrequency <= channel.getMinFrequency() && channel.getMaxFrequency() <= maxLockedFrequency) - { - return true; - } - - //Requested channel is higher than min locked frequency - if(channel.getMaxFrequency() > minLockedFrequency && - channel.getMaxFrequency() - minLockedFrequency <= usableBandwidth) - { - return true; - } - - //Requested channel is lower than the max locked frequency - if(channel.getMinFrequency() <= maxLockedFrequency && - maxLockedFrequency - channel.getMinFrequency() <= usableBandwidth) - { - return true; - } - } - } - - return false; - } - - /** - * Constructs a digital drop channel (DDC) as a tuner channel from from the tuner, or returns null if the channel - * cannot be sourced from the tuner. - * - * This controller can't provide a channel if the channel, along with the current set of sourced channels, can't be - * accomodated within the current tuner bandwidth, or if a center frequency cannot be calculated that will ensure - * all of the channels can be accomodated and any defined central DC spike avoided. - * - * @param tuner to source the channel from - * @param channel with defined center frequency and bandwidth - * @return fully constructed tuner channel or null - * @throws RejectedExecutionException if the decimation processor has an error - */ - public TunerChannelSource getChannel(Tuner tuner, TunerChannel channel) throws RejectedExecutionException - { - TunerChannelSource source = null; - - if(canTune(channel)) - { - try - { - mTunedChannels.add(channel); - - if(requiresLOUpdate(channel)) - { - updateLOFrequency(); - } - - source = new CICTunerChannelSource(tuner, channel); - } - catch(SourceException se) - { - mTunedChannels.remove(channel); - source = null; - } - } - - return source; - } - - public int getChannelCount() - { - return mTunedChannels.size(); - } - - /** - * Indicates if the tuner's LO frequency must be updated in order to accommodate the tuner channel - */ - private boolean requiresLOUpdate(TunerChannel channel) throws SourceException - { - int usableBandwidth = getFrequencyController().getUsableBandwidth(); - - long minimumTunedFrequency = getFrequencyController().getFrequency() - (usableBandwidth / 2); - boolean isBelow = channel.getMinFrequency() < minimumTunedFrequency; - - long maximumTunedFrequency = getFrequencyController().getFrequency() + (usableBandwidth / 2); - boolean isAbove = channel.getMaxFrequency() > maximumTunedFrequency; - - long centerBlackoutBandwidth = getFrequencyController().getCenterBlackoutBandwidth(); - boolean overlapsCenter = centerBlackoutBandwidth > 0 && - channel.overlaps(getFrequencyController().getFrequency() - centerBlackoutBandwidth, - getFrequencyController().getFrequency() + centerBlackoutBandwidth); - - return isAbove || isBelow || overlapsCenter; - } - - /** - * Releases the currently sourced tuner channel from this tuner and shuts down the tuner if no other sources exist. - */ - public void releaseChannel(TunerChannelSource tunerChannelSource) - { - if(tunerChannelSource != null) - { - mTunedChannels.remove(tunerChannelSource.getTunerChannel()); - } - } - - /** - * Sets the Local Oscillator frequency to accomodate the current set of tuned channels. - * - * If there is only a single tuned channel, it is placed immediately to the right of the usable bandwidth right - * of the central DC spike. - * - * Otherwise, it places the highest channel frequency at the upper end of the tuner bandwidth, and then iteratively - * moves the center frequency higher until all channels fit within the bandwidth and none of the channels overlap - * any defined central DC spike unusable region. If a center tune frequency cannot be calculated, throw an - * exception so that the most recently added channel can be removed. - * - * Note: the tuned frequency is not changed until a legitimate new frequency can be calculated. If an exception - * is thrown, the current frequency is retained, so that the recently added channel can be removed and all other - * channels can continue as previously arranged. - * - * @throws SourceException if the set of tuner channels, including a recently added channel, cannot be tuned - * within the current tuner bandwidth and any central DC spike unusable region. - */ - private void updateLOFrequency() throws SourceException - { - long candidateFrequency; - - boolean isValidCandidateFrequency = true; - - //If there is only 1 channel set the center frequency so that the channel is positioned to the right of center - if(mTunedChannels.size() == 1) - { - candidateFrequency = mTunedChannels.first().getMinFrequency() - - getFrequencyController().getCenterBlackoutBandwidth() + 1; - } - else - { - long minLockedFrequency = mTunedChannels.first().getMinFrequency(); - long maxLockedFrequency = mTunedChannels.last().getMaxFrequency(); - - //Start by placing the highest frequency channel at the high end of the spectrum - candidateFrequency = maxLockedFrequency - (getFrequencyController().getUsableBandwidth() / 2); - - //Iterate the channels and make sure that none of them overlap the center DC spike buffer, if one exists - if(getFrequencyController().getCenterBlackoutBandwidth() > 0) - { - boolean processingRequired = true; - - while(isValidCandidateFrequency && processingRequired) - { - processingRequired = false; - - long centerBlackout = getFrequencyController().getCenterBlackoutBandwidth(); - - long minAvoid = candidateFrequency - centerBlackout; - long maxAvoid = candidateFrequency + centerBlackout; - - //If any of the center channel(s) overlap the central DC spike avoid area, we'll iteratively - //increase the tuned frequency causing the set of channels to move left in the tuned bandwidth until - //we either find a good center tune frequency, or we walk the lowest frequency channel out of the - //minimum tuned range, in which case we'll throw an exception indicating we don't have a solution. - for(TunerChannel channel : mTunedChannels) - { - if(channel.overlaps(minAvoid, maxAvoid)) - { - //Calculate a tuned frequency adjustment that places this overlapping channel just to the - //left of the central DC spike avoid zone - long adjustment = channel.getMaxFrequency() - minAvoid + 1; - - //If the candidate frequency doesn't push the lowest channel out of bounds, make adjustment - if(candidateFrequency + adjustment - - (getFrequencyController().getUsableBandwidth() / 2) <= minLockedFrequency) - { - candidateFrequency += adjustment; - processingRequired = true; - } - //Otherwise, punt and indicate that we can't find a center frequency or add the channel - else - { - isValidCandidateFrequency = false; - } - - //break out of the for/each loop, so that we can start over again with all of the channels - break; - } - } - } - } - } - - if(isValidCandidateFrequency) - { - mFrequencyController.setLockedFrequency(candidateFrequency); - } - else - { - throw new SourceException("Couldn't calculate viable center frequency from set of tuner channels"); - } - } - -} diff --git a/src/source/tuner/channel/FrequencyController.java b/src/source/tuner/channel/FrequencyController.java deleted file mode 100644 index 8ec85198..00000000 --- a/src/source/tuner/channel/FrequencyController.java +++ /dev/null @@ -1,344 +0,0 @@ -/******************************************************************************* - * 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 source.tuner.channel; - -import org.slf4j.Logger; -import org.slf4j.LoggerFactory; -import source.ISourceEventProcessor; -import source.InvalidFrequencyException; -import source.SourceEvent; -import source.SourceException; - -import java.util.ArrayList; -import java.util.List; - -public class FrequencyController -{ - private final static Logger mLog = LoggerFactory.getLogger(FrequencyController.class); - - private Tunable mTunable; - private long mFrequency = 101100000; - private long mTunedFrequency = 101100000; - private long mMinimumFrequency; - private long mMaximumFrequency; - private double mFrequencyCorrection = 0.0d; - private double mSampleRate = 0.0d; - private long mCenterBlackoutBandwidth; - private double mUsableBandwidthPercentage; - private boolean mLocked; - - private List mProcessors = new ArrayList<>(); - - public FrequencyController(Tunable tunable, long minFrequency, long maxFrequency, double frequencyCorrection, - long centerBlackoutBandwidth, double usableBandwidthPercentage) - { - mTunable = tunable; - mMinimumFrequency = minFrequency; - mMaximumFrequency = maxFrequency; - mFrequencyCorrection = frequencyCorrection; - mCenterBlackoutBandwidth = centerBlackoutBandwidth; - mUsableBandwidthPercentage = usableBandwidthPercentage; - } - - public boolean isLocked() - { - return mLocked; - } - - /** - * Sets the locked status. When this control is locked, the sample rate cannot be changed and the center - * tuned frequency can only be changed via the setLockedFrequency() method. - * - * Note: this method is package-private and is intended to be used (only) by the ChannelManager. - * - * @param locked - */ - void setLocked(boolean locked) - { - mLocked = locked; - } - - - /** - * Set sample rate/bandwidth in hertz - */ - public void setSampleRate(int sampleRate) throws SourceException - { - if(isLocked()) - { - throw new FrequencyControllerLockedException(); - } - - mSampleRate = sampleRate; - - broadcastSampleRateChange(); - } - - /** - * Get sample rate/bandwidth in hertz - */ - public double getSampleRate() - { - return mSampleRate; - } - - /** - * Get frequency in hertz - */ - public long getFrequency() - { - return mFrequency; - } - - /** - * Set frequency - * - * @param frequency in hertz - * @throws SourceException if tunable doesn't support tuning a corrected - * version of the requested frequency - */ - public void setFrequency(long frequency) throws SourceException - { - if(isLocked()) - { - throw new FrequencyControllerLockedException(); - } - - setFrequency(frequency, true); - } - - /** - * Sets the frequency when the frequency controller is locked. Note: this method is package-private and is - * only intended to be used by the ChannelManager. - * - * @param frequency to set as the center tuned frequency - * @throws SourceException if there is an error while setting the frequency - */ - void setLockedFrequency(long frequency) throws SourceException - { - setFrequency(frequency, true); - } - - /** - * Indicates if the specified frequency can be tuned (ie is within min/max frequency) by this controller. - * @param frequency to evaluate - * @return true if the frequency falls within the tuning range of this controller - */ - public boolean canTune(long frequency) - { - return getMinimumFrequency() <= frequency && frequency <= getMaximumFrequency(); - } - - /** - * Set frequency with optional broadcast of frequency change event. This - * method supports changing the frequency correction value without - * broadcasting a frequency change event. - */ - private void setFrequency(long frequency, boolean broadcastChange) throws SourceException - { - long tunedFrequency = getTunedFrequency(frequency); - - if(tunedFrequency < mMinimumFrequency) - { - throw new InvalidFrequencyException("Requested frequency not valid", frequency, mMinimumFrequency); - } - - if(tunedFrequency > mMaximumFrequency) - { - throw new InvalidFrequencyException("Requested frequency not valid", frequency, mMaximumFrequency); - } - - mFrequency = frequency; - - mTunedFrequency = tunedFrequency; - - if(mTunable != null) - { - mTunable.setTunedFrequency(mTunedFrequency); - } - - /* Broadcast to all listeners that the frequency has changed */ - if(broadcastChange) - { - broadcastFrequencyChange(); - } - } - - public long getTunedFrequency() - { - return mTunedFrequency; - } - - public long getMinimumFrequency() - { - return mMinimumFrequency; - } - - public long getMaximumFrequency() - { - return mMaximumFrequency; - } - - /** - * Calculate the tuned frequency by adding frequency correction to the - * corrected frequency. - * - * @param correctedFrequency - */ - private long getTunedFrequency(long correctedFrequency) - { - return (long)((double)correctedFrequency / - (1.0 + (mFrequencyCorrection / 1000000.0))); - } - - /** - * Calculate the corrected frequency by subtracting frequency correction - * from the tuned frequency. - * - * @param tunedFrequency - */ - @SuppressWarnings("unused") - private long getCorrectedFrequency(long tunedFrequency) - { - return (long)((double)tunedFrequency / - (1.0 - (mFrequencyCorrection / 1000000.0))); - } - - public double getFrequencyCorrection() - { - return mFrequencyCorrection; - } - - public void setFrequencyCorrection(double correction) throws SourceException - { - mFrequencyCorrection = correction; - - if(mFrequency > 0) - { - setFrequency(mFrequency, true); - } - - broadcastFrequencyCorrectionChange(); - } - - /** - * Adds listener to receive frequency change events - */ - public void addListener(ISourceEventProcessor processor) - { - if(!mProcessors.contains(processor)) - { - mProcessors.add(processor); - } - } - - /** - * Removes listener from receiving frequency change events - */ - public void removeFrequencyChangeProcessor(ISourceEventProcessor processor) - { - mProcessors.remove(processor); - } - - - /** - * Broadcasts a change/update to the current (uncorrected) frequency or the - * bandwidth/sample rate value. - */ - protected void broadcastFrequencyChange() throws SourceException - { - broadcast(SourceEvent.frequencyChange(mFrequency)); - } - - /** - * Broadcast a frequency error/correction value change - */ - protected void broadcastFrequencyCorrectionChange() throws SourceException - { - broadcast(SourceEvent.frequencyCorrectionChange((int)mFrequencyCorrection)); - } - - /** - * Broadcasts a sample rate change - */ - protected void broadcastSampleRateChange() throws SourceException - { - broadcast(SourceEvent.sampleRateChange(mSampleRate)); - } - - public void broadcast(SourceEvent event) throws SourceException - { - for(ISourceEventProcessor processor : mProcessors) - { - processor.process(event); - } - } - - /** - * Identifies the bandwidth of a blackout frequency range centered about the center frequency. This is - * used to block the frequency range where a DC spike may exist. - * @return center blackout bandwidth in hertz - */ - public long getCenterBlackoutBandwidth() - { - return mCenterBlackoutBandwidth; - } - - /** - * Percentage of the available bandwidth that is usable. Sources with filter roll-off at the ends of the - * spectral range can limit the usable bandwidth to avoid providing samples at these spectral ends. - * - * @return bandwidth percentage (0.0 <> 1.0) - */ - public double getUsableBandwidthPercentage() - { - return mUsableBandwidthPercentage; - } - - /** - * Usable bandwidth in hertz calculated as the sample rate constrained by the usable bandwidth percentage. - */ - public int getUsableBandwidth() - { - return (int)(getSampleRate() * getUsableBandwidthPercentage()); - } - - public interface Tunable - { - /** - * Gets the tuned frequency of the device - */ - public long getTunedFrequency() throws SourceException; - - /** - * Sets the tuned frequency of the device - */ - public void setTunedFrequency(long frequency) throws SourceException; - - /** - * Gets the current bandwidth setting of the device - */ - public double getCurrentSampleRate() throws SourceException; - - /** - * Indicates if this tunable can tune the frequency - */ - public boolean canTune(long frequency); - } -} diff --git a/src/source/tuner/channel/FrequencyControllerLockedException.java b/src/source/tuner/channel/FrequencyControllerLockedException.java deleted file mode 100644 index 9ad9f09a..00000000 --- a/src/source/tuner/channel/FrequencyControllerLockedException.java +++ /dev/null @@ -1,35 +0,0 @@ -/******************************************************************************* - * 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 source.tuner.channel; - -import source.SourceException; - -public class FrequencyControllerLockedException extends SourceException -{ - private static final long serialVersionUID = 1L; - - /** - * Indicates an attempt to adjust frequency or sample rate on a locked frequency controller. - */ - public FrequencyControllerLockedException() - { - super( "Frequency controller is locked -- frequency and sample rate cannot be changed until the controller" + - " is unlocked by the channel manager" ); - } -} diff --git a/src/source/tuner/configuration/TunerConfigurationModel.java b/src/source/tuner/configuration/TunerConfigurationModel.java deleted file mode 100644 index 67fbaf04..00000000 --- a/src/source/tuner/configuration/TunerConfigurationModel.java +++ /dev/null @@ -1,384 +0,0 @@ -/******************************************************************************* - * SDR Trunk - * Copyright (C) 2014-2016 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 source.tuner.configuration; - -import java.util.ArrayList; -import java.util.List; - -import javax.swing.table.AbstractTableModel; - -import org.slf4j.Logger; -import org.slf4j.LoggerFactory; - -import sample.Listener; -import source.tuner.Tuner; -import source.tuner.TunerType; -import source.tuner.configuration.TunerConfigurationEvent.Event; - -public class TunerConfigurationModel extends AbstractTableModel -{ - private static final long serialVersionUID = 1L; - private final static Logger mLog = - LoggerFactory.getLogger( TunerConfigurationModel.class ); - - public static final int TUNER_TYPE = 0; - public static final int CONFIGURATION_NAME = 1; - public static final int ASSIGNED = 2; - private static final String[] COLUMN_HEADERS = { "Tuner Type", "Name", "Assigned" }; - - private List> mConfigurationChangeListeners = - new ArrayList<>(); - private List mTunerConfigurations = new ArrayList<>(); - - /** - * Table model for managing tuner configurations. Provides tuner configuration events to - * registered listeners - */ - public TunerConfigurationModel() - { - } - - /** - * Adds the list of tuner configurations to the model - */ - public void addTunerConfigurations( List configs ) - { - for( TunerConfiguration config: configs ) - { - addTunerConfiguration( config ); - } - } - - /** - * Returns of list of tuner configurations from the model - */ - public List getTunerConfigurations() - { - return mTunerConfigurations; - } - - /** - * Adds the tuner configuration to the model firing a table model row add - * event and broadcasting a tuner configuration event. - */ - public void addTunerConfiguration( TunerConfiguration config ) - { - if( !mTunerConfigurations.contains( config ) && - config.getUniqueID() != null && - config.getName() != null ) - { - mTunerConfigurations.add( config ); - - int index = mTunerConfigurations.indexOf( config ); - - fireTableRowsInserted( index, index ); - - broadcast( new TunerConfigurationEvent( config, Event.ADD ) ); - } - } - - /** - * Removes the tuner configuration from the model firing a table model row - * delete event and broadcasting a tuner configuration event. - */ - public void removeTunerConfiguration( TunerConfiguration config ) - { - if( mTunerConfigurations.contains( config ) ) - { - int index = mTunerConfigurations.indexOf( config ); - - mTunerConfigurations.remove( config ); - - fireTableRowsDeleted( index, index ); - - broadcast( new TunerConfigurationEvent( config, Event.REMOVE ) ); - } - } - - /** - * Removes all tuner configurations and assignments from the model and - * fires a table clear event and broadcasts a tuner configuration event for - * each row removed. - */ - public void clear() - { - for( TunerConfiguration config: mTunerConfigurations ) - { - broadcast( new TunerConfigurationEvent( config, Event.REMOVE ) ); - } - - int size = mTunerConfigurations.size(); - - mTunerConfigurations.clear(); - - fireTableRowsDeleted( 0, size - 1 ); - } - - /** - * Registers a tuner configuration event listener - */ - public void addListener( Listener listener ) - { - mConfigurationChangeListeners.add( listener ); - } - - /** - * Removes the tuner configuration event listener - */ - public void removeListener( Listener listener ) - { - mConfigurationChangeListeners.remove( listener ); - } - - /** - * Broadcasts a tuner configuration event - */ - public void broadcast( TunerConfigurationEvent event ) - { - for( Listener listener: mConfigurationChangeListeners ) - { - listener.receive( event ); - } - } - - /** - * Designates the configuration argument as the currently assigned configuration and unassigns - * any other configurations with the same tuner type and unique ID. - */ - public void assignTunerConfiguration( TunerConfiguration configToAssign ) - { - if( configToAssign != null ) - { - List configs = - getTunerConfigurations( configToAssign.getTunerType(), configToAssign.getUniqueID() ); - - for( TunerConfiguration config: configs ) - { - if( config.isAssigned() ) - { - config.setAssigned( false ); - - int index = mTunerConfigurations.indexOf( config ); - fireTableCellUpdated( index, ASSIGNED ); - broadcast( new TunerConfigurationEvent( config, Event.CHANGE ) ); - } - } - - configToAssign.setAssigned( true ); - int index = mTunerConfigurations.indexOf( configToAssign ); - fireTableCellUpdated( index, ASSIGNED ); - broadcast( new TunerConfigurationEvent( configToAssign, Event.CHANGE ) ); - } - } - - /** - * Returns the tuner configuration that is assigned to the tuner with the - * tuner type and uniqueID, or selects the first existing configuration that - * matches the tuner type and unique ID and assigns it, or creates a new - * tuner configuration and assigns it to the tuner. - * - * This method will always return a tuner configuration that is assigned to - * the tuner type and unique ID. - */ - public TunerConfiguration getTunerConfiguration( TunerType type, String uniqueID ) - { - List configs = getTunerConfigurations( type, uniqueID ); - - for( TunerConfiguration config: configs ) - { - if( config.isAssigned() && - config.getUniqueID() != null && - config.getUniqueID().contentEquals( uniqueID ) ) - { - return config; - } - } - - //Reuse an existing configuration to assign - if( !configs.isEmpty() ) - { - configs.get( 0 ).setAssigned( true ); - return configs.get( 0 ); - } - - String name = getDistinctName( type, uniqueID ); - - //We didn't find the config so create a new one and assign it - TunerConfiguration config = TunerConfigurationFactory - .getTunerConfiguration( type, uniqueID, name ); - - config.setAssigned( true ); - - addTunerConfiguration( config ); - - return config; - } - - /** - * Indicates if a configuration exists in this model matching the tuner type - * and unique ID and name. - */ - public boolean hasTunerConfiguration( TunerType type, String uniqueID, String name ) - { - for( TunerConfiguration config: mTunerConfigurations ) - { - if( config.getTunerType() == type && - config.getUniqueID() != null && - config.getUniqueID().contentEquals( uniqueID ) && - config.getName() != null && - config.getName().contentEquals( name ) ) - { - return true; - } - } - - return false; - } - - /** - * Creates a configuration name for the tuner configuration that is distinct - * among the existing tuner configurations of the same type and uniqueID - */ - public String getDistinctName( TunerType type, String uniqueID ) - { - int counter = 0; - - String name = "New"; - - while( hasTunerConfiguration( type, uniqueID, name ) ) - { - counter++; - - name = "New (" + counter + ")"; - } - - return name; - } - - /** - * Returns all tuner configurations for the specified tuner type - */ - public List getTunerConfigurations( TunerType type, String uniqueID ) - { - List configs = new ArrayList<>(); - - for( TunerConfiguration config: mTunerConfigurations ) - { - if( config.getTunerType() == type && - config.getUniqueID() != null && - config.getUniqueID().contentEquals( uniqueID ) ) - { - configs.add( config ); - } - } - - return configs; - } - - /** - * Column names for the table model - */ - @Override - public String getColumnName( int column ) - { - return COLUMN_HEADERS[ column ]; - } - - /** - * Tuner configuration model row count - */ - @Override - public int getRowCount() - { - return mTunerConfigurations.size(); - } - - /** - * Tuner configuration model column count - */ - @Override - public int getColumnCount() - { - return COLUMN_HEADERS.length; - } - - /** - * Returns the row index for the specified configuration or -1 - */ - public int getRowIndex( TunerConfiguration configuration ) - { - if( mTunerConfigurations.contains( configuration ) ) - { - return mTunerConfigurations.indexOf( configuration ); - } - - return -1; - } - - /** - * Returns the configuration at the specified index or null - */ - public TunerConfiguration getTunerConfiguration( int index ) - { - if( index >= 0 && index < mTunerConfigurations.size() ) - { - return mTunerConfigurations.get( index ); - } - - return null; - } - - /** - * Returns the value at the indicated table cell - */ - @Override - public Object getValueAt( int row, int column ) - { - if( row >= 0 && row < mTunerConfigurations.size() ) - { - TunerConfiguration config = mTunerConfigurations.get( row ); - - switch( column ) - { - case TUNER_TYPE: - return config.getTunerType().getLabel(); - case CONFIGURATION_NAME: - return config.getName(); - case ASSIGNED: - return config.isAssigned() ? "*" : null; - default: - break; - } - } - - return null; - } - - /** - * Stores the latest tuned frequency for the tuner in the currently assigned configuration - */ - public void tunerFrequencyChanged( Tuner tuner ) - { - TunerConfiguration assigned = getTunerConfiguration( tuner.getTunerType(), tuner.getUniqueID() ); - - assigned.setFrequency( tuner.getTunerController().getFrequencyController().getFrequency() ); - - broadcast( new TunerConfigurationEvent( assigned, Event.CHANGE ) ); - } -} diff --git a/src/source/tuner/fcd/FCDTuner.java b/src/source/tuner/fcd/FCDTuner.java deleted file mode 100644 index 18c89fc6..00000000 --- a/src/source/tuner/fcd/FCDTuner.java +++ /dev/null @@ -1,64 +0,0 @@ -/******************************************************************************* - * SDR Trunk - * Copyright (C) 2014-2016 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 source.tuner.fcd; - -import source.tuner.Tuner; -import source.tuner.TunerClass; -import source.tuner.TunerType; - -public class FCDTuner extends Tuner -{ - public FCDTuner(FCDTunerController controller) - { - super( controller.getConfiguration().toString(), controller); - } - - public void dispose() - { - getController().dispose(); - } - - public FCDTunerController getController() - { - return (FCDTunerController)getTunerController(); - } - - @Override - public TunerClass getTunerClass() - { - return getController().getTunerClass(); - } - - @Override - public TunerType getTunerType() - { - return getController().getTunerType(); - } - - @Override - public String getUniqueID() - { - return getController().getUSBAddress(); - } - - @Override - public double getSampleSize() - { - return 16.0; - } -} diff --git a/src/source/tuner/fcd/FCDTunerController.java b/src/source/tuner/fcd/FCDTunerController.java deleted file mode 100644 index b85721d0..00000000 --- a/src/source/tuner/fcd/FCDTunerController.java +++ /dev/null @@ -1,657 +0,0 @@ -/******************************************************************************* - * 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 source.tuner.fcd; - -import org.slf4j.Logger; -import org.slf4j.LoggerFactory; -import org.usb4java.Device; -import org.usb4java.DeviceDescriptor; -import org.usb4java.DeviceHandle; -import org.usb4java.LibUsb; -import org.usb4java.LibUsbException; -import sample.Listener; -import sample.adapter.ShortAdapter; -import sample.complex.ComplexBuffer; -import source.SourceException; -import source.mixer.ComplexMixer; -import source.tuner.MixerTunerDataLine; -import source.tuner.TunerClass; -import source.tuner.TunerController; -import source.tuner.TunerType; -import source.tuner.configuration.TunerConfiguration; -import source.tuner.fcd.proV1.FCD1TunerController.Block; - -import javax.sound.sampled.AudioFormat; -import javax.usb.UsbClaimException; -import javax.usb.UsbException; -import java.nio.ByteBuffer; -import java.nio.ByteOrder; -import java.nio.IntBuffer; - -public abstract class FCDTunerController extends TunerController -{ - private final static Logger mLog = LoggerFactory.getLogger(FCDTunerController.class); - - public static final double USABLE_BANDWIDTH_PERCENT = 1.0; - public static final int DC_SPIKE_AVOID_BUFFER = 5000; - - public final static byte FCD_INTERFACE = (byte)0x2; - public final static byte FCD_ENDPOINT_IN = (byte)0x82; - public final static byte FCD_ENDPOINT_OUT = (byte)0x2; - - private Device mDevice; - private DeviceDescriptor mDeviceDescriptor; - private DeviceHandle mDeviceHandle; - - private FCDConfiguration mConfiguration = new FCDConfiguration(); - protected ComplexMixer mComplexMixer; - - - /** - * Generic FCD tuner controller - contains functionality common across both - * funcube dongle tuners - * - * @param device - * @param descriptor - * @param minTunableFrequency - * @param maxTunableFrequency - */ - public FCDTunerController(MixerTunerDataLine mixerTDL, Device device, DeviceDescriptor descriptor, String tunerName, - int minTunableFrequency, int maxTunableFrequency, AudioFormat audioFormat) - { - super(minTunableFrequency, maxTunableFrequency, DC_SPIKE_AVOID_BUFFER, USABLE_BANDWIDTH_PERCENT); - mDevice = device; - mDeviceDescriptor = descriptor; - - try - { - mFrequencyController.setSampleRate((int)audioFormat.getSampleRate()); - } - catch(SourceException se) - { - mLog.error("Error setting sample rate to [" + audioFormat.getSampleRate() + "]", se); - } - - mComplexMixer = new ComplexMixer( mixerTDL.getTargetDataLine(), audioFormat, tunerName, new ShortAdapter(), - mSampleBroadcaster); - } - - /** - * Overrides the super class functionality to auto-start the complex mixer and provide samples to listeners - */ - @Override - public void addComplexBufferListener(Listener listener) - { - boolean hasExistingListeners = hasComplexBufferListeners(); - - super.addComplexBufferListener(listener); - - if(!hasExistingListeners) - { - mComplexMixer.start(); - } - } - - /** - * Overrides the super class functionality to auto-stop the complex mixer and stop sample stream when there are no - * more registered listeners - */ - @Override - public void removeComplexBufferListener(Listener listener) - { - super.removeComplexBufferListener(listener); - - if(!hasComplexBufferListeners()) - { - mComplexMixer.stop(); - } - } - - /** - * Initializes the controller by opening the USB device and claiming the - * HID interface. - * - * Invoke this method after constructing this class to setup the - * controller. - * - * @throws SourceException if cannot open and claim the USB device - */ - public void init() throws SourceException - { - mDeviceHandle = new DeviceHandle(); - - int result = LibUsb.open(mDevice, mDeviceHandle); - - if(result != LibUsb.SUCCESS) - { - mDeviceHandle = null; - - throw new SourceException("libusb couldn't open funcube usb device [" + LibUsb.errorName(result) + "]"); - } - - claimInterface(); - } - - /** - * Disposes of resources. Closes the USB device and interface. - */ - public void dispose() - { - if(mDeviceHandle != null) - { - try - { - LibUsb.close(mDeviceHandle); - } - catch(Exception e) - { - mLog.error("error while closing device handle", e); - } - - mDeviceHandle = null; - } - - mDeviceDescriptor = null; - mDevice = null; - } - - /** - * Sample rate of the tuner - */ - public abstract double getCurrentSampleRate(); - - /** - * Tuner class - */ - public abstract TunerClass getTunerClass(); - - /** - * Tuner type - */ - public abstract TunerType getTunerType(); - - /** - * Applies the settings in the tuner configuration - */ - public abstract void apply(TunerConfiguration config) throws SourceException; - - /** - * USB address (bus/port) - */ - public String getUSBAddress() - { - if(mDevice != null) - { - StringBuilder sb = new StringBuilder(); - - sb.append("Bus:"); - int bus = LibUsb.getBusNumber(mDevice); - sb.append(bus); - - sb.append(" Port:"); - int port = LibUsb.getPortNumber(mDevice); - sb.append(port); - - return sb.toString(); - } - - return "UNKNOWN"; - } - - /** - * USB Vendor and Product ID - */ - public String getUSBID() - { - if(mDeviceDescriptor != null) - { - StringBuilder sb = new StringBuilder(); - - sb.append(String.format("%04X", (int)(mDeviceDescriptor.idVendor() & 0xFFFF))); - sb.append(":"); - sb.append(String.format("%04X", (int)(mDeviceDescriptor.idProduct() & 0xFFFF))); - - return sb.toString(); - } - - return "UNKNOWN"; - } - - /** - * USB Port Speed. Should be 2.0 for both types of funcube dongles - */ - public String getUSBSpeed() - { - if(mDevice != null) - { - int speed = LibUsb.getDeviceSpeed(mDevice); - - switch(speed) - { - case 0: - return "1.1 LOW"; - case 1: - return "1.1 FULL"; - case 2: - return "2.0 HIGH"; - case 3: - return "3.0 SUPER"; - default: - } - } - - return "UNKNOWN"; - } - - /** - * Set fcd interface mode - */ - public void setFCDMode(Mode mode) throws UsbException, UsbClaimException - { - ByteBuffer response = null; - - switch(mode) - { - case APPLICATION: - response = send(FCDCommand.BL_QUERY); - break; - case BOOTLOADER: - response = send(FCDCommand.APP_RESET); - break; - default: - break; - } - - if(response != null) - { - mConfiguration.set(response); - } - else - { - mConfiguration.setModeUnknown(); - } - } - - /** - * Sets the actual (uncorrected) device frequency - */ - public void setTunedFrequency(long frequency) throws SourceException - { - try - { - send(FCDCommand.APP_SET_FREQUENCY_HZ, frequency); - } - catch(Exception e) - { - throw new SourceException("Couldn't set FCD Local " + - "Oscillator Frequency [" + frequency + "]", e); - } - } - - /** - * Gets the actual (uncorrected) device frequency - */ - public long getTunedFrequency() throws SourceException - { - try - { - ByteBuffer buffer = send(FCDCommand.APP_GET_FREQUENCY_HZ); - - buffer.order(ByteOrder.LITTLE_ENDIAN); - - return (int)(buffer.getInt(2) & 0xFFFFFFFF); - } - catch(Exception e) - { - throw new SourceException("FCDTunerController - " - + "couldn't get LO frequency", e); - } - } - - /** - * Returns the FCD device configuration - */ - public FCDConfiguration getConfiguration() - { - return mConfiguration; - } - - /** - * Claims the USB interface. Attempts to detach the active kernel driver if - * one is currently attached. - */ - private void claimInterface() throws SourceException - { - if(mDeviceHandle != null) - { - int result = LibUsb.kernelDriverActive(mDeviceHandle, FCD_INTERFACE); - - if(result == 1) - { - result = LibUsb.detachKernelDriver(mDeviceHandle, FCD_INTERFACE); - - if(result != LibUsb.SUCCESS) - { - mLog.error("failed attempt to detach kernel driver [" + LibUsb.errorName(result) + "]"); - } - } - - result = LibUsb.claimInterface(mDeviceHandle, FCD_INTERFACE); - - if(result != LibUsb.SUCCESS) - { - throw new SourceException("couldn't claim usb interface [" + LibUsb.errorName(result) + "]"); - } - } - else - { - throw new SourceException("couldn't claim usb hid interface - no device handle"); - } - } - - /** - * Performs an interrupt write to the OUT endpoint. - * - * @param buffer - direct allocated buffer. Must be 64 bytes in length. - * @throws LibUsbException on error - */ - private void write(ByteBuffer buffer) throws LibUsbException - { - if(mDeviceHandle != null) - { - IntBuffer transferred = IntBuffer.allocate(1); - - int result = LibUsb.interruptTransfer(mDeviceHandle, FCD_ENDPOINT_OUT, buffer, transferred, 500l); - - if(result != LibUsb.SUCCESS) - { - throw new LibUsbException("error writing byte buffer", result); - } - } - else - { - throw new LibUsbException("device handle is null", LibUsb.ERROR_NO_DEVICE); - } - } - - /** - * Performs an interrupt write to the OUT endpoint for the FCD command. - * - * @param command - no-argument command to write - * @throws LibUsbException - on error - */ - private void write(FCDCommand command) throws LibUsbException - { - ByteBuffer buffer = ByteBuffer.allocateDirect(64); - - buffer.put(0, command.getCommand()); - buffer.put(1, (byte)0x00); - - write(buffer); - } - - /** - * Convenience logger for debugging read/write operations - */ - @SuppressWarnings("unused") - private void log(String label, ByteBuffer buffer) - { - StringBuilder sb = new StringBuilder(); - - sb.append(label); - - sb.append(" "); - - sb.append(buffer.get(0)); - sb.append(" | "); - - for(int x = 0; x < 64; x++) - { - sb.append(String.format("%02X", (int)(buffer.get(x) & 0xFF))); - sb.append(" "); - } - - mLog.debug(sb.toString()); - } - - /** - * Performs an interrupt write to the OUT endpoint for the FCD command. - * - * @param command - command to write - * @param argument - value to write with the command - * @throws LibUsbException - on error - */ - private void write(FCDCommand command, long argument) throws LibUsbException - { - ByteBuffer buffer = ByteBuffer.allocateDirect(64); - - /* The FCD expects little-endian formatted values */ - buffer.order(ByteOrder.LITTLE_ENDIAN); - - buffer.put(0, command.getCommand()); - buffer.putLong(1, argument); - - write(buffer); - } - - /** - * Performs an interrupt read against the endpoint - * - * @return buffer read from FCD - * @throws LibUsbException on error - */ - private ByteBuffer read() throws LibUsbException - { - if(mDeviceHandle != null) - { - ByteBuffer buffer = ByteBuffer.allocateDirect(64); - - IntBuffer transferred = IntBuffer.allocate(1); - - - int result = LibUsb.interruptTransfer(mDeviceHandle, FCD_ENDPOINT_IN, buffer, transferred, 500l); - - if(result != LibUsb.SUCCESS) - { - throw new LibUsbException("error reading byte buffer", result); - } - else if(transferred.get(0) != buffer.capacity()) - { - throw new LibUsbException("received bytes [" + transferred.get(0) + - "] didn't match expected length [" + buffer.capacity() + "]", result); - } - - return buffer; - } - - throw new LibUsbException("device handle is null", LibUsb.ERROR_NO_DEVICE); - } - - /** - * Sends the FCD command and argument. Performs a read to complete the - * command. - * - * @param command - command to send - * @param argument - command argument to send - * @throws LibUsbException - on error - */ - protected void send(FCDCommand command, long argument) throws LibUsbException - { - write(command, argument); - read(); - } - - /** - * Sends the no-argument FCD command. Performs a read to complete the - * command. - * - * @param command - command to send - * @throws LibUsbException - on error - */ - protected ByteBuffer send(FCDCommand command) throws LibUsbException - { - write(command); - return read(); - } - - /** - * FCD configuration string parsing class - */ - public class FCDConfiguration - { - private String mConfig; - private Mode mMode; - - public FCDConfiguration() - { - mConfig = null; - mMode = Mode.UNKNOWN; - } - - private void setModeUnknown() - { - mConfig = null; - mMode = Mode.UNKNOWN; - } - - /** - * Extracts the configuration string from the buffer - */ - public void set(ByteBuffer buffer) - { - if(buffer.capacity() == 64) - { - byte[] data = new byte[64]; - - for(int x = 0; x < 64; x++) - { - data[x] = buffer.get(x); - } - - mConfig = new String(data); - mMode = Mode.getMode(mConfig); - } - else - { - mConfig = null; - mMode = Mode.ERROR; - } - } - - public Mode getMode() - { - return mMode; - } - - public FCDModel getModel() - { - FCDModel retVal = FCDModel.FUNCUBE_UNKNOWN; - - switch(mMode) - { - case APPLICATION: - retVal = FCDModel.getFCD(mConfig.substring(15, 22)); - break; - case BOOTLOADER: - case UNKNOWN: - case ERROR: - break; - } - - return retVal; - } - - public Block getBandBlocking() - { - Block retVal = Block.UNKNOWN; - - switch(mMode) - { - case APPLICATION: - retVal = Block.getBlock(mConfig.substring(23, 33).trim()); - break; - case BOOTLOADER: - case UNKNOWN: - case ERROR: - break; - } - - return retVal; - } - - public String getFirmware() - { - String retVal = null; - - switch(mMode) - { - case APPLICATION: - retVal = mConfig.substring(9, 14); - break; - case BOOTLOADER: - case UNKNOWN: - case ERROR: - break; - } - - return retVal; - } - - public String toString() - { - return getModel().getLabel(); - } - } - - public enum Mode - { - APPLICATION, - BOOTLOADER, - ERROR, - UNKNOWN; - - public static Mode getMode(String config) - { - Mode retVal = UNKNOWN; - - if(config == null) - { - retVal = ERROR; - } - else - { - if(config.length() >= 8) - { - String mode = config.substring(2, 8).trim(); - - if(mode.equalsIgnoreCase("FCDAPP")) - { - retVal = APPLICATION; - } - else if(mode.equalsIgnoreCase("FCDBL")) - { - retVal = BOOTLOADER; - } - } - } - - return retVal; - } - } -} diff --git a/src/source/tuner/fcd/proV1/FCD1TunerController.java b/src/source/tuner/fcd/proV1/FCD1TunerController.java deleted file mode 100644 index 6b576835..00000000 --- a/src/source/tuner/fcd/proV1/FCD1TunerController.java +++ /dev/null @@ -1,717 +0,0 @@ -/******************************************************************************* - * SDR Trunk - * Copyright (C) 2014 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 source.tuner.fcd.proV1; - -import org.slf4j.Logger; -import org.slf4j.LoggerFactory; -import org.usb4java.Device; -import org.usb4java.DeviceDescriptor; -import source.SourceException; -import source.tuner.MixerTunerDataLine; -import source.tuner.MixerTunerType; -import source.tuner.TunerClass; -import source.tuner.TunerType; -import source.tuner.configuration.TunerConfiguration; -import source.tuner.fcd.FCDCommand; -import source.tuner.fcd.FCDTunerController; - -import javax.usb.UsbClaimException; -import javax.usb.UsbException; -import java.nio.ByteBuffer; -import java.nio.ByteOrder; - -public class FCD1TunerController extends FCDTunerController -{ - private final static Logger mLog = LoggerFactory.getLogger(FCD1TunerController.class); - - public static final int MINIMUM_TUNABLE_FREQUENCY = 64000000; - public static final int MAXIMUM_TUNABLE_FREQUENCY = 1700000000; - public static final int SAMPLE_RATE = 96000; - - private double mDCCorrectionInPhase = 0.0; - private double mDCCorrectionQuadrature = 0.0; - private double mPhaseCorrection = 0.0; - private double mGainCorrection = 0.0; - private LNAGain mLNAGain; - private LNAEnhance mLNAEnhance; - private MixerGain mMixerGain; - private FCD1TunerConfiguration mTunerConfiguration; - private FCD1TunerEditor mEditor; - - public FCD1TunerController(MixerTunerDataLine mixerTDL, Device device, DeviceDescriptor descriptor) - { - super(mixerTDL, device, descriptor, MixerTunerType.FUNCUBE_DONGLE_PRO.getDisplayString(), MINIMUM_TUNABLE_FREQUENCY, - MAXIMUM_TUNABLE_FREQUENCY, MixerTunerType.FUNCUBE_DONGLE_PRO.getAudioFormat()); - } - - public void init() throws SourceException - { - super.init(); - - getFrequencyController().setSampleRate(SAMPLE_RATE); - - try - { - setFCDMode(Mode.APPLICATION); - getPhaseAndGainCorrection(); - getDCIQCorrection(); - getLNAGainSetting(); - getLNAEnhanceSetting(); - getMixerGainSetting(); - - send(FCDCommand.APP_SET_MIXER_GAIN, 1l); - } - catch(Exception e) - { - e.printStackTrace(); - - throw new SourceException("FCDTunerController error " + - "during construction", e); - } - } - - public double getCurrentSampleRate() - { - return SAMPLE_RATE; - } - - @Override - public TunerClass getTunerClass() - { - return TunerClass.FUNCUBE_DONGLE_PRO; - } - - @Override - public TunerType getTunerType() - { - return TunerType.FUNCUBE_DONGLE_PRO; - } - - /** - * Get the tuner configuration - * - * @return current config or null if one hasn't been applied yet - */ - public FCD1TunerConfiguration getTunerConfiguration() - { - return mTunerConfiguration; - } - - @Override - public void apply(TunerConfiguration tunerConfig) - throws SourceException - { - FCD1TunerConfiguration config = (FCD1TunerConfiguration)tunerConfig; - - try - { - getFrequencyController().setFrequencyCorrection(config.getFrequencyCorrection()); - setLNAGain(config.getLNAGain()); - setLNAEnhance(config.getLNAEnhance()); - setMixerGain(config.getMixerGain()); - setDCCorrectionInPhase(config.getInphaseDCCorrection()); - setDCCorrectionQuadrature(config.getQuadratureDCCorrection()); - setGainCorrection(config.getGainCorrection()); - setPhaseCorrection(config.getPhaseCorrection()); - - //If we get to here without exception, store this config - mTunerConfiguration = config; - } - catch(Exception e) - { - //Re-cast any usb exceptions as source exceptions - throw new SourceException("FCD 1 Controller encountered an " - + "exception while applying tuner config - " + - e.getLocalizedMessage()); - } - - try - { - getFrequencyController().setFrequency(config.getFrequency()); - } - catch(SourceException se) - { - //Do nothing, we couldn't set the frequency - } - } - - /** - * Gets the current LNA Gain value from the controller and stores it - */ - private void getLNAGainSetting() throws UsbClaimException, UsbException - { - ByteBuffer buffer = send(FCDCommand.APP_GET_LNA_GAIN); - - buffer.order(ByteOrder.LITTLE_ENDIAN); - - int gain = buffer.getInt(2); - - mLNAGain = LNAGain.valueOf(gain); - } - - /** - * @return - current lna gain setting - */ - public LNAGain getLNAGain() - { - return mLNAGain; - } - - /** - * Sets lna gain for the controller - * - * @param gain - * @throws UsbException - * @throws UsbClaimException - */ - public void setLNAGain(LNAGain gain) - throws UsbException, UsbClaimException - { - send(FCDCommand.APP_SET_LNA_GAIN, gain.getSetting()); - } - - /** - * @return - current phase correction setting - */ - public double getPhaseCorrection() - { - return mPhaseCorrection; - } - - /** - * Sets the phase correction - * - * @param value - new phase correction - * @throws IllegalArgumentException for value outside limits (0.0 <> 1.0) - * @throws UsbClaimException - if cannot claim the FCD HID controller - * @throws UsbException - if there was a usb error - */ - public void setPhaseCorrection(double value) - throws UsbClaimException, UsbException - { - if(value < -1.0 || value > 1.0) - { - throw new IllegalArgumentException("Phase correction value [" + - value + "] outside limits ( -1.0 <> 1.0 )"); - } - - mPhaseCorrection = value; - setPhaseAndGainCorrection(); - } - - /** - * @return current gain correction setting - */ - public double getGainCorrection() - { - return mGainCorrection; - } - - /** - * @param value - gain correction value - * @throws IllegalArgumentException for value outside limits (0.0 <> 1.0) - * @throws UsbClaimException - if cannot claim the FCD HID controller - * @throws UsbException - if there was a usb error - */ - public void setGainCorrection(double value) - throws UsbClaimException, UsbException - { - if(value < -1.0 || value > 1.0) - { - throw new IllegalArgumentException("Gain correction value [" + - value + "] outside limits ( -1.0 <> 1.0 )"); - } - - mGainCorrection = value; - setPhaseAndGainCorrection(); - } - - /** - * Sends the phase and gain correction values to FCD. - * - * The interface expects an argument with the SIGNED phase value in the first - * 16 bits and the UNSIGNED gain value in the second 16 bits, both in - * big-endian format. Since we're sending the argument in java little endian - * format, we place the phase in the high-order bits and the gain in the - * low-order bits, so that the send(command) function will reorder the bytes - * into big endian format, and place the arguments correctly. - * - * Gain correction values range -1.0 to 0.0 to 1.0 and applied to the dongle - * as values 0 to 65534 - * - * Phase correction values range -1.0 to 0.0 to 1.0 and applied to the - * dongle as signed values -32768 to 32767 - */ - private void setPhaseAndGainCorrection() throws UsbClaimException, - UsbException - { - //UNSIGNED short gain value - masked into a long to avoid sign-extension - long gain = (long)((1.0 + mGainCorrection) * Short.MAX_VALUE); - - //Left shift gain to place value in upper 16 bits - long longGain = Long.rotateLeft((0xFFFF & gain), 16); - - //SIGNED short phase value - short phase = (short)(mPhaseCorrection * Short.MAX_VALUE); - - //Merge the results - long correction = longGain | phase; - - send(FCDCommand.APP_SET_IQ_CORRECTION, correction); - } - - /** - * Retrieves the stored phase and gain correction values from the FCD. - * - * Note: testing shows that when the dongle is unplugged, any stored phase - * and gain correction values are reset. Reading the dongle after a fresh - * plugin shows the values reset. - * - * @throws UsbClaimException - * @throws UsbException - */ - private void getPhaseAndGainCorrection() throws UsbClaimException, - UsbException - { - ByteBuffer buffer = send(FCDCommand.APP_GET_IQ_CORRECTION); - - buffer.order(ByteOrder.LITTLE_ENDIAN); - - int correction = buffer.getInt(2); - - /** - * Gain: mask the upper 16 phase bits and right shift to get the - * unsigned short value and then divide by 32768 to get the double - * value. We place the unsigned short value in an int to avoid sign - * issues. - */ - int gain = (int)(Long.rotateRight(correction & 0xFFFF0000, 16)); - - mGainCorrection = ((double)gain / (double)Short.MAX_VALUE) - 1.0; - - /** - * Phase: mask the lower 16 bits and divide by 32768 to get the double - * value - */ - short phase = (short)(correction & 0x0000FFFF); - - mPhaseCorrection = ((double)phase / (double)Short.MAX_VALUE); - } - - /** - * @return current DC correction for the I (inphase) component - */ - public double getDCCorrectionInPhase() - { - return mDCCorrectionInPhase; - } - - /** - * Sets DC correction for the I (inphase) component and sends the new value - * to the FCD controller - * - * @param value - new DC correction value for the inphase component - * @throws IllegalArgumentException for values outside limits (-1.0 <> 1.0) - * @throws UsbClaimException - if cannot claim the FCD HID controller - * @throws UsbException - if there was a usb error - */ - public void setDCCorrectionInPhase(double value) - throws UsbClaimException, UsbException - { - if(value < -1.0 || value > 1.0) - { - throw new IllegalArgumentException("DC inphase correction value " - + "[" + value + "] outside limits ( 0.0 <> 1.0 )"); - } - - mDCCorrectionInPhase = value; - setDCIQCorrection(); - } - - /** - * @return current DC correction for the Q (quadrature) component - */ - public double getDCCorrectionQuadrature() - { - return mDCCorrectionQuadrature; - } - - /** - * Sets the DC correction for the Q (quadrature) component and send the - * new value to the FCD controller - * - * @param value - new DC correction value for the quadrature component - * @throws IllegalArgumentException for values outside limits (-1.0 <> 1.0) - * @throws UsbClaimException - if cannot claim the FCD HID controller - * @throws UsbException - if there was a usb error - */ - public void setDCCorrectionQuadrature(double value) - throws UsbClaimException, UsbException - { - if(value < -1.0 || value > 1.0) - { - throw new IllegalArgumentException("DC quadrature correction " - + "value [" + value + "] outside limits ( 0.0 <> 1.0 )"); - } - - mDCCorrectionQuadrature = value; - setDCIQCorrection(); - } - - /** - * Sends the I/Q DC offset correction values to FCD. - * - * The interface expects an argument with the signed inphase value in the - * first 16 bits and the signed quadrature value in the second 16 bits, - * both in big-endian format. - * - * Since we're sending the argument in java little endian format, we place - * the quadrature in the high-order bits and the inphase in the low-order - * bits, so that the send(command) function will reorder the bytes into big - * endian format, and place the arguments correctly. - * - * Both Inphase and Quadrature correction values range -1.0 to 0.0 to 1.0 - * and are applied to the dongle as signed short values -32768 to 0 to 32767 - */ - private void setDCIQCorrection() throws UsbClaimException, UsbException - { - //I & Q DC offset correction values are signed short values - short inphase = (short)(mDCCorrectionInPhase * Short.MAX_VALUE); - short quadrature = (short)(mDCCorrectionQuadrature * Short.MAX_VALUE); - - //Mask the shorts into longs to preserve the sign bit and prepare - //for merging to a single 32-bit value - long maskedInphase = inphase & 0x0000FFFF; - long maskedQuadrature = quadrature & 0x0000FFFF; - - //Left shift quadrature to place value in upper 16 bits - long shiftedQuadrature = Long.rotateLeft(maskedQuadrature, 16); - - //Merge the results - long correction = shiftedQuadrature | maskedInphase; - -// Log.info( "FCD1: setting iq correction," + -// " inphase:" + inphase + -// " masked inphase:" + maskedInphase + -// " quad:" + quadrature + -// " masked quad:" + maskedQuadrature + -// " shifted quadrature:" + shiftedQuadrature + -// " correction:" + correction ); - - send(FCDCommand.APP_SET_DC_CORRECTION, correction); - } - - private void getDCIQCorrection() throws UsbClaimException, - UsbException - { - ByteBuffer buffer = send(FCDCommand.APP_GET_DC_CORRECTION); - - buffer.order(ByteOrder.LITTLE_ENDIAN); - - int correction = buffer.getInt(2); - - /** - * Quadrature: mask the upper 16 bits and divide by 32768 to get the - * stored quadrature value. Cast the 16-bit value to a short to get - * the signed short value - */ - long shiftedQuadrature = correction & 0xFFFF0000; - long quadrature = Long.rotateRight(shiftedQuadrature, 16); - - mDCCorrectionQuadrature = ((short)quadrature / (double)Short.MAX_VALUE); - - /** - * InPhase: mask the lower 16 bits to get the short - * value and then divide by 32768 to get the stored inphase value. - * Cast the 16-bit value to a short to get the signed short value - */ - long inphase = correction & 0x0000FFFF; - mDCCorrectionInPhase = ((short)inphase / (double)Short.MAX_VALUE); - -// Log.info( "FCD1:" + -// " correction:" + correction + -// " Q:" + quadrature + -// " I:" + inphase + -// " QCorr:" + mDCCorrectionQuadrature + -// " ICorr:" + mDCCorrectionInPhase ); - } - - private void getLNAEnhanceSetting() throws UsbClaimException, UsbException - { - ByteBuffer buffer = send(FCDCommand.APP_GET_LNA_ENHANCE); - - buffer.order(ByteOrder.LITTLE_ENDIAN); - - int enhance = buffer.getInt(2); - - mLNAEnhance = LNAEnhance.valueOf(enhance); - } - - public LNAEnhance getLNAEnhance() - { - return mLNAEnhance; - } - - public void setLNAEnhance(LNAEnhance enhance) - throws UsbClaimException, UsbException - { - send(FCDCommand.APP_SET_LNA_ENHANCE, enhance.getSetting()); - } - - private void getMixerGainSetting() throws UsbClaimException, UsbException - { - ByteBuffer buffer = send(FCDCommand.APP_GET_MIXER_GAIN); - - buffer.order(ByteOrder.LITTLE_ENDIAN); - - int gain = buffer.getInt(2); - - mMixerGain = MixerGain.valueOf(gain); - } - - public MixerGain getMixerGain() - { - return mMixerGain; - } - - public void setMixerGain(MixerGain gain) throws UsbClaimException, UsbException - { - send(FCDCommand.APP_SET_MIXER_GAIN, gain.getSetting()); - } - - public enum Block - { - CELLULAR_BAND_BLOCKED("Blocked"), - NO_BAND_BLOCK("Unblocked"), - UNKNOWN("Unknown"); - - private String mLabel; - - private Block(String label) - { - mLabel = label; - } - - public String getLabel() - { - return mLabel; - } - - public static Block getBlock(String block) - { - Block retVal = UNKNOWN; - - if(block.equalsIgnoreCase("No blk")) - { - retVal = NO_BAND_BLOCK; - } - else if(block.equalsIgnoreCase("Cell blk")) - { - retVal = CELLULAR_BAND_BLOCKED; - } - - return retVal; - } - } - - /** - * LNA Gain values suppported by the FCD Pro 1.0 - */ - public enum LNAGain - { - LNA_GAIN_MINUS_5_0(0, "-5.0 dB"), - LNA_GAIN_MINUS_2_5(1, "-2.5 dB"), - LNA_GAIN_PLUS_0_0(4, "0.0 dB"), - LNA_GAIN_PLUS_2_5(5, "2.5 dB"), - LNA_GAIN_PLUS_5_0(6, "5.0 dB"), - LNA_GAIN_PLUS_7_5(7, "7.5 dB"), - LNA_GAIN_PLUS_10_0(8, "10.0 dB"), - LNA_GAIN_PLUS_12_5(9, "12.5 dB"), - LNA_GAIN_PLUS_15_0(10, "15.0 dB"), - LNA_GAIN_PLUS_17_5(11, "17.5 dB"), - LNA_GAIN_PLUS_20_0(12, "20.0 dB"), - LNA_GAIN_PLUS_25_0(13, "25.0 dB"), - LNA_GAIN_PLUS_30_0(14, "30.0 dB"); - - private int mSetting; - private String mLabel; - - private LNAGain(int setting, String label) - { - mSetting = setting; - mLabel = label; - } - - public static LNAGain valueOf(int setting) - { - switch(setting) - { - case 0: - return LNA_GAIN_MINUS_5_0; - case 1: - return LNA_GAIN_MINUS_2_5; - case 4: - return LNA_GAIN_PLUS_0_0; - case 5: - return LNA_GAIN_PLUS_2_5; - case 6: - return LNA_GAIN_PLUS_5_0; - case 7: - return LNA_GAIN_PLUS_7_5; - case 8: - return LNA_GAIN_PLUS_10_0; - case 9: - return LNA_GAIN_PLUS_12_5; - case 10: - return LNA_GAIN_PLUS_15_0; - case 11: - return LNA_GAIN_PLUS_17_5; - case 12: - return LNA_GAIN_PLUS_20_0; - case 13: - return LNA_GAIN_PLUS_25_0; - case 14: - return LNA_GAIN_PLUS_30_0; - default: - throw new IllegalArgumentException("FCD 1.0 Tuner " - + "Controller - unrecognized LNA gain setting " - + "value [" + setting + "]"); - } - } - - public int getSetting() - { - return mSetting; - } - - public String getLabel() - { - return mLabel; - } - - public String toString() - { - return getLabel(); - } - } - - /** - * LNA Enhance values suppported by the FCD Pro 1.0 - */ - public enum LNAEnhance - { - LNA_ENHANCE_OFF(0, "Off"), - LNA_ENHANCE_0(1, "0"), - LNA_ENHANCE_1(3, "1"), - LNA_ENHANCE_2(5, "2"), - LNA_ENHANCE_3(7, "3"); - - private int mSetting; - private String mLabel; - - private LNAEnhance(int setting, String label) - { - mSetting = setting; - mLabel = label; - } - - public static LNAEnhance valueOf(int setting) - { - switch(setting) - { - case 0: - return LNA_ENHANCE_OFF; - case 1: - return LNA_ENHANCE_0; - case 3: - return LNA_ENHANCE_1; - case 5: - return LNA_ENHANCE_2; - case 7: - return LNA_ENHANCE_3; - default: - throw new IllegalArgumentException("FCD 1.0 Tuner " - + "Controller - unrecognized LNA enhance setting " - + "value [" + setting + "]"); - } - } - - public int getSetting() - { - return mSetting; - } - - public String getLabel() - { - return mLabel; - } - - public String toString() - { - return getLabel(); - } - } - - /** - * Mixer Gain values suppported by the FCD Pro 1.0 - */ - public enum MixerGain - { - MIXER_GAIN_PLUS_4_0(0, "4.0 dB"), - MIXER_GAIN_PLUS_12_0(1, "12.0 dB"); - - private int mSetting; - private String mLabel; - - private MixerGain(int setting, String label) - { - mSetting = setting; - mLabel = label; - } - - public static MixerGain valueOf(int setting) - { - switch(setting) - { - case 0: - return MIXER_GAIN_PLUS_4_0; - case 1: - return MIXER_GAIN_PLUS_12_0; - default: - throw new IllegalArgumentException("FCD 1.0 Tuner " - + "Controller - unrecognized mixer gain setting " - + "value [" + setting + "]"); - } - } - - public int getSetting() - { - return mSetting; - } - - public String getLabel() - { - return mLabel; - } - - public String toString() - { - return getLabel(); - } - } -} diff --git a/src/source/tuner/fcd/proV1/FCD1TunerEditor.java b/src/source/tuner/fcd/proV1/FCD1TunerEditor.java deleted file mode 100644 index 40a330b0..00000000 --- a/src/source/tuner/fcd/proV1/FCD1TunerEditor.java +++ /dev/null @@ -1,528 +0,0 @@ -/******************************************************************************* - * SDR Trunk - * Copyright (C) 2014-2016 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 source.tuner.fcd.proV1; - -import java.awt.event.ActionEvent; -import java.awt.event.ActionListener; -import java.awt.event.FocusEvent; -import java.awt.event.FocusListener; -import java.text.DecimalFormat; - -import javax.swing.JButton; -import javax.swing.JComboBox; -import javax.swing.JLabel; -import javax.swing.JOptionPane; -import javax.swing.JSeparator; -import javax.swing.JSpinner; -import javax.swing.JTextField; -import javax.swing.SpinnerModel; -import javax.swing.SpinnerNumberModel; -import javax.swing.SwingConstants; -import javax.swing.event.ChangeEvent; -import javax.swing.event.ChangeListener; - -import net.miginfocom.swing.MigLayout; - -import org.slf4j.Logger; -import org.slf4j.LoggerFactory; - -import source.SourceException; -import source.tuner.configuration.TunerConfiguration; -import source.tuner.configuration.TunerConfigurationEditor; -import source.tuner.configuration.TunerConfigurationEvent; -import source.tuner.configuration.TunerConfigurationEvent.Event; -import source.tuner.configuration.TunerConfigurationModel; -import source.tuner.fcd.FCDTuner; -import source.tuner.fcd.proV1.FCD1TunerController.LNAEnhance; -import source.tuner.fcd.proV1.FCD1TunerController.LNAGain; -import source.tuner.fcd.proV1.FCD1TunerController.MixerGain; - -public class FCD1TunerEditor extends TunerConfigurationEditor -{ - private static final long serialVersionUID = 1L; - private final static Logger mLog = LoggerFactory.getLogger( FCD1TunerEditor.class ); - - private JTextField mConfigurationName; - private JButton mTunerInfo; - private JSpinner mFrequencyCorrection; - private JComboBox mComboLNAGain; - private JComboBox mComboLNAEnhance; - private JComboBox mComboMixerGain; - private CorrectionSpinner mCorrectionDCI; - private CorrectionSpinner mCorrectionDCQ; - private CorrectionSpinner mCorrectionGain; - private CorrectionSpinner mCorrectionPhase; - private boolean mLoading; - - private FCD1TunerController mController; - - public FCD1TunerEditor( TunerConfigurationModel tunerConfigurationModel, FCDTuner tuner ) - { - super( tunerConfigurationModel ); - mController = (FCD1TunerController)tuner.getController(); - - init(); - } - - private FCD1TunerConfiguration getConfiguration() - { - if( hasItem() ) - { - return (FCD1TunerConfiguration)getItem(); - } - - return null; - } - - private void init() - { - setLayout( new MigLayout( "fill,wrap 4", "[right][grow,fill][right][grow,fill]", - "[][][][][][][][][][][][grow]" ) ); - - add( new JLabel( "FCD Pro Tuner Configuration" ), "span,align center" ); - - mConfigurationName = new JTextField(); - mConfigurationName.setEnabled( false ); - mConfigurationName.addFocusListener( new FocusListener() - { - @Override - public void focusLost( FocusEvent e ) - { - save(); - } - @Override - public void focusGained( FocusEvent e ) {} - } ); - - add( new JLabel( "Name:" ) ); - add( mConfigurationName, "span 2" ); - - mTunerInfo = new JButton( "Tuner Info" ); - mTunerInfo.setEnabled( false ); - mTunerInfo.addActionListener( new ActionListener() - { - @Override - public void actionPerformed( ActionEvent e ) - { - JOptionPane.showMessageDialog( FCD1TunerEditor.this, getTunerInfo(), - "Tuner Info", JOptionPane.INFORMATION_MESSAGE ); - } - } ); - add( mTunerInfo ); - - SpinnerModel model = new SpinnerNumberModel( 0.0, //initial value - -1000.0, //min - 1000.0, //max - 0.1 ); //step - - mFrequencyCorrection = new JSpinner( model ); - mFrequencyCorrection.setEnabled( false ); - JSpinner.NumberEditor editor = - (JSpinner.NumberEditor)mFrequencyCorrection.getEditor(); - - DecimalFormat format = editor.getFormat(); - format.setMinimumFractionDigits( 1 ); - editor.getTextField().setHorizontalAlignment( SwingConstants.CENTER ); - - mFrequencyCorrection.addChangeListener( new ChangeListener() - { - @Override - public void stateChanged( ChangeEvent e ) - { - double value = ((SpinnerNumberModel)mFrequencyCorrection - .getModel()).getNumber().doubleValue(); - - try - { - mController.getFrequencyController().setFrequencyCorrection( value ); - save(); - } - catch ( SourceException e1 ) - { - JOptionPane.showMessageDialog( FCD1TunerEditor.this, "FCD Pro Tuner " - + "Controller - couldn't apply frequency correction value: " + value + - e1.getLocalizedMessage() ); - - mLog.error( "FuncubeDonglePro Controller - couldn't apply " - + "frequency correction value: " + value, e1 ); - } - } - } ); - - add( new JLabel( "PPM:" ) ); - add( mFrequencyCorrection, "wrap" ); - - add( new JSeparator( JSeparator.HORIZONTAL ), "span,grow" ); - add( new JLabel( "Gain" ), "span,align center" ); - - mComboLNAGain = new JComboBox( LNAGain.values() ); - mComboLNAGain.setEnabled( false ); - mComboLNAGain.setToolTipText( "Adjust the low noise amplifier gain setting." ); - mComboLNAGain.addActionListener( new ActionListener() - { - @Override - public void actionPerformed( ActionEvent arg0 ) - { - LNAGain gain = (LNAGain)mComboLNAGain.getSelectedItem(); - - - try - { - mController.setLNAGain( gain ); - save(); - } - catch ( Exception e ) - { - JOptionPane.showMessageDialog( FCD1TunerEditor.this, "FCD Pro Tuner Controller" - + " - error setting LNA gain [" + gain.toString() + "]" ); - - mLog.error( "FuncubeDonglePro Controller - error setting gain [" + gain.toString() + "]", e ); - } - } - } ); - - add( new JLabel( "LNA:" ) ); - add( mComboLNAGain ); - - /** - * LNA Enhance - */ - mComboLNAEnhance = new JComboBox( LNAEnhance.values() ); - mComboLNAEnhance.setToolTipText( "Adjust the LNA enhance setting. Default value is OFF" ); - - mComboLNAEnhance.addActionListener( new ActionListener() - { - @Override - public void actionPerformed( ActionEvent arg0 ) - { - LNAEnhance enhance = (LNAEnhance)mComboLNAEnhance.getSelectedItem(); - - try - { - mController.setLNAEnhance( enhance ); - save(); - } - catch ( Exception e1 ) - { - JOptionPane.showMessageDialog( FCD1TunerEditor.this, "FCD Pro Tuner" - + " error setting LNA enhance gain [" + enhance.toString() + "]" ); - - mLog.error( "FCDPro - error setting LNA enhance [" + enhance.toString() + "]", e1 ); - } - } - } ); - - add( new JLabel( "Enhance:" ) ); - add( mComboLNAEnhance ); - - /** - * Mixer Gain - */ - mComboMixerGain = new JComboBox( MixerGain.values() ); - mComboMixerGain.setToolTipText( "Adjust mixer gain setting" ); - - - mComboMixerGain.addActionListener( new ActionListener() - { - @Override - public void actionPerformed( ActionEvent arg0 ) - { - MixerGain gain = (MixerGain)mComboMixerGain.getSelectedItem(); - - try - { - mController.setMixerGain( gain ); - save(); - } - catch ( Exception e1 ) - { - JOptionPane.showMessageDialog( FCD1TunerEditor.this, "FCDPro - error setting" - + " mixer gain [" + gain.toString() + "]" ); - - mLog.error( "FCDPro - error setting mixer gain [" + gain.toString() + "]", e1 ); - } - } - } ); - - add( new JLabel( "Mixer:" ) ); - add( mComboMixerGain, "wrap" ); - - add( new JSeparator( JSeparator.HORIZONTAL ), "span,grow" ); - add( new JLabel( "Correction" ), "span,align center" ); - - /** - * Inphase DC Correction - */ - mCorrectionDCI = new CorrectionSpinner( Correction.DC_INPHASE, 0.0, 0.00001, 5 ); - mCorrectionDCI.setEnabled( false ); - mCorrectionDCI.setToolTipText( "DC Bias Correction/Inphase " - + "Component: valid values are -1.0 to 1.0 (default: 0.0)" ); - add( new JLabel( "DC Inphase:" ) ); - add( mCorrectionDCI ); - - /** - * Quadrature DC Correction - */ - mCorrectionDCQ = new CorrectionSpinner( Correction.DC_QUADRATURE, 0.0, 0.00001, 5 ); - mCorrectionDCQ.setToolTipText( "DC Bias Correction/Quadrature " - + "Component: valid values are -1.0 to 1.0 (default: 0.0)" ); - add( new JLabel( "DC Quadrature:" ) ); - add( mCorrectionDCQ ); - - /** - * Gain Correction - */ - mCorrectionGain = new CorrectionSpinner( Correction.GAIN, 0.0, 0.00001, 5 ); - mCorrectionGain.setToolTipText( "Gain Correction: valid values are " - + "-1.0 to 1.0 (default: 0.0)" ); - add( new JLabel( "Gain:" ) ); - add( mCorrectionGain ); - - /** - * Phase Correction - */ - mCorrectionPhase = new CorrectionSpinner( Correction.PHASE, 0.0, 0.00001, 5 ); - mCorrectionPhase.setToolTipText( "Phase Correction: valid values are " - + "-1.0 to 1.0 (default: 0.0)" ); - add( new JLabel( "Phase:" ) ); - add( mCorrectionPhase ); - } - - /** - * Sets each of the tuner configuration controls to the enabled argument state - */ - private void setControlsEnabled( boolean enabled ) - { - if( mConfigurationName.isEnabled() != enabled ) - { - mConfigurationName.setEnabled( enabled ); - } - - if( mTunerInfo.isEnabled() != enabled ) - { - mTunerInfo.setEnabled( enabled ); - } - - if( mFrequencyCorrection.isEnabled() != enabled ) - { - mFrequencyCorrection.setEnabled( enabled ); - } - - if( mComboLNAEnhance.isEnabled() != enabled ) - { - mComboLNAEnhance.setEnabled( enabled ); - } - - if( mComboLNAGain.isEnabled() != enabled ) - { - mComboLNAGain.setEnabled( enabled ); - } - - if( mComboMixerGain.isEnabled() != enabled ) - { - mComboMixerGain.setEnabled( enabled ); - } - - if( mCorrectionDCI.isEnabled() != enabled ) - { - mCorrectionDCI.setEnabled( enabled ); - } - - if( mCorrectionDCQ.isEnabled() != enabled ) - { - mCorrectionDCQ.setEnabled( enabled ); - } - - if( mCorrectionGain.isEnabled() != enabled ) - { - mCorrectionGain.setEnabled( enabled ); - } - - if( mCorrectionPhase.isEnabled() != enabled ) - { - mCorrectionPhase.setEnabled( enabled ); - } - } - - private String getTunerInfo() - { - StringBuilder sb = new StringBuilder(); - - sb.append( "

Funcube Dongle Pro Tuner

" ); - - sb.append( "USB ID: " ); - sb.append( mController.getUSBID() ); - sb.append( "
" ); - - sb.append( "USB Address: " ); - sb.append( mController.getUSBAddress() ); - sb.append( "
" ); - - sb.append( "USB Speed: " ); - sb.append( mController.getUSBSpeed() ); - sb.append( "
" ); - - sb.append( "Cellular Band: " ); - sb.append( mController.getConfiguration().getBandBlocking() ); - sb.append( "
" ); - - sb.append( "Firmware: " ); - sb.append( mController.getConfiguration().getFirmware() ); - sb.append( "
" ); - - return sb.toString(); - } - - @Override - public void save() - { - if( hasItem() && !mLoading ) - { - FCD1TunerConfiguration config = getConfiguration(); - - config.setName( mConfigurationName.getText() ); - - double value = ((SpinnerNumberModel)mFrequencyCorrection - .getModel()).getNumber().doubleValue(); - config.setFrequencyCorrection( value ); - config.setLNAEnhance( (LNAEnhance)mComboLNAEnhance.getSelectedItem() ); - config.setLNAGain( (LNAGain)mComboLNAGain.getSelectedItem() ); - config.setMixerGain( (MixerGain)mComboMixerGain.getSelectedItem() ); - - double dci = ((SpinnerNumberModel)mCorrectionDCI.getModel()).getNumber().doubleValue(); - config.setInphaseDCCorrection( dci ); - - double dcq = ((SpinnerNumberModel)mCorrectionDCQ.getModel()).getNumber().doubleValue(); - config.setQuadratureDCCorrection( dcq ); - - double gain = ((SpinnerNumberModel)mCorrectionGain.getModel()).getNumber().doubleValue(); - config.setGainCorrection( gain ); - - double phase = ((SpinnerNumberModel)mCorrectionPhase.getModel()).getNumber().doubleValue(); - config.setPhaseCorrection( phase ); - - getTunerConfigurationModel().broadcast( new TunerConfigurationEvent( getConfiguration(), Event.CHANGE ) ); - } - } - - @Override - public void setItem( TunerConfiguration tunerConfiguration ) - { - super.setItem( tunerConfiguration ); - - //Toggle loading so that we don't fire a change event and schedule a settings file save - mLoading = true; - - if( hasItem() ) - { - FCD1TunerConfiguration config = getConfiguration(); - - if( tunerConfiguration.isAssigned() ) - { - setControlsEnabled( true ); - mConfigurationName.setText( config.getName() ); - mFrequencyCorrection.setValue( config.getFrequencyCorrection() ); - mComboLNAEnhance.setSelectedItem( config.getLNAEnhance() ); - mComboLNAGain.setSelectedItem( config.getLNAGain() ); - mComboMixerGain.setSelectedItem( config.getMixerGain() ); - mCorrectionDCI.setValue( config.getInphaseDCCorrection() ); - mCorrectionDCQ.setValue( config.getQuadratureDCCorrection() ); - mCorrectionGain.setValue( config.getGainCorrection() ); - mCorrectionPhase.setValue( config.getPhaseCorrection() ); - } - else - { - setControlsEnabled( false ); - mConfigurationName.setText( config.getName() ); - } - } - else - { - setControlsEnabled( false ); - mConfigurationName.setText( "" ); - } - - mLoading = false; - } - - public enum Correction { GAIN, PHASE, DC_INPHASE, DC_QUADRATURE }; - - public class CorrectionSpinner extends JSpinner - { - private static final long serialVersionUID = 1L; - private static final double MIN_VALUE = -1.0d; - private static final double MAX_VALUE = 1.0d; - - private Correction mCorrectionComponent; - - public CorrectionSpinner( Correction component, double initialValue, double step, int decimalPlaces ) - { - mCorrectionComponent = component; - - SpinnerModel model = new SpinnerNumberModel( initialValue, MIN_VALUE, MAX_VALUE, step ); - setModel( model ); - - JSpinner.NumberEditor editor = (JSpinner.NumberEditor)getEditor(); - - DecimalFormat format = editor.getFormat(); - format.setMinimumFractionDigits( decimalPlaces ); - - editor.getTextField().setHorizontalAlignment( SwingConstants.CENTER ); - - addChangeListener( new ChangeListener() - { - @Override - public void stateChanged( ChangeEvent e ) - { - double value = - ((SpinnerNumberModel)getModel()).getNumber().doubleValue(); - - try - { - switch( mCorrectionComponent ) - { - case DC_INPHASE: - mController.setDCCorrectionInPhase( value ); - break; - case DC_QUADRATURE: - mController.setDCCorrectionQuadrature( value ); - break; - case GAIN: - mController.setGainCorrection( value ); - break; - case PHASE: - mController.setPhaseCorrection( value ); - break; - } - - save(); - } - catch ( Exception e1 ) - { - JOptionPane.showMessageDialog( FCD1TunerEditor.this, "FCDPro - error " - + "applying " + mCorrectionComponent.toString() + " correction value [" - + value + "]" ); - - mLog.error( "FCDPro - error applying "+ mCorrectionComponent.toString() - + " correction value [" + value + "]", e1 ); - } - } - } ); - } - } -} diff --git a/src/source/tuner/fcd/proplusV2/FCD2TunerController.java b/src/source/tuner/fcd/proplusV2/FCD2TunerController.java deleted file mode 100644 index 5e2412c4..00000000 --- a/src/source/tuner/fcd/proplusV2/FCD2TunerController.java +++ /dev/null @@ -1,238 +0,0 @@ -/******************************************************************************* - * 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 source.tuner.fcd.proplusV2; - -import org.slf4j.Logger; -import org.slf4j.LoggerFactory; -import org.usb4java.Device; -import org.usb4java.DeviceDescriptor; -import source.SourceException; -import source.tuner.MixerTunerDataLine; -import source.tuner.MixerTunerType; -import source.tuner.TunerClass; -import source.tuner.TunerType; -import source.tuner.configuration.TunerConfiguration; -import source.tuner.fcd.FCDCommand; -import source.tuner.fcd.FCDTunerController; - -import java.nio.ByteBuffer; -import java.nio.ByteOrder; - -public class FCD2TunerController extends FCDTunerController -{ - private final static Logger mLog = LoggerFactory.getLogger(FCD2TunerController.class); - - public static final int MINIMUM_TUNABLE_FREQUENCY = 150000; - public static final int MAXIMUM_TUNABLE_FREQUENCY = 2050000000; - public static final int SAMPLE_RATE = 192000; - - public FCD2TunerController(MixerTunerDataLine mixerTDL, Device device, DeviceDescriptor descriptor) - { - super(mixerTDL, device, descriptor, MixerTunerType.FUNCUBE_DONGLE_PRO_PLUS.getDisplayString(), - MINIMUM_TUNABLE_FREQUENCY, MAXIMUM_TUNABLE_FREQUENCY, MixerTunerType.FUNCUBE_DONGLE_PRO.getAudioFormat()); - } - - public void init() throws SourceException - { - super.init(); - - getFrequencyController().setSampleRate(SAMPLE_RATE); - - try - { - setFCDMode(Mode.APPLICATION); - } - catch(Exception e) - { - throw new SourceException("error setting Mode to APPLICATION", e); - } - } - - public double getCurrentSampleRate() - { - return SAMPLE_RATE; - } - - @Override - public TunerClass getTunerClass() - { - return TunerClass.FUNCUBE_DONGLE_PRO_PLUS; - } - - @Override - public TunerType getTunerType() - { - return TunerType.FUNCUBE_DONGLE_PRO_PLUS; - } - - public void setLNAGain(boolean enabled) throws SourceException - { - try - { - send(FCDCommand.APP_SET_LNA_GAIN, enabled ? 1 : 0); - } - catch(Exception e) - { - throw new SourceException("error while setting LNA Gain", e); - } - } - - public void setMixerGain(boolean enabled) throws SourceException - { - try - { - send(FCDCommand.APP_SET_MIXER_GAIN, enabled ? 1 : 0); - } - catch(Exception e) - { - throw new SourceException("error while setting Mixer Gain", e); - } - } - - @Override - public void apply(TunerConfiguration config) throws SourceException - { - if(config instanceof FCD2TunerConfiguration) - { - FCD2TunerConfiguration fcd2 = (FCD2TunerConfiguration)config; - - getFrequencyController().setFrequencyCorrection(fcd2.getFrequencyCorrection()); - - long frequency = MINIMUM_TUNABLE_FREQUENCY; - - if(fcd2.getFrequency() >= MINIMUM_TUNABLE_FREQUENCY && - fcd2.getFrequency() <= MAXIMUM_TUNABLE_FREQUENCY) - { - frequency = fcd2.getFrequency(); - } - - getFrequencyController().setFrequency(frequency); - setLNAGain(fcd2.getGainLNA()); - setMixerGain(fcd2.getGainMixer()); - - try - { - getFrequencyController().setFrequency(fcd2.getFrequency()); - } - catch(SourceException se) - { - //Do nothing, we couldn't set the frequency - } - } - } - - public int getDCCorrection() - { - int dcCorrection = -999; - - try - { - ByteBuffer buffer = send(FCDCommand.APP_GET_DC_CORRECTION); - - buffer.order(ByteOrder.LITTLE_ENDIAN); - - return buffer.getInt(2); - } - catch(Exception e) - { - mLog.error("error getting dc correction value", e); - } - - return dcCorrection; - } - - public void setDCCorrection(int value) - { - try - { - send(FCDCommand.APP_SET_DC_CORRECTION, value); - } - catch(Exception e) - { - mLog.error("error setting dc correction to [" + value + "]", e); - } - } - - public int getIQCorrection() - { - int iqCorrection = -999; - - try - { - ByteBuffer buffer = send(FCDCommand.APP_GET_IQ_CORRECTION); - - buffer.order(ByteOrder.LITTLE_ENDIAN); - - return buffer.getInt(2); - } - catch(Exception e) - { - mLog.error("error reading IQ correction value", e); - } - - return iqCorrection; - } - - public void setIQCorrection(int value) - { - try - { - send(FCDCommand.APP_SET_IQ_CORRECTION, value); - } - catch(Exception e) - { - mLog.error("error setting IQ correction to [" + value + "]", e); - } - } - - public enum Block - { - CELLULAR_BAND_BLOCKED("Blocked"), - NO_BAND_BLOCK("Unblocked"), - UNKNOWN("Unknown"); - - private String mLabel; - - private Block(String label) - { - mLabel = label; - } - - public String getLabel() - { - return mLabel; - } - - public static Block getBlock(String block) - { - Block retVal = UNKNOWN; - - if(block.equalsIgnoreCase("No blk")) - { - retVal = NO_BAND_BLOCK; - } - else if(block.equalsIgnoreCase("Cell blk")) - { - retVal = CELLULAR_BAND_BLOCKED; - } - - return retVal; - } - } -} diff --git a/src/source/tuner/fcd/proplusV2/FCD2TunerEditor.java b/src/source/tuner/fcd/proplusV2/FCD2TunerEditor.java deleted file mode 100644 index e5c5c8be..00000000 --- a/src/source/tuner/fcd/proplusV2/FCD2TunerEditor.java +++ /dev/null @@ -1,328 +0,0 @@ -/******************************************************************************* - * SDR Trunk - * Copyright (C) 2014-2016 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 source.tuner.fcd.proplusV2; - -import java.awt.event.ActionEvent; -import java.awt.event.ActionListener; -import java.awt.event.FocusEvent; -import java.awt.event.FocusListener; -import java.text.DecimalFormat; - -import javax.swing.JButton; -import javax.swing.JCheckBox; -import javax.swing.JLabel; -import javax.swing.JOptionPane; -import javax.swing.JSeparator; -import javax.swing.JSpinner; -import javax.swing.JTextField; -import javax.swing.SpinnerModel; -import javax.swing.SpinnerNumberModel; -import javax.swing.SwingConstants; -import javax.swing.event.ChangeEvent; -import javax.swing.event.ChangeListener; - -import net.miginfocom.swing.MigLayout; - -import org.slf4j.Logger; -import org.slf4j.LoggerFactory; - -import source.SourceException; -import source.tuner.configuration.TunerConfiguration; -import source.tuner.configuration.TunerConfigurationEditor; -import source.tuner.configuration.TunerConfigurationEvent; -import source.tuner.configuration.TunerConfigurationEvent.Event; -import source.tuner.configuration.TunerConfigurationModel; -import source.tuner.fcd.FCDTuner; - -public class FCD2TunerEditor extends TunerConfigurationEditor -{ - private final static Logger mLog = LoggerFactory.getLogger( FCD2TunerEditor.class ); - private static final long serialVersionUID = 1L; - - private JTextField mConfigurationName; - private JButton mTunerInfo; - private JSpinner mFrequencyCorrection; - private JCheckBox mLNAGain; - private JCheckBox mMixerGain; - private boolean mLoading; - - private FCD2TunerController mController; - - public FCD2TunerEditor( TunerConfigurationModel tunerConfigurationModel, FCDTuner tuner ) - { - super( tunerConfigurationModel ); - mController = (FCD2TunerController)tuner.getController(); - - init(); - } - - private FCD2TunerConfiguration getConfiguration() - { - if( hasItem() ) - { - return (FCD2TunerConfiguration)getItem(); - } - - return null; - } - - private void init() - { - setLayout( new MigLayout( "fill,wrap 4", "[right][grow,fill][right][grow,fill]", - "[][][][][][grow]" ) ); - - add( new JLabel( "FCD Pro+ Tuner Configuration" ), "span,align center" ); - - mConfigurationName = new JTextField(); - mConfigurationName.setEnabled( false ); - mConfigurationName.addFocusListener( new FocusListener() - { - @Override - public void focusLost( FocusEvent e ) - { - save(); - } - @Override - public void focusGained( FocusEvent e ) {} - } ); - - add( new JLabel( "Name:" ) ); - add( mConfigurationName, "span 2" ); - - mTunerInfo = new JButton( "Tuner Info" ); - mTunerInfo.setEnabled( false ); - mTunerInfo.addActionListener( new ActionListener() - { - @Override - public void actionPerformed( ActionEvent e ) - { - JOptionPane.showMessageDialog( FCD2TunerEditor.this, getTunerInfo(), - "Tuner Info", JOptionPane.INFORMATION_MESSAGE ); - } - } ); - add( mTunerInfo ); - - SpinnerModel model = new SpinnerNumberModel( 0.0, //initial value - -1000.0, //min - 1000.0, //max - 0.1 ); //step - - mFrequencyCorrection = new JSpinner( model ); - mFrequencyCorrection.setEnabled( false ); - JSpinner.NumberEditor editor = - (JSpinner.NumberEditor)mFrequencyCorrection.getEditor(); - - DecimalFormat format = editor.getFormat(); - format.setMinimumFractionDigits( 1 ); - editor.getTextField().setHorizontalAlignment( SwingConstants.CENTER ); - - mFrequencyCorrection.addChangeListener( new ChangeListener() - { - @Override - public void stateChanged( ChangeEvent e ) - { - double value = ((SpinnerNumberModel)mFrequencyCorrection - .getModel()).getNumber().doubleValue(); - - try - { - mController.getFrequencyController().setFrequencyCorrection( value ); - save(); - } - catch ( SourceException e1 ) - { - JOptionPane.showMessageDialog( FCD2TunerEditor.this, "FCD Pro Plus Tuner " - + "Controller - couldn't apply frequency correction value: " + value + - e1.getLocalizedMessage() ); - - mLog.error( "FuncubeDongleProPlus Controller - couldn't apply " - + "frequency correction value: " + value, e1 ); - } - } - } ); - - add( new JLabel( "PPM:" ) ); - add( mFrequencyCorrection, "wrap" ); - - add( new JSeparator( JSeparator.HORIZONTAL ), "span,grow" ); - - mLNAGain = new JCheckBox( "LNA Gain" ); - mLNAGain.setEnabled( false ); - mLNAGain.addActionListener( new ActionListener() - { - @Override - public void actionPerformed( ActionEvent event ) - { - try - { - mController.setLNAGain( mLNAGain.isSelected() ); - save(); - } - catch( SourceException e ) - { - mLog.error( "Couldn't set LNA gain for FCD2", e ); - } - } - } ); - - add( new JLabel( "" ) ); - add( mLNAGain ); - - /** - * Mixer Gain - */ - mMixerGain = new JCheckBox( "Mixer Gain" ); - - mMixerGain.addActionListener( new ActionListener() - { - @Override - public void actionPerformed( ActionEvent event ) - { - try - { - mController.setMixerGain( mMixerGain.isSelected() ); - save(); - } - catch( SourceException e ) - { - mLog.error( "Couldn't set mixer gain for FCD2", e ); - } - } - } ); - - add( new JLabel( "" ) ); - add( mMixerGain ); - } - - /** - * Sets each of the tuner configuration controls to the enabled argument state - */ - private void setControlsEnabled( boolean enabled ) - { - if( mConfigurationName.isEnabled() != enabled ) - { - mConfigurationName.setEnabled( enabled ); - } - - if( mTunerInfo.isEnabled() != enabled ) - { - mTunerInfo.setEnabled( enabled ); - } - - if( mFrequencyCorrection.isEnabled() != enabled ) - { - mFrequencyCorrection.setEnabled( enabled ); - } - - if( mLNAGain.isEnabled() != enabled ) - { - mLNAGain.setEnabled( enabled ); - } - - if( mMixerGain.isEnabled() != enabled ) - { - mMixerGain.setEnabled( enabled ); - } - } - - private String getTunerInfo() - { - StringBuilder sb = new StringBuilder(); - - sb.append( "

Funcube Dongle Pro Plus Tuner

" ); - - sb.append( "USB ID: " ); - sb.append( mController.getUSBID() ); - sb.append( "
" ); - - sb.append( "USB Address: " ); - sb.append( mController.getUSBAddress() ); - sb.append( "
" ); - - sb.append( "USB Speed: " ); - sb.append( mController.getUSBSpeed() ); - sb.append( "
" ); - - sb.append( "Cellular Band: " ); - sb.append( mController.getConfiguration().getBandBlocking() ); - sb.append( "
" ); - - sb.append( "Firmware: " ); - sb.append( mController.getConfiguration().getFirmware() ); - sb.append( "
" ); - - return sb.toString(); - } - - @Override - public void save() - { - if( hasItem() && !mLoading ) - { - FCD2TunerConfiguration config = getConfiguration(); - - config.setName( mConfigurationName.getText() ); - - double value = ((SpinnerNumberModel)mFrequencyCorrection - .getModel()).getNumber().doubleValue(); - config.setFrequencyCorrection( value ); - - config.setGainLNA( mLNAGain.isSelected() ); - config.setGainMixer( mMixerGain.isSelected() ); - - getTunerConfigurationModel().broadcast( - new TunerConfigurationEvent( getConfiguration(), Event.CHANGE ) ); - } - } - - @Override - public void setItem( TunerConfiguration tunerConfiguration ) - { - super.setItem( tunerConfiguration ); - - //Toggle loading so that we don't fire a change event and schedule a settings file save - mLoading = true; - - if( hasItem() ) - { - FCD2TunerConfiguration config = getConfiguration(); - - - if( tunerConfiguration.isAssigned() ) - { - setControlsEnabled( true ); - mConfigurationName.setText( config.getName() ); - mFrequencyCorrection.setValue( config.getFrequencyCorrection() ); - mLNAGain.setSelected( config.getGainLNA() ); - mMixerGain.setSelected( config.getGainMixer() ); - } - else - { - setControlsEnabled( false ); - mConfigurationName.setText( config.getName() ); - } - } - else - { - setControlsEnabled( false ); - mConfigurationName.setText( "" ); - } - - mLoading = false; - } -} diff --git a/src/source/tuner/frequency/DCTrackingSourceControl.java b/src/source/tuner/frequency/DCTrackingSourceControl.java deleted file mode 100644 index de042699..00000000 --- a/src/source/tuner/frequency/DCTrackingSourceControl.java +++ /dev/null @@ -1,130 +0,0 @@ -/******************************************************************************* - * 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 source.tuner.frequency; - -import module.Module; -import sample.Listener; -import sample.real.RealBuffer; -import source.ISourceEventProvider; -import source.SourceEvent; - -import java.util.concurrent.ScheduledExecutorService; - -public class DCTrackingSourceControl extends Module - implements Listener, ISourceEventProvider -{ - private static float COARSE_THRESHOLD = 0.1f; - private static float LOOP_GAIN = 0.1f; - private static float COARSE_GAIN = 1000.0f; - private static float FINE_GAIN = 100.0f; - private int mErrorCorrection = 0; - private int mMaximumFrequencyCorrection = 0; - private Listener mListener; - - /** - * DC offset Automatic Frequency Control. Monitors DC bias present in an FM - * demodulated audio stream and issues automatic frequency corrections to - * maintain the DC bias close to zero. - * - * @param maximum - maximum allowable (+/-) frequency correction value. - */ - public DCTrackingSourceControl(int maximum) - { - mMaximumFrequencyCorrection = maximum; - } - - public void dispose() - { - mListener = null; - } - - @Override - public void receive(RealBuffer buffer) - { - double sum = 0.0d; - - for(float sample : buffer.getSamples()) - { - sum += sample; - } - - float mean = (float)(sum / (double)buffer.getSamples().length); - - float adjustment = 0.0f; - - if(mean > COARSE_THRESHOLD || mean < -COARSE_THRESHOLD) - { - adjustment = mean * COARSE_GAIN; - } - else - { - adjustment = mean * FINE_GAIN; - } - - setErrorCorrection(mErrorCorrection + (int)((float)adjustment * LOOP_GAIN)); - } - - private void setErrorCorrection(int correction) - { - if(mListener != null) - { - if(correction > mMaximumFrequencyCorrection) - { - correction = mMaximumFrequencyCorrection; - } - else if(correction < -mMaximumFrequencyCorrection) - { - correction = -mMaximumFrequencyCorrection; - } - - if(correction != mErrorCorrection) - { - mListener.receive(SourceEvent.channelFrequencyCorrectionRequest(correction)); - } - } - } - - @Override - public void setSourceEventListener(Listener listener) - { - mListener = listener; - } - - @Override - public void removeSourceEventListener() - { - mListener = null; - } - - @Override - public void start() - { - } - - @Override - public void stop() - { - } - - @Override - public void reset() - { - setErrorCorrection(0); - } -} diff --git a/src/source/tuner/hackrf/HackRFTuner.java b/src/source/tuner/hackrf/HackRFTuner.java deleted file mode 100644 index fa0ff210..00000000 --- a/src/source/tuner/hackrf/HackRFTuner.java +++ /dev/null @@ -1,79 +0,0 @@ -/******************************************************************************* - * SDR Trunk - * Copyright (C) 2014-2016 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 source.tuner.hackrf; - -import org.slf4j.Logger; -import org.slf4j.LoggerFactory; - -import source.SourceException; -import source.tuner.Tuner; -import source.tuner.TunerClass; -import source.tuner.TunerType; -import source.tuner.hackrf.HackRFTunerController.BoardID; - -public class HackRFTuner extends Tuner -{ - private final static Logger mLog = LoggerFactory.getLogger( HackRFTuner.class ); - - public HackRFTuner( HackRFTunerController controller ) throws SourceException - { - super( "HackRF", controller ); - } - - public HackRFTunerController getController() - { - return (HackRFTunerController)getTunerController(); - } - - public void dispose() - { - } - - @Override - public TunerClass getTunerClass() - { - return TunerClass.HACKRF_ONE; - } - - @Override - public TunerType getTunerType() - { - return TunerType.HACKRF; - } - - @Override - public double getSampleSize() - { - return 11.0; - } - - @Override - public String getUniqueID() - { - try - { - return getController().getSerial().getSerialNumber(); - } - catch( Exception e ) - { - mLog.error( "error gettting serial number", e ); - } - - return BoardID.HACKRF_ONE.getLabel(); - } -} diff --git a/src/source/tuner/hackrf/HackRFTunerController.java b/src/source/tuner/hackrf/HackRFTunerController.java deleted file mode 100644 index 2af8ed00..00000000 --- a/src/source/tuner/hackrf/HackRFTunerController.java +++ /dev/null @@ -1,884 +0,0 @@ -/******************************************************************************* - * SDR Trunk - * Copyright (C) 2014-2017 Dennis Sheirer - * - * Ported from libhackrf at: - * https://github.com/mossmann/hackrf/tree/master/host/libhackrf - * - * Copyright (c) 2012, Jared Boone - * Copyright (c) 2013, Benjamin Vernoux - * copyright (c) 2013, Michael Ossmann - * - * All rights reserved. - * - * Redistribution and use in source and binary forms, with or without - * modification, are permitted provided that the following conditions are - * met: - * - * Redistributions of source code must retain the above copyright notice, - * this list of conditions and the following disclaimer. - * Redistributions in binary form must reproduce the above copyright - * notice, this list of conditions and the following disclaimer in the - * documentation and/or other materials provided with the distribution. - * - * Neither the name of Great Scott Gadgets nor the names of its contributors - * may be used to endorse or promote products derived from this software - * without specific prior written permission. - * - * THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS - * "AS IS" AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED - * TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A - * PARTICULAR PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT - * HOLDER OR CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, - * SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT LIMITED - * TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, DATA, OR - * PROFITS; OR BUSINESS INTERRUPTION) - * HOWEVER CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, - * STRICT LIABILITY, OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN - * ANY WAY OUT OF THE USE OF THIS SOFTWARE, EVEN IF ADVISED OF THE - * POSSIBILITY OF SUCH DAMAGE. - * - * 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 source.tuner.hackrf; - -import org.apache.commons.io.EndianUtils; -import org.slf4j.Logger; -import org.slf4j.LoggerFactory; -import org.usb4java.Device; -import org.usb4java.DeviceDescriptor; -import org.usb4java.DeviceHandle; -import org.usb4java.LibUsb; -import org.usb4java.LibUsbException; -import sample.Broadcaster; -import sample.Listener; -import sample.adapter.ByteSampleAdapter; -import sample.complex.ComplexBuffer; -import source.SourceException; -import source.tuner.TunerController; -import source.tuner.configuration.TunerConfiguration; -import source.tuner.usb.USBTransferProcessor; -import source.tuner.usb.USBTunerController; - -import javax.usb.UsbException; -import java.nio.ByteBuffer; -import java.nio.ByteOrder; -import java.util.Arrays; -import java.util.concurrent.LinkedTransferQueue; - -public class HackRFTunerController extends USBTunerController -{ - private final static Logger mLog = LoggerFactory.getLogger(HackRFTunerController.class); - - public final static long USB_TIMEOUT_US = 1000000l; //uSeconds - public static final byte USB_ENDPOINT = (byte)0x81; - public static final byte USB_INTERFACE = (byte)0x0; - public static final int USB_TRANSFER_BUFFER_SIZE = 262144; - - public static final byte REQUEST_TYPE_IN = (byte)(LibUsb.ENDPOINT_IN | LibUsb.REQUEST_TYPE_VENDOR | LibUsb.RECIPIENT_DEVICE); - public static final byte REQUEST_TYPE_OUT = (byte)(LibUsb.ENDPOINT_OUT | LibUsb.REQUEST_TYPE_VENDOR | LibUsb.RECIPIENT_DEVICE); - - public static final long MIN_FREQUENCY = 10000000l; - public static final long MAX_FREQUENCY = 6000000000l; - public static final long DEFAULT_FREQUENCY = 101100000; - public static final double USABLE_BANDWIDTH_PERCENT = 0.95; - public static final int DC_SPIKE_AVOID_BUFFER = 5000; - - private ByteSampleAdapter mSampleAdapter = new ByteSampleAdapter(); - private USBTransferProcessor mUSBTransferProcessor; - - private HackRFSampleRate mSampleRate = HackRFSampleRate.RATE2_016MHZ; - private boolean mAmplifierEnabled = false; - - private Device mDevice; - private DeviceDescriptor mDeviceDescriptor; - private DeviceHandle mDeviceHandle; - - public HackRFTunerController(Device device, DeviceDescriptor descriptor) throws SourceException - { - super(MIN_FREQUENCY, MAX_FREQUENCY, DC_SPIKE_AVOID_BUFFER, USABLE_BANDWIDTH_PERCENT); - - mDevice = device; - mDeviceDescriptor = descriptor; - } - - public void init() throws SourceException - { - mDeviceHandle = new DeviceHandle(); - - int result = LibUsb.open(mDevice, mDeviceHandle); - - if(result != 0) - { - if(result == LibUsb.ERROR_ACCESS) - { - mLog.error("Unable to access HackRF - insufficient permissions. " - + "If you are running a Linux OS, have you installed the " - + "hackRF rules file in \\etc\\udev\\rules.d ??"); - } - - throw new SourceException("Couldn't open hackrf device - " + - LibUsb.strError(result)); - } - - try - { - claimInterface(); - - setMode(Mode.RECEIVE); - - getFrequencyController().setFrequency(DEFAULT_FREQUENCY); - } - catch(Exception e) - { - throw new SourceException("HackRF Tuner Controller - couldn't " - + "claim USB interface or get endpoint or pipe", e); - } - - String name; - - try - { - name = "HackRF " + getSerial().getSerialNumber(); - } - catch(UsbException ue) - { - //Do nothing, we couldn't determine the serial number - name = "HackRF - Unidentified Serial"; - } - - mUSBTransferProcessor = new USBTransferProcessor(name, mDeviceHandle, mSampleAdapter, USB_TRANSFER_BUFFER_SIZE); - } - - @Override - protected USBTransferProcessor getUSBTransferProcessor() - { - return mUSBTransferProcessor; - } - - /** - * Claims the USB interface. If another application currently has - * the interface claimed, the USB_FORCE_CLAIM_INTERFACE setting - * will dictate if the interface is forcibly claimed from the other - * application - */ - private void claimInterface() throws SourceException - { - if(mDeviceHandle != null) - { - int result = LibUsb.kernelDriverActive(mDeviceHandle, USB_INTERFACE); - - if(result == 1) - { - result = LibUsb.detachKernelDriver(mDeviceHandle, USB_INTERFACE); - - if(result != LibUsb.SUCCESS) - { - mLog.error("failed attempt to detach kernel driver [" + - LibUsb.errorName(result) + "]"); - - throw new SourceException("couldn't detach kernel driver " - + "from device"); - } - } - - result = LibUsb.claimInterface(mDeviceHandle, USB_INTERFACE); - - if(result != LibUsb.SUCCESS) - { - throw new SourceException("couldn't claim usb interface [" + - LibUsb.errorName(result) + "]"); - } - } - else - { - throw new SourceException("couldn't claim usb interface - no " - + "device handle"); - } - } - - /** - * HackRF board identifier/type - */ - public BoardID getBoardID() throws UsbException - { - int id = readByte(Request.BOARD_ID_READ, (byte)0, (byte)0, false); - - return BoardID.lookup(id); - } - - /** - * HackRF firmware version string - */ - public String getFirmwareVersion() throws UsbException - { - ByteBuffer buffer = readArray(Request.VERSION_STRING_READ, 0, 0, 255); - - byte[] data = new byte[255]; - - buffer.get(data); - - return new String(data); - } - - /** - * HackRF part id number and serial number - */ - public Serial getSerial() throws UsbException - { - ByteBuffer buffer = readArray(Request.BOARD_PARTID_SERIALNO_READ, 0, 0, 24); - - return new Serial(buffer); - } - - /** - * Sets the HackRF transceiver mode - */ - public void setMode(Mode mode) throws UsbException - { - write(Request.SET_TRANSCEIVER_MODE, mode.getNumber(), 0); - } - - /** - * Sets the HackRF baseband filter - */ - public void setBasebandFilter(BasebandFilter filter) throws UsbException - { - write(Request.BASEBAND_FILTER_BANDWIDTH_SET, - filter.getLowValue(), - filter.getHighValue()); - } - - /** - * Enables (true) or disables (false) the amplifier - */ - public void setAmplifierEnabled(boolean enabled) throws UsbException - { - write(Request.AMP_ENABLE, (enabled ? 1 : 0), 0); - - mAmplifierEnabled = enabled; - } - - public boolean getAmplifier() - { - return mAmplifierEnabled; - } - - /** - * Sets the IF LNA Gain - */ - public void setLNAGain(HackRFLNAGain gain) throws UsbException - { - int result = readByte(Request.SET_LNA_GAIN, 0, gain.getValue(), true); - - if(result != 1) - { - throw new UsbException("couldn't set lna gain to " + gain); - } - } - - /** - * Sets the Baseband VGA Gain - */ - public void setVGAGain(HackRFVGAGain gain) throws UsbException - { - int result = readByte(Request.SET_VGA_GAIN, 0, gain.getValue(), true); - - if(result != 1) - { - throw new UsbException("couldn't set vga gain to " + gain); - } - } - - /** - * Not implemented - */ - public long getTunedFrequency() throws SourceException - { - return getFrequencyController().getTunedFrequency(); - } - - @Override - public void setTunedFrequency(long frequency) throws SourceException - { - ByteBuffer buffer = ByteBuffer.allocateDirect(8); - buffer.order(ByteOrder.LITTLE_ENDIAN); - - int mhz = (int)(frequency / 1E6); - int hz = (int)(frequency - (mhz * 1E6)); - - buffer.putInt(mhz); - buffer.putInt(hz); - - buffer.rewind(); - - try - { - write(Request.SET_FREQUENCY, 0, 0, buffer); - } - catch(UsbException e) - { - mLog.error("error setting frequency [" + frequency + "]", e); - - throw new SourceException("error setting frequency [" + - frequency + "]", e); - } - } - - @Override - public double getCurrentSampleRate() - { - return mSampleRate.getRate(); - } - - @Override - public void apply(TunerConfiguration config) throws SourceException - { - if(config instanceof HackRFTunerConfiguration) - { - HackRFTunerConfiguration hackRFConfig = - (HackRFTunerConfiguration)config; - - try - { - setSampleRate(hackRFConfig.getSampleRate()); - getFrequencyController().setFrequencyCorrection(hackRFConfig.getFrequencyCorrection()); - setAmplifierEnabled(hackRFConfig.getAmplifierEnabled()); - setLNAGain(hackRFConfig.getLNAGain()); - setVGAGain(hackRFConfig.getVGAGain()); - getFrequencyController().setFrequency(getFrequencyController().getFrequency()); - } - catch(UsbException e) - { - throw new SourceException("Error while applying tuner " - + "configuration", e); - } - - try - { - getFrequencyController().setFrequency(hackRFConfig.getFrequency()); - } - catch(SourceException se) - { - //Do nothing, we couldn't set the frequency - } - } - else - { - throw new IllegalArgumentException("Invalid tuner configuration " - + "type [" + config.getClass() + "]"); - } - } - - public ByteBuffer readArray(Request request, - int value, - int index, - int length) throws UsbException - { - if(mDeviceHandle != null) - { - ByteBuffer buffer = ByteBuffer.allocateDirect(length); - - int transferred = LibUsb.controlTransfer(mDeviceHandle, - REQUEST_TYPE_IN, - request.getRequestNumber(), - (short)value, - (short)index, - buffer, - USB_TIMEOUT_US); - - if(transferred < 0) - { - throw new LibUsbException("read error", transferred); - } - - return buffer; - } - else - { - throw new LibUsbException("device handle is null", - LibUsb.ERROR_NO_DEVICE); - } - } - - public int read(Request request, int value, int index, int length) - throws UsbException - { - if(!(length == 1 || length == 2 || length == 4)) - { - throw new IllegalArgumentException("invalid length [" + length + - "] must be: byte=1, short=2, int=4 to read a primitive"); - } - - ByteBuffer buffer = readArray(request, value, index, length); - - byte[] data = new byte[buffer.capacity()]; - - buffer.get(data); - - switch(data.length) - { - case 1: - return data[0]; - case 2: - return EndianUtils.readSwappedShort(data, 0); - case 4: - return EndianUtils.readSwappedInteger(data, 0); - default: - throw new UsbException("read() primitive returned an " - + "unrecognized byte array " + Arrays.toString(data)); - } - } - - public int readByte(Request request, int value, int index, boolean signed) - throws UsbException - { - ByteBuffer buffer = readArray(request, value, index, 1); - - if(signed) - { - return (int)(buffer.get()); - } - else - { - return (int)(buffer.get() & 0xFF); - } - } - - public void write(Request request, - int value, - int index, - ByteBuffer buffer) throws UsbException - { - if(mDeviceHandle != null) - { - int transferred = LibUsb.controlTransfer(mDeviceHandle, - REQUEST_TYPE_OUT, - request.getRequestNumber(), - (short)value, - (short)index, - buffer, - USB_TIMEOUT_US); - - if(transferred < 0) - { - throw new LibUsbException("error writing byte buffer", - transferred); - } - else if(transferred != buffer.capacity()) - { - throw new LibUsbException("transferred bytes [" + - transferred + "] is not what was expected [" + - buffer.capacity() + "]", transferred); - } - } - else - { - throw new LibUsbException("device handle is null", - LibUsb.ERROR_NO_DEVICE); - } - } - - /** - * Sends a request that doesn't have a data payload - */ - public void write(Request request, - int value, - int index) throws UsbException - { - write(request, value, index, ByteBuffer.allocateDirect(0)); - } - - /** - * Sample Rate - * - * Note: the libhackrf set sample rate method is designed to allow fractional - * sample rates. However, since we're only using integral sample rates, we - * simply invoke the setSampleRateManual method directly. - */ - public void setSampleRate(HackRFSampleRate rate) throws UsbException, SourceException - { - setSampleRateManual(rate.getRate(), 1); - - getFrequencyController().setSampleRate(rate.getRate()); - - setBasebandFilter(rate.getFilter()); - - mSampleRate = rate; - } - - public void setSampleRateManual(int frequency, int divider) - throws UsbException - { - ByteBuffer buffer = ByteBuffer.allocateDirect(8); - - buffer.order(ByteOrder.LITTLE_ENDIAN); - - buffer.putInt(frequency); - buffer.putInt(divider); - - write(Request.SET_SAMPLE_RATE, 0, 0, buffer); - } - - public double getSampleRate() - { - return mSampleRate.getRate(); - } - - public enum Request - { - SET_TRANSCEIVER_MODE(1), - MAX2837_TRANSCEIVER_WRITE(2), - MAX2837_TRANSCEIVER_READ(3), - SI5351C_CLOCK_GENERATOR_WRITE(4), - SI5351C_CLOCK_GENERATOR_READ(5), - SET_SAMPLE_RATE(6), - BASEBAND_FILTER_BANDWIDTH_SET(7), - RFFC5071_MIXER_WRITE(8), - RFFC5071_MIXER_READ(9), - SPIFLASH_ERASE(10), - SPIFLASH_WRITE(11), - SPIFLASH_READ(12), - BOARD_ID_READ(14), - VERSION_STRING_READ(15), - SET_FREQUENCY(16), - AMP_ENABLE(17), - BOARD_PARTID_SERIALNO_READ(18), - SET_LNA_GAIN(19), - SET_VGA_GAIN(20), - SET_TXVGA_GAIN(21), - ANTENNA_ENABLE(23), - SET_FREQUENCY_EXPLICIT(24); - - private byte mRequestNumber; - - private Request(int number) - { - mRequestNumber = (byte)number; - } - - public byte getRequestNumber() - { - return mRequestNumber; - } - } - - public enum HackRFSampleRate - { - RATE2_016MHZ(2016000, "2.016 MHz", BasebandFilter.F3_50), - RATE3_024MHZ(3024000, "3.024 MHz", BasebandFilter.F5_00), - RATE4_464MHZ(4464000, "4.464 MHz", BasebandFilter.F6_00), - RATE5_376MHZ(5376000, "5.376 MHz", BasebandFilter.F7_00), - RATE7_488MHZ(7488000, "7.488 MHz", BasebandFilter.F9_00), - RATE10_080MHZ(10080000, "10.080 MHz", BasebandFilter.F12_00), - RATE12_000MHZ(12000000, "12.000 MHz", BasebandFilter.F14_00), - RATE13_440MHZ(13440000, "13.440 MHz", BasebandFilter.F15_00), - RATE14_976MHZ(14976000, "14.976 MHz", BasebandFilter.F20_00), - RATE19_968MHZ(19968000, "19.968 MHz", BasebandFilter.F24_00); - - private int mRate; - private String mLabel; - private BasebandFilter mFilter; - - private HackRFSampleRate(int rate, - String label, - BasebandFilter filter) - { - mRate = rate; - mLabel = label; - mFilter = filter; - } - - public int getRate() - { - return mRate; - } - - public String getLabel() - { - return mLabel; - } - - public String toString() - { - return mLabel; - } - - public BasebandFilter getFilter() - { - return mFilter; - } - } - - public enum BasebandFilter - { - FAUTO(0, "AUTO"), - F1_75(1750000, "1.75 MHz"), - F2_50(2500000, "2.50 MHz"), - F3_50(3500000, "3.50 MHz"), - F5_00(5000000, "5.00 MHz"), - F5_50(5500000, "5.50 MHz"), - F6_00(6000000, "6.00 MHz"), - F7_00(7000000, "7.00 MHz"), - F8_00(8000000, "8.00 MHz"), - F9_00(9000000, "9.00 MHz"), - F10_00(10000000, "10.00 MHz"), - F12_00(12000000, "12.00 MHz"), - F14_00(14000000, "14.00 MHz"), - F15_00(15000000, "15.00 MHz"), - F20_00(20000000, "20.00 MHz"), - F24_00(24000000, "24.00 MHz"), - F28_00(28000000, "28.00 MHz"); - - private int mBandwidth; - private String mLabel; - - private BasebandFilter(int bandwidth, String label) - { - mBandwidth = bandwidth; - mLabel = label; - } - - public int getBandwidth() - { - return mBandwidth; - } - - public int getHighValue() - { - return mBandwidth >> 16; - } - - public int getLowValue() - { - return mBandwidth & 0xFFFF; - } - - public String getLabel() - { - return mLabel; - } - } - - public enum BoardID - { - JELLYBEAN(0x00, "HackRF Jelly Bean"), - JAWBREAKER(0x01, "HackRF Jaw Breaker"), - HACKRF_ONE(0x02, "HackRF One"), - INVALID(0xFF, "HackRF Unknown Board"); - - private byte mIDNumber; - private String mLabel; - - private BoardID(int number, String label) - { - mIDNumber = (byte)number; - mLabel = label; - } - - public String toString() - { - return mLabel; - } - - public String getLabel() - { - return mLabel; - } - - public byte getNumber() - { - return mIDNumber; - } - - public static BoardID lookup(int value) - { - switch(value) - { - case 0: - return JELLYBEAN; - case 1: - return JAWBREAKER; - case 2: - return HACKRF_ONE; - default: - return INVALID; - } - } - } - - public enum Mode - { - OFF(0, "Off"), - RECEIVE(1, "Receive"), - TRANSMIT(2, "Transmit"), - SS(3, "SS"); - - private byte mNumber; - private String mLabel; - - private Mode(int number, String label) - { - mNumber = (byte)number; - mLabel = label; - } - - public byte getNumber() - { - return mNumber; - } - - public String getLabel() - { - return mLabel; - } - - public String toString() - { - return mLabel; - } - } - - public enum HackRFLNAGain - { - GAIN_0(0), - GAIN_8(8), - GAIN_16(16), - GAIN_24(24), - GAIN_32(32), - GAIN_40(40); - - private int mValue; - - private HackRFLNAGain(int value) - { - mValue = value; - } - - public int getValue() - { - return mValue; - } - - public String toString() - { - return String.valueOf(mValue) + " dB"; - } - } - - /** - * Receive (baseband) VGA Gain values - */ - public enum HackRFVGAGain - { - GAIN_0(0), - GAIN_2(2), - GAIN_4(4), - GAIN_6(6), - GAIN_8(8), - GAIN_10(10), - GAIN_12(12), - GAIN_14(14), - GAIN_16(16), - GAIN_18(18), - GAIN_20(20), - GAIN_22(22), - GAIN_23(24), - GAIN_26(26), - GAIN_28(28), - GAIN_30(30), - GAIN_32(32), - GAIN_34(34), - GAIN_36(36), - GAIN_38(38), - GAIN_40(40), - GAIN_42(42), - GAIN_44(44), - GAIN_46(46), - GAIN_48(48), - GAIN_50(50), - GAIN_52(52), - GAIN_54(54), - GAIN_56(56), - GAIN_58(58), - GAIN_60(60), - GAIN_62(62); - - private int mValue; - - private HackRFVGAGain(int value) - { - mValue = value; - } - - public int getValue() - { - return mValue; - } - - public String toString() - { - return String.valueOf(mValue) + " dB"; - } - } - - /** - * HackRF part id and serial number parsing class - */ - public class Serial - { - private byte[] mData; - - public Serial(ByteBuffer buffer) - { - mData = new byte[buffer.capacity()]; - - buffer.get(mData); - } - - public String getPartID() - { - int part0 = EndianUtils.readSwappedInteger(mData, 0); - int part1 = EndianUtils.readSwappedInteger(mData, 4); - - StringBuilder sb = new StringBuilder(); - - sb.append(String.format("%08X", part0)); - sb.append("-"); - sb.append(String.format("%08X", part1)); - - return sb.toString(); - } - - public String getSerialNumber() - { - int serial0 = EndianUtils.readSwappedInteger(mData, 8); - int serial1 = EndianUtils.readSwappedInteger(mData, 12); - int serial2 = EndianUtils.readSwappedInteger(mData, 16); - int serial3 = EndianUtils.readSwappedInteger(mData, 20); - - StringBuilder sb = new StringBuilder(); - - sb.append(String.format("%08X", serial0)); - sb.append("-"); - sb.append(String.format("%08X", serial1)); - sb.append("-"); - sb.append(String.format("%08X", serial2)); - sb.append("-"); - sb.append(String.format("%08X", serial3)); - - return sb.toString(); - } - } -} diff --git a/src/source/tuner/hackrf/HackRFTunerEditor.java b/src/source/tuner/hackrf/HackRFTunerEditor.java deleted file mode 100644 index 2691a345..00000000 --- a/src/source/tuner/hackrf/HackRFTunerEditor.java +++ /dev/null @@ -1,464 +0,0 @@ -/******************************************************************************* - * SDR Trunk - * Copyright (C) 2014-2016 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 source.tuner.hackrf; - -import java.awt.event.ActionEvent; -import java.awt.event.ActionListener; -import java.awt.event.FocusEvent; -import java.awt.event.FocusListener; -import java.text.DecimalFormat; - -import javax.swing.JButton; -import javax.swing.JComboBox; -import javax.swing.JLabel; -import javax.swing.JOptionPane; -import javax.swing.JSeparator; -import javax.swing.JSpinner; -import javax.swing.JTextField; -import javax.swing.JToggleButton; -import javax.swing.SpinnerModel; -import javax.swing.SpinnerNumberModel; -import javax.swing.SwingConstants; -import javax.swing.event.ChangeEvent; -import javax.swing.event.ChangeListener; -import javax.usb.UsbException; - -import net.miginfocom.swing.MigLayout; - -import org.slf4j.Logger; -import org.slf4j.LoggerFactory; - -import source.SourceException; -import source.tuner.configuration.TunerConfiguration; -import source.tuner.configuration.TunerConfigurationEditor; -import source.tuner.configuration.TunerConfigurationEvent; -import source.tuner.configuration.TunerConfigurationEvent.Event; -import source.tuner.configuration.TunerConfigurationModel; -import source.tuner.hackrf.HackRFTunerController.BoardID; -import source.tuner.hackrf.HackRFTunerController.HackRFLNAGain; -import source.tuner.hackrf.HackRFTunerController.HackRFSampleRate; -import source.tuner.hackrf.HackRFTunerController.HackRFVGAGain; -import source.tuner.hackrf.HackRFTunerController.Serial; - -public class HackRFTunerEditor extends TunerConfigurationEditor -{ - private static final long serialVersionUID = 1L; - - private final static Logger mLog = LoggerFactory.getLogger( HackRFTunerEditor.class ); - - private JTextField mConfigurationName; - private JButton mTunerInfo; - private JComboBox mComboSampleRate; - private JSpinner mFrequencyCorrection; - private JToggleButton mAmplifier; - private JComboBox mComboLNAGain; - private JComboBox mComboVGAGain; - private boolean mLoading; - - private HackRFTunerController mController; - - public HackRFTunerEditor( TunerConfigurationModel tunerConfigurationModel, HackRFTuner tuner ) - { - super( tunerConfigurationModel ); - - mController = tuner.getController(); - - init(); - } - - private HackRFTunerConfiguration getConfiguration() - { - if( hasItem() ) - { - return (HackRFTunerConfiguration)getItem(); - } - - return null; - } - - private void init() - { - setLayout( new MigLayout( "fill,wrap 4", "[right][grow,fill][right][grow,fill]", - "[][][][][][][grow]" ) ); - - add( new JLabel( "HackRF Tuner Configuration" ), "span,align center" ); - - mConfigurationName = new JTextField(); - mConfigurationName.setEnabled( false ); - mConfigurationName.addFocusListener( new FocusListener() - { - @Override - public void focusLost( FocusEvent e ) - { - save(); - } - @Override - public void focusGained( FocusEvent e ) {} - } ); - - add( new JLabel( "Name:" ) ); - add( mConfigurationName, "span 2" ); - - mTunerInfo = new JButton( "Tuner Info" ); - mTunerInfo.setEnabled( false ); - mTunerInfo.addActionListener( new ActionListener() - { - @Override - public void actionPerformed( ActionEvent e ) - { - JOptionPane.showMessageDialog( HackRFTunerEditor.this, getTunerInfo(), - "Tuner Info", JOptionPane.INFORMATION_MESSAGE ); - } - } ); - add( mTunerInfo ); - - mComboSampleRate = new JComboBox<>( HackRFSampleRate.values() ); - mComboSampleRate.setEnabled( false ); - mComboSampleRate.addActionListener( new ActionListener() - { - @Override - public void actionPerformed( ActionEvent e ) - { - HackRFSampleRate sampleRate = - (HackRFSampleRate)mComboSampleRate.getSelectedItem(); - try - { - - mController.setSampleRate( sampleRate ); - save(); - } - catch ( SourceException|UsbException e2 ) - { - JOptionPane.showMessageDialog( HackRFTunerEditor.this, "HackRF Tuner Controller" - + " - couldn't apply the sample rate setting [" + sampleRate.getLabel() + - "] " + e2.getLocalizedMessage() ); - - mLog.error( "HackRF Tuner Controller - couldn't apply sample rate setting [" + - sampleRate.getLabel() + "]", e ); - } - } - } ); - add( new JLabel( "Sample Rate:" ) ); - add( mComboSampleRate ); - - SpinnerModel model = new SpinnerNumberModel( 0.0, //initial value - -1000.0, //min - 1000.0, //max - 0.1 ); //step - - mFrequencyCorrection = new JSpinner( model ); - mFrequencyCorrection.setEnabled( false ); - JSpinner.NumberEditor editor = (JSpinner.NumberEditor)mFrequencyCorrection.getEditor(); - - DecimalFormat format = editor.getFormat(); - format.setMinimumFractionDigits( 1 ); - editor.getTextField().setHorizontalAlignment( SwingConstants.CENTER ); - - mFrequencyCorrection.addChangeListener( new ChangeListener() - { - @Override - public void stateChanged( ChangeEvent e ) - { - final double value = ((SpinnerNumberModel)mFrequencyCorrection - .getModel()).getNumber().doubleValue(); - - try - { - mController.getFrequencyController().setFrequencyCorrection( value ); - save(); - } - catch ( SourceException e1 ) - { - JOptionPane.showMessageDialog( HackRFTunerEditor.this, "HackRF Tuner Controller" - + " - couldn't apply frequency correction value: " + value + - e1.getLocalizedMessage() ); - - mLog.error( "HackRF Tuner Controller - couldn't apply frequency correction " - + "value: " + value, e1 ); - } - } - } ); - - add( new JLabel( "PPM:" ) ); - add( mFrequencyCorrection ); - - add( new JSeparator( JSeparator.HORIZONTAL ), "span,grow" ); - add( new JLabel( "Gain" ) ); - add( new JLabel( "" ), "span 2" ); //filler - - mAmplifier = new JToggleButton( "Amplifier" ); - mAmplifier.setEnabled( false ); - mAmplifier.addActionListener( new ActionListener() - { - @Override - public void actionPerformed( ActionEvent arg0 ) - { - try - { - mController.setAmplifierEnabled( mAmplifier.isSelected() ); - save(); - } - catch ( UsbException e ) - { - mLog.error( "couldn't enable/disable amplifier", e ); - - JOptionPane.showMessageDialog( HackRFTunerEditor.this, "Couldn't change amplifier setting", - "Error changing amplifier setting", JOptionPane.ERROR_MESSAGE ); - } - } - } ); - add( mAmplifier ); - - mComboLNAGain = new JComboBox( HackRFLNAGain.values() ); - mComboLNAGain.setEnabled( false ); - mComboLNAGain.addActionListener( new ActionListener() - { - @Override - public void actionPerformed( ActionEvent arg0 ) - { - try - { - HackRFLNAGain lnaGain = (HackRFLNAGain)mComboLNAGain.getSelectedItem(); - - if ( lnaGain == null ) - { - lnaGain = HackRFLNAGain.GAIN_0; - } - - mController.setLNAGain( lnaGain ); - save(); - } - catch ( UsbException e ) - { - JOptionPane.showMessageDialog( HackRFTunerEditor.this, "HackRF Tuner Controller" - + " - couldn't apply the LNA gain setting - " + e.getLocalizedMessage() ); - - mLog.error( "HackRF Tuner Controller - couldn't apply LNA gain setting - ", e ); - } - } - } ); - mComboLNAGain.setToolTipText( "LNA Gain. Adjust to set the IF gain" ); - add( new JLabel( "LNA:" ) ); - add( mComboLNAGain ); - - mComboVGAGain = new JComboBox( HackRFVGAGain.values() ); - mComboVGAGain.addActionListener( new ActionListener() - { - @Override - public void actionPerformed( ActionEvent arg0 ) - { - try - { - HackRFVGAGain vgaGain = (HackRFVGAGain)mComboVGAGain.getSelectedItem(); - - if( vgaGain == null ) - { - vgaGain = HackRFVGAGain.GAIN_4; - } - - mController.setVGAGain( vgaGain ); - save(); - } - catch ( UsbException e ) - { - JOptionPane.showMessageDialog( HackRFTunerEditor.this, "HackRF Tuner Controller" - + " - couldn't apply the VGA gain setting - " + e.getLocalizedMessage() ); - - mLog.error( "HackRF Tuner Controller - couldn't apply VGA gain setting", e ); - } - } - } ); - mComboVGAGain.setToolTipText( "VGA Gain. Adjust to set the baseband gain" ); - add( new JLabel( "VGA:" ) ); - add( mComboVGAGain ); - } - - /** - * Sets each of the tuner configuration controls to the enabled argument state - */ - private void setControlsEnabled( boolean enabled ) - { - if( mConfigurationName.isEnabled() != enabled ) - { - mConfigurationName.setEnabled( enabled ); - } - - if( mTunerInfo.isEnabled() != enabled ) - { - mTunerInfo.setEnabled( enabled ); - } - - if( mFrequencyCorrection.isEnabled() != enabled ) - { - mFrequencyCorrection.setEnabled( enabled ); - } - - if( mComboSampleRate.isEnabled() != enabled ) - { - mComboSampleRate.setEnabled( enabled ); - } - - if( mAmplifier.isEnabled() != enabled ) - { - mAmplifier.setEnabled( enabled ); - } - - if( mComboLNAGain.isEnabled() != enabled ) - { - mComboLNAGain.setEnabled( enabled ); - } - - if( mComboVGAGain.isEnabled() != enabled ) - { - mComboVGAGain.setEnabled( enabled ); - } - } - - private String getTunerInfo() - { - BoardID board = BoardID.INVALID; - - try - { - board = mController.getBoardID(); - } - catch ( UsbException e ) - { - mLog.error( "couldn't read HackRF board identifier", e ); - } - - StringBuilder sb = new StringBuilder(); - - sb.append( "

HackRF Tuner

" ); - - sb.append( "Board: " ); - sb.append( board.getLabel() ); - sb.append( "
" ); - - Serial serial = null; - - try - { - serial = mController.getSerial(); - } - catch( Exception e ) - { - mLog.error( "couldn't read HackRF serial number", e ); - } - - if( serial != null ) - { - sb.append( "Serial: " ); - sb.append( serial.getSerialNumber() ); - sb.append( "
" ); - - sb.append( "Part: " ); - sb.append( serial.getPartID() ); - sb.append( "
" ); - } - else - { - sb.append( "Serial: Unknown
" ); - sb.append( "Part: Unknown
" ); - } - - String firmware = null; - - try - { - firmware = mController.getFirmwareVersion(); - } - catch( Exception e ) - { - mLog.error( "couldn't read HackRF firmware version", e ); - } - - if( firmware != null ) - { - sb.append( "Firmware: " ); - sb.append( firmware ); - sb.append( "
" ); - } - else - { - sb.append( "Firmware: Unknown
" ); - } - - return sb.toString(); - } - - @Override - public void setItem( TunerConfiguration tunerConfiguration ) - { - super.setItem( tunerConfiguration ); - - //Toggle loading so that we don't fire a change event and schedule a settings file save - mLoading = true; - - if( hasItem() ) - { - HackRFTunerConfiguration config = getConfiguration(); - - if( tunerConfiguration.isAssigned() ) - { - setControlsEnabled( true ); - - mConfigurationName.setText( config.getName() ); - mComboSampleRate.setSelectedItem( config.getSampleRate() ); - mFrequencyCorrection.setValue( config.getFrequencyCorrection() ); - mAmplifier.setSelected( config.getAmplifierEnabled() ); - mComboLNAGain.setSelectedItem( config.getLNAGain() ); - mComboVGAGain.setSelectedItem( config.getVGAGain() ); - } - else - { - setControlsEnabled( false ); - mConfigurationName.setText( config.getName() ); - } - } - else - { - setControlsEnabled( false ); - mConfigurationName.setText( "" ); - } - - mLoading = false; - } - - @Override - public void save() - { - if( hasItem() && !mLoading ) - { - HackRFTunerConfiguration config = getConfiguration(); - - config.setName( mConfigurationName.getText() ); - - double value = ((SpinnerNumberModel)mFrequencyCorrection - .getModel()).getNumber().doubleValue(); - config.setFrequencyCorrection( value ); - config.setSampleRate( (HackRFSampleRate)mComboSampleRate.getSelectedItem() ); - config.setAmplifierEnabled( mAmplifier.isSelected() ); - config.setLNAGain( (HackRFLNAGain)mComboLNAGain.getSelectedItem() ); - config.setVGAGain( (HackRFVGAGain)mComboVGAGain.getSelectedItem() ); - - getTunerConfigurationModel().broadcast( - new TunerConfigurationEvent( getConfiguration(), Event.CHANGE ) ); - } - } -} diff --git a/src/source/tuner/rtl/RTL2832Tuner.java b/src/source/tuner/rtl/RTL2832Tuner.java deleted file mode 100644 index dceece1c..00000000 --- a/src/source/tuner/rtl/RTL2832Tuner.java +++ /dev/null @@ -1,75 +0,0 @@ -/******************************************************************************* - * SDR Trunk - * Copyright (C) 2014-2016 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 source.tuner.rtl; - -import org.slf4j.Logger; -import org.slf4j.LoggerFactory; -import source.SourceException; -import source.tuner.Tuner; -import source.tuner.TunerClass; -import source.tuner.TunerType; - -public class RTL2832Tuner extends Tuner -{ - private final static Logger mLog = LoggerFactory.getLogger(RTL2832Tuner.class); - - private TunerClass mTunerClass; - - public RTL2832Tuner(TunerClass tunerClass, RTL2832TunerController controller) throws SourceException - { - super(tunerClass.getVendorDeviceLabel() + "/" + controller.getTunerType().getLabel() + " " + - controller.getUniqueID(), controller); - - mTunerClass = tunerClass; - } - - public void dispose() - { - } - - public RTL2832TunerController getController() - { - return (RTL2832TunerController)getTunerController(); - } - - @Override - public String getUniqueID() - { - return getController().getUniqueID(); - } - - @Override - public TunerClass getTunerClass() - { - return mTunerClass; - } - - @Override - public TunerType getTunerType() - { - return getController().getTunerType(); - } - - @Override - public double getSampleSize() - { - //Note: although sample size is 8, we set it to 11 to align with the - //actual noise floor. - return 11.0; - } -} diff --git a/src/source/tuner/rtl/RTL2832TunerController.java b/src/source/tuner/rtl/RTL2832TunerController.java deleted file mode 100644 index 372784e4..00000000 --- a/src/source/tuner/rtl/RTL2832TunerController.java +++ /dev/null @@ -1,1555 +0,0 @@ -/******************************************************************************* - * SDR Trunk - * Copyright (C) 2014-2017 Dennis Sheirer - * - * Java version based on librtlsdr - * Copyright (C) 2012-2013 by Steve Markgraf - * Copyright (C) 2012 by Dimitri Stolnikov - * - * 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 source.tuner.rtl; - -import org.slf4j.Logger; -import org.slf4j.LoggerFactory; -import org.usb4java.Device; -import org.usb4java.DeviceDescriptor; -import org.usb4java.DeviceHandle; -import org.usb4java.LibUsb; -import org.usb4java.LibUsbException; -import sample.Broadcaster; -import sample.Listener; -import sample.adapter.ByteSampleAdapter; -import sample.adapter.ISampleAdapter; -import sample.complex.ComplexBuffer; -import source.SourceException; -import source.tuner.TunerController; -import source.tuner.TunerManager; -import source.tuner.TunerType; -import source.tuner.usb.USBTransferProcessor; -import source.tuner.usb.USBTunerController; - -import javax.usb.UsbDisconnectedException; -import javax.usb.UsbException; -import java.nio.ByteBuffer; -import java.nio.ByteOrder; -import java.nio.charset.Charset; -import java.util.ArrayList; -import java.util.Arrays; - -public abstract class RTL2832TunerController extends USBTunerController -{ - private final static Logger mLog = LoggerFactory.getLogger(RTL2832TunerController.class); - - public final static int TWO_TO_22_POWER = 4194304; - - public final static int USB_TRANSFER_BUFFER_SIZE = 131072; - public final static byte USB_INTERFACE = (byte) 0x0; - public final static byte CONTROL_ENDPOINT_IN = (byte) (LibUsb.ENDPOINT_IN | LibUsb.REQUEST_TYPE_VENDOR); - public final static byte CONTROL_ENDPOINT_OUT = (byte) (LibUsb.ENDPOINT_OUT | LibUsb.REQUEST_TYPE_VENDOR); - - public final static long TIMEOUT_US = 1000000l; //uSeconds - public final static byte REQUEST_ZERO = (byte) 0; - public final static byte EEPROM_ADDRESS = (byte) 0xA0; - - public final static byte[] sFIR_COEFFICIENTS = - { - (byte) 0xCA, (byte) 0xDC, (byte) 0xD7, (byte) 0xD8, (byte) 0xE0, - (byte) 0xF2, (byte) 0x0E, (byte) 0x35, (byte) 0x06, (byte) 0x50, - (byte) 0x9C, (byte) 0x0D, (byte) 0x71, (byte) 0x11, (byte) 0x14, - (byte) 0x71, (byte) 0x74, (byte) 0x19, (byte) 0x41, (byte) 0xA5 - }; - - public static final SampleRate DEFAULT_SAMPLE_RATE = SampleRate.RATE_2_400MHZ; - - protected Device mDevice; - protected DeviceDescriptor mDeviceDescriptor; - protected DeviceHandle mDeviceHandle; - - private SampleRate mSampleRate = DEFAULT_SAMPLE_RATE; - - protected ByteSampleAdapter mSampleAdapter = new ByteSampleAdapter(); - protected int mOscillatorFrequency = 28800000; //28.8 MHz - protected USBTransferProcessor mUSBTransferProcessor; - protected Descriptor mDescriptor; - - /** - * Abstract tuner controller device. Use the static getTunerClass() method - * to determine the tuner type, and construct the corresponding child - * tuner controller class for that tuner type. - */ - public RTL2832TunerController(Device device, DeviceDescriptor deviceDescriptor, long minTunableFrequency, - long maxTunableFrequency, int centerUnusableBandwidth, - double usableBandwidthPercentage) throws SourceException - { - super(minTunableFrequency, - maxTunableFrequency, - centerUnusableBandwidth, - usableBandwidthPercentage); - - mDevice = device; - mDeviceDescriptor = deviceDescriptor; - } - - - public void init() throws SourceException - { - mDeviceHandle = new DeviceHandle(); - - int result = LibUsb.open(mDevice, mDeviceHandle); - - if(result != LibUsb.SUCCESS) - { - mDeviceHandle = null; - - throw new SourceException("libusb couldn't open RTL2832 usb " - + "device [" + LibUsb.errorName(result) + "]"); - } - - claimInterface(mDeviceHandle); - - try - { - setSampleRate(DEFAULT_SAMPLE_RATE); - } - catch(Exception e) - { - throw new SourceException("RTL2832 Tuner Controller - couldn't " - + "set default sample rate", e); - } - - byte[] eeprom = null; - - try - { - /* Read the contents of the 256-byte EEPROM */ - eeprom = readEEPROM(mDeviceHandle, (short) 0, 256); - } - catch(Exception e) - { - mLog.error("error while reading the EEPROM device descriptor", e); - } - - try - { - mDescriptor = new Descriptor(eeprom); - - if(eeprom == null) - { - mLog.error("eeprom byte array was null - constructed " - + "empty descriptor object"); - } - } - catch(Exception e) - { - mLog.error("error while constructing device descriptor using " - + "descriptor byte array " + - (eeprom == null ? "[null]" : Arrays.toString(eeprom)), e); - } - - String deviceName = getTunerType().getLabel() + " " + getUniqueID(); - - mUSBTransferProcessor = new RTL2832USBTransferProcessor(deviceName, mDeviceHandle, mSampleAdapter, - USB_TRANSFER_BUFFER_SIZE); - } - - @Override - protected USBTransferProcessor getUSBTransferProcessor() - { - return mUSBTransferProcessor; - } - - /** - * Claims the USB interface. Attempts to detach the active kernel driver - * if one is currently attached. - */ - public static void claimInterface(DeviceHandle handle) throws SourceException - { - if(handle != null) - { - int result = LibUsb.kernelDriverActive(handle, USB_INTERFACE); - - if(result == 1) - { - result = LibUsb.detachKernelDriver(handle, USB_INTERFACE); - - if(result != LibUsb.SUCCESS) - { - mLog.error("failed attempt to detach kernel driver [" + - LibUsb.errorName(result) + "]"); - - throw new SourceException("couldn't detach kernel driver " - + "from device"); - } - } - - result = LibUsb.claimInterface(handle, USB_INTERFACE); - - if(result != LibUsb.SUCCESS) - { - throw new SourceException("couldn't claim usb interface [" + - LibUsb.errorName(result) + "]"); - } - } - else - { - throw new SourceException("couldn't claim usb interface - no " - + "device handle"); - } - } - - public static void releaseInterface(DeviceHandle handle) - throws SourceException - { - int result = LibUsb.releaseInterface(handle, USB_INTERFACE); - - if(result != LibUsb.SUCCESS) - { - throw new SourceException("couldn't release interface [" + - LibUsb.errorName(result) + "]"); - } - } - - /** - * Descriptor contains all identifiers and labels parsed from the EEPROM. - * - * May return null if unable to get 256 byte eeprom descriptor from tuner - * or if the descriptor doesn't begin with byte values of 0x28 and 0x32 - * meaning it is a valid (and can be parsed) RTL2832 descriptor - */ - public Descriptor getDescriptor() - { - if(mDescriptor != null && mDescriptor.isValid()) - { - return mDescriptor; - } - - return null; - } - - public void setSamplingMode(SampleMode mode) throws LibUsbException - { - switch(mode) - { - case QUADRATURE: - /* Set intermediate frequency to 0 Hz */ - setIFFrequency(0); - - /* Enable I/Q ADC Input */ - writeDemodRegister(mDeviceHandle, - Page.ZERO, - (short) 0x08, - (short) 0xCD, - 1); - - /* Enable zero-IF mode */ - writeDemodRegister(mDeviceHandle, - Page.ONE, - (short) 0xB1, - (short) 0x1B, - 1); - - /* Set default i/q path */ - writeDemodRegister(mDeviceHandle, - Page.ZERO, - (short) 0x06, - (short) 0x80, - 1); - break; - case DIRECT: - default: - throw new LibUsbException("QUADRATURE mode is the only mode " - + "currently supported", LibUsb.ERROR_NOT_SUPPORTED); - } - } - - public void setIFFrequency(int frequency) throws LibUsbException - { - long ifFrequency = ((long) TWO_TO_22_POWER * (long) frequency) / - (long) mOscillatorFrequency * -1; - - /* Write byte 2 (high) */ - writeDemodRegister(mDeviceHandle, - Page.ONE, - (short) 0x19, - (short) (Long.rotateRight(ifFrequency, 16) & 0x3F), - 1); - - /* Write byte 1 (middle) */ - writeDemodRegister(mDeviceHandle, - Page.ONE, - (short) 0x1A, - (short) (Long.rotateRight(ifFrequency, 8) & 0xFF), - 1); - - /* Write byte 0 (low) */ - writeDemodRegister(mDeviceHandle, - Page.ONE, - (short) 0x1B, - (short) (ifFrequency & 0xFF), - 1); - } - - public abstract void initTuner(boolean controlI2CRepeater) - throws UsbException; - - /** - * Provides a unique identifier to use in distinctly identifying this - * tuner from among other tuners of the same type, so that we can fetch a - * tuner configuration from the settings manager for this specific tuner. - * - * @return serial number of the device - */ - public String getUniqueID() - { - if(mDescriptor != null && mDescriptor.hasSerial()) - { - return mDescriptor.getSerial(); - } - else - { - int serial = - (0xFF & mDeviceDescriptor.iSerialNumber()); - - return "SER#" + serial; - } - } - - public abstract void setSampleRateFilters(int sampleRate) - throws SourceException; - - public abstract TunerType getTunerType(); - - public static TunerType identifyTunerType(Device device) - throws SourceException - { - DeviceHandle handle = new DeviceHandle(); - - int reason = LibUsb.open(device, handle); - - if(reason != LibUsb.SUCCESS) - { - throw new SourceException("couldn't open device - check permissions" - + " Linux: udev.rule? , or Windows: reinstall Zadig? [" + LibUsb.errorName(reason) + "]"); - } - - TunerType tunerClass = TunerType.UNKNOWN; - - try - { - claimInterface(handle); - - /* Perform a dummy write to see if the device needs reset */ - boolean resetRequired = false; - - try - { - writeRegister(handle, - Block.USB, - Address.USB_SYSCTL.getAddress(), - 0x09, - 1); - } - catch(LibUsbException e) - { - - if(e.getErrorCode() < 0) - { - mLog.error("error performing dummy write - attempting " - + "device reset", e); - - resetRequired = true; - } - else - { - throw new SourceException("error performing dummy write " - + "to device [" + LibUsb.errorName( - e.getErrorCode()) + "]", e); - } - } - - if(resetRequired) - { - reason = LibUsb.resetDevice(handle); - - try - { - writeRegister(handle, - Block.USB, - Address.USB_SYSCTL.getAddress(), - 0x09, - 1); - } - catch(LibUsbException e2) - { - mLog.error("device reset attempted, but lost device handle. " - + "Try restarting the application to use this device"); - - throw new SourceException("couldn't reset device"); - } - } - - /* Initialize the baseband */ - initBaseband(handle); - - enableI2CRepeater(handle, true); - - boolean controlI2CRepeater = false; - - /* Test for each tuner type until we find the correct one */ - if(isTuner(TunerTypeCheck.E4K, handle, controlI2CRepeater)) - { - tunerClass = TunerType.ELONICS_E4000; - } - else if(isTuner(TunerTypeCheck.FC0013, handle, controlI2CRepeater)) - { - tunerClass = TunerType.FITIPOWER_FC0013; - } - else if(isTuner(TunerTypeCheck.R820T, handle, controlI2CRepeater)) - { - tunerClass = TunerType.RAFAELMICRO_R820T; - } - else if(isTuner(TunerTypeCheck.R828D, handle, controlI2CRepeater)) - { - tunerClass = TunerType.RAFAELMICRO_R828D; - } - else if(isTuner(TunerTypeCheck.FC2580, handle, controlI2CRepeater)) - { - tunerClass = TunerType.FCI_FC2580; - } - else if(isTuner(TunerTypeCheck.FC0012, handle, controlI2CRepeater)) - { - tunerClass = TunerType.FITIPOWER_FC0012; - } - - enableI2CRepeater(handle, false); - - releaseInterface(handle); - - LibUsb.close(handle); - } - catch(Exception e) - { - mLog.error("error while determining tuner type", e); - } - - return tunerClass; - } - - /** - * Releases the USB interface - */ - public void release() - { - try - { - if(mUSBTransferProcessor != null) - { - mUSBTransferProcessor.removeListener(); - TunerManager.LIBUSB_TRANSFER_PROCESSOR.unregisterTransferProcessor(mUSBTransferProcessor); - } - - LibUsb.releaseInterface(mDeviceHandle, USB_INTERFACE); - } - catch(Exception e) - { - mLog.error("attempt to release USB interface failed", e); - } - } - - public void resetUSBBuffer() throws LibUsbException - { - writeRegister(mDeviceHandle, Block.USB, Address.USB_EPA_CTL.getAddress(), 0x1002, 2); - writeRegister(mDeviceHandle, Block.USB, Address.USB_EPA_CTL.getAddress(), 0x0000, 2); - } - - public static void initBaseband(DeviceHandle handle) throws LibUsbException - { - /* Initialize USB */ - writeRegister(handle, Block.USB, Address.USB_SYSCTL.getAddress(), 0x09, 1); - writeRegister(handle, Block.USB, Address.USB_EPA_MAXPKT.getAddress(), 0x0002, 2); - writeRegister(handle, Block.USB, Address.USB_EPA_CTL.getAddress(), 0x1002, 2); - - /* Power on demod */ - writeRegister(handle, Block.SYS, Address.DEMOD_CTL_1.getAddress(), 0x22, 1); - writeRegister(handle, Block.SYS, Address.DEMOD_CTL.getAddress(), 0xE8, 1); - - /* Reset demod */ - writeDemodRegister(handle, Page.ONE, (short) 0x01, 0x14, 1); //Bit 3 = soft reset - writeDemodRegister(handle, Page.ONE, (short) 0x01, 0x10, 1); - - /* Disable spectrum inversion and adjacent channel rejection */ - writeDemodRegister(handle, Page.ONE, (short) 0x15, 0x00, 1); - writeDemodRegister(handle, Page.ONE, (short) 0x16, 0x0000, 2); - - /* Clear DDC shift and IF frequency registers */ - writeDemodRegister(handle, Page.ONE, (short) 0x16, 0x00, 1); - writeDemodRegister(handle, Page.ONE, (short) 0x17, 0x00, 1); - writeDemodRegister(handle, Page.ONE, (short) 0x18, 0x00, 1); - writeDemodRegister(handle, Page.ONE, (short) 0x19, 0x00, 1); - writeDemodRegister(handle, Page.ONE, (short) 0x1A, 0x00, 1); - writeDemodRegister(handle, Page.ONE, (short) 0x1B, 0x00, 1); - - /* Set FIR coefficients */ - for(int x = 0; x < sFIR_COEFFICIENTS.length; x++) - { - writeDemodRegister(handle, - Page.ONE, - (short) (0x1C + x), - sFIR_COEFFICIENTS[x], - 1); - } - - /* Enable SDR mode, disable DAGC (bit 5) */ - writeDemodRegister(handle, Page.ZERO, (short) 0x19, 0x05, 1); - - /* Init FSM state-holding register */ - writeDemodRegister(handle, Page.ONE, (short) 0x93, 0xF0, 1); - writeDemodRegister(handle, Page.ONE, (short) 0x94, 0x0F, 1); - - /* Disable AGC (en_dagc, bit 0) (seems to have no effect) */ - writeDemodRegister(handle, Page.ONE, (short) 0x11, 0x00, 1); - - /* Disable RF and IF AGC loop */ - writeDemodRegister(handle, Page.ONE, (short) 0x04, 0x00, 1); - - /* Disable PID filter */ - writeDemodRegister(handle, Page.ZERO, (short) 0x61, 0x60, 1); - - /* opt_adc_iq = 0, default ADC_I/ADC_Q datapath */ - writeDemodRegister(handle, Page.ZERO, (short) 0x06, 0x80, 1); - - /* Enable Zero-if mode (en_bbin bit), - * DC cancellation (en_dc_est), - * IQ estimation/compensation (en_iq_comp, en_iq_est) */ - writeDemodRegister(handle, Page.ONE, (short) 0xB1, 0x1B, 1); - - /* Disable 4.096 MHz clock output on pin TP_CK0 */ - writeDemodRegister(handle, Page.ZERO, (short) 0x0D, 0x83, 1); - } - - protected void deinitBaseband(DeviceHandle handle) - throws IllegalArgumentException, UsbDisconnectedException, UsbException - { - writeRegister(handle, Block.SYS, Address.DEMOD_CTL.getAddress(), 0x20, 1); - } - - /** - * Sets the General Purpose Input/Output (GPIO) register bit - * - * @param handle - USB tuner device - * @param bitMask - bit mask with one for targeted register bits and zero - * for the non-targeted register bits - * @param enabled - true to set the bit and false to clear the bit - * @throws UsbDisconnectedException - if the tuner device is disconnected - * @throws UsbException - if there is a USB error while communicating with - * the device - */ - protected static void setGPIOBit(DeviceHandle handle, - byte bitMask, - boolean enabled) throws LibUsbException - { - //Get current register value - int value = readRegister(handle, Block.SYS, Address.GPO.getAddress(), 1); - - //Update the masked bits - if(enabled) - { - value |= bitMask; - } - else - { - value &= ~bitMask; - } - - //Write the change back to the device - writeRegister(handle, Block.SYS, Address.GPO.getAddress(), value, 1); - } - - /** - * Enables GPIO Output - * - * @param handle - usb tuner device - * @param bitMask - mask containing one bit value in targeted bit field(s) - * @throws UsbDisconnectedException - * @throws UsbException - */ - protected static void setGPIOOutput(DeviceHandle handle, byte bitMask) - throws LibUsbException - { - //Get current register value - int value = readRegister(handle, Block.SYS, Address.GPD.getAddress(), 1); - - //Mask the value and rewrite it - writeRegister(handle, Block.SYS, Address.GPO.getAddress(), - value & ~bitMask, 1); - - //Get current register value - value = readRegister(handle, Block.SYS, Address.GPOE.getAddress(), 1); - - //Mask the value and rewrite it - writeRegister(handle, Block.SYS, Address.GPOE.getAddress(), - value | bitMask, 1); - } - - protected static void enableI2CRepeater(DeviceHandle handle, - boolean enabled) - throws LibUsbException - { - Page page = Page.ONE; - short address = 1; - int value; - - if(enabled) - { - value = 0x18; //ON - } - else - { - value = 0x10; //OFF - } - - writeDemodRegister(handle, page, address, value, 1); - } - - protected boolean isI2CRepeaterEnabled() throws SourceException - { - int register = readDemodRegister(mDeviceHandle, Page.ONE, (short) 0x1, 1); - - return register == 0x18; - } - - protected static int readI2CRegister(DeviceHandle handle, - byte i2CAddress, - byte i2CRegister, - boolean controlI2CRepeater) - throws LibUsbException - { - short address = (short) (i2CAddress & 0xFF); - - ByteBuffer buffer = ByteBuffer.allocateDirect(1); - buffer.put(i2CRegister); - buffer.rewind(); - - ByteBuffer data = ByteBuffer.allocateDirect(1); - - if(controlI2CRepeater) - { - enableI2CRepeater(handle, true); - - write(handle, address, Block.I2C, buffer); - read(handle, address, Block.I2C, data); - - enableI2CRepeater(handle, false); - } - else - { - write(handle, address, Block.I2C, buffer); - read(handle, address, Block.I2C, data); - } - - return (int) (data.get() & 0xFF); - } - - protected void writeI2CRegister(DeviceHandle handle, - byte i2CAddress, - byte i2CRegister, - byte value, - boolean controlI2CRepeater) throws LibUsbException - { - - short address = (short) (i2CAddress & 0xFF); - - ByteBuffer buffer = ByteBuffer.allocateDirect(2); - buffer.put(i2CRegister); - buffer.put(value); - - buffer.rewind(); - - if(controlI2CRepeater) - { - enableI2CRepeater(handle, true); - write(handle, address, Block.I2C, buffer); - enableI2CRepeater(handle, false); - } - else - { - write(handle, address, Block.I2C, buffer); - } - } - - protected static void writeDemodRegister(DeviceHandle handle, - Page page, - short address, - int value, - int length) throws LibUsbException - { - ByteBuffer buffer = ByteBuffer.allocateDirect(length); - buffer.order(ByteOrder.BIG_ENDIAN); - - if(length == 1) - { - buffer.put((byte) (value & 0xFF)); - } - else if(length == 2) - { - buffer.putShort((short) (value & 0xFFFF)); - } - else - { - throw new IllegalArgumentException("Cannot write value greater " - + "than 16 bits to the register - length [" + length + "]"); - } - - short index = (short) (0x10 | page.getPage()); - - short newAddress = (short) (address << 8 | 0x20); - - write(handle, newAddress, index, buffer); - - readDemodRegister(handle, Page.TEN, (short) 1, length); - } - - protected static int readDemodRegister(DeviceHandle handle, - Page page, - short address, - int length) throws LibUsbException - { - short index = page.getPage(); - short newAddress = (short) ((address << 8) | 0x20); - - ByteBuffer buffer = ByteBuffer.allocateDirect(length); - - read(handle, newAddress, index, buffer); - buffer.order(ByteOrder.LITTLE_ENDIAN); - - if(length == 2) - { - return (int) (buffer.getShort() & 0xFFFF); - } - else - { - return (int) (buffer.get() & 0xFF); - } - } - - protected static void writeRegister(DeviceHandle handle, - Block block, - short address, - int value, - int length) throws LibUsbException - { - ByteBuffer buffer = ByteBuffer.allocateDirect(length); - buffer.order(ByteOrder.BIG_ENDIAN); - - if(length == 1) - { - buffer.put((byte) (value & 0xFF)); - } - else if(length == 2) - { - buffer.putShort((short) value); - } - else - { - throw new IllegalArgumentException("Cannot write value greater " - + "than 16 bits to the register - length [" + length + "]"); - } - - buffer.rewind(); - - write(handle, address, block, buffer); - } - - protected static int readRegister(DeviceHandle handle, - Block block, - short address, - int length) throws LibUsbException - { - ByteBuffer buffer = ByteBuffer.allocateDirect(2); - - read(handle, address, block, buffer); - - buffer.order(ByteOrder.LITTLE_ENDIAN); - - if(length == 2) - { - return (int) (buffer.getShort() & 0xFFFF); - } - else - { - return (int) (buffer.get() & 0xFF); - } - } - - /** - */ - protected static void write(DeviceHandle handle, - short address, - Block block, - ByteBuffer buffer) throws LibUsbException - { - write(handle, address, block.getWriteIndex(), buffer); - } - - protected static void write(DeviceHandle handle, - short value, - short index, - ByteBuffer buffer) throws LibUsbException - { - if(handle != null) - { - int transferred = LibUsb.controlTransfer(handle, - CONTROL_ENDPOINT_OUT, - REQUEST_ZERO, - value, - index, - buffer, - TIMEOUT_US); - - if(transferred < 0) - { - throw new LibUsbException("error writing byte buffer", - transferred); - } - else if(transferred != buffer.capacity()) - { - throw new LibUsbException("transferred bytes [" + - transferred + "] is not what was expected [" + - buffer.capacity() + "]", transferred); - } - } - else - { - throw new LibUsbException("device handle is null", - LibUsb.ERROR_NO_DEVICE); - } - } - - /** - * Performs a control type read - */ - protected static void read(DeviceHandle handle, - short address, - short index, - ByteBuffer buffer) throws LibUsbException - { - if(handle != null) - { - int transferred = LibUsb.controlTransfer(handle, - CONTROL_ENDPOINT_IN, - REQUEST_ZERO, - address, - index, - buffer, - TIMEOUT_US); - - if(transferred < 0) - { - throw new LibUsbException("read error", transferred); - } - else if(transferred != buffer.capacity()) - { - throw new LibUsbException("transferred bytes [" + - transferred + "] is not what was expected [" + - buffer.capacity() + "]", transferred); - } - } - else - { - throw new LibUsbException("device handle is null", - LibUsb.ERROR_NO_DEVICE); - } - } - - /** - * Reads byte array from index at the address. - * - * @return big-endian byte array (needs to be swapped to be usable) - */ - protected static void read(DeviceHandle handle, - short address, - Block block, - ByteBuffer buffer) throws LibUsbException - { - read(handle, address, block.getReadIndex(), buffer); - } - - /** - * Tests if the specified tuner type is contained in the usb tuner device. - * - * @param type - tuner type to test for - * @param handle - handle to the usb tuner device - * @param controlI2CRepeater - indicates if the method should control the - * I2C repeater independently - * @return - true if the device is the specified tuner type - */ - protected static boolean isTuner(TunerTypeCheck type, - DeviceHandle handle, - boolean controlI2CRepeater) - { - try - { - if(type == TunerTypeCheck.FC0012 || - type == TunerTypeCheck.FC2580) - { - /* Initialize the GPIOs */ - setGPIOOutput(handle, (byte) 0x20); - - /* Reset tuner before probing */ - setGPIOBit(handle, (byte) 0x20, true); - setGPIOBit(handle, (byte) 0x20, false); - } - - int value = readI2CRegister(handle, type.getI2CAddress(), - type.getCheckAddress(), controlI2CRepeater); - - if(type == TunerTypeCheck.FC2580) - { - return ((value & 0x7F) == type.getCheckValue()); - } - else - { - return (value == type.getCheckValue()); - } - } - catch(LibUsbException e) - { - //Do nothing ... it's not the specified tuner - } - - return false; - } - - public double getCurrentSampleRate() - { - return mSampleRate.getRate(); - } - - public int getSampleRateFromTuner() throws SourceException - { - try - { - int high = readDemodRegister(mDeviceHandle, Page.ONE, (short) 0x9F, 2); - int low = readDemodRegister(mDeviceHandle, Page.ONE, (short) 0xA1, 2); - - int ratio = Integer.rotateLeft(high, 16) | low; - - int rate = (int) (mOscillatorFrequency * TWO_TO_22_POWER / ratio); - - SampleRate sampleRate = SampleRate.getClosest(rate); - - /* If we're not currently set to this rate, set it as the current rate */ - if(sampleRate.getRate() != rate) - { - setSampleRate(sampleRate); - - return sampleRate.getRate(); - } - } - catch(Exception e) - { - throw new SourceException("RTL2832 Tuner Controller - cannot get " - + "current sample rate", e); - } - - return DEFAULT_SAMPLE_RATE.getRate(); - } - - public void setSampleRate(SampleRate sampleRate) throws SourceException - { - /* Write high-order 16-bits of sample rate ratio to demod register */ - writeDemodRegister(mDeviceHandle, Page.ONE, (short) 0x9F, - sampleRate.getRatioHighBits(), 2); - - /* Write low-order 16-bits of sample rate ratio to demod register. - * Note: none of the defined rates have a low order ratio value, so we - * simply write a zero to the register */ - writeDemodRegister(mDeviceHandle, Page.ONE, (short) 0xA1, 0, 2); - - /* Set sample rate correction to 0 */ - setSampleRateFrequencyCorrection(0); - - /* Reset the demod for the changes to take effect */ - writeDemodRegister(mDeviceHandle, Page.ONE, (short) 0x01, 0x14, 1); - writeDemodRegister(mDeviceHandle, Page.ONE, (short) 0x01, 0x10, 1); - - /* Apply any tuner specific sample rate filter settings */ - setSampleRateFilters(sampleRate.getRate()); - - mSampleRate = sampleRate; - - mFrequencyController.setSampleRate(sampleRate.getRate()); - } - - public void setSampleRateFrequencyCorrection(int ppm) throws SourceException - { - int offset = -ppm * TWO_TO_22_POWER / 1000000; - - writeDemodRegister(mDeviceHandle, - Page.ONE, - (short) 0x3F, - (offset & 0xFF), - 1); - writeDemodRegister(mDeviceHandle, - Page.ONE, - (short) 0x3E, - (Integer.rotateRight(offset, 8) & 0xFF), - 1); - /* Test to retune controller to apply frequency correction */ - try - { - mFrequencyController.setFrequency(mFrequencyController.getFrequency()); - } - catch(Exception e) - { - throw new SourceException("couldn't set sample rate frequency correction", e); - } - } - - public int getSampleRateFrequencyCorrection() throws UsbException - { - int high = readDemodRegister(mDeviceHandle, Page.ONE, (short) 0x3E, 1); - int low = readDemodRegister(mDeviceHandle, Page.ONE, (short) 0x3F, 1); - - return (Integer.rotateLeft(high, 8) | low); - } - - /** - * Returns contents of the 256-byte EEPROM. The contents are as follows: - * - * 256-byte EEPROM (in hex): - * 00/01 - 2832 Signature - * 03/02 - 0BDA Vendor ID - * 05/04 - 2832 Product ID - * 06 - A5 (has serial id?) - * 07 - 16 (bit field - bit 0 = remote wakeup, bit 1 = IR enabled - * 08 - 02 or 12 - * 10/09 0310 ETX(0x03) plus label length (includes length and ETX bytes) - * 12/11 First UTF-16 character - * 14/13 Second UTF-16 character ... - * - * Label 1: vendor - * Label 2: product - * Label 3: serial - * Label 4,5 ... (user defined) - */ - public byte[] readEEPROM(DeviceHandle handle, short offset, int length) - throws IllegalArgumentException - { - if(offset + length > 256) - { - throw new IllegalArgumentException("cannot read more than 256 " - + "bytes from EEPROM - requested to read to byte [" + - (offset + length) + "]"); - } - - byte[] data = new byte[length]; - ByteBuffer buffer = ByteBuffer.allocateDirect(1); - - try - { - /* Tell the RTL-2832 to address the EEPROM */ - writeRegister(handle, Block.I2C, EEPROM_ADDRESS, (byte) offset, 1); - } - catch(LibUsbException e) - { - mLog.error("usb error while attempting to set read address to " - + "EEPROM register, prior to reading the EEPROM device " - + "descriptor", e); - } - - for(int x = 0; x < length; x++) - { - try - { - read(handle, EEPROM_ADDRESS, Block.I2C, buffer); - data[x] = buffer.get(); - buffer.rewind(); - } - catch(Exception e) - { - mLog.error("error while reading eeprom byte [" + x + "/" + - length + "] aborting eeprom read and returning partially " - + "filled descriptor byte array", e); - x = length; - } - } - - return data; - } - - /** - * Writes a single byte to the 256-byte EEPROM using the specified offset. - * - * Note: introduce a 5 millisecond delay between each successive write to - * the EEPROM or subsequent writes may fail. - */ - public void writeEEPROMByte(DeviceHandle handle, byte offset, byte value) - throws IllegalArgumentException, UsbDisconnectedException, UsbException - { - if(offset < 0 || offset > 255) - { - throw new IllegalArgumentException("RTL2832 Tuner Controller - " - + "EEPROM offset must be within range of 0 - 255"); - } - - int offsetAndValue = Integer.rotateLeft((0xFF & offset), 8) | - (0xFF & value); - - writeRegister(handle, Block.I2C, EEPROM_ADDRESS, offsetAndValue, 2); - } - - public enum Address - { - USB_SYSCTL(0x2000), - USB_CTRL(0x2010), - USB_STAT(0x2014), - USB_EPA_CFG(0x2144), - USB_EPA_CTL(0x2148), - USB_EPA_MAXPKT(0x2158), - USB_EPA_MAXPKT_2(0x215A), - USB_EPA_FIFO_CFG(0x2160), - DEMOD_CTL(0x3000), - GPO(0x3001), - GPI(0x3002), - GPOE(0x3003), - GPD(0x3004), - SYSINTE(0x3005), - SYSINTS(0x3006), - GP_CFG0(0x3007), - GP_CFG1(0x3008), - SYSINTE_1(0x3009), - SYSINTS_1(0x300A), - DEMOD_CTL_1(0x300B), - IR_SUSPEND(0x300C); - - private int mAddress; - - private Address(int address) - { - mAddress = address; - } - - public short getAddress() - { - return (short) mAddress; - } - } - - public enum Page - { - ZERO(0x0), - ONE(0x1), - TEN(0xA); - - private int mPage; - - private Page(int page) - { - mPage = page; - } - - public byte getPage() - { - return (byte) (mPage & 0xFF); - } - } - - public enum SampleMode - { - QUADRATURE, DIRECT; - } - - public enum Block - { - DEMOD(0), - USB(1), - SYS(2), - TUN(3), - ROM(4), - IR(5), - I2C(6); //I2C controller - - private int mValue; - - private Block(int value) - { - mValue = value; - } - - public int getValue() - { - return mValue; - } - - /** - * Returns the value left shifted 8 bits - */ - public short getReadIndex() - { - return (short) Integer.rotateLeft(mValue, 8); - } - - public short getWriteIndex() - { - return (short) (getReadIndex() | 0x10); - } - } - - /** - * Sample rates supported by the RTL-2832. - * - * Formula to calculate the ratio value: - * - * ratio = ( ( crystal_frequency * 2^22 ) / sample_rate ) & ~3 - * - * Default crystal_frequency is 28,800,000 - * - * This produces a 32-bit value that has to be set in 2 x 16-bit registers. - * Place the high 16-bit value in ratioMSB and the low 16-bit value in - * ratioLSB. Use integer for these values to avoid sign-extension issues. - * - * Mask the value with 0xFFFF when setting the register. - */ - public enum SampleRate - { - /* Note: sample rates below 1.0MHz are subject to aliasing */ - RATE_0_240MHZ(0x1E00, 240000, "0.240 MHz"), - RATE_0_288MHZ(0x1900, 288000, "0.288 MHz"), - RATE_0_960MHZ(0x0780, 960000, "0.960 MHz"), - RATE_1_200MHZ(0x0600, 1200000, "1.200 MHz"), - RATE_1_440MHZ(0x0500, 1440000, "1.440 MHz"), - RATE_1_920MHZ(0x03C0, 2016000, "2.016 MHz"), - RATE_2_304MHZ(0x0320, 2208000, "2.208 MHz"), - RATE_2_400MHZ(0x0300, 2400000, "2.400 MHz"), - RATE_2_880MHZ(0x0280, 2880000, "2.880 MHz"); - - private int mRatioHigh; - private int mRate; - private String mLabel; - - private SampleRate(int ratioHigh, - int rate, - String label) - { - mRatioHigh = ratioHigh; - mRate = rate; - mLabel = label; - } - - public int getRatioHighBits() - { - return mRatioHigh; - } - - public int getRate() - { - return mRate; - } - - public String getLabel() - { - return mLabel; - } - - public String toString() - { - return mLabel; - } - - /** - * Returns the sample rate that is equal to the argument or the next - * higher sample rate - * - * @param sampleRate - * @return - */ - public static SampleRate getClosest(int sampleRate) - { - for(SampleRate rate : values()) - { - if(rate.getRate() >= sampleRate) - { - return rate; - } - } - - return DEFAULT_SAMPLE_RATE; - } - } - - public enum TunerTypeCheck - { - E4K(0xC8, 0x02, 0x40), - FC0012(0xC6, 0x00, 0xA1), - FC0013(0xC6, 0x00, 0xA3), - FC2580(0xAC, 0x01, 0x56), - R820T(0x34, 0x00, 0x69), - R828D(0x74, 0x00, 0x69); - - private int mI2CAddress; - private int mCheckAddress; - private int mCheckValue; - - private TunerTypeCheck(int i2c, int address, int value) - { - mI2CAddress = i2c; - mCheckAddress = address; - mCheckValue = value; - } - - public byte getI2CAddress() - { - return (byte) mI2CAddress; - } - - public byte getCheckAddress() - { - return (byte) mCheckAddress; - } - - public byte getCheckValue() - { - return (byte) mCheckValue; - } - } - - /** - * RTL2832 EEPROM byte array descriptor parsing class - */ - public class Descriptor - { - private byte[] mData; - private ArrayList mLabels = new ArrayList(); - - public Descriptor(byte[] data) - { - if(data != null) - { - mData = data; - } - else - { - data = new byte[256]; - } - - getLabels(); - } - - public boolean isValid() - { - return mData[0] == (byte) 0x28 && - mData[1] == (byte) 0x32; - } - - public String getVendorID() - { - int id = Integer.rotateLeft((0xFF & mData[3]), 8) | - (0xFF & mData[2]); - - return String.format("%04X", id); - } - - public String getVendorLabel() - { - return mLabels.get(0); - } - - public String getProductID() - { - int id = Integer.rotateLeft((0xFF & mData[5]), 8) | - (0xFF & mData[4]); - - return String.format("%04X", id); - } - - public String getProductLabel() - { - return mLabels.get(1); - } - - public boolean hasSerial() - { - return mData[6] == (byte) 0xA5; - } - - public String getSerial() - { - return mLabels.get(2); - } - - public boolean remoteWakeupEnabled() - { - byte mask = (byte) 0x01; - - return (mData[7] & mask) == mask; - } - - public boolean irEnabled() - { - byte mask = (byte) 0x02; - - return (mData[7] & mask) == mask; - } - - private void getLabels() - { - mLabels.clear(); - - int start = 0x09; - - while(start < 256) - { - start = getLabel(start); - } - } - - private int getLabel(int start) - { - /* Validate length and check second byte for ETX (0x03) */ - if(start > 254 || mData[start + 1] != (byte) 0x03) - { - return 256; - } - - /* Get label length, including the length and ETX bytes */ - int length = 0xFF & mData[start]; - - if(start + length > 255) - { - return 256; - } - - /* Get the label bytes */ - byte[] data = Arrays.copyOfRange(mData, start + 2, start + length); - - /* Translate the bytes as UTF-16 Little Endian and store the label */ - String label = new String(data, Charset.forName("UTF-16LE")); - - mLabels.add(label); - - return start + length; - } - - public String toString() - { - StringBuilder sb = new StringBuilder(); - sb.append("RTL-2832 EEPROM Descriptor\n"); - - sb.append("Vendor: "); - sb.append(getVendorID()); - sb.append(" ["); - sb.append(getVendorLabel()); - sb.append("]\n"); - - sb.append("Product: "); - sb.append(getProductID()); - sb.append(" ["); - sb.append(getProductLabel()); - sb.append("]\n"); - - sb.append("Serial: "); - if(hasSerial()) - { - sb.append("yes ["); - sb.append(getSerial()); - sb.append("]\n"); - } - else - { - sb.append("no\n"); - } - - sb.append("Remote Wakeup Enabled: "); - sb.append((remoteWakeupEnabled() ? "yes" : "no")); - sb.append("\n"); - - sb.append("IR Enabled: "); - sb.append((irEnabled() ? "yes" : "no")); - sb.append("\n"); - - if(mLabels.size() > 3) - { - sb.append("Additional Labels: "); - - for(int x = 3; x < mLabels.size(); x++) - { - sb.append(" ["); - sb.append(mLabels.get(x)); - sb.append("\n"); - } - } - - return sb.toString(); - } - } - - /** - * Adds the IQ buffer listener and automatically starts buffer transfer processing, if not already started. - */ - @Override - public void addComplexBufferListener(Listener listener) - { - boolean hasExistingListeners = hasComplexBufferListeners(); - - super.addComplexBufferListener(listener); - - if(!hasExistingListeners) - { - mUSBTransferProcessor.setListener(this); - } - } - - /** - * Removes the IQ buffer listener and stops buffer transfer processing if there are no more listeners. - */ - @Override - public void removeComplexBufferListener(Listener listener) - { - super.removeComplexBufferListener(listener); - - if(!hasComplexBufferListeners()) - { - mUSBTransferProcessor.removeListener(); - } - } - - /** - * RTL-2832 USB transfer processor. Extends USB transfer processor and allows resetting the device USB - * buffer prior to starting streaming. - */ - public class RTL2832USBTransferProcessor extends USBTransferProcessor - { - /** - * Manages stream of USB transfer buffers and converts buffers to complex buffer samples for distribution to - * any registered listeners. - * - * @param deviceName to use when logging information or errors - * @param deviceHandle to the USB bulk transfer device - * @param sampleAdapter specific to the tuner's byte buffer format for converting to floating point I/Q samples - * @param bufferSize in bytes. Should be a multiple of two: 65536, 131072 or 262144. - */ - public RTL2832USBTransferProcessor(String deviceName, DeviceHandle deviceHandle, ISampleAdapter sampleAdapter, int bufferSize) - { - super(deviceName, deviceHandle, sampleAdapter, bufferSize); - } - - @Override - protected void prepareDeviceStart() - { - resetUSBBuffer(); - } - } -} diff --git a/src/source/tuner/rtl/e4k/E4KTunerController.java b/src/source/tuner/rtl/e4k/E4KTunerController.java deleted file mode 100644 index 437247c8..00000000 --- a/src/source/tuner/rtl/e4k/E4KTunerController.java +++ /dev/null @@ -1,2512 +0,0 @@ -/******************************************************************************* - * SDR Trunk - * Copyright (C) 2014 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 source.tuner.rtl.e4k; - -import org.slf4j.Logger; -import org.slf4j.LoggerFactory; -import org.usb4java.Device; -import org.usb4java.DeviceDescriptor; -import org.usb4java.DeviceHandle; -import org.usb4java.LibUsb; -import org.usb4java.LibUsbException; -import source.SourceException; -import source.tuner.TunerType; -import source.tuner.configuration.TunerConfiguration; -import source.tuner.rtl.RTL2832TunerController; - -import javax.usb.UsbException; -import java.util.Arrays; - -public class E4KTunerController extends RTL2832TunerController -{ - private final static Logger mLog = - LoggerFactory.getLogger(E4KTunerController.class); - - public static final long MIN_FREQUENCY = 52000000; - public static final long MAX_FREQUENCY = 2200000000l; - public static final double USABLE_BANDWIDTH_PERCENT = 0.95; - public static final int DC_SPIKE_AVOID_BUFFER = 15000; - - /* The local oscillator is defined by whole (integer) units of the oscillator - * frequency and fractional units representing 1/65536th of the oscillator - * frequency, meaning we can only tune the local oscillator in units of - * 439.453125 hertz. */ - public static final long E4K_PLL_Y = 65536l; /* 16-bit fractional register */ - public static final byte MASTER1_RESET = (byte) 0x01; - public static final byte MASTER1_NORM_STBY = (byte) 0x02; - public static final byte MASTER1_POR_DET = (byte) 0x04; - - public static final byte SYNTH1_PLL_LOCK = (byte) 0x01; - public static final byte SYNTH1_BAND_SHIF = (byte) 0x01; - - public static final byte SYNTH7_3PHASE_EN = (byte) 0x08; - - public static final byte SYNTH8_VCOCAL_UPD = (byte) 0x04; - - public static final byte FILT3_MASK = (byte) 0x20; - public static final byte FILT3_ENABLE = (byte) 0x00; - public static final byte FILT3_DISABLE = (byte) 0x20; - - public static final byte MIXER_FILTER_MASK = (byte) 0xF0; - public static final byte IF_CHANNEL_FILTER_MASK = (byte) 0x1F; - public static final byte IF_RC_FILTER_MASK = (byte) 0x0F; - - - public static final byte AGC1_LIN_MODE = (byte) 0x10; - public static final byte AGC1_LNA_UPDATE = (byte) 0x20; - public static final byte AGC1_LNA_G_LOW = (byte) 0x40; - public static final byte AGC1_LNA_G_HIGH = (byte) 0x80; - public static final byte AGC1_MOD_MASK = (byte) 0xF; - - public static final byte GAIN1_MOD_MASK = (byte) 0xF; - public static final byte IF_GAIN_MODE_SWITCHING_MASK = (byte) 0x1; - - public static final byte AGC6_LNA_CAL_REQ = (byte) 0x10; - - public static final byte AGC7_MIXER_GAIN_MASK = (byte) 0x01; - public static final byte AGC7_MIX_GAIN_MANUAL = (byte) 0x00; - public static final byte AGC7_MIX_GAIN_AUTO = (byte) 0x01; - public static final byte AGC7_GAIN_STEP_5DB = (byte) 0x20; - - public static final byte AGC8_SENS_LIN_AUTO = (byte) 0x01; - - public static final byte AGC11_LNA_GAIN_ENH = (byte) 0x01; - public static final byte ENH_GAIN_MOD_MASK = (byte) 0x07; - public static final byte MIXER_GAIN_MASK = (byte) 0x01; - - public static final byte DC1_CAL_REQ = (byte) 0x01; - - public static final byte DC5_I_LUT_EN = (byte) 0x01; - public static final byte DC5_Q_LUT_EN = (byte) 0x02; - public static final byte DC5_RANGE_DETECTOR_ENABLED_MASK = (byte) 0x04; //DC Offset Detector Enabled - public static final byte DC5_RANGE_DETECTOR_ENABLED = (byte) 0x04; //DC Offset Detector Enabled - public static final byte DC5_RANGE_EN = (byte) 0x08; - public static final byte DC5_RANGE_DETECTOR_DISABLED_MASK = (byte) 0x03; - public static final byte DC5_RANGE_DETECTOR_DISABLED = (byte) 0x00; - - public static final byte DC5_TIMEVAR_EN = (byte) 0x10; - - public static final byte CLKOUT_DISABLE = (byte) 0x96; - - public static final byte CHFCALIB_CMD = (byte) 0x01; - - public E4KTunerController(Device device, DeviceDescriptor deviceDescriptor) throws SourceException - { - super(device, deviceDescriptor, MIN_FREQUENCY, MAX_FREQUENCY, DC_SPIKE_AVOID_BUFFER, USABLE_BANDWIDTH_PERCENT); - } - - @Override - public TunerType getTunerType() - { - return TunerType.ELONICS_E4000; - } - - @Override - public void apply(TunerConfiguration config) throws SourceException - { - if(config != null && config instanceof E4KTunerConfiguration) - { - E4KTunerConfiguration e4kConfig = (E4KTunerConfiguration) config; - - try - { - SampleRate sampleRate = e4kConfig.getSampleRate(); - setSampleRate(sampleRate); - - double correction = e4kConfig.getFrequencyCorrection(); - getFrequencyController().setFrequencyCorrection(correction); - - E4KGain masterGain = e4kConfig.getMasterGain(); - setGain(masterGain, true); - - if(masterGain == E4KGain.MANUAL) - { - E4KMixerGain mixerGain = e4kConfig.getMixerGain(); - setMixerGain(mixerGain, true); - - E4KLNAGain lnaGain = e4kConfig.getLNAGain(); - setLNAGain(lnaGain, true); - - E4KEnhanceGain enhanceGain = e4kConfig.getEnhanceGain(); - setEnhanceGain(enhanceGain, true); - } - - try - { - getFrequencyController().setFrequency(e4kConfig.getFrequency()); - } - catch(SourceException se) - { - //Do nothing, we couldn't set the frequency - } - } - catch(UsbException e) - { - throw new SourceException("E4KTunerController - usb error " - + "while applying tuner config", e); - } - - } - else - { - throw new IllegalArgumentException("E4KTunerController - cannot " - + "apply tuner configuration of type:[" + - config.getClass() + "]"); - } - - } - - public void init() throws SourceException - { - mDeviceHandle = new DeviceHandle(); - - int result = LibUsb.open(mDevice, mDeviceHandle); - - if(result != LibUsb.SUCCESS) - { - mDeviceHandle = null; - - throw new SourceException("libusb couldn't open RTL2832 usb " - + "device [" + LibUsb.errorName(result) + "]"); - } - - claimInterface(mDeviceHandle); - - byte[] eeprom = null; - - try - { - /* Read the contents of the 256-byte EEPROM */ - eeprom = readEEPROM(mDeviceHandle, (short) 0, 256); - } - catch(Exception e) - { - mLog.error("error while reading the EEPROM device descriptor", e); - } - - try - { - mDescriptor = new Descriptor(eeprom); - - if(eeprom == null) - { - mLog.error("eeprom byte array was null - constructed " - + "empty descriptor object"); - } - } - catch(Exception e) - { - mLog.error("error while constructing device descriptor using " - + "descriptor byte array " + - (eeprom == null ? "[null]" : Arrays.toString(eeprom)), e); - } - - try - { - /* Dummy write to test USB interface */ - writeRegister(mDeviceHandle, Block.USB, - Address.USB_SYSCTL.getAddress(), (short) 0x09, 1); - - initBaseband(mDeviceHandle); - - enableI2CRepeater(mDeviceHandle, true); - - boolean i2CRepeaterControl = false; - - initTuner(i2CRepeaterControl); - - enableI2CRepeater(mDeviceHandle, false); - - try - { - setSampleRate(DEFAULT_SAMPLE_RATE); - } - catch(Exception e) - { - throw new SourceException("RTL2832 Tuner Controller - couldn't " - + "set default sample rate", e); - } - - } - catch(UsbException e) - { - e.printStackTrace(); - throw new SourceException("E4K Tuner Controller - error during " - + "init()", e); - } - - String deviceName = getTunerType().getLabel() + " " + getUniqueID(); - - mUSBTransferProcessor = new RTL2832USBTransferProcessor(deviceName, mDeviceHandle, mSampleAdapter, - USB_TRANSFER_BUFFER_SIZE); - } - - /** - * Sets the IF filters (mixer, channel and RC) to the correct filter setting - * for the selected bandwidth/sample rate - */ - @Override - public void setSampleRateFilters(int bandwidth) throws SourceException - { - /* Determine repeater state so we can restore it when done */ - boolean i2CRepeaterEnabled = isI2CRepeaterEnabled(); - - if(!i2CRepeaterEnabled) - { - enableI2CRepeater(mDeviceHandle, true); - } - - boolean controlI2CRepeater = false; - - MixerFilter mixer = MixerFilter.getFilter(bandwidth); - setMixerFilter(mixer, controlI2CRepeater); - - ChannelFilter channel = ChannelFilter.getFilter(bandwidth); - setChannelFilter(channel, controlI2CRepeater); - - RCFilter rc = RCFilter.getFilter(bandwidth); - setRCFilter(rc, controlI2CRepeater); - - if(!i2CRepeaterEnabled) - { - enableI2CRepeater(mDeviceHandle, false); - } - } - - @Override - public long getTunedFrequency() throws SourceException - { - try - { - /* Determine repeater state so we can restore it when done */ - boolean i2CRepeaterEnabled = isI2CRepeaterEnabled(); - - if(!i2CRepeaterEnabled) - { - enableI2CRepeater(mDeviceHandle, true); - } - - boolean controlI2CRepeater = false; - - byte z = (byte) readE4KRegister(Register.SYNTH3, controlI2CRepeater); - - int xHigh = readE4KRegister(Register.SYNTH4, controlI2CRepeater); - int xLow = readE4KRegister(Register.SYNTH5, controlI2CRepeater); - - int x = (Integer.rotateLeft(xHigh, 8)) | xLow; - - int pllSetting = readE4KRegister(Register.SYNTH7, controlI2CRepeater); - PLL pll = PLL.fromSetting(pllSetting); - - /* Return the repeater to its previous state */ - if(!i2CRepeaterEnabled) - { - enableI2CRepeater(mDeviceHandle, false); - } - - return calculateActualFrequency(pll, z, x); - } - catch(LibUsbException e) - { - throw new SourceException("E4K tuner controller - couldn't get " - + "tuned frequency", e); - } - } - - @Override - public void setTunedFrequency(long frequency) throws SourceException - { - /* Get the phase locked loop setting */ - PLL pll = PLL.fromFrequency(frequency); - - /* Z is an integer representing the number of scaled oscillator frequency - * increments the multiplied frequency. */ - byte z = (byte) (frequency / pll.getScaledOscillator()); - - /* remainder is just as it describes. It is what is left over after we - * carve out scaled oscillator frequency increments (z) from the desired - * frequency. */ - int remainder = (int) (frequency - ((z & 0xFF) * pll.getScaledOscillator())); - - /* X is a 16-bit representation of the remainder */ - int x = (int) ((double) remainder / (double) pll.getScaledOscillator() * E4K_PLL_Y); - - /* Calculate the exact (tunable) frequency and apply that to the tuner */ - long actualFrequency = calculateActualFrequency(pll, z, x); - - /** - * Hack: if we're trying to set the minimum frequency for the E4K, due - * to rounding errors, 52 mhz becomes 51.999993, so we need to adjust - * x to get 52.000003 mhz ... otherwise the PLL won't lock on the freq - */ - if(actualFrequency < getFrequencyController().getMinimumFrequency()) - { - x++; - actualFrequency = calculateActualFrequency(pll, z, x); - } - - /* Apply the actual frequency */ - try - { - enableI2CRepeater(mDeviceHandle, true); - - boolean controlI2CRepeater = false; - - /* Write the PLL setting */ - writeE4KRegister(Register.SYNTH7, pll.getIndex(), controlI2CRepeater); - - /* Write z (integral) value */ - writeE4KRegister(Register.SYNTH3, z, controlI2CRepeater); - - /* Write the x (fractional) value high-order byte to synth4 register */ - writeE4KRegister(Register.SYNTH4, (byte) (x & 0xFF), controlI2CRepeater); - - /* Write the x (fractional) value low-order byte to synth5 register */ - writeE4KRegister(Register.SYNTH5, - (byte) ((Integer.rotateRight(x, 8)) & 0xFF), - controlI2CRepeater); - - /* Set the band for the new frequency */ - setBand(actualFrequency, controlI2CRepeater); - - /* Set the filter */ - setRFFilter(actualFrequency, controlI2CRepeater); - - /* Check for PLL lock */ - int lock = readE4KRegister(Register.SYNTH1, controlI2CRepeater); - - if(!((lock & 0x1) == 0x1)) - { - throw new SourceException("E4K tuner controller - couldn't " - + "achieve PLL lock for frequency [" + - actualFrequency + "] lock value [" + lock + "]"); - } - - enableI2CRepeater(mDeviceHandle, false); - } - catch(UsbException e) - { - throw new SourceException("E4K tuner controller - error tuning " - + "frequency [" + frequency + "]", e); - } - } - - private long calculateActualFrequency(PLL pll, byte z, int x) - { - long whole = pll.getScaledOscillator() * (z & 0xFF); - - int fractional = (int) (pll.getScaledOscillator() * - ((double) x / (double) E4K_PLL_Y)); - - return whole + fractional; - } - - public void initTuner(boolean controlI2CRepeater) throws UsbException - { - if(controlI2CRepeater) - { - enableI2CRepeater(mDeviceHandle, true); - } - - boolean i2CRepeaterControl = false; - - /* Perform dummy read */ - readE4KRegister(Register.DUMMY, i2CRepeaterControl); - - /* Reset everything and clear POR indicator */ - /* NOTE: register value remains 0010 even after we write 0111 to it */ - writeE4KRegister(Register.MASTER1, (byte) (MASTER1_RESET | - MASTER1_NORM_STBY | - MASTER1_POR_DET), - i2CRepeaterControl); - - /* Configure clock input */ - writeE4KRegister(Register.CLK_INP, (byte) 0x00, i2CRepeaterControl); - - /* Disable clock output */ - writeE4KRegister(Register.REF_CLK, (byte) 0x00, i2CRepeaterControl); - writeE4KRegister(Register.CLKOUT_PWDN, (byte) 0x96, i2CRepeaterControl); - - /* Magic Init */ - magicInit(i2CRepeaterControl); - -// /* Set common mode voltage a bit higher for more margin - 850 mv */ -// writeMaskedE4KRegister( Register.DC7, (byte)0x7, -// (byte)0x4, i2CRepeaterControl ); -// -// /* Initialize DC offset lookup tables */ -// generateDCOffsetTables( i2CRepeaterControl ); -// -// /* Enable time variant DC correction */ -// writeE4KRegister( Register.DCTIME1, (byte)0x01, i2CRepeaterControl ); -// writeE4KRegister( Register.DCTIME2, (byte)0x01, i2CRepeaterControl ); -// - /* Set LNA Mode */ - writeE4KRegister(Register.AGC4, (byte) 0x10, i2CRepeaterControl); //High Threshold - writeE4KRegister(Register.AGC5, (byte) 0x04, i2CRepeaterControl); //Low Threshold - writeE4KRegister(Register.AGC6, (byte) 0x1A, i2CRepeaterControl); //LNA calibrate + loop rate - - //TODO: lna and mixer gain were already set to manual when we generated - //the DC offset tables. Check the LNA gain setting after we set the - //LNA mode above, to see if the lna or mixer gain settings have changed - //and if not, remove the next two steps - -// /* Set LNA gain to manual using current auto LNA gain setting */ -// LNAGain lnaGain = getLNAGain( i2CRepeaterControl ); -// setLNAGain( lnaGain, i2CRepeaterControl ); -// -// /* Set Mixer gain to manual using current auto Mixer gain setting */ -// MixerGain mixerGain = getMixerGain( i2CRepeaterControl ); -// setMixerGain( mixerGain, i2CRepeaterControl ); - - //Temp - manual set lna mode to manual - writeMaskedE4KRegister(Register.AGC1, - AGC1_MOD_MASK, - AGCMode.SERIAL.getValue(), - false); - //Temp - set mixer gain to manual - writeMaskedE4KRegister(Register.AGC7, - AGC7_MIXER_GAIN_MASK, - AGC7_MIX_GAIN_MANUAL, - false); - - -// /* Enable enhance gain */ -// setEnhanceGain( EnhanceGain.GAIN_5, i2CRepeaterControl ); -// -// /* Enable automatic IF gain mode switching */ -// writeMaskedE4KRegister( Register.AGC8, -// IF_GAIN_MODE_SWITCHING_MASK, -// AGC8_SENS_LIN_AUTO, -// i2CRepeaterControl ); - - - /* Use automatic gain as a default */ - setLNAGain(E4KLNAGain.AUTOMATIC, i2CRepeaterControl); - setMixerGain(E4KMixerGain.AUTOMATIC, i2CRepeaterControl); - setEnhanceGain(E4KEnhanceGain.AUTOMATIC, i2CRepeaterControl); - - /* Set IF gain stages */ - setIFStage1Gain(IFStage1Gain.GAIN_PLUS6, i2CRepeaterControl); - setIFStage2Gain(IFStage2Gain.GAIN_PLUS0, i2CRepeaterControl); - setIFStage3Gain(IFStage3Gain.GAIN_PLUS0, i2CRepeaterControl); - setIFStage4Gain(IFStage4Gain.GAIN_PLUS0, i2CRepeaterControl); - setIFStage5Gain(IFStage5Gain.GAIN_PLUS9, i2CRepeaterControl); - setIFStage6Gain(IFStage6Gain.GAIN_PLUS9, i2CRepeaterControl); - - /* Set the most narrow filter we can possible use */ - setMixerFilter(MixerFilter.BW_1M9, i2CRepeaterControl); - setRCFilter(RCFilter.BW_1M0, i2CRepeaterControl); - setChannelFilter(ChannelFilter.BW_2M15, i2CRepeaterControl); - - setChannelFilterEnabled(true, i2CRepeaterControl); - -// /* Disable DC detector */ -// setDCRangeDetectorEnabled( false, i2CRepeaterControl ); - - /* Disable time variant DC correction */ - writeMaskedE4KRegister(Register.DC5, (byte) 0x03, (byte) 0, i2CRepeaterControl); - writeMaskedE4KRegister(Register.DCTIME1, (byte) 0x03, (byte) 0, i2CRepeaterControl); - writeMaskedE4KRegister(Register.DCTIME2, (byte) 0x03, (byte) 0, i2CRepeaterControl); - - if(controlI2CRepeater) - { - enableI2CRepeater(mDeviceHandle, false); - } - } - - @SuppressWarnings("unused") - private void generateDCOffsetTables(boolean controlI2CRepeater) - throws UsbException - { - boolean i2CRepeaterControl = false; - - if(controlI2CRepeater) - { - enableI2CRepeater(mDeviceHandle, true); - } - - /* Capture current gain settings to reapply at the end */ - IFStage1Gain stage1 = getIFStage1Gain(i2CRepeaterControl); - IFStage2Gain stage2 = getIFStage2Gain(i2CRepeaterControl); - IFStage3Gain stage3 = getIFStage3Gain(i2CRepeaterControl); - IFStage4Gain stage4 = getIFStage4Gain(i2CRepeaterControl); - IFStage5Gain stage5 = getIFStage5Gain(i2CRepeaterControl); - IFStage6Gain stage6 = getIFStage6Gain(i2CRepeaterControl); - - /* Set Mixer gain to manual using current auto Mixer gain setting */ - E4KMixerGain mixerGain = getMixerGain(i2CRepeaterControl); - setMixerGain(mixerGain, i2CRepeaterControl); - - /* Set LNA gain to manual using current auto LNA gain setting */ - E4KLNAGain lnaGain = getLNAGain(i2CRepeaterControl); - setLNAGain(lnaGain, i2CRepeaterControl); - - /* Set IF Stage 2 - 6 gains to maximum */ - setIFStage2Gain(IFStage2Gain.GAIN_PLUS9, i2CRepeaterControl); - setIFStage3Gain(IFStage3Gain.GAIN_PLUS9, i2CRepeaterControl); - setIFStage4Gain(IFStage4Gain.GAIN_PLUS2B, i2CRepeaterControl); - setIFStage5Gain(IFStage5Gain.GAIN_PLUS15D, i2CRepeaterControl); - setIFStage6Gain(IFStage6Gain.GAIN_PLUS15D, i2CRepeaterControl); - - /** - * Iterate all DC gain combinations, apply gain settings, read the - * DC offset value and apply it to the lookup table entry - */ - for(DCGainCombination combo : DCGainCombination.values()) - { - /* Set mixer and stage1 gain values */ - setMixerGain(combo.getMixerGain(), i2CRepeaterControl); - setIFStage1Gain(combo.getIFStage1Gain(), i2CRepeaterControl); - - /* Turn on the DC range detector */ - setDCRangeDetectorEnabled(true, i2CRepeaterControl); - - /* Calibrate the DC value */ - writeE4KRegister(Register.DC1, (byte) 0x01, i2CRepeaterControl); - - /* Get the I/Q offset and range values */ - byte offsetI = (byte) (readE4KRegister(Register.DC2, - i2CRepeaterControl) & 0x3F); - byte offsetQ = (byte) (readE4KRegister(Register.DC3, - i2CRepeaterControl) & 0x3F); - byte range = (byte) readE4KRegister(Register.DC4, - i2CRepeaterControl); - byte rangeI = (byte) (range & 0x3); - byte rangeQ = (byte) ((range >> 4) & 0x3); - - /* Write the offset and range values to the lookup table */ - writeE4KRegister(combo.getIRegister(), - (byte) (offsetI | (rangeI << 6)), - i2CRepeaterControl); - - writeE4KRegister(combo.getQRegister(), - (byte) (offsetQ | (rangeQ << 6)), - i2CRepeaterControl); - } - - if(controlI2CRepeater) - { - enableI2CRepeater(mDeviceHandle, false); - } - } - - public void setDCRangeDetectorEnabled(boolean enabled, - boolean controlI2CRepeater) throws UsbException - { - if(enabled) - { - writeMaskedE4KRegister(Register.DC5, - DC5_RANGE_DETECTOR_ENABLED_MASK, - DC5_RANGE_DETECTOR_ENABLED, - controlI2CRepeater); - } - else - { - writeMaskedE4KRegister(Register.DC5, - DC5_RANGE_DETECTOR_DISABLED_MASK, - DC5_RANGE_DETECTOR_DISABLED, - controlI2CRepeater); - } - } - - /** - * Sets the LNA gain within the E4K Tuner - * - * Note: requires I2C repeater enabled - * - * @param gain - * @throws UsbException - */ - public void setLNAGain(E4KLNAGain gain, boolean controlI2CRepeater) - throws UsbException - { - if(controlI2CRepeater) - { - enableI2CRepeater(mDeviceHandle, true); - } - - if(gain == E4KLNAGain.AUTOMATIC) - { - writeMaskedE4KRegister(Register.AGC1, - AGC1_MOD_MASK, - AGCMode.IF_SERIAL_LNA_AUTON.getValue(), - false); - } - else - { - writeMaskedE4KRegister(Register.AGC1, - AGC1_MOD_MASK, - AGCMode.SERIAL.getValue(), - false); - - - writeMaskedE4KRegister(E4KLNAGain.getRegister(), - E4KLNAGain.getMask(), - gain.getValue(), - false); - } - - if(controlI2CRepeater) - { - enableI2CRepeater(mDeviceHandle, false); - } - } - - /** - * Reads LNA gain from E4K tuner - * - * Note: requires I2C repeater enabled - */ - public E4KLNAGain getLNAGain(boolean controlI2CRepeater) throws UsbException - { - return E4KLNAGain.fromRegisterValue( - readE4KRegister(E4KLNAGain.getRegister(), controlI2CRepeater)); - } - - /** - * Sets enhanced gain for E4K repeater. - * - * Note: requires I2C repeater enabled - * - * @param gain - * @throws UsbException - */ - public void setEnhanceGain(E4KEnhanceGain gain, boolean controlI2CRepeater) - throws UsbException - { - writeMaskedE4KRegister(E4KEnhanceGain.getRegister(), - E4KEnhanceGain.getMask(), - gain.getValue(), - controlI2CRepeater); - } - - /** - * Gets the enhanced gain setting in the E4K tuner. - * - * Note: requires I2C repeater - * - * @return - * @throws UsbException - */ - public E4KEnhanceGain getEnhanceGain(boolean controlI2CRepeater) - throws UsbException - { - return E4KEnhanceGain.fromRegisterValue( - readE4KRegister(E4KEnhanceGain.getRegister(), controlI2CRepeater)); - } - - public void setMixerGain(E4KMixerGain gain, boolean controlI2CRepeater) - throws UsbException - { - if(controlI2CRepeater) - { - enableI2CRepeater(mDeviceHandle, true); - } - - boolean localI2CRepeaterControl = false; - - if(gain == E4KMixerGain.AUTOMATIC) - { - writeMaskedE4KRegister(Register.AGC7, - AGC7_MIXER_GAIN_MASK, - AGC7_MIX_GAIN_AUTO, - localI2CRepeaterControl); - } - else - { - writeMaskedE4KRegister(Register.AGC7, - AGC7_MIXER_GAIN_MASK, - AGC7_MIX_GAIN_MANUAL, - localI2CRepeaterControl); - - /* Set the desired manual gain setting */ - writeMaskedE4KRegister(E4KMixerGain.getRegister(), - E4KMixerGain.getMask(), - gain.getValue(), - localI2CRepeaterControl); - } - - if(controlI2CRepeater) - { - enableI2CRepeater(mDeviceHandle, false); - } - } - - public E4KMixerGain getMixerGain(boolean controlI2CRepeater) - throws UsbException - { - byte autoOrManual = readMaskedE4KRegister(Register.AGC7, - AGC7_MIXER_GAIN_MASK, - controlI2CRepeater); - - if(autoOrManual == AGC7_MIX_GAIN_AUTO) - { - return E4KMixerGain.AUTOMATIC; - } - else - { - int register = readE4KRegister(E4KMixerGain.getRegister(), - controlI2CRepeater); - - return E4KMixerGain.fromRegisterValue(register); - } - } - - public void setIFStage1Gain(IFStage1Gain gain, - boolean controlI2CRepeater) throws UsbException - { - writeMaskedE4KRegister(IFStage1Gain.getRegister(), - IFStage1Gain.getMask(), - gain.getValue(), - controlI2CRepeater); - } - - public IFStage1Gain getIFStage1Gain(boolean controlI2CRepeater) - throws UsbException - { - return IFStage1Gain.fromRegisterValue( - readE4KRegister(IFStage1Gain.getRegister(), controlI2CRepeater)); - } - - public void setIFStage2Gain(IFStage2Gain gain, - boolean controlI2CRepeater) throws UsbException - { - writeMaskedE4KRegister(IFStage2Gain.getRegister(), - IFStage2Gain.getMask(), - gain.getValue(), - controlI2CRepeater); - } - - public IFStage2Gain getIFStage2Gain(boolean controlI2CRepeater) - throws UsbException - { - return IFStage2Gain.fromRegisterValue( - readE4KRegister(IFStage2Gain.getRegister(), controlI2CRepeater)); - } - - public void setIFStage3Gain(IFStage3Gain gain, - boolean controlI2CRepeater) throws UsbException - { - writeMaskedE4KRegister(IFStage3Gain.getRegister(), - IFStage3Gain.getMask(), - gain.getValue(), - controlI2CRepeater); - } - - public IFStage3Gain getIFStage3Gain(boolean controlI2CRepeater) - throws UsbException - { - return IFStage3Gain.fromRegisterValue( - readE4KRegister(IFStage3Gain.getRegister(), controlI2CRepeater)); - } - - public void setIFStage4Gain(IFStage4Gain gain, - boolean controlI2CRepeater) throws UsbException - { - writeMaskedE4KRegister(IFStage4Gain.getRegister(), - IFStage4Gain.getMask(), - gain.getValue(), - controlI2CRepeater); - } - - public IFStage4Gain getIFStage4Gain(boolean controlI2CRepeater) - throws UsbException - { - return IFStage4Gain.fromRegisterValue( - readE4KRegister(IFStage4Gain.getRegister(), controlI2CRepeater)); - } - - public void setIFStage5Gain(IFStage5Gain gain, - boolean controlI2CRepeater) throws UsbException - { - writeMaskedE4KRegister(IFStage5Gain.getRegister(), - IFStage5Gain.getMask(), - gain.getValue(), - controlI2CRepeater); - } - - public IFStage5Gain getIFStage5Gain(boolean controlI2CRepeater) - throws UsbException - { - return IFStage5Gain.fromRegisterValue( - readE4KRegister(IFStage5Gain.getRegister(), controlI2CRepeater)); - } - - public void setIFStage6Gain(IFStage6Gain gain, - boolean controlI2CRepeater) throws UsbException - { - writeMaskedE4KRegister(IFStage6Gain.getRegister(), - IFStage6Gain.getMask(), - gain.getValue(), - controlI2CRepeater); - } - - public IFStage6Gain getIFStage6Gain(boolean controlI2CRepeater) - throws UsbException - { - return IFStage6Gain.fromRegisterValue( - readE4KRegister(IFStage6Gain.getRegister(), controlI2CRepeater)); - } - - public void setMixerFilter(MixerFilter filter, - boolean controlI2CRepeater) throws LibUsbException - { - writeMaskedE4KRegister(MixerFilter.getRegister(), - MixerFilter.getMask(), - filter.getValue(), - controlI2CRepeater); - } - - public MixerFilter getMixerFilter(boolean controlI2CRepeater) - throws UsbException - { - int value = readE4KRegister(MixerFilter.getRegister(), - controlI2CRepeater); - - return MixerFilter.fromRegisterValue(value); - } - - public void setRCFilter(RCFilter filter, - boolean controlI2CRepeater) throws LibUsbException - { - writeMaskedE4KRegister(RCFilter.getRegister(), - RCFilter.getMask(), - filter.getValue(), - controlI2CRepeater); - } - - public RCFilter getRCFilter(boolean controlI2CRepeater) throws UsbException - { - int value = readE4KRegister(RCFilter.getRegister(), controlI2CRepeater); - - return RCFilter.fromRegisterValue(value); - } - - public void setChannelFilter(ChannelFilter filter, - boolean controlI2CRepeater) throws LibUsbException - { - writeMaskedE4KRegister(ChannelFilter.getRegister(), - ChannelFilter.getMask(), - filter.getValue(), - controlI2CRepeater); - } - - public ChannelFilter getChannelFilter(boolean controlI2CRepeater) - throws UsbException - { - int value = readE4KRegister(ChannelFilter.getRegister(), controlI2CRepeater); - - return ChannelFilter.fromRegisterValue(value); - - } - - public void setChannelFilterEnabled(boolean enabled, - boolean controlI2CRepeater) throws UsbException - { - if(enabled) - { - writeMaskedE4KRegister(Register.FILT3, - FILT3_MASK, - FILT3_ENABLE, - controlI2CRepeater); - } - else - { - writeMaskedE4KRegister(Register.FILT3, - FILT3_MASK, - FILT3_DISABLE, - controlI2CRepeater); - } - } - - public void setBand(long frequency, - boolean controlI2CRepeater) throws UsbException - { - if(controlI2CRepeater) - { - enableI2CRepeater(mDeviceHandle, true); - } - - Band band = Band.fromFrequency(frequency); - - /* Set the bias */ - switch(band) - { - case UHF: - case VHF2: - case VHF3: - writeE4KRegister(Register.BIAS, (byte) 0x3, false); - break; - case L: - default: - writeE4KRegister(Register.BIAS, (byte) 0x0, false); - break; - - } - - /** - * Workaround - reset register (set value to 0) before writing to it, to - * avoid a gap around 325-350 MHz - */ - writeMaskedE4KRegister(Register.SYNTH1, Band.getMask(), (byte) 0x0, false); - writeMaskedE4KRegister(Register.SYNTH1, Band.getMask(), band.getValue(), false); - - if(controlI2CRepeater) - { - enableI2CRepeater(mDeviceHandle, false); - } - } - - private void setRFFilter(long frequency, - boolean controlI2CRepeater) throws UsbException - { - RFFilter filter = RFFilter.fromFrequency(frequency); - - writeMaskedE4KRegister(Register.FILT1, - RFFilter.getMask(), - filter.getValue(), - controlI2CRepeater); - } - - /** - * Sets the gain combination setting. - * - * Note: does not respond to the MANUAL setting. Use the manual setting - * to configure gui components to expose the individual Mixer, LNA and - * Enhance gain settings, to allow the user to change those settings - * individually. - * - * @param gain - requested gain setting - * @throws UsbException if there are any errors - */ - public void setGain(E4KGain gain, boolean controlI2CRepeater) throws UsbException - { - if(gain != E4KGain.MANUAL) - { - if(controlI2CRepeater) - { - enableI2CRepeater(mDeviceHandle, true); - } - - boolean i2CRepeaterControl = false; - - setLNAGain(gain.getLNAGain(), i2CRepeaterControl); - setMixerGain(gain.getMixerGain(), i2CRepeaterControl); - setEnhanceGain(gain.getEnhanceGain(), i2CRepeaterControl); - - if(controlI2CRepeater) - { - enableI2CRepeater(mDeviceHandle, false); - } - } - } - - /* - * Performs magic initialization ... and that's all we need to know, I guess - */ - private void magicInit(boolean controlI2CRepeater) throws UsbException - { - if(controlI2CRepeater) - { - enableI2CRepeater(mDeviceHandle, true); - } - - writeE4KRegister(Register.MAGIC_1, (byte) 0x01, false); - writeE4KRegister(Register.MAGIC_2, (byte) 0xFE, false); - writeE4KRegister(Register.MAGIC_3, (byte) 0x00, false); - writeE4KRegister(Register.MAGIC_4, (byte) 0x50, false); //Polarity A - writeE4KRegister(Register.MAGIC_5, (byte) 0x20, false); - writeE4KRegister(Register.MAGIC_6, (byte) 0x01, false); - writeE4KRegister(Register.MAGIC_7, (byte) 0x7F, false); - writeE4KRegister(Register.MAGIC_8, (byte) 0x07, false); - - if(controlI2CRepeater) - { - enableI2CRepeater(mDeviceHandle, false); - } - } - - private byte readMaskedE4KRegister(Register register, - byte mask, - boolean controlI2CRepeater) - throws UsbException - { - int temp = readE4KRegister(register, controlI2CRepeater); - return (byte) (temp & mask); - } - - private void writeMaskedE4KRegister(Register register, - byte mask, - byte value, - boolean controlI2CRepeater) throws LibUsbException - { - int temp = readE4KRegister(register, controlI2CRepeater); - - /* If the register is not set to the masked value, then change it */ - if((byte) (temp & mask) != value) - { - writeE4KRegister(register, - (byte) ((temp & ~mask) | (value & mask)), - controlI2CRepeater); - - readE4KRegister(register, controlI2CRepeater); - } - } - - private int readE4KRegister(Register register, - boolean controlI2CRepeater) throws LibUsbException - { - return readI2CRegister(mDeviceHandle, Register.I2C_REGISTER.getValue(), - register.getValue(), controlI2CRepeater); - } - - - private void writeE4KRegister(Register register, - byte value, - boolean controlI2CRepeater) throws LibUsbException - { - writeI2CRegister(mDeviceHandle, Register.I2C_REGISTER.getValue(), - register.getValue(), value, controlI2CRepeater); - } - - public enum Band - { - VHF2(0), - VHF3(2), - UHF(4), - L(6); - - private int mValue; - - private Band(int value) - { - mValue = value; - } - - public static byte getMask() - { - return (byte) 0x06; - } - - public byte getValue() - { - return (byte) mValue; - } - - public String getLabel() - { - return toString(); - } - - public static Band fromFrequency(long frequency) - { - if(frequency < 140000000) //140 MHz - { - return VHF2; - } - else if(frequency < 350000000) //350 MHz - { - return VHF3; - } - else if(frequency < 1135000000) //1135 MHz - { - return UHF; - } - else - { - return L; - } - } - } - - public enum PLL - { - PLL_72M4(0x0F, 72400000, 48, 600000, true, "72.4 MHz"), - PLL_81M2(0x0E, 81200000, 40, 720000, true, "81.2 MHz"), - PLL_108M3(0x0D, 108300000, 32, 900000, true, "108.3 MHz"), - PLL_162M5(0x0C, 162500000, 24, 1200000, true, "162.5 MHz"), - PLL_216M6(0x0B, 216600000, 16, 1800000, true, "216.6 MHz"), - PLL_325M0(0x0A, 325000000, 12, 2400000, true, "325.0 MHz"), - PLL_350M0(0x09, 350000000, 8, 3600000, true, "350.0 MHz"), - PLL_432M0(0x03, 432000000, 8, 3600000, false, "432.0 MHz"), - PLL_667M0(0x02, 667000000, 6, 4800000, false, "667.0 MHz"), - PLL_1200M0(0x01, 1200000000, 4, 7200000, false, "1200.0 MHz"); - - private int mValue; - private long mFrequency; - private int mMultiplier; - private int mScaledOscillator; - private boolean mRequires3PhaseMixing; - private String mLabel; - - private PLL(int value, - long frequency, - int multiplier, - int scaledOscillator, - boolean requires3Phase, - String label) - { - mValue = value; - mFrequency = frequency; - mMultiplier = multiplier; - mScaledOscillator = scaledOscillator; - mRequires3PhaseMixing = requires3Phase; - mLabel = label; - - } - - public byte getIndex() - { - return (byte) mValue; - } - - public long getFrequency() - { - return mFrequency; - } - - public byte getMultiplier() - { - return (byte) mMultiplier; - } - - public int getScaledOscillator() - { - return mScaledOscillator; - } - - public boolean requires3PhaseMixing() - { - return mRequires3PhaseMixing; - } - - public String getLabel() - { - return mLabel; - } - - public String toString() - { - return mLabel; - } - - /* Returns the PLL setting with the closest frequency that is greater - * than the frequency argument */ - public static PLL fromFrequency(long frequency) - { - for(PLL pll : values()) - { - if(frequency < pll.getFrequency()) - { - return pll; - } - } - - //Default - return PLL.PLL_72M4; - } - - public static PLL fromSetting(int setting) - { - int value = setting & 0xF; - - for(PLL pll : values()) - { - if(value == pll.getIndex()) - { - return pll; - } - } - - //Default - return PLL.PLL_72M4; - } - } - - /** - * RF filters - UHF Band. Each enum entry defines the center frequency for - * the filter and the applicable frequency minimums and maximums for the - * filter. - * - * Note: max frequency overlaps with the next minimum frequency, so valid - * frequencies should be less than the maximum frequency defined for the - * filter. - */ - public enum RFFilter - { - NO_FILTER(0, 0, 0), - - //UHF band Filters - M360(0, 350000000, 370000000), - M380(1, 370000000, 392500000), - M405(2, 392500000, 417500000), - M425(3, 417500000, 437500000), - M450(4, 437500000, 462500000), - M475(5, 462500000, 490000000), - M505(6, 490000000, 522500000), - M540(7, 522500000, 557500000), - M575(8, 557500000, 595000000), - M615(9, 595000000, 642500000), - M670(10, 642500000, 695000000), - M720(11, 695000000, 740000000), - M760(12, 740000000, 800000000), - M840(13, 800000000, 865000000), - M890(14, 865000000, 930000000), - M970(15, 930000000, 1135000000), - - //L band filters - M1300(0, 1135000000, 1310000000), - M1320(1, 1310000000, 1340000000), - M1360(2, 1340000000, 1385000000), - M1410(3, 1385000000, 1427500000), - M1445(4, 1427500000, 1452500000), - M1460(5, 1452500000, 1475000000), - M1490(6, 1475000000, 1510000000), - M1530(7, 1510000000, 1545000000), - M1560(8, 1545000000, 1575000000), - M1590(9, 1575000000, 1615000000), - M1640(10, 1615000000, 1650000000), - M1660(11, 1650000000, 1670000000), - M1680(12, 1670000000, 1690000000), - M1700(13, 1690000000, 1710000000), - M1720(14, 1710000000, 1735000000), - - /* Note: max frequency is currently limited by the max integer value - * and we set the max frequency for this filter to 2147 MHz */ - M1750(15, 1735000000, 2147000000); - - private int mValue; - private long mMinFrequency; - private long mMaxFrequency; - - private RFFilter(int value, long minFrequency, long maxFrequency) - { - mValue = value; - mMinFrequency = minFrequency; - mMaxFrequency = maxFrequency; - } - - public static Register getRegister() - { - return Register.FILT1; - } - - public static byte getMask() - { - return (byte) 0xF; //TODO: make this a constant - } - - public byte getValue() - { - return (byte) mValue; - } - - /** - * Minimum frequency for this filter. - */ - public long getMinimumFrequency() - { - return mMinFrequency; - } - - /** - * Maximum frequency for this filter is less than this value, but does - * not include this value. - */ - public long getMaximumFrequency() - { - return mMaxFrequency; - } - - /** - * Selects the appropriate filter for the frequency - * - * @param frequency - * @return selected filter - * @throws IllegalArgumentException if the frequency is outside the - * max value (2200 MHz) - */ - public static RFFilter fromFrequency(long frequency) - { - if(frequency < 350000000) - { - return NO_FILTER; - } - - for(RFFilter filter : values()) - { - if(filter.getMinimumFrequency() <= frequency && - frequency < filter.getMaximumFrequency()) - { - return filter; - } - } - - throw new IllegalArgumentException("E4KTunerController - " - + "cannot find RF filter for frequency [" + frequency + "]"); - } - } - - public enum MixerFilter - { - BW_27M0(0x00, 27000000, 4800000, 28800000, "27.0 MHz"), - BW_4M6(0x80, 4600000, 4400000, 4800000, "4.6 MHz"), - BW_4M2(0x90, 4200000, 4000000, 4400000, "4.2 MHz"), - BW_3M8(0xA0, 3800000, 3600000, 4000000, "3.8 MHz"), - BW_3M4(0xB0, 3400000, 3200000, 3600000, "3.4 MHz"), - BW_3M0(0xC0, 3000000, 2850000, 3200000, "3.0 MHz"), - BW_2M7(0xD0, 2700000, 2500000, 2850000, "2.7 MHz"), - BW_2M3(0xE0, 2300000, 2100000, 2500000, "2.3 MHz"), - BW_1M9(0xF0, 1900000, 0, 2100000, "1.9 MHz"); - - private int mValue; - private int mBandwidth; - private int mMinBandwidth; - private int mMaxBandwidth; - private String mLabel; - - private MixerFilter(int value, - int bandwidth, - int minimumBandwidth, - int maximumBandwidth, - String label) - { - mValue = value; - mBandwidth = bandwidth; - mMinBandwidth = minimumBandwidth; - mMaxBandwidth = maximumBandwidth; - mLabel = label; - } - - public static Register getRegister() - { - return Register.FILT2; - } - - public static byte getMask() - { - return MIXER_FILTER_MASK; - } - - public byte getValue() - { - return (byte) mValue; - } - - public int getBandwidth() - { - return mBandwidth; - } - - public int getMinimumBandwidth() - { - return mMinBandwidth; - } - - public int getMaximumBandwidth() - { - return mMaxBandwidth; - } - - public String getLabel() - { - return mLabel; - } - - /** - * Returns the correct filter to use for the bandwidth/sample rate - * - * @param bandwidth - current sample rate - * @return - correct filter to use for the bandwidth/sample rate - */ - public static MixerFilter getFilter(int bandwidth) - { - for(MixerFilter filter : values()) - { - if(filter.getMinimumBandwidth() <= bandwidth && - bandwidth < filter.getMaximumBandwidth()) - { - return filter; - } - } - - /* default */ - return MixerFilter.BW_27M0; - } - - public static MixerFilter fromRegisterValue(int registerValue) - { - int value = registerValue & getMask(); - - for(MixerFilter filter : values()) - { - if(value == filter.getValue()) - { - return filter; - } - } - - throw new IllegalArgumentException("E4KTunerController - " - + "unrecognized mixer filter value [" + value + "]"); - } - } - - public enum RCFilter - { - BW_21M4(0x00, 21400000, 21200000, 28800000, "21.4 MHz"), - BW_21M0(0x01, 21000000, 19300000, 21200000, "21.0 MHz"), - BW_17M6(0x02, 17600000, 16150000, 19300000, "17.6 MHz"), - BW_14M7(0x03, 14700000, 13550000, 16150000, "14.7 MHz"), - BW_12M4(0x04, 12400000, 11500000, 13550000, "12.4 MHz"), - BW_10M6(0x05, 10600000, 9800000, 11500000, "10.6 MHz"), - BW_9M0(0x06, 9000000, 8350000, 9800000, "9.0 MHz"), - BW_7M7(0x07, 7700000, 7050000, 8350000, "7.7 MHz"), - BW_6M4(0x08, 6400000, 5805000, 7050000, "6.4 MHz"), - BW_5M3(0x09, 5300000, 4850000, 5805000, "5.3 MHz"), - BW_4M4(0x0A, 4400000, 3900000, 4850000, "4.4 MHz"), - BW_3M4(0x0B, 3400000, 3000000, 3900000, "3.4 MHz"), - BW_2M6(0x0C, 2600000, 2200000, 3000000, "2.6 MHz"), - BW_1M8(0x0D, 1800000, 1500000, 2200000, "1.8 MHz"), - BW_1M2(0x0E, 1200000, 1100000, 1500000, "1.2 MHz"), - BW_1M0(0x0F, 1000000, 0, 1100000, "1.0 MHz"); - - private int mValue; - private int mBandwidth; - private int mMinBandwidth; - private int mMaxBandwidth; - private String mLabel; - - private RCFilter(int value, - int bandwidth, - int minimumBandwidth, - int maximumBandwidth, - String label) - { - mValue = value; - mBandwidth = bandwidth; - mMinBandwidth = minimumBandwidth; - mMaxBandwidth = maximumBandwidth; - mLabel = label; - } - - public static Register getRegister() - { - return Register.FILT2; - } - - public static byte getMask() - { - return IF_RC_FILTER_MASK; - } - - public byte getValue() - { - return (byte) mValue; - } - - public int getBandwidth() - { - return mBandwidth; - } - - public int getMinimumBandwidth() - { - return mMinBandwidth; - } - - public int getMaximumBandwidth() - { - return mMaxBandwidth; - } - - public String getLabel() - { - return mLabel; - } - - /** - * Returns the correct filter to use for the bandwidth/sample rate - * - * @param bandwidth - current sample rate - * @return - correct filter to use for the bandwidth/sample rate - */ - public static RCFilter getFilter(int bandwidth) - { - for(RCFilter filter : values()) - { - if(filter.getMinimumBandwidth() <= bandwidth && - bandwidth < filter.getMaximumBandwidth()) - { - return filter; - } - } - - /* default */ - return RCFilter.BW_21M4; - } - - public static RCFilter fromRegisterValue(int registerValue) - { - int value = registerValue & getMask(); - - for(RCFilter filter : values()) - { - if(value == filter.getValue()) - { - return filter; - } - } - - throw new IllegalArgumentException("E4KTunerController - " - + "unrecognized rc filter value [" + value + "]"); - } - } - - public enum ChannelFilter - { - BW_5M50(0x00, 5500000, 5400000, 28800000, "5.50 MHz"), - BW_5M30(0x01, 5300000, 5150000, 5400000, "5.30 MHz"), - BW_5M00(0x02, 5000000, 4900000, 5150000, "5.00 MHz"), - BW_4M80(0x03, 4800000, 4700000, 4900000, "4.80 MHz"), - BW_4M60(0x04, 4600000, 4500000, 4700000, "4.60 MHz"), - BW_4M40(0x05, 4400000, 4350000, 4500000, "4.40 MHz"), - BW_4M30(0x06, 4300000, 4200000, 4350000, "4.30 MHz"), - BW_4M10(0x07, 4100000, 4000000, 4200000, "4.10 MHz"), - BW_3M90(0x08, 3900000, 3850000, 4000000, "3.90 MHz"), - BW_3M80(0x09, 3800000, 3750000, 3850000, "3.80 MHz"), - BW_3M70(0x0A, 3700000, 3650000, 3750000, "3.70 MHz"), - BW_3M60(0x0B, 3600000, 3500000, 3650000, "3.60 MHz"), - BW_3M40(0x0C, 3400000, 3350000, 3500000, "3.40 MHz"), - BW_3M30(0x0D, 3300000, 3250000, 3350000, "3.30 MHz"), - BW_3M20(0x0E, 3200000, 3150000, 3250000, "3.20 MHz"), - BW_3M10(0x0F, 3100000, 3050000, 3150000, "3.10 MHz"), - BW_3M00(0x10, 3000000, 2975000, 3050000, "3.00 MHz"), - BW_2M95(0x11, 2950000, 2925000, 2975000, "2.95 MHz"), - BW_2M90(0x12, 2900000, 2850000, 2925000, "2.90 MHz"), - BW_2M80(0x13, 2800000, 2775000, 2850000, "2.80 MHz"), - BW_2M75(0x14, 2750000, 2750000, 2775000, "2.75 MHz"), - BW_2M70(0x15, 2700000, 2650000, 2750000, "2.70 MHz"), - BW_2M60(0x16, 2600000, 2575000, 2650000, "2.60 MHz"), - BW_2M55(0x17, 2550000, 2525000, 2575000, "2.55 MHz"), - BW_2M50(0x18, 2500000, 2475000, 2525000, "2.50 MHz"), - BW_2M45(0x19, 2450000, 2425000, 2475000, "2.45 MHz"), - BW_2M40(0x1A, 2400000, 2350000, 2425000, "2.40 MHz"), - BW_2M30(0x1B, 2300000, 2290000, 2350000, "2.30 MHz"), - BW_2M28(0x1C, 2280000, 2260000, 2290000, "2.28 MHz"), - BW_2M24(0x1D, 2240000, 2220000, 2260000, "2.24 MHz"), - BW_2M20(0x1E, 2200000, 2175000, 2220000, "2.20 MHz"), - BW_2M15(0x1F, 2150000, 0, 2175000, "2.15 MHz"); - - private int mValue; - private int mBandwidth; - private int mMinBandwidth; - private int mMaxBandwidth; - private String mLabel; - - private ChannelFilter(int value, - int bandwidth, - int minimumBandwidth, - int maximumBandwidth, - String label) - { - mValue = value; - mBandwidth = bandwidth; - mMinBandwidth = minimumBandwidth; - mMaxBandwidth = maximumBandwidth; - mLabel = label; - } - - public static Register getRegister() - { - return Register.FILT3; - } - - public static byte getMask() - { - return IF_CHANNEL_FILTER_MASK; - } - - public byte getValue() - { - return (byte) mValue; - } - - public int getBandwidth() - { - return mBandwidth; - } - - public int getMinimumBandwidth() - { - return mMinBandwidth; - } - - public int getMaximumBandwidth() - { - return mMaxBandwidth; - } - - public String getLabel() - { - return mLabel; - } - - /** - * Returns the correct filter to use for the bandwidth/sample rate - * - * @param bandwidth - current sample rate - * @return - correct filter to use for the bandwidth/sample rate - */ - public static ChannelFilter getFilter(int bandwidth) - { - for(ChannelFilter filter : values()) - { - if(filter.getMinimumBandwidth() <= bandwidth && - bandwidth < filter.getMaximumBandwidth()) - { - return filter; - } - } - - /* default */ - return ChannelFilter.BW_5M50; - } - - public static ChannelFilter fromRegisterValue(int registerValue) - { - int value = registerValue & getMask(); - - for(ChannelFilter filter : values()) - { - if(value == filter.getValue()) - { - return filter; - } - } - - throw new IllegalArgumentException("E4KTunerController - " - + "unrecognized channel filter value [" + value + "]"); - } - } - - public enum E4KLNAGain - { - AUTOMATIC(-1, "Auto"), - GAIN_MINUS_50(0, "-5.0 db"), - GAIN_MINUS_25(1, "-2.5 db"), - GAIN_PLUS_0(4, "0.0 db"), - GAIN_PLUS_25(5, "2.5 db"), - GAIN_PLUS_50(6, "5.0 db"), - GAIN_PLUS_75(7, "7.5 db"), - GAIN_PLUS_100(8, "10.0 db"), - GAIN_PLUS_125(9, "12.5 db"), - GAIN_PLUS_150(10, "15.0 db"), - GAIN_PLUS_175(11, "17.5 db"), - GAIN_PLUS_200(12, "20.0 db"), - GAIN_PLUS_250(13, "25.0 db"), - GAIN_PLUS_300(14, "30.0 db"); - - private int mValue; - private String mLabel; - - private E4KLNAGain(int value, String label) - { - mValue = value; - mLabel = label; - } - - public static Register getRegister() - { - return Register.GAIN1; - } - - public static byte getMask() - { - return GAIN1_MOD_MASK; - } - - public byte getValue() - { - return (byte) mValue; - } - - public String getLabel() - { - return mLabel; - } - - public String toString() - { - return mLabel; - } - - public static E4KLNAGain fromRegisterValue(int registerValue) - { - int value = registerValue & getMask(); - - for(E4KLNAGain setting : values()) - { - if(value == setting.getValue()) - { - return setting; - } - } - - throw new IllegalArgumentException("E4KTunerController" - + " - unrecognized LNA Gain value [" + - value + "]"); - } - } - - /** - * Defines the default gain settings that are exposed to the user. - * - * Manual is an override that keeps each of the settings intact and allows - * the user to change each of them individually - */ - public enum E4KGain - { - AUTOMATIC("Auto", E4KMixerGain.AUTOMATIC, E4KLNAGain.AUTOMATIC, E4KEnhanceGain.AUTOMATIC), - MANUAL("Manual", null, null, null), - MINUS_10("-1.0 db", E4KMixerGain.GAIN_4, E4KLNAGain.GAIN_MINUS_50, E4KEnhanceGain.AUTOMATIC), - PLUS_15("1.5 db", E4KMixerGain.GAIN_4, E4KLNAGain.GAIN_MINUS_25, E4KEnhanceGain.AUTOMATIC), - PLUS_40("4.0 db", E4KMixerGain.GAIN_4, E4KLNAGain.GAIN_PLUS_0, E4KEnhanceGain.AUTOMATIC), - PLUS_65("6.5 db", E4KMixerGain.GAIN_4, E4KLNAGain.GAIN_PLUS_25, E4KEnhanceGain.AUTOMATIC), - PLUS_90("9.0 db", E4KMixerGain.GAIN_4, E4KLNAGain.GAIN_PLUS_50, E4KEnhanceGain.AUTOMATIC), - PLUS_115("11.5 db", E4KMixerGain.GAIN_4, E4KLNAGain.GAIN_PLUS_75, E4KEnhanceGain.AUTOMATIC), - PLUS_140("14.0 db", E4KMixerGain.GAIN_4, E4KLNAGain.GAIN_PLUS_100, E4KEnhanceGain.AUTOMATIC), - PLUS_165("16.5 db", E4KMixerGain.GAIN_4, E4KLNAGain.GAIN_PLUS_125, E4KEnhanceGain.AUTOMATIC), - PLUS_190("19.0 db", E4KMixerGain.GAIN_4, E4KLNAGain.GAIN_PLUS_150, E4KEnhanceGain.AUTOMATIC), - PLUS_215("21.5 db", E4KMixerGain.GAIN_4, E4KLNAGain.GAIN_PLUS_175, E4KEnhanceGain.AUTOMATIC), - PLUS_240("24.0 db", E4KMixerGain.GAIN_4, E4KLNAGain.GAIN_PLUS_200, E4KEnhanceGain.AUTOMATIC), - PLUS_290("29.0 db", E4KMixerGain.GAIN_4, E4KLNAGain.GAIN_PLUS_250, E4KEnhanceGain.AUTOMATIC), - PLUS_340("34.0 db", E4KMixerGain.GAIN_4, E4KLNAGain.GAIN_PLUS_300, E4KEnhanceGain.AUTOMATIC), - PLUS_420("42.0 db", E4KMixerGain.GAIN_12, E4KLNAGain.GAIN_PLUS_300, E4KEnhanceGain.AUTOMATIC); - - private String mLabel; - private E4KMixerGain mMixerGain; - private E4KLNAGain mLNAGain; - private E4KEnhanceGain mEnhanceGain; - - private E4KGain(String label, - E4KMixerGain mixerGain, - E4KLNAGain lnaGain, - E4KEnhanceGain enhanceGain) - { - mLabel = label; - mMixerGain = mixerGain; - mLNAGain = lnaGain; - mEnhanceGain = enhanceGain; - } - - public String getLabel() - { - return mLabel; - } - - public String toString() - { - return mLabel; - } - - public E4KMixerGain getMixerGain() - { - return mMixerGain; - } - - public E4KLNAGain getLNAGain() - { - return mLNAGain; - } - - public E4KEnhanceGain getEnhanceGain() - { - return mEnhanceGain; - } - } - - public enum E4KEnhanceGain - { - AUTOMATIC(0, "Auto"), - GAIN_1(1, "10 db"), - GAIN_3(3, "30 db"), - GAIN_5(5, "50 db"), - GAIN_7(7, "70 db"); - - private int mValue; - private String mLabel; - - private E4KEnhanceGain(int value, String label) - { - mValue = value; - mLabel = label; - } - - public static Register getRegister() - { - return Register.AGC11; - } - - public static byte getMask() - { - return ENH_GAIN_MOD_MASK; - } - - public byte getValue() - { - return (byte) mValue; - } - - public String getLabel() - { - return mLabel; - } - - public String toString() - { - return mLabel; - } - - public static E4KEnhanceGain fromRegisterValue(int registerValue) - { - int value = registerValue & getMask(); - - for(E4KEnhanceGain setting : values()) - { - if(value == setting.getValue()) - { - return setting; - } - } - - throw new IllegalArgumentException("E4KTunerController" - + " - unrecognized Enhance Gain value [" + - value + "]"); - } - } - - public enum DCGainCombination - { - LOOKUP_TABLE_0(Register.QLUT0, - Register.ILUT0, - E4KMixerGain.GAIN_4, - IFStage1Gain.GAIN_MINUS3), - LOOKUP_TABLE_1(Register.QLUT1, - Register.ILUT1, - E4KMixerGain.GAIN_4, - IFStage1Gain.GAIN_PLUS6), - LOOKUP_TABLE_2(Register.QLUT2, - Register.ILUT2, - E4KMixerGain.GAIN_12, - IFStage1Gain.GAIN_MINUS3), - LOOKUP_TABLE_3(Register.QLUT3, - Register.ILUT3, - E4KMixerGain.GAIN_12, - IFStage1Gain.GAIN_PLUS6); - - private Register mQRegister; - private Register mIRegister; - private E4KMixerGain mMixerGain; - private IFStage1Gain mIFStage1Gain; - - private DCGainCombination(Register qRegister, - Register iRegister, - E4KMixerGain mixer, - IFStage1Gain stage1) - { - mQRegister = qRegister; - mIRegister = iRegister; - mMixerGain = mixer; - mIFStage1Gain = stage1; - } - - public Register getQRegister() - { - return mQRegister; - } - - public Register getIRegister() - { - return mIRegister; - } - - public E4KMixerGain getMixerGain() - { - return mMixerGain; - } - - public IFStage1Gain getIFStage1Gain() - { - return mIFStage1Gain; - } - } - - - public enum E4KMixerGain - { - AUTOMATIC(-1, "Auto"), - GAIN_4(0, "4 db"), - GAIN_12(1, "12 db"); - - private int mValue; - private String mLabel; - - private E4KMixerGain(int value, String label) - { - mValue = value; - mLabel = label; - } - - public static Register getRegister() - { - return Register.GAIN2; - } - - public static byte getMask() - { - return MIXER_GAIN_MASK; - } - - public byte getValue() - { - return (byte) mValue; - } - - public String getLabel() - { - return mLabel; - } - - public String toString() - { - return mLabel; - } - - public static E4KMixerGain fromRegisterValue(int registerValue) - { - int value = registerValue & getMask(); - - for(E4KMixerGain setting : values()) - { - if(value == setting.getValue()) - { - return setting; - } - } - - throw new IllegalArgumentException("E4KTunerController" - + " - unrecognized Mixer Gain value [" + - value + "]"); - } - } - - public enum IFStage1Gain - { - GAIN_MINUS3(0x0, "-3 db"), - GAIN_PLUS6(0x1, "6 db"); - - private int mValue; - private String mLabel; - - private IFStage1Gain(int value, String label) - { - mValue = value; - mLabel = label; - } - - public static Register getRegister() - { - return Register.GAIN3; - } - - public static byte getMask() - { - return (byte) 0x1; - } - - public byte getValue() - { - return (byte) mValue; - } - - public String getLabel() - { - return mLabel; - } - - public String toString() - { - return mLabel; - } - - public static IFStage1Gain fromRegisterValue(int registerValue) - { - int value = registerValue & getMask(); - - for(IFStage1Gain setting : values()) - { - if(value == setting.getValue()) - { - return setting; - } - } - - throw new IllegalArgumentException("E4KTunerController" - + " - unrecognized IF Gain Stage 1 value [" + - value + "]"); - } - } - - public enum IFStage2Gain - { - GAIN_PLUS0(0x0, "0 db"), - GAIN_PLUS3(0x2, "3 db"), - GAIN_PLUS6(0x4, "6 db"), - GAIN_PLUS9(0x6, "9 db"); - - private int mValue; - private String mLabel; - - private IFStage2Gain(int value, String label) - { - mValue = value; - mLabel = label; - } - - public static Register getRegister() - { - return Register.GAIN3; - } - - public static byte getMask() - { - return (byte) 0x6; - } - - public byte getValue() - { - return (byte) mValue; - } - - public String getLabel() - { - return mLabel; - } - - public String toString() - { - return mLabel; - } - - public static IFStage2Gain fromRegisterValue(int registerValue) - { - int value = registerValue & getMask(); - - for(IFStage2Gain setting : values()) - { - if(value == setting.getValue()) - { - return setting; - } - } - - throw new IllegalArgumentException("E4KTunerController" - + " - unrecognized IF Gain Stage 2 value [" + - value + "]"); - } - } - - public enum IFStage3Gain - { - GAIN_PLUS0(0x00, "0 db"), - GAIN_PLUS3(0x08, "3 db"), - GAIN_PLUS6(0x10, "6 db"), - GAIN_PLUS9(0x30, "9 db"); - - private int mValue; - private String mLabel; - - private IFStage3Gain(int value, String label) - { - mValue = value; - mLabel = label; - } - - public static Register getRegister() - { - return Register.GAIN3; - } - - public static byte getMask() - { - return (byte) 0x18; - } - - public byte getValue() - { - return (byte) mValue; - } - - public String getLabel() - { - return mLabel; - } - - public String toString() - { - return mLabel; - } - - public static IFStage3Gain fromRegisterValue(int registerValue) - { - int value = registerValue & getMask(); - - for(IFStage3Gain setting : values()) - { - if(value == setting.getValue()) - { - return setting; - } - } - - throw new IllegalArgumentException("E4KTunerController" - + " - unrecognized IF Gain Stage 3 value [" + - value + "]"); - } - } - - public enum IFStage4Gain - { - GAIN_PLUS0(0x00, "0 db"), - GAIN_PLUS1(0x20, "1 db"), - GAIN_PLUS2A(0x40, "2 db"), - GAIN_PLUS2B(0x60, "2 db"); - - private int mValue; - private String mLabel; - - private IFStage4Gain(int value, String label) - { - mValue = value; - mLabel = label; - } - - public static Register getRegister() - { - return Register.GAIN3; - } - - public static byte getMask() - { - return (byte) 0x60; - } - - public byte getValue() - { - return (byte) mValue; - } - - public String getLabel() - { - return mLabel; - } - - public String toString() - { - return mLabel; - } - - public static IFStage4Gain fromRegisterValue(int registerValue) - { - int value = registerValue & getMask(); - - for(IFStage4Gain setting : values()) - { - if(value == setting.getValue()) - { - return setting; - } - } - - throw new IllegalArgumentException("E4KTunerController" - + " - unrecognized IF Gain Stage 4 value [" + - value + "]"); - } - } - - public enum IFStage5Gain - { - GAIN_PLUS3(0x0, "3 db"), - GAIN_PLUS6(0x1, "6 db"), - GAIN_PLUS9(0x2, "9 db"), - GAIN_PLUS12(0x3, "12 db"), - GAIN_PLUS15A(0x4, "15 db"), - GAIN_PLUS15B(0x5, "15 db"), - GAIN_PLUS15C(0x6, "15 db"), - GAIN_PLUS15D(0x7, "15 db"); - - private int mValue; - private String mLabel; - - private IFStage5Gain(int value, String label) - { - mValue = value; - mLabel = label; - } - - public static Register getRegister() - { - return Register.GAIN4; - } - - public static byte getMask() - { - return (byte) 0x7; - } - - public byte getValue() - { - return (byte) mValue; - } - - public String getLabel() - { - return mLabel; - } - - public String toString() - { - return mLabel; - } - - public static IFStage5Gain fromRegisterValue(int registerValue) - { - int value = registerValue & getMask(); - - for(IFStage5Gain setting : values()) - { - if(value == setting.getValue()) - { - return setting; - } - } - - throw new IllegalArgumentException("E4KTunerController" - + " - unrecognized IF Gain Stage 5 value [" + - value + "]"); - } - - } - - public enum IFStage6Gain - { - GAIN_PLUS3(0x00, "3 db"), - GAIN_PLUS6(0x08, "6 db"), - GAIN_PLUS9(0x10, "9 db"), - GAIN_PLUS12(0x18, "12 db"), - GAIN_PLUS15A(0x20, "15 db"), - GAIN_PLUS15B(0x28, "15 db"), - GAIN_PLUS15C(0x30, "15 db"), - GAIN_PLUS15D(0x38, "15 db"); - - private int mValue; - private String mLabel; - - private IFStage6Gain(int value, String label) - { - mValue = value; - mLabel = label; - } - - public static Register getRegister() - { - return Register.GAIN4; - } - - public static byte getMask() - { - return (byte) 0x38; - } - - public byte getValue() - { - return (byte) mValue; - } - - public String getLabel() - { - return mLabel; - } - - public String toString() - { - return mLabel; - } - - public static IFStage6Gain fromRegisterValue(int registerValue) - { - int value = registerValue & getMask(); - - for(IFStage6Gain setting : values()) - { - if(value == setting.getValue()) - { - return setting; - } - } - - throw new IllegalArgumentException("E4KTunerController" - + " - unrecognized IF Gain Stage 6 value [" + - value + "]"); - } - } - - public enum IFFilter - { - MIX("Mix"), - CHAN("Channel"), - RC("RC"); - - private String mLabel; - - private IFFilter(String label) - { - mLabel = label; - } - - public String getLabel() - { - return mLabel; - } - } - - public enum AGCMode - { - SERIAL((byte) 0x0), - IF_PWM_LNA_SERIAL((byte) 0x1), - IF_PWM_LNA_AUTONL((byte) 0x2), - IF_PWM_LNA_SUPERV((byte) 0x3), - IF_SERIAL_LNA_PWM((byte) 0x4), - IF_PWM_LNA_PWM((byte) 0x5), - IF_DIG_LNA_SERIAL((byte) 0x6), - IF_DIG_LNA_AUTON((byte) 0x7), - IF_DIG_LNA_SUPERV((byte) 0x8), - IF_SERIAL_LNA_AUTON((byte) 0x9), - IF_SERIAL_LNA_SUPERV((byte) 0xA); - - private byte mValue; - - private AGCMode(byte value) - { - mValue = value; - } - - public byte getValue() - { - return mValue; - } - } - - public enum Register - { - DUMMY(0x00), - MASTER1(0x00), - MASTER2(0x01), - MASTER3(0x02), - MASTER4(0x03), - MASTER5(0x04), - CLK_INP(0x05), - REF_CLK(0x06), - SYNTH1(0x07), - SYNTH2(0x08), - SYNTH3(0x09), - SYNTH4(0x0A), - SYNTH5(0x0B), - SYNTH6(0x0C), - SYNTH7(0x0D), - SYNTH8(0x0E), - SYNTH9(0x0F), - FILT1(0x10), - FILT2(0x11), - FILT3(0x12), - GAIN1(0x14), - GAIN2(0x15), - GAIN3(0x16), - GAIN4(0x17), - AGC1(0x1A), - AGC2(0x1B), - AGC3(0x1C), - AGC4(0x1D), - AGC5(0x1E), - AGC6(0x1F), - AGC7(0x20), - AGC8(0x21), - AGC11(0x24), - AGC12(0x25), - DC1(0x29), - DC2(0x2A), - DC3(0x2B), - DC4(0x2C), - DC5(0x2D), - DC6(0x2E), - DC7(0x2F), - DC8(0x30), - QLUT0(0x50), - QLUT1(0x51), - QLUT2(0x52), - QLUT3(0x53), - ILUT0(0x60), - ILUT1(0x61), - ILUT2(0x62), - ILUT3(0x63), - DCTIME1(0x70), - DCTIME2(0x71), - DCTIME3(0x72), - DCTIME4(0x73), - PWM1(0x74), - PWM2(0x75), - PWM3(0x76), - PWM4(0x77), - BIAS(0x78), - CLKOUT_PWDN(0x7A), - CHFILT_CALIB(0x7B), - I2C_REG_ADDR(0x7D), - MAGIC_1(0x7E), - MAGIC_2(0x7F), - MAGIC_3(0x82), - MAGIC_4(0x86), - MAGIC_5(0x87), - MAGIC_6(0x88), - MAGIC_7(0x9F), - MAGIC_8(0xA0), - I2C_REGISTER(0xC8); - - private int mValue; - - private Register(int value) - { - mValue = value; - } - - public byte getValue() - { - return (byte) mValue; - } - } -} diff --git a/src/source/tuner/rtl/e4k/E4KTunerEditor.java b/src/source/tuner/rtl/e4k/E4KTunerEditor.java deleted file mode 100644 index 064da0ad..00000000 --- a/src/source/tuner/rtl/e4k/E4KTunerEditor.java +++ /dev/null @@ -1,499 +0,0 @@ -/******************************************************************************* - * SDR Trunk - * Copyright (C) 2014-2016 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 source.tuner.rtl.e4k; - -import java.awt.event.ActionEvent; -import java.awt.event.ActionListener; -import java.awt.event.FocusEvent; -import java.awt.event.FocusListener; -import java.text.DecimalFormat; - -import javax.swing.JButton; -import javax.swing.JComboBox; -import javax.swing.JLabel; -import javax.swing.JOptionPane; -import javax.swing.JSeparator; -import javax.swing.JSpinner; -import javax.swing.JTextField; -import javax.swing.SpinnerModel; -import javax.swing.SpinnerNumberModel; -import javax.swing.SwingConstants; -import javax.swing.event.ChangeEvent; -import javax.swing.event.ChangeListener; -import javax.usb.UsbException; - -import net.miginfocom.swing.MigLayout; - -import org.slf4j.Logger; -import org.slf4j.LoggerFactory; -import org.usb4java.LibUsbException; - -import source.SourceException; -import source.tuner.configuration.TunerConfiguration; -import source.tuner.configuration.TunerConfigurationEditor; -import source.tuner.configuration.TunerConfigurationEvent; -import source.tuner.configuration.TunerConfigurationModel; -import source.tuner.rtl.RTL2832Tuner; -import source.tuner.rtl.RTL2832TunerController.Descriptor; -import source.tuner.rtl.RTL2832TunerController.SampleRate; -import source.tuner.rtl.e4k.E4KTunerController.E4KEnhanceGain; -import source.tuner.rtl.e4k.E4KTunerController.E4KGain; -import source.tuner.rtl.e4k.E4KTunerController.E4KLNAGain; -import source.tuner.rtl.e4k.E4KTunerController.E4KMixerGain; - -public class E4KTunerEditor extends TunerConfigurationEditor -{ - private final static Logger mLog = LoggerFactory.getLogger( E4KTunerEditor.class ); - - private static final long serialVersionUID = 1L; - - private JTextField mConfigurationName; - private JButton mTunerInfo; - private JComboBox mComboSampleRate; - private JSpinner mFrequencyCorrection; - private JComboBox mComboMasterGain; - private JComboBox mComboMixerGain; - private JComboBox mComboLNAGain; - private JComboBox mComboEnhanceGain; - - private E4KTunerController mController; - private boolean mLoading; - - public E4KTunerEditor( TunerConfigurationModel tunerConfigurationModel, RTL2832Tuner tuner ) - { - super( tunerConfigurationModel ); - mController = (E4KTunerController)tuner.getController(); - - init(); - } - - private E4KTunerConfiguration getConfiguration() - { - if( hasItem() ) - { - return (E4KTunerConfiguration)getItem(); - } - - return null; - } - - private void init() - { - setLayout( new MigLayout( "fill,wrap 4", "[right][grow,fill][right][grow,fill]", - "[][][][][][][][grow]" ) ); - - add( new JLabel( "E4000 Tuner Configuration" ), "span,align center" ); - - /** - * Tuner Configuration Name - */ - add( new JLabel( "Name:" ) ); - mConfigurationName = new JTextField(); - mConfigurationName.setEnabled( false ); - mConfigurationName.addFocusListener( new FocusListener() - { - @Override - public void focusLost( FocusEvent e ) - { - save(); - } - @Override - public void focusGained( FocusEvent e ) {} - } ); - - add( mConfigurationName, "span 2" ); - - mTunerInfo = new JButton( "Tuner Info" ); - mTunerInfo.setEnabled( false ); - mTunerInfo.addActionListener( new ActionListener() - { - @Override - public void actionPerformed( ActionEvent e ) - { - JOptionPane.showMessageDialog( E4KTunerEditor.this, getTunerInfo(), - "Tuner Info", JOptionPane.INFORMATION_MESSAGE ); - } - } ); - add( mTunerInfo ); - - mComboSampleRate = new JComboBox<>( SampleRate.values() ); - mComboSampleRate.setEnabled( false ); - mComboSampleRate.addActionListener( new ActionListener() - { - @Override - public void actionPerformed( ActionEvent e ) - { - SampleRate sampleRate = (SampleRate)mComboSampleRate.getSelectedItem(); - - try - { - mController.setSampleRate( sampleRate ); - save(); - } - catch ( SourceException | LibUsbException eSampleRate ) - { - JOptionPane.showMessageDialog( E4KTunerEditor.this, - "E4K Tuner Controller - couldn't apply the sample rate setting [" + - sampleRate.getLabel() + "] " + eSampleRate.getLocalizedMessage() ); - - mLog.error( "E4K Tuner Controller - couldn't apply sample rate setting [" + - sampleRate.getLabel() + "]", eSampleRate ); - } - } - } ); - add( new JLabel( "Sample Rate:" ) ); - add( mComboSampleRate ); - - SpinnerModel model = new SpinnerNumberModel( 0.0, //initial value - -1000.0, //min - 1000.0, //max - 0.1 ); //step - - mFrequencyCorrection = new JSpinner( model ); - mFrequencyCorrection.setEnabled( false ); - - JSpinner.NumberEditor editor = - (JSpinner.NumberEditor)mFrequencyCorrection.getEditor(); - - DecimalFormat format = editor.getFormat(); - format.setMinimumFractionDigits( 1 ); - editor.getTextField().setHorizontalAlignment( SwingConstants.CENTER ); - - mFrequencyCorrection.addChangeListener( new ChangeListener() - { - @Override - public void stateChanged( ChangeEvent e ) - { - final double value = ((SpinnerNumberModel)mFrequencyCorrection - .getModel()).getNumber().doubleValue(); - - try - { - mController.getFrequencyController().setFrequencyCorrection( value ); - save(); - } - catch ( SourceException e1 ) - { - JOptionPane.showMessageDialog( E4KTunerEditor.this, "E4K Tuner Controller - " - + "couldn't apply frequency correction value: " + value + e1.getLocalizedMessage() ); - - mLog.error( "E4K Tuner Controller - couldn't apply frequency correction value:" - + value, e1 ); - } - } - } ); - - add( new JLabel( "PPM:" ) ); - add( mFrequencyCorrection ); - - add( new JSeparator( JSeparator.HORIZONTAL ), "span,grow" ); - add( new JLabel( "Gain" ), "wrap" ); - - mComboMasterGain = new JComboBox( E4KGain.values() ); - mComboMasterGain.setEnabled( false ); - mComboMasterGain.addActionListener( new ActionListener() - { - @Override - public void actionPerformed( ActionEvent arg0 ) - { - try - { - E4KGain gain = (E4KGain)mComboMasterGain.getSelectedItem(); - - mController.setGain( (E4KGain)mComboMasterGain.getSelectedItem(), true ); - - if( gain == E4KGain.MANUAL ) - { - mComboMixerGain.setSelectedItem( mController.getMixerGain( true ) ); - mComboMixerGain.setEnabled( true ); - - mComboLNAGain.setSelectedItem( mController.getLNAGain( true ) ); - mComboLNAGain.setEnabled( true ); - - mComboEnhanceGain.setSelectedItem( mController.getEnhanceGain( true ) ); - mComboEnhanceGain.setEnabled( true ); - } - else - { - mComboMixerGain.setEnabled( false ); - mComboMixerGain.setSelectedItem( gain.getMixerGain() ); - - mComboLNAGain.setEnabled( false ); - mComboLNAGain.setSelectedItem( gain.getLNAGain() ); - - mComboEnhanceGain.setEnabled( false ); - mComboEnhanceGain.setSelectedItem( gain.getEnhanceGain() ); - } - - save(); - } - catch ( UsbException e ) - { - JOptionPane.showMessageDialog( E4KTunerEditor.this, "E4K Tuner Controller - " - + "couldn't apply the gain setting - " + e.getLocalizedMessage() ); - - mLog.error( "E4K Tuner Controller - couldn't apply gain setting", e ); - } - } - } ); - - mComboMasterGain.setToolTipText( "Select AUTOMATIC for auto " - + "gain, MANUAL to enable
independent control of " - + "Mixer, LNA and Enhance gain
" - + "settings, or one of the individual gain settings for
" - + "semi-manual gain control" ); - add( new JLabel( "Master:" ) ); - add( mComboMasterGain ); - - mComboMixerGain = new JComboBox( E4KMixerGain.values() ); - mComboMixerGain.addActionListener( new ActionListener() - { - @Override - public void actionPerformed( ActionEvent arg0 ) - { - try - { - E4KMixerGain mixerGain = (E4KMixerGain)mComboMixerGain.getSelectedItem(); - mController.setMixerGain( mixerGain, true ); - save(); - } - catch ( UsbException e ) - { - JOptionPane.showMessageDialog( E4KTunerEditor.this, "E4K Tuner Controller - " - + "couldn't apply the mixer gain setting - " + e.getLocalizedMessage() ); - - mLog.error( "E4K Tuner Controller - couldn't apply mixer gain setting", e ); - } - } - } ); - mComboMixerGain.setToolTipText( "Mixer Gain. Set master gain " - + "to MASTER to enable adjustment" ); - mComboMixerGain.setEnabled( false ); - add( new JLabel( "Mixer:" ) ); - add( mComboMixerGain ); - - mComboLNAGain = new JComboBox( E4KLNAGain.values() ); - mComboLNAGain.addActionListener( new ActionListener() - { - @Override - public void actionPerformed( ActionEvent arg0 ) - { - try - { - E4KLNAGain lnaGain = (E4KLNAGain)mComboLNAGain.getSelectedItem(); - mController.setLNAGain( lnaGain, true ); - save(); - } - catch ( UsbException e ) - { - JOptionPane.showMessageDialog( E4KTunerEditor.this, "E4K Tuner Controller - " - + "couldn't apply the LNA gain setting - " + e.getLocalizedMessage() ); - - mLog.error( "E4K Tuner Controller - couldn't apply LNA gain setting - ", e ); - } - } - } ); - mComboLNAGain.setToolTipText( "LNA Gain. Set master gain " - + "to MANUAL to enable adjustment" ); - mComboLNAGain.setEnabled( false ); - add( new JLabel( "LNA:" ) ); - add( mComboLNAGain ); - - mComboEnhanceGain = new JComboBox( E4KEnhanceGain.values() ); - mComboEnhanceGain.addActionListener( new ActionListener() - { - @Override - public void actionPerformed( ActionEvent arg0 ) - { - try - { - E4KEnhanceGain enhance = (E4KEnhanceGain)mComboEnhanceGain.getSelectedItem(); - mController.setEnhanceGain( enhance, true ); - save(); - } - catch ( UsbException e ) - { - JOptionPane.showMessageDialog( E4KTunerEditor.this, "E4K Tuner Controller - " - + "couldn't apply the enhance gain setting - " + e.getLocalizedMessage() ); - - mLog.error( "E4K Tuner Controller - couldn't apply enhance gain setting", e ); - } - } - } ); - mComboEnhanceGain.setToolTipText( "Enhance Gain. Set master gain " - + "to MANUAL to enable adjustment" ); - mComboEnhanceGain.setEnabled( false ); - add( new JLabel( "Enhance:" ) ); - add( mComboEnhanceGain ); - } - - /** - * Sets each of the tuner configuration controls to the enabled argument state - */ - private void setControlsEnabled( boolean enabled ) - { - if( mConfigurationName.isEnabled() != enabled ) - { - mConfigurationName.setEnabled( enabled ); - } - - if( mTunerInfo.isEnabled() != enabled ) - { - mTunerInfo.setEnabled( enabled ); - } - - if( mFrequencyCorrection.isEnabled() != enabled ) - { - mFrequencyCorrection.setEnabled( enabled ); - } - - if( mComboSampleRate.isEnabled() != enabled ) - { - mComboSampleRate.setEnabled( enabled ); - } - - if( mComboMasterGain.isEnabled() != enabled ) - { - mComboMasterGain.setEnabled( enabled ); - } - - if( mComboMixerGain.isEnabled() != enabled ) - { - mComboMixerGain.setEnabled( enabled ); - } - - if( mComboLNAGain.isEnabled() != enabled ) - { - mComboLNAGain.setEnabled( enabled ); - } - - if( mComboEnhanceGain.isEnabled() != enabled ) - { - mComboEnhanceGain.setEnabled( enabled ); - } - } - - private String getTunerInfo() - { - StringBuilder sb = new StringBuilder(); - - Descriptor descriptor = mController.getDescriptor(); - - sb.append( "

RTL-2832 with E4000 Tuner

" ); - - if( descriptor == null ) - { - sb.append( "No EEPROM Descriptor Available" ); - } - else - { - sb.append( "USB ID: " ); - sb.append( descriptor.getVendorID() ); - sb.append( ":" ); - sb.append( descriptor.getProductID() ); - sb.append( "
" ); - - sb.append( "Vendor: " ); - sb.append( descriptor.getVendorLabel() ); - sb.append( "
" ); - - sb.append( "Product: " ); - sb.append( descriptor.getProductLabel() ); - sb.append( "
" ); - - sb.append( "Serial: " ); - sb.append( descriptor.getSerial() ); - sb.append( "
" ); - - sb.append( "IR Enabled: " ); - sb.append( descriptor.irEnabled() ); - sb.append( "
" ); - - sb.append( "Remote Wake: " ); - sb.append( descriptor.remoteWakeupEnabled() ); - sb.append( "
" ); - } - - return sb.toString(); - } - - @Override - public void save() - { - if( hasItem() && !mLoading ) - { - E4KTunerConfiguration config = getConfiguration(); - - config.setName( mConfigurationName.getText() ); - - config.setSampleRate( (SampleRate)mComboSampleRate.getSelectedItem() ); - - double value = ((SpinnerNumberModel)mFrequencyCorrection - .getModel()).getNumber().doubleValue(); - config.setFrequencyCorrection( value ); - - config.setMasterGain( (E4KGain)mComboMasterGain.getSelectedItem() ); - config.setMixerGain( (E4KMixerGain)mComboMixerGain.getSelectedItem() ); - config.setLNAGain( (E4KLNAGain)mComboLNAGain.getSelectedItem() ); - config.setEnhanceGain( (E4KEnhanceGain)mComboEnhanceGain.getSelectedItem() ); - - getTunerConfigurationModel().broadcast( new TunerConfigurationEvent( - getConfiguration(), TunerConfigurationEvent.Event.CHANGE ) ); - } - - } - - @Override - public void setItem( TunerConfiguration tunerConfiguration ) - { - super.setItem( tunerConfiguration ); - - //Toggle loading so that we don't fire a change event and schedule a settings file save - mLoading = true; - - if( hasItem() ) - { - E4KTunerConfiguration config = getConfiguration(); - - if( tunerConfiguration.isAssigned() ) - { - setControlsEnabled( true ); - - mConfigurationName.setText( config.getName() ); - mFrequencyCorrection.setValue( config.getFrequencyCorrection() ); - mComboSampleRate.setSelectedItem( config.getSampleRate() ); - mComboMasterGain.setSelectedItem( config.getMasterGain() ); - mComboMixerGain.setSelectedItem( config.getMixerGain() ); - mComboLNAGain.setSelectedItem( config.getLNAGain() ); - mComboEnhanceGain.setSelectedItem( config.getEnhanceGain() ); - } - else - { - setControlsEnabled( false ); - mConfigurationName.setText( config.getName() ); - } - } - else - { - setControlsEnabled( false ); - mConfigurationName.setText( "" ); - } - - mLoading = false; - } -} diff --git a/src/source/tuner/rtl/r820t/R820TTunerController.java b/src/source/tuner/rtl/r820t/R820TTunerController.java deleted file mode 100644 index 4fc9f782..00000000 --- a/src/source/tuner/rtl/r820t/R820TTunerController.java +++ /dev/null @@ -1,1296 +0,0 @@ -/******************************************************************************* - * SDR Trunk - * Copyright (C) 2014-2016 Dennis Sheirer - * - * Java port of librtlsdr - * - * Copyright (C) 2013 Mauro Carvalho Chehab - * Copyright (C) 2013 Steve Markgraf - * - * 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 source.tuner.rtl.r820t; - -import org.slf4j.Logger; -import org.slf4j.LoggerFactory; -import org.usb4java.Device; -import org.usb4java.DeviceDescriptor; -import org.usb4java.LibUsbException; -import source.SourceException; -import source.tuner.TunerType; -import source.tuner.configuration.TunerConfiguration; -import source.tuner.rtl.RTL2832TunerController; - -import javax.swing.*; -import javax.usb.UsbException; -import java.nio.ByteBuffer; - -public class R820TTunerController extends RTL2832TunerController -{ - private final static Logger mLog = - LoggerFactory.getLogger(R820TTunerController.class); - - public static final long MIN_FREQUENCY = 3180000; - public static final long MAX_FREQUENCY = 1782030000; - public static final double USABLE_BANDWIDTH_PERCENT = 1.0; - public static final int DC_SPIKE_AVOID_BUFFER = 5000; - - public static final int R820T_IF_FREQUENCY = 3570000; - public static final byte VERSION = (byte) 49; - - public static final byte[] BIT_REV_LOOKUP_TABLE = - {(byte) 0x0, (byte) 0x8, (byte) 0x4, (byte) 0xC, - (byte) 0x2, (byte) 0xA, (byte) 0x6, (byte) 0xE, - (byte) 0x1, (byte) 0x9, (byte) 0x5, (byte) 0xD, - (byte) 0x3, (byte) 0xB, (byte) 0x7, (byte) 0xF}; - - /* R820T I2C address */ - private byte mI2CAddress = (byte) 0x34; - - /* Shadow register is used to keep a cached (in-memory) copy of all - * registers, so that we don't have to read a full byte from a register in - * order to apply a masked value and then re-write the full byte. With the - * shadow register, we can apply the masked value to the cached value, and - * then just write the masked byte, skipping the need to read the byte - * first. */ - private int[] mShadowRegister = - {0x00, 0x00, 0x00, 0x00, 0x00, 0x83, 0x32, 0x75, - 0xC0, 0x40, 0xD6, 0x6C, 0xF5, 0x63, 0x75, 0x68, - 0x6C, 0x83, 0x80, 0x00, 0x0F, 0x00, 0xC0, 0x30, - 0x48, 0xCC, 0x60, 0x00, 0x54, 0xAE, 0x4A, 0xC0}; - - private JPanel mEditor; - - public R820TTunerController(Device device, DeviceDescriptor deviceDescriptor) throws SourceException - { - super(device, deviceDescriptor, MIN_FREQUENCY, MAX_FREQUENCY, DC_SPIKE_AVOID_BUFFER, - USABLE_BANDWIDTH_PERCENT); - } - - @Override - public TunerType getTunerType() - { - return TunerType.RAFAELMICRO_R820T; - } - - @Override - public void setSampleRateFilters(int sampleRate) throws LibUsbException - { - //TODO: why is this being forced as an abstract method on sub classes? - } - - @Override - public void apply(TunerConfiguration tunerConfig) throws SourceException - { - if(tunerConfig != null && - tunerConfig instanceof R820TTunerConfiguration) - { - R820TTunerConfiguration config = (R820TTunerConfiguration) tunerConfig; - - try - { - SampleRate sampleRate = config.getSampleRate(); - setSampleRate(sampleRate); - - double correction = config.getFrequencyCorrection(); - getFrequencyController().setFrequencyCorrection(correction); - - R820TGain masterGain = config.getMasterGain(); - setGain(masterGain, true); - - if(masterGain == R820TGain.MANUAL) - { - R820TMixerGain mixerGain = config.getMixerGain(); - setMixerGain(mixerGain, true); - - R820TLNAGain lnaGain = config.getLNAGain(); - setLNAGain(lnaGain, true); - - R820TVGAGain vgaGain = config.getVGAGain(); - setVGAGain(vgaGain, true); - } - - try - { - getFrequencyController().setFrequency(config.getFrequency()); - } - catch(SourceException se) - { - //Do nothing, we couldn't set the frequency - } - } - catch(UsbException e) - { - throw new SourceException("R820TTunerController - usb error " - + "while applying tuner config", e); - } - } - } - - /** - * Not implemented. - */ - @Override - public long getTunedFrequency() throws SourceException - { - return 0; - } - - /** - * Sets the center frequency. Setting the frequency is a two-part process - * of setting the multiplexer and then setting the Oscillator (PLL). - */ - @Override - public void setTunedFrequency(long frequency) throws SourceException - { - try - { - enableI2CRepeater(mDeviceHandle, true); - - boolean controlI2C = false; - - long offsetFrequency = frequency + R820T_IF_FREQUENCY; - - setMux(offsetFrequency, controlI2C); - - setPLL(offsetFrequency, controlI2C); - - enableI2CRepeater(mDeviceHandle, false); - } - catch(UsbException e) - { - throw new SourceException("R820TTunerController - exception " - + "while setting frequency [" + frequency + "] - " + - e.getLocalizedMessage()); - } - } - - /** - * Overrides the same method from the RTL2832 tuner controller to apply - * settings specific to the R820T tuner. - */ - public void setSamplingMode(SampleMode mode) throws LibUsbException - { - switch(mode) - { - case QUADRATURE: - /* Set intermediate frequency to R820T IF frequency */ - setIFFrequency(R820T_IF_FREQUENCY); - - /* Enable spectrum inversion */ - writeDemodRegister(mDeviceHandle, - Page.ONE, - (short) 0x15, - (short) 0x01, - 1); - - /* Set default i/q path */ - writeDemodRegister(mDeviceHandle, - Page.ZERO, - (short) 0x06, - (short) 0x80, - 1); - break; - default: - break; - - } - } - - /** - * Sets the multiplexer for the desired center frequency. - */ - private void setMux(long frequency, boolean controlI2C) throws UsbException - { - FrequencyRange range = FrequencyRange.getRangeForFrequency(frequency); - - /* Set open drain */ - writeR820TRegister(Register.DRAIN, range.getOpenDrain(), controlI2C); - - /* RF_MUX, Polymux */ - writeR820TRegister(Register.RF_POLY_MUX, range.getRFMuxPolyMux(), controlI2C); - - /* TF Band */ - writeR820TRegister(Register.TF_BAND, range.getTFC(), controlI2C); - - /* XTAL CAP & Drive */ - writeR820TRegister(Register.PLL_XTAL_CAPACITOR_AND_DRIVE, - range.getXTALHighCap0P(), controlI2C); - - /* Register 8 - what is it? */ - writeR820TRegister(Register.UNKNOWN_REGISTER_8, (byte) 0x00, controlI2C); - - /* Register 9 - what is it? */ - writeR820TRegister(Register.UNKNOWN_REGISTER_9, (byte) 0x00, controlI2C); - } - - /** - * Initializes the tuner for use. - */ - public void init() throws SourceException - { - /* Initialize the super class to open and claim the usb interface*/ - super.init(); - - try - { - initBaseband(mDeviceHandle); - - enableI2CRepeater(mDeviceHandle, true); - - boolean i2CRepeaterControl = false; - - initTuner(i2CRepeaterControl); - - enableI2CRepeater(mDeviceHandle, false); - } - catch(UsbException e) - { - throw new SourceException("error during init()", e); - } - } - - /** - * Initializes the tuner section. - */ - public void initTuner(boolean controlI2C) throws UsbException - { - /* Disable zero IF mode */ - writeDemodRegister(mDeviceHandle, - Page.ONE, - (short) 0xB1, - (short) 0x1A, - 1); - - /* Only enable in-phase ADC input */ - writeDemodRegister(mDeviceHandle, - Page.ZERO, - (short) 0x08, - (short) 0x4D, - 1); - - /* Set intermediate frequency to R820T IF frequency (3.57 MHz) */ - setIFFrequency(R820T_IF_FREQUENCY); - - /* Enable spectrum inversion */ - writeDemodRegister(mDeviceHandle, - Page.ONE, - (short) 0x15, - (short) 0x01, - 1); - - initializeRegisters(controlI2C); - - setTVStandard(controlI2C); - - systemFrequencySelect(0, controlI2C); - } - - /** - * Partially implements the r82xx_set_tv_standard() method from librtlsdr. - * Sets standard to digital tv to support sdr operations only. - */ - private void setTVStandard(boolean controlI2C) throws UsbException - { - /* Init Flag & Xtal check Result */ - writeR820TRegister(Register.XTAL_CHECK, (byte) 0x00, controlI2C); - - /* Set version */ - writeR820TRegister(Register.VERSION, VERSION, controlI2C); - - /* LT Gain Test */ - writeR820TRegister(Register.LNA_TOP, (byte) 0x00, controlI2C); - - int calibrationCode = 0; - - for(int x = 0; x < 2; x++) - { - /* Set filter cap */ - writeR820TRegister(Register.FILTER_CAPACITOR, (byte) 0x6B, controlI2C); - - /* Set calibration clock on */ - writeR820TRegister(Register.CALIBRATION_CLOCK, (byte) 0x04, controlI2C); - - /* XTAL capacitor 0pF for PLL */ - writeR820TRegister(Register.PLL_XTAL_CAPACITOR, (byte) 0x00, controlI2C); - - setPLL(56000 * 1000, controlI2C); - - /* Start trigger */ - writeR820TRegister(Register.CALIBRATION_TRIGGER, (byte) 0x10, controlI2C); - - /* Stop trigger */ - writeR820TRegister(Register.CALIBRATION_TRIGGER, (byte) 0x00, controlI2C); - - /* Set calibration clock off */ - writeR820TRegister(Register.CALIBRATION_CLOCK, (byte) 0x00, controlI2C); - - calibrationCode = getCalibrationCode(controlI2C); - - if(!calibrationSuccessful(calibrationCode)) - { - mLog.error("Calibration NOT successful - code: " + - calibrationCode); - } - } - - if(calibrationCode == 0x0F) - { - calibrationCode = 0; - } - - /* Write calibration code */ - byte filt_q = 0x10; - - writeR820TRegister(Register.FILTER_CALIBRATION_CODE, - (byte) (calibrationCode | filt_q), controlI2C); - - /* Set BW, Filter gain & HP Corner */ - writeR820TRegister(Register.BANDWIDTH_FILTER_GAIN_HIGHPASS_FILTER_CORNER, - (byte) 0x6B, controlI2C); - - /* Set Image_R */ - writeR820TRegister(Register.IMAGE_REVERSE, (byte) 0x00, controlI2C); - - /* Set filter_3db, V6MHz */ - writeR820TRegister(Register.FILTER_GAIN, (byte) 0x10, controlI2C); - - /* Channel filter extension */ - writeR820TRegister(Register.CHANNEL_FILTER_EXTENSION, (byte) 0x60, controlI2C); - - /* Loop through */ - writeR820TRegister(Register.LOOP_THROUGH, (byte) 0x00, controlI2C); - - /* Loop through attenuation */ - writeR820TRegister(Register.LOOP_THROUGH_ATTENUATION, (byte) 0x00, controlI2C); - - /* Filter extension widest */ - writeR820TRegister(Register.FILTER_EXTENSION_WIDEST, (byte) 0x00, controlI2C); - - /* RF poly filter current */ - writeR820TRegister(Register.RF_POLY_FILTER_CURRENT, (byte) 0x60, controlI2C); - } - - /** - * Indicates if a calibration request was successful. - */ - private boolean calibrationSuccessful(int calibrationCode) - { - return calibrationCode != 0 && calibrationCode != 0x0F; - } - - /** - * Returns the calibration code resulting from a calibration request. - */ - private int getCalibrationCode(boolean controlI2C) throws UsbException - { - return getStatusRegister(4, controlI2C) & 0x0F; - } - - /** - * Sets the system IF frequency - */ - private void systemFrequencySelect(long frequency, boolean controlI2C) - throws UsbException - { - /* LNA top? */ - writeR820TRegister(Register.LNA_TOP2, (byte) 0xE5, controlI2C); - - byte mixer_top; - byte cp_cur; - byte div_buf_cur; - - if(frequency == 506000000 || - frequency == 666000000 || - frequency == 818000000) - { - mixer_top = (byte) 0x14; - cp_cur = (byte) 0x28; - div_buf_cur = (byte) 0x20; - } - else - { - mixer_top = (byte) 0x24; - cp_cur = (byte) 0x38; - div_buf_cur = (byte) 0x30; - } - - writeR820TRegister(Register.MIXER_TOP, mixer_top, controlI2C); - - writeR820TRegister(Register.LNA_VTH_L, (byte) 0x53, controlI2C); - - writeR820TRegister(Register.MIXER_VTH_L, (byte) 0x75, controlI2C); - - /* Air-In only for Astrometa */ - writeR820TRegister(Register.AIR_CABLE1_INPUT_SELECTOR, (byte) 0x00, controlI2C); - - writeR820TRegister(Register.CABLE2_INPUT_SELECTOR, (byte) 0x00, controlI2C); - - writeR820TRegister(Register.CP_CUR, cp_cur, controlI2C); - - writeR820TRegister(Register.DIVIDER_BUFFER_CURRENT, div_buf_cur, controlI2C); - - writeR820TRegister(Register.FILTER_CURRENT, (byte) 0x40, controlI2C); - - /* if( type != TUNER_ANALOG_TV ) ... */ - - writeR820TRegister(Register.LNA_TOP, (byte) 0x00, controlI2C); - - writeR820TRegister(Register.MIXER_TOP2, (byte) 0x00, controlI2C); - - writeR820TRegister(Register.PRE_DETECT, (byte) 0x00, controlI2C); - - writeR820TRegister(Register.AGC_CLOCK, (byte) 0x30, controlI2C); - - writeR820TRegister(Register.LNA_TOP, (byte) 0x18, controlI2C); - - writeR820TRegister(Register.MIXER_TOP2, mixer_top, controlI2C); - - /* LNA discharge current */ - writeR820TRegister(Register.LNA_DISCHARGE_CURRENT, (byte) 0x14, controlI2C); - - /* AGC clock 1 khz, external det1 cap 1u */ - writeR820TRegister(Register.AGC_CLOCK, (byte) 0x20, controlI2C); - } - - /** - * Sets the tuner's Phase-Locked-Loop (PLL) oscillator used for frequency - * (tuning) control - * - * @param frequency - desired center frequency - * @param controlI2C - control the I2C repeater locally - * @throws UsbException - if unable to set any of the R820T registers - */ - private void setPLL(long frequency, boolean controlI2C) throws UsbException - { - /* Set reference divider to 0 */ - writeR820TRegister(Register.REFERENCE_DIVIDER_2, (byte) 0x00, controlI2C); - - /* Set PLL autotune to 128kHz */ - writeR820TRegister(Register.PLL_AUTOTUNE, (byte) 0x00, controlI2C); - - /* Set VCO current to 100 */ - writeR820TRegister(Register.VCO_CURRENT, (byte) 0x80, controlI2C); - - /* Set the frequency divider - adjust for vco_fine_tune status */ - FrequencyDivider divider = FrequencyDivider.fromFrequency(frequency); - - int statusRegister4 = getStatusRegister(4, controlI2C); - - int vco_fine_tune = (statusRegister4 & 0x30) >> 4; - - int div_num = divider.getDividerNumber(vco_fine_tune); - - writeR820TRegister(Register.DIVIDER, (byte) (div_num << 5), controlI2C); - - /* Get the integral number for this divider and frequency */ - Integral integral = divider.getIntegral(frequency); - - writeR820TRegister(Register.PLL, integral.getRegisterValue(), controlI2C); - - /* Calculate the sigma-delta modulator fractional setting. If it's - * non-zero, power up the sdm and apply the fractional setting, - * otherwise turn it off */ - int sdm = divider.getSDM(integral, frequency); - - if(sdm != 0) - { - writeR820TRegister(Register.SIGMA_DELTA_MODULATOR_POWER, - (byte) 0x00, controlI2C); - - writeR820TRegister(Register.SIGMA_DELTA_MODULATOR_MSB, - (byte) ((sdm >> 8) & 0xFF), controlI2C); - - writeR820TRegister(Register.SIGMA_DELTA_MODULATOR_LSB, - (byte) (sdm & 0xFF), controlI2C); - } - else - { - writeR820TRegister(Register.SIGMA_DELTA_MODULATOR_POWER, - (byte) 0x08, controlI2C); - } - - /* Check to see if the PLL locked with these divider, integral and sdm - * settings */ - if(!isPLLLocked(controlI2C)) - { - /* Increase VCO current */ - writeR820TRegister(Register.VCO_CURRENT, (byte) 0x60, controlI2C); - - if(!isPLLLocked(controlI2C)) - { - throw new UsbException("R820T Tuner Controller - couldn't " - + "achieve PLL lock on frequency [" + frequency + "]"); - } - } - - /* set pll autotune to 8kHz */ - writeR820TRegister(Register.PLL_AUTOTUNE_VARIANT, (byte) 0x08, controlI2C); - } - - /** - * Indicates if the Phase Locked Loop (PLL) oscillator is locked following - * a change in the tuned center frequency. Checks status register 2 to see - * if the PLL locked indicator bit is set. - */ - private boolean isPLLLocked(boolean controlI2C) throws UsbException - { - int register = getStatusRegister(2, controlI2C); - - return (register & 0x40) == 0x40; - } - - /** - * Writes initial starting value of registers 0x05 through 0x1F using the - * default value initialized in the shadow register array. This method only - * needs to be called once, upon initialization. - * - * @throws UsbException - */ - private void initializeRegisters(boolean controlI2C) throws UsbException - { - for(int x = 5; x < mShadowRegister.length; x++) - { - writeI2CRegister(mDeviceHandle, - mI2CAddress, - (byte) x, - (byte) mShadowRegister[x], - controlI2C); - } - } - - /** - * Returns the contents of status registers 0 through 4 - */ - private int getStatusRegister(int register, boolean controlI2C) throws UsbException - { - ByteBuffer buffer = ByteBuffer.allocateDirect(5); - - read(mDeviceHandle, mI2CAddress, Block.I2C, buffer); - - return bitReverse(buffer.get(register) & 0xFF); - } - - /** - * Assumes value is a byte value and reverses the bits in the byte. - */ - private static int bitReverse(int value) - { - return BIT_REV_LOOKUP_TABLE[value & 0x0F] << 4 | - BIT_REV_LOOKUP_TABLE[(value & 0xF0) >> 4]; - } - - /** - * Writes the byte value to the specified register, optionally controlling - * the I2C repeater as needed. - */ - public void writeR820TRegister(Register register, - byte value, - boolean controlI2C) throws UsbException - { - if(register.isMasked()) - { - int current = mShadowRegister[register.getRegister()]; - - value = (byte) ((current & ~register.getMask()) | - (value & register.getMask())); - } - - writeI2CRegister(mDeviceHandle, mI2CAddress, - (byte) register.getRegister(), value, controlI2C); - - mShadowRegister[register.getRegister()] = value; - -// Log.info( "R820T writing register " + -// String.format( "%02X", register.getRegister() ) + " value " + -// String.format( "%02X", value ) ); - } - - /** - * Reads the specified register, optionally controlling the I2C repeater - */ - public int readR820TRegister(Register register, boolean controlI2C) - throws UsbException - { - int value = readI2CRegister(mDeviceHandle, - mI2CAddress, - (byte) register.getRegister(), - controlI2C); - - return value; - } - - /** - * Sets master gain by applying gain component values to LNA, Mixer and - * VGA gain registers. - */ - public void setGain(R820TGain gain, boolean controlI2C) throws UsbException - { - setLNAGain(gain.getLNAGain(), controlI2C); - setMixerGain(gain.getMixerGain(), controlI2C); - setVGAGain(gain.getVGAGain(), controlI2C); - } - - /** - * Sets LNA gain - */ - public void setLNAGain(R820TLNAGain gain, boolean controlI2C) throws UsbException - { - writeR820TRegister(Register.LNA_GAIN, gain.getSetting(), controlI2C); - } - - /** - * Sets Mixer gain - */ - public void setMixerGain(R820TMixerGain gain, boolean controlI2C) throws UsbException - { - writeR820TRegister(Register.MIXER_GAIN, gain.getSetting(), controlI2C); - } - - /** - * Sets VGA gain - */ - public void setVGAGain(R820TVGAGain gain, boolean controlI2C) throws UsbException - { - writeR820TRegister(Register.VGA_GAIN, gain.getSetting(), controlI2C); - } - - /** - * R820T VGA gain settings - */ - public enum R820TVGAGain - { - GAIN_0("0", 0x00), - GAIN_26("26", 0x01), - GAIN_52("52", 0x02), - GAIN_82("82", 0x03), - GAIN_124("124", 0x04), - GAIN_159("159", 0x05), - GAIN_183("183", 0x06), - GAIN_196("196", 0x07), - GAIN_210("210", 0x08), - GAIN_242("242", 0x09), - GAIN_278("278", 0x0A), - GAIN_312("312", 0x0B), - GAIN_347("347", 0x0C), - GAIN_384("384", 0x0D), - GAIN_419("419", 0x0E), - GAIN_455("455", 0x0F); - - private String mLabel; - private int mSetting; - - private R820TVGAGain(String label, int setting) - { - mLabel = label; - mSetting = setting; - } - - public String toString() - { - return mLabel; - } - - public byte getSetting() - { - return (byte) mSetting; - } - } - - /** - * R820T Low Noise Amplifier gain settings - */ - public enum R820TLNAGain - { - AUTOMATIC("Automatic", 0x00), - GAIN_0("0", 0x10), - GAIN_9("9", 0x11), - GAIN_21("21", 0x12), - GAIN_61("61", 0x13), - GAIN_99("99", 0x14), - GAIN_112("112", 0x15), - GAIN_143("143", 0x16), - GAIN_165("165", 0x17), - GAIN_191("191", 0x18), - GAIN_222("222", 0x19), - GAIN_248("248", 0x1A), - GAIN_262("262", 0x1B), - GAIN_281("281", 0x1C), - GAIN_286("286", 0x1D), - GAIN_321("321", 0x1E), - GAIN_334("334", 0x1F); - - private String mLabel; - private int mSetting; - - private R820TLNAGain(String label, int setting) - { - mLabel = label; - mSetting = setting; - } - - public String toString() - { - return mLabel; - } - - public byte getSetting() - { - return (byte) mSetting; - } - } - - /** - * R820T mixer gain settings - */ - public enum R820TMixerGain - { - AUTOMATIC("Automatic", 0x10), - GAIN_0("0", 0x00), - GAIN_5("5", 0x01), - GAIN_15("15", 0x02), - GAIN_25("25", 0x03), - GAIN_44("44", 0x04), - GAIN_53("53", 0x05), - GAIN_63("63", 0x06), - GAIN_88("88", 0x07), - GAIN_105("105", 0x08), - GAIN_115("115", 0x09), - GAIN_123("123", 0x0A), - GAIN_139("139", 0x0B), - GAIN_152("152", 0x0C), - GAIN_158("158", 0x0D), - GAIN_161("161", 0x0E), - GAIN_153("153", 0x0F); - - private String mLabel; - private int mSetting; - - private R820TMixerGain(String label, int setting) - { - mLabel = label; - mSetting = setting; - } - - public String toString() - { - return mLabel; - } - - public byte getSetting() - { - return (byte) mSetting; - } - } - - /** - * R820T Master gain settings - */ - public enum R820TGain - { - AUTOMATIC("Automatic", R820TVGAGain.GAIN_312, R820TLNAGain.AUTOMATIC, R820TMixerGain.AUTOMATIC), - MANUAL("Manual", R820TVGAGain.GAIN_210, R820TLNAGain.GAIN_248, R820TMixerGain.GAIN_123), - GAIN_0("0", R820TVGAGain.GAIN_210, R820TLNAGain.GAIN_0, R820TMixerGain.GAIN_0), - GAIN_9("9", R820TVGAGain.GAIN_210, R820TLNAGain.GAIN_9, R820TMixerGain.GAIN_0), - GAIN_14("14", R820TVGAGain.GAIN_210, R820TLNAGain.GAIN_9, R820TMixerGain.GAIN_5), - GAIN_26("26", R820TVGAGain.GAIN_210, R820TLNAGain.GAIN_21, R820TMixerGain.GAIN_5), - GAIN_36("36", R820TVGAGain.GAIN_210, R820TLNAGain.GAIN_21, R820TMixerGain.GAIN_15), - GAIN_76("76", R820TVGAGain.GAIN_210, R820TLNAGain.GAIN_61, R820TMixerGain.GAIN_15), - GAIN_86("86", R820TVGAGain.GAIN_210, R820TLNAGain.GAIN_61, R820TMixerGain.GAIN_25), - GAIN_124("124", R820TVGAGain.GAIN_210, R820TLNAGain.GAIN_99, R820TMixerGain.GAIN_25), - GAIN_143("143", R820TVGAGain.GAIN_210, R820TLNAGain.GAIN_99, R820TMixerGain.GAIN_44), - GAIN_156("156", R820TVGAGain.GAIN_210, R820TLNAGain.GAIN_112, R820TMixerGain.GAIN_44), - GAIN_165("165", R820TVGAGain.GAIN_210, R820TLNAGain.GAIN_112, R820TMixerGain.GAIN_53), - GAIN_196("196", R820TVGAGain.GAIN_210, R820TLNAGain.GAIN_143, R820TMixerGain.GAIN_53), - GAIN_208("208", R820TVGAGain.GAIN_210, R820TLNAGain.GAIN_143, R820TMixerGain.GAIN_63), - GAIN_228("228", R820TVGAGain.GAIN_210, R820TLNAGain.GAIN_165, R820TMixerGain.GAIN_63), - GAIN_253("253", R820TVGAGain.GAIN_210, R820TLNAGain.GAIN_165, R820TMixerGain.GAIN_88), - GAIN_279("279", R820TVGAGain.GAIN_210, R820TLNAGain.GAIN_191, R820TMixerGain.GAIN_88), - GAIN_296("296", R820TVGAGain.GAIN_210, R820TLNAGain.GAIN_191, R820TMixerGain.GAIN_105), - GAIN_327("327", R820TVGAGain.GAIN_210, R820TLNAGain.GAIN_222, R820TMixerGain.GAIN_105), - GAIN_337("337", R820TVGAGain.GAIN_210, R820TLNAGain.GAIN_222, R820TMixerGain.GAIN_115), - GAIN_363("363", R820TVGAGain.GAIN_210, R820TLNAGain.GAIN_248, R820TMixerGain.GAIN_115), - GAIN_371("371", R820TVGAGain.GAIN_210, R820TLNAGain.GAIN_248, R820TMixerGain.GAIN_123), - GAIN_385("385", R820TVGAGain.GAIN_210, R820TLNAGain.GAIN_262, R820TMixerGain.GAIN_123), - GAIN_401("401", R820TVGAGain.GAIN_210, R820TLNAGain.GAIN_262, R820TMixerGain.GAIN_139), - GAIN_420("420", R820TVGAGain.GAIN_210, R820TLNAGain.GAIN_281, R820TMixerGain.GAIN_139), - GAIN_433("433", R820TVGAGain.GAIN_210, R820TLNAGain.GAIN_281, R820TMixerGain.GAIN_152), - GAIN_438("438", R820TVGAGain.GAIN_210, R820TLNAGain.GAIN_286, R820TMixerGain.GAIN_152), - GAIN_444("444", R820TVGAGain.GAIN_210, R820TLNAGain.GAIN_286, R820TMixerGain.GAIN_158), - GAIN_479("479", R820TVGAGain.GAIN_210, R820TLNAGain.GAIN_321, R820TMixerGain.GAIN_158), - GAIN_482("482", R820TVGAGain.GAIN_210, R820TLNAGain.GAIN_321, R820TMixerGain.GAIN_161), - GAIN_495("495", R820TVGAGain.GAIN_210, R820TLNAGain.GAIN_334, R820TMixerGain.GAIN_161); - - private String mLabel; - private R820TVGAGain mVGAGain; - private R820TLNAGain mLNAGain; - private R820TMixerGain mMixerGain; - - private R820TGain(String label, R820TVGAGain vga, R820TLNAGain lna, R820TMixerGain mixer) - { - mLabel = label; - mVGAGain = vga; - mLNAGain = lna; - mMixerGain = mixer; - } - - public String toString() - { - return mLabel; - } - - public R820TVGAGain getVGAGain() - { - return mVGAGain; - } - - public R820TLNAGain getLNAGain() - { - return mLNAGain; - } - - public R820TMixerGain getMixerGain() - { - return mMixerGain; - } - } - - /** - * R820T tuner registers and register mask values - */ - public enum Register - { - LNA_GAIN(0x05, 0x1F), - AIR_CABLE1_INPUT_SELECTOR(0x05, 0x60), - LOOP_THROUGH(0x05, 0x80), - CABLE2_INPUT_SELECTOR(0x06, 0x08), - FILTER_GAIN(0x06, 0x30), - PRE_DETECT(0x06, 0x40), - MIXER_GAIN(0x07, 0x1F), - IMAGE_REVERSE(0x07, 0x80), - UNKNOWN_REGISTER_8(0x08, 0x3F), - UNKNOWN_REGISTER_9(0x09, 0x3F), - FILTER_CALIBRATION_CODE(0x0A, 0x1F), - FILTER_CURRENT(0x0A, 0x60), - CALIBRATION_TRIGGER(0x0B, 0x10), - FILTER_CAPACITOR(0x0B, 0x60), - BANDWIDTH_FILTER_GAIN_HIGHPASS_FILTER_CORNER(0x0B, 0xEF), - XTAL_CHECK(0x0C, 0x0F), - VGA_GAIN(0x0C, 0x9F), - LNA_VTH_L(0x0D, 0x0), - MIXER_VTH_L(0x0E, 0x0), - CALIBRATION_CLOCK(0x0F, 0x04), - FILTER_EXTENSION_WIDEST(0x0F, 0x80), - PLL_XTAL_CAPACITOR(0x10, 0x03), - UNKNOWN_REGISTER_10(0x10, 0x04), - PLL_XTAL_CAPACITOR_AND_DRIVE(0x10, 0x0B), - REFERENCE_DIVIDER_2(0x10, 0x10), - CAPACITOR_SELECTOR(0x10, 0x1B), - DIVIDER(0x10, 0xE0), - CP_CUR(0x11, 0x38), - SIGMA_DELTA_MODULATOR_POWER(0x12, 0x08), - VCO_CURRENT(0x12, 0xE0), - VERSION(0x13, 0x3F), - PLL(0x14, 0x0), - SIGMA_DELTA_MODULATOR_LSB(0x15, 0x0), - SIGMA_DELTA_MODULATOR_MSB(0x16, 0x0), - DRAIN(0x17, 0x08), - DIVIDER_BUFFER_CURRENT(0x17, 0x30), - RF_POLY_FILTER_CURRENT(0x19, 0x60), - PLL_AUTOTUNE(0x1A, 0x0C), - PLL_AUTOTUNE_VARIANT(0x1A, 0x08), - AGC_CLOCK(0x1A, 0x30), - RF_POLY_MUX(0x1A, 0xC3), - TF_BAND(0x1B, 0x0), - MIXER_TOP(0x1C, 0xF8), - MIXER_TOP2(0X1C, 0x04), - LNA_TOP(0x1D, 0x38), - LNA_TOP2(0x1D, 0xC7), - CHANNEL_FILTER_EXTENSION(0x1E, 0x60), - LNA_DISCHARGE_CURRENT(0x1E, 0x1F), - LOOP_THROUGH_ATTENUATION(0x1F, 0x80); - - private int mRegister; - private int mMask; - - private Register(int register, int mask) - { - mRegister = register; - mMask = mask; - } - - public int getRegister() - { - return mRegister; - } - - public byte getMask() - { - return (byte) mMask; - } - - public boolean isMasked() - { - return mMask != 0; - } - } - - public enum FrequencyRange - { - RANGE_024(24000000, 49999999, 0x08, 0x02, 0xDF, 0x02, 0x01), - RANGE_050(50000000, 54999999, 0x08, 0x02, 0xBE, 0x02, 0x01), - RANGE_055(55000000, 59999999, 0x08, 0x02, 0x8B, 0x02, 0x01), - RANGE_060(60000000, 64999999, 0x08, 0x02, 0x7B, 0x02, 0x01), - RANGE_065(65000000, 69999999, 0x08, 0x02, 0x69, 0x02, 0x01), - RANGE_070(70000000, 74999999, 0x08, 0x02, 0x58, 0x02, 0x01), - RANGE_075(75000000, 79999999, 0x00, 0x02, 0x44, 0x02, 0x01), - RANGE_080(80000000, 89999999, 0x00, 0x02, 0x44, 0x02, 0x01), - RANGE_090(90000000, 99999999, 0x00, 0x02, 0x34, 0x01, 0x01), - RANGE_100(100000000, 109999999, 0x00, 0x02, 0x34, 0x01, 0x01), - RANGE_110(110000000, 119999999, 0x00, 0x02, 0x24, 0x01, 0x01), - RANGE_120(120000000, 139999999, 0x00, 0x02, 0x24, 0x01, 0x01), - RANGE_140(140000000, 179999999, 0x00, 0x02, 0x14, 0x01, 0x01), - RANGE_180(180000000, 219999999, 0x00, 0x02, 0x13, 0x00, 0x00), - RANGE_220(220000000, 249999999, 0x00, 0x02, 0x13, 0x00, 0x00), - RANGE_250(250000000, 279999999, 0x00, 0x02, 0x11, 0x00, 0x00), - RANGE_280(280000000, 309999999, 0x00, 0x02, 0x00, 0x00, 0x00), - RANGE_310(310000000, 449999999, 0x00, 0x41, 0x00, 0x00, 0x00), - RANGE_450(450000000, 587999999, 0x00, 0x41, 0x00, 0x00, 0x00), - RANGE_588(588000000, 649999999, 0x00, 0x40, 0x00, 0x00, 0x00), - RANGE_650(650000000, 1766000000, 0x00, 0x40, 0x00, 0x00, 0x00), - RANGE_UNK(0, 0, 0, 0, 0, 0, 0); - - private long mMinFrequency; - private long mMaxFrequency; - private int mOpenDrain; - private int mRFMux_PolyMux; - private int mTF_c; - private int mXtalCap20p; - private int mXtalCap10p; - - private FrequencyRange(long minFrequency, - long maxFrequency, - int openDrain, - int rfMuxPloy, - int tf_c, - int xtalCap20p, - int xtalCap10p) - { - mMinFrequency = minFrequency; - mMaxFrequency = maxFrequency; - mOpenDrain = openDrain; - mRFMux_PolyMux = rfMuxPloy; - mTF_c = tf_c; - mXtalCap20p = xtalCap20p; - mXtalCap10p = xtalCap10p; - } - - public boolean contains(long frequency) - { - return mMinFrequency <= frequency && frequency <= mMaxFrequency; - } - - public static FrequencyRange getRangeForFrequency(long frequency) - { - for(FrequencyRange range : values()) - { - if(range.contains(frequency)) - { - return range; - } - } - - return RANGE_UNK; - } - - public long getMinFrequency() - { - return mMinFrequency; - } - - public long getMaxFrequency() - { - return mMaxFrequency; - } - - public byte getOpenDrain() - { - return (byte) mOpenDrain; - } - - public byte getRFMuxPolyMux() - { - return (byte) mRFMux_PolyMux; - } - - public byte getTFC() - { - return (byte) mTF_c; - } - - public byte getXTALCap20P() - { - return (byte) mXtalCap20p; - } - - public byte getXTALCap10P() - { - return (byte) mXtalCap10p; - } - - public byte getXTALLowCap0P() - { - return (byte) 0x08; - } - - public byte getXTALHighCap0P() - { - return (byte) 0x00; - } - } - - /** - * Frequency Divider Ranges - * - * Actual Tuned Frequency Ranges (after subtracting IF = 3.57 MHz ) - * - * Divider 0: 860.43 to 1782.03 MHz - * Divider 1: 428.43 to 889.23 MHz - * Divider 2: 212.43 to 457.23 MHz - * Divider 3: 104.43 to 219.63 MHz - * Divider 4: 50.43 to 108.03 MHz - * Divider 5: 23.43 to 52.23 MHz - * Divider 6: 9.93 to 24.33 MHz - * Divider 7: 3.18 to 10.38 MHz - */ - public enum FrequencyDivider - { - DIVIDER_0(0, 2, 864000000, 1785600000, 0x00, 28800000), - DIVIDER_1(1, 4, 432000000, 892800000, 0x20, 14400000), - DIVIDER_2(2, 8, 216000000, 460800000, 0x40, 7200000), - DIVIDER_3(3, 16, 108000000, 223200000, 0x60, 3600000), - DIVIDER_4(4, 32, 54000000, 111600000, 0x80, 1800000), - DIVIDER_5(5, 64, 27000000, 55800000, 0xA0, 900000), - DIVIDER_6(6, 128, 13500000, 27900000, 0xC0, 450000), - DIVIDER_7(7, 256, 6750000, 13950000, 0xE0, 225000); - - private int mDividerNumber; - private int mMixerDivider; - private long mMinimumFrequency; - private long mMaximumFrequency; - private int mRegisterSetting; - private int mIntegralValue; - private static final int mVCOPowerReference = 2; - - private FrequencyDivider(int dividerNumber, - int mixerDivider, - long minimumFrequency, - long maximumFrequency, - int registerSetting, - int integralValue) - { - mDividerNumber = dividerNumber; - mMixerDivider = mixerDivider; - mMinimumFrequency = minimumFrequency; - mMaximumFrequency = maximumFrequency; - mRegisterSetting = registerSetting; - mIntegralValue = integralValue; - } - - public int getDividerNumber(int vcoFineTune) - { - if(vcoFineTune == mVCOPowerReference) - { - return mDividerNumber; - } - else if(vcoFineTune < mVCOPowerReference) - { - return mDividerNumber - 1; - } - else if(vcoFineTune > mVCOPowerReference) - { - return mDividerNumber + 1; - } - - return mDividerNumber; - } - - public int getMixerDivider() - { - return mMixerDivider; - } - - public long getMinimumFrequency() - { - return mMinimumFrequency; - } - - public long getMaximumFrequency() - { - return mMaximumFrequency; - } - - public byte getDividerRegisterSetting() - { - return (byte) mRegisterSetting; - } - - public boolean contains(long frequency) - { - return mMinimumFrequency <= frequency && - frequency <= mMaximumFrequency; - } - - /** - * Returns the correct frequency divider for the specified frequency, - * or returns divider #5, if the frequency is outside of the specified - * frequency ranges. - * - * @param frequency - desired frequency - * @return - FrequencyDivider to use for the specified frequency - */ - public static FrequencyDivider fromFrequency(long frequency) - { - for(FrequencyDivider divider : FrequencyDivider.values()) - { - if(divider.contains(frequency)) - { - return divider; - } - } - - return FrequencyDivider.DIVIDER_5; - } - - /** - * Returns the integral to use for this frequency and divider - */ - public Integral getIntegral(long frequency) - { - if(contains(frequency)) - { - int delta = (int) (frequency - mMinimumFrequency); - - int integral = (int) ((double) delta / (double) mIntegralValue); - - return Integral.fromValue(integral); - } - - throw new IllegalArgumentException("PLL frequency [" + frequency + - "] is not valid for this frequency divider " + this.toString()); - } - - /** - * Calculates the 16-bit value of the sigma-delta modulator setting - * which represents the fractional portion of the requested frequency - * that is left over after subtracting the divider minimum frequency - * and the integral frequency units. That residual value is divided - * by the integral unit value to derive a 16-bit fractional value, - * returned as an integer - */ - public int getSDM(Integral integral, long frequency) - { - if(contains(frequency)) - { - int delta = (int) (frequency - mMinimumFrequency - - (integral.getNumber() * mIntegralValue)); - - double fractional = (double) delta / (double) mIntegralValue; - - //Left shift the double value 16 bits and truncate to an integer - return (int) (fractional * 0x10000) & 0xFFFF; - } - - return 0; - } - } - - /** - * PLL Integral values. Each value represents one unit of the divided - * crystal frequency as follows: - * - * Divider Value - * 64 I * 0.9 MHz - * 32 I * 1.8 MHz - * 16 I * 3.6 MHz - * 8 I * 7.2 MHz - * 4 I * 14.4 MHz - * 2 I * 28.8 MHz - */ - public enum Integral - { - I00(0, 0x44), - I01(1, 0x84), - I02(2, 0xC4), - I03(3, 0x05), - I04(4, 0x45), - I05(5, 0x85), - I06(6, 0xC5), - I07(7, 0x06), - I08(8, 0x46), - I09(9, 0x86), - I10(10, 0xC6), - I11(11, 0x07), - I12(12, 0x47), - I13(13, 0x87), - I14(14, 0xC7), - I15(15, 0x08), - I16(16, 0x48), - I17(17, 0x88), - I18(18, 0xC8), - I19(19, 0x09), - I20(20, 0x49), - I21(21, 0x89), - I22(22, 0xC9), - I23(23, 0x0A), - I24(24, 0x4A), - I25(25, 0x8A), - I26(26, 0xCA), - I27(27, 0x0B), - I28(28, 0x4B), - I29(29, 0x8B), - I30(30, 0xCB), - I31(31, 0x0C); - - private int mNumber; - private int mRegister; - - private Integral(int number, int register) - { - mNumber = number; - mRegister = register; - } - - public int getNumber() - { - return mNumber; - } - - public byte getRegisterValue() - { - return (byte) mRegister; - } - - public static Integral fromValue(int value) - { - if(0 <= value && value <= 31) - { - return Integral.values()[value]; - } - - throw new IllegalArgumentException("PLL integral value [" + value + - "] must be in the range 0 - 31"); - } - } -} diff --git a/src/source/tuner/rtl/r820t/R820TTunerEditor.java b/src/source/tuner/rtl/r820t/R820TTunerEditor.java deleted file mode 100644 index bf3cbf16..00000000 --- a/src/source/tuner/rtl/r820t/R820TTunerEditor.java +++ /dev/null @@ -1,559 +0,0 @@ -/******************************************************************************* - * SDR Trunk - * Copyright (C) 2014-2016 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 source.tuner.rtl.r820t; - -import java.awt.event.ActionEvent; -import java.awt.event.ActionListener; -import java.awt.event.FocusEvent; -import java.awt.event.FocusListener; -import java.text.DecimalFormat; - -import javax.swing.JButton; -import javax.swing.JComboBox; -import javax.swing.JLabel; -import javax.swing.JOptionPane; -import javax.swing.JSeparator; -import javax.swing.JSpinner; -import javax.swing.JTextField; -import javax.swing.SpinnerModel; -import javax.swing.SpinnerNumberModel; -import javax.swing.SwingConstants; -import javax.swing.event.ChangeEvent; -import javax.swing.event.ChangeListener; -import javax.usb.UsbException; - -import net.miginfocom.swing.MigLayout; - -import org.slf4j.Logger; -import org.slf4j.LoggerFactory; -import org.usb4java.LibUsbException; - -import source.SourceException; -import source.tuner.configuration.TunerConfiguration; -import source.tuner.configuration.TunerConfigurationEditor; -import source.tuner.configuration.TunerConfigurationEvent; -import source.tuner.configuration.TunerConfigurationEvent.Event; -import source.tuner.configuration.TunerConfigurationModel; -import source.tuner.rtl.RTL2832Tuner; -import source.tuner.rtl.RTL2832TunerController.Descriptor; -import source.tuner.rtl.RTL2832TunerController.SampleRate; -import source.tuner.rtl.r820t.R820TTunerController.R820TGain; -import source.tuner.rtl.r820t.R820TTunerController.R820TLNAGain; -import source.tuner.rtl.r820t.R820TTunerController.R820TMixerGain; -import source.tuner.rtl.r820t.R820TTunerController.R820TVGAGain; - -public class R820TTunerEditor extends TunerConfigurationEditor -{ - private final static Logger mLog = LoggerFactory.getLogger( R820TTunerEditor.class ); - - private static final long serialVersionUID = 1L; - private static final R820TGain DEFAULT_GAIN = R820TGain.GAIN_279; - - private R820TTunerController mController; - - private JTextField mConfigurationName; - private JButton mTunerInfo; - private JComboBox mComboSampleRate; - private JSpinner mFrequencyCorrection; - private JComboBox mComboMasterGain; - private JComboBox mComboMixerGain; - private JComboBox mComboLNAGain; - private JComboBox mComboVGAGain; - private boolean mLoading; - - public R820TTunerEditor( TunerConfigurationModel tunerConfigurationModel, RTL2832Tuner tuner ) - { - super( tunerConfigurationModel ); - mController = (R820TTunerController)tuner.getController(); - - init(); - } - - private R820TTunerConfiguration getConfiguration() - { - if( hasItem() ) - { - return (R820TTunerConfiguration)getItem(); - } - - return null; - } - - private void init() - { - setLayout( new MigLayout( "fill,wrap 4", "[right][grow,fill][right][grow,fill]", - "[][][][][][][][grow]" ) ); - - add( new JLabel( "R820T Tuner Configuration" ), "span,align center" ); - - mConfigurationName = new JTextField(); - mConfigurationName.setEnabled( false ); - mConfigurationName.addFocusListener( new FocusListener() - { - @Override - public void focusLost( FocusEvent e ) - { - save(); - } - @Override - public void focusGained( FocusEvent e ) {} - } ); - - add( new JLabel( "Name:" ) ); - add( mConfigurationName, "span 2" ); - - mTunerInfo = new JButton( "Tuner Info" ); - mTunerInfo.setEnabled( false ); - mTunerInfo.addActionListener( new ActionListener() - { - @Override - public void actionPerformed( ActionEvent e ) - { - JOptionPane.showMessageDialog( R820TTunerEditor.this, getTunerInfo(), - "Tuner Info", JOptionPane.INFORMATION_MESSAGE ); - } - } ); - add( mTunerInfo ); - - mComboSampleRate = new JComboBox<>( SampleRate.values() ); - mComboSampleRate.setEnabled( false ); - mComboSampleRate.addActionListener( new ActionListener() - { - @Override - public void actionPerformed( ActionEvent e ) - { - SampleRate sampleRate = - (SampleRate)mComboSampleRate.getSelectedItem(); - try - { - - mController.setSampleRate( sampleRate ); - save(); - } - catch ( SourceException | LibUsbException eSampleRate ) - { - JOptionPane.showMessageDialog( - R820TTunerEditor.this, - "R820T Tuner Controller - couldn't apply the sample " - + "rate setting [" + sampleRate.getLabel() + "] " + - eSampleRate.getLocalizedMessage() ); - - mLog.error( "R820T Tuner Controller - couldn't apply sample " - + "rate setting [" + sampleRate.getLabel() + "]", - eSampleRate ); - } - } - } ); - add( new JLabel( "Sample Rate:" ) ); - add( mComboSampleRate ); - - /** - * Frequency Correction - */ - SpinnerModel model = - new SpinnerNumberModel( 0.0, //initial value - -1000.0, //min - 1000.0, //max - 0.1 ); //step - - mFrequencyCorrection = new JSpinner( model ); - mFrequencyCorrection.setEnabled( false ); - - JSpinner.NumberEditor editor = - (JSpinner.NumberEditor)mFrequencyCorrection.getEditor(); - - DecimalFormat format = editor.getFormat(); - format.setMinimumFractionDigits( 1 ); - editor.getTextField().setHorizontalAlignment( SwingConstants.CENTER ); - - mFrequencyCorrection.addChangeListener( new ChangeListener() - { - @Override - public void stateChanged( ChangeEvent e ) - { - final double value = ((SpinnerNumberModel)mFrequencyCorrection - .getModel()).getNumber().doubleValue(); - - try - { - mController.getFrequencyController().setFrequencyCorrection( value ); - } - catch ( SourceException e1 ) - { - mLog.error( "Error setting frequency correction value", e1 ); - } - - save(); - } - } ); - - add( new JLabel( "PPM:" ) ); - add( mFrequencyCorrection ); - - add( new JSeparator( JSeparator.HORIZONTAL ), "span,grow" ); - - /** - * Gain Controls - */ - add( new JLabel( "Gain" ), "wrap" ); - - /* Master Gain Control */ - mComboMasterGain = new JComboBox( R820TGain.values() ); - mComboMasterGain.setEnabled( false ); - mComboMasterGain.addActionListener( new ActionListener() - { - @Override - public void actionPerformed( ActionEvent arg0 ) - { - try - { - R820TGain gain = (R820TGain)mComboMasterGain.getSelectedItem(); - - mController.setGain( (R820TGain)mComboMasterGain.getSelectedItem(), true ); - - if( gain == R820TGain.MANUAL ) - { - mComboMixerGain.setSelectedItem( gain.getMixerGain() ); - mComboMixerGain.setEnabled( true ); - - mComboLNAGain.setSelectedItem( gain.getLNAGain() ); - mComboLNAGain.setEnabled( true ); - - mComboVGAGain.setSelectedItem( gain.getVGAGain() ); - mComboVGAGain.setEnabled( true ); - } - else - { - mComboMixerGain.setEnabled( false ); - mComboMixerGain.setSelectedItem( gain.getMixerGain() ); - - mComboLNAGain.setEnabled( false ); - mComboLNAGain.setSelectedItem( gain.getLNAGain() ); - - mComboVGAGain.setEnabled( false ); - mComboVGAGain.setSelectedItem( gain.getVGAGain() ); - } - - save(); - } - catch ( UsbException e ) - { - JOptionPane.showMessageDialog( - R820TTunerEditor.this, - "R820T Tuner Controller - couldn't apply the gain " - + "setting - " + e.getLocalizedMessage() ); - - mLog.error( "R820T Tuner Controller - couldn't apply " - + "gain setting - ", e ); - } - } - } ); - mComboMasterGain.setToolTipText( "Select AUTOMATIC for auto " - + "gain, MANUAL to enable
independent control of " - + "Mixer, LNA and Enhance gain
" - + "settings, or one of the individual gain settings for
" - + "semi-manual gain control" ); - add( new JLabel( "Master:" ) ); - add( mComboMasterGain ); - - /* Mixer Gain Control */ - mComboMixerGain = new JComboBox( R820TMixerGain.values() ); - mComboMixerGain.setEnabled( false ); - mComboMixerGain.addActionListener( new ActionListener() - { - @Override - public void actionPerformed( ActionEvent arg0 ) - { - try - { - R820TMixerGain mixerGain = - (R820TMixerGain)mComboMixerGain.getSelectedItem(); - - if( mixerGain == null ) - { - mixerGain = DEFAULT_GAIN.getMixerGain(); - } - - if( mComboMixerGain.isEnabled() ) - { - mController.setMixerGain( mixerGain, true ); - } - - save(); - } - catch ( UsbException e ) - { - JOptionPane.showMessageDialog( R820TTunerEditor.this, - "R820T Tuner Controller - couldn't apply the mixer " - + "gain setting - " + e.getLocalizedMessage() ); - - mLog.error( "R820T Tuner Controller - couldn't apply mixer " - + "gain setting - ", e ); - } - } - } ); - mComboMixerGain.setToolTipText( "Mixer Gain. Set master gain " - + "to MANUAL to enable adjustment" ); - - add( new JLabel( "Mixer:" ) ); - add( mComboMixerGain ); - - /* LNA Gain Control */ - mComboLNAGain = new JComboBox( R820TLNAGain.values() ); - mComboLNAGain.setEnabled( false ); - mComboLNAGain.addActionListener( new ActionListener() - { - @Override - public void actionPerformed( ActionEvent arg0 ) - { - try - { - R820TLNAGain lnaGain = - (R820TLNAGain)mComboLNAGain.getSelectedItem(); - - if ( lnaGain == null ) - { - lnaGain = DEFAULT_GAIN.getLNAGain(); - } - - if( mComboLNAGain.isEnabled() ) - { - mController.setLNAGain( lnaGain, true ); - } - - save(); - } - catch ( UsbException e ) - { - JOptionPane.showMessageDialog( R820TTunerEditor.this, - "R820T Tuner Controller - couldn't apply the LNA " - + "gain setting - " + e.getLocalizedMessage() ); - - mLog.error( "R820T Tuner Controller - couldn't apply LNA " - + "gain setting - ", e ); - } - } - } ); - mComboLNAGain.setToolTipText( "LNA Gain. Set master gain " - + "to MANUAL to enable adjustment" ); - - add( new JLabel( "LNA:" ) ); - add( mComboLNAGain ); - - /* VGA Gain Control */ - mComboVGAGain = new JComboBox( R820TVGAGain.values() ); - mComboVGAGain.setEnabled( false ); - mComboVGAGain.addActionListener( new ActionListener() - { - @Override - public void actionPerformed( ActionEvent arg0 ) - { - try - { - R820TVGAGain vgaGain = - (R820TVGAGain)mComboVGAGain.getSelectedItem(); - - if( vgaGain == null ) - { - vgaGain = DEFAULT_GAIN.getVGAGain(); - } - - if( mComboVGAGain.isEnabled() ) - { - mController.setVGAGain( vgaGain, true ); - } - - save(); - } - catch ( UsbException e ) - { - JOptionPane.showMessageDialog( R820TTunerEditor.this, - "R820T Tuner Controller - couldn't apply the VGA " - + "gain setting - " + e.getLocalizedMessage() ); - - mLog.error( "R820T Tuner Controller - couldn't apply VGA " - + "gain setting", e ); - } - } - } ); - mComboVGAGain.setToolTipText( "VGA Gain. Set master gain " - + "to MANUAL to enable adjustment" ); - add( new JLabel( "VGA:" ) ); - add( mComboVGAGain ); - } - - /** - * Sets each of the tuner configuration controls to the enabled argument state - */ - private void setControlsEnabled( boolean enabled ) - { - if( mConfigurationName.isEnabled() != enabled ) - { - mConfigurationName.setEnabled( enabled ); - } - - if( mTunerInfo.isEnabled() != enabled ) - { - mTunerInfo.setEnabled( enabled ); - } - - if( mFrequencyCorrection.isEnabled() != enabled ) - { - mFrequencyCorrection.setEnabled( enabled ); - } - - if( mComboSampleRate.isEnabled() != enabled ) - { - mComboSampleRate.setEnabled( enabled ); - } - - if( mComboMasterGain.isEnabled() != enabled ) - { - mComboMasterGain.setEnabled( enabled ); - } - - if( mComboMixerGain.isEnabled() != enabled ) - { - mComboMixerGain.setEnabled( enabled ); - } - - if( mComboLNAGain.isEnabled() != enabled ) - { - mComboLNAGain.setEnabled( enabled ); - } - - if( mComboVGAGain.isEnabled() != enabled ) - { - mComboVGAGain.setEnabled( enabled ); - } - } - - private String getTunerInfo() - { - StringBuilder sb = new StringBuilder(); - - Descriptor descriptor = mController.getDescriptor(); - - sb.append( "

RTL-2832 with R820T Tuner

" ); - - if( descriptor == null ) - { - sb.append( "No EEPROM Descriptor Available" ); - } - else - { - sb.append( "USB ID: " ); - sb.append( descriptor.getVendorID() ); - sb.append( ":" ); - sb.append( descriptor.getProductID() ); - sb.append( "
" ); - - sb.append( "Vendor: " ); - sb.append( descriptor.getVendorLabel() ); - sb.append( "
" ); - - sb.append( "Product: " ); - sb.append( descriptor.getProductLabel() ); - sb.append( "
" ); - - sb.append( "Serial: " ); - sb.append( descriptor.getSerial() ); - sb.append( "
" ); - - sb.append( "IR Enabled: " ); - sb.append( descriptor.irEnabled() ); - sb.append( "
" ); - - sb.append( "Remote Wake: " ); - sb.append( descriptor.remoteWakeupEnabled() ); - sb.append( "
" ); - } - - return sb.toString(); - } - - @Override - public void save() - { - if( hasItem() && !mLoading ) - { - R820TTunerConfiguration config = getConfiguration(); - - config.setName( mConfigurationName.getText() ); - - double value = ((SpinnerNumberModel)mFrequencyCorrection - .getModel()).getNumber().doubleValue(); - config.setFrequencyCorrection( value ); - - config.setSampleRate( (SampleRate)mComboSampleRate.getSelectedItem() ); - - R820TGain gain = (R820TGain)mComboMasterGain.getSelectedItem(); - config.setMasterGain( gain ); - - R820TMixerGain mixerGain = (R820TMixerGain)mComboMixerGain.getSelectedItem(); - config.setMixerGain( mixerGain ); - - R820TLNAGain lnaGain = (R820TLNAGain)mComboLNAGain.getSelectedItem(); - config.setLNAGain( lnaGain ); - - R820TVGAGain vgaGain = (R820TVGAGain)mComboVGAGain.getSelectedItem(); - config.setVGAGain( vgaGain ); - - getTunerConfigurationModel().broadcast( - new TunerConfigurationEvent( getConfiguration(), Event.CHANGE ) ); - } - } - - @Override - public void setItem( TunerConfiguration tunerConfiguration ) - { - super.setItem( tunerConfiguration ); - - //Toggle loading so that we don't fire a change event and schedule a settings file save - mLoading = true; - - if( hasItem() ) - { - R820TTunerConfiguration config = getConfiguration(); - - if( tunerConfiguration.isAssigned() ) - { - setControlsEnabled( true ); - mConfigurationName.setText( config.getName() ); - mFrequencyCorrection.setValue( config.getFrequencyCorrection() ); - mComboSampleRate.setSelectedItem( config.getSampleRate() ); - mComboMasterGain.setSelectedItem( config.getMasterGain() ); - mComboMixerGain.setSelectedItem( config.getMixerGain() ); - mComboLNAGain.setSelectedItem( config.getLNAGain() ); - mComboVGAGain.setSelectedItem( config.getVGAGain() ); - } - else - { - setControlsEnabled( false ); - mConfigurationName.setText( config.getName() ); - } - } - else - { - setControlsEnabled( false ); - mConfigurationName.setText( "" ); - } - - mLoading = false; - } -} diff --git a/src/source/tuner/usb/USBTransferProcessor.java b/src/source/tuner/usb/USBTransferProcessor.java deleted file mode 100644 index 60d6642a..00000000 --- a/src/source/tuner/usb/USBTransferProcessor.java +++ /dev/null @@ -1,434 +0,0 @@ -/******************************************************************************* - * 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 source.tuner.usb; - -import org.slf4j.Logger; -import org.slf4j.LoggerFactory; -import org.usb4java.DeviceHandle; -import org.usb4java.LibUsb; -import org.usb4java.LibUsbException; -import org.usb4java.Transfer; -import org.usb4java.TransferCallback; -import sample.Broadcaster; -import sample.Listener; -import sample.OverflowableTransferQueue; -import sample.adapter.ISampleAdapter; -import sample.complex.ComplexBuffer; -import sample.real.IOverflowListener; -import source.tuner.TunerManager; -import util.ThreadPool; - -import javax.usb.UsbException; -import java.nio.ByteBuffer; -import java.util.ArrayList; -import java.util.List; -import java.util.concurrent.LinkedTransferQueue; -import java.util.concurrent.ScheduledFuture; -import java.util.concurrent.TimeUnit; -import java.util.concurrent.atomic.AtomicBoolean; - -public class USBTransferProcessor implements TransferCallback -{ - private final static Logger mLog = LoggerFactory.getLogger(USBTransferProcessor.class); - - private static final byte USB_BULK_TRANSFER_ENDPOINT = (byte) 0x81; - private static final long USB_TIMEOUT_MS = 2000l; //milliseconds - - //Number of native byte buffers to allocate for transferring data from the USB device - private static final int TRANSFER_BUFFER_POOL_SIZE = 16; - - //Maximum number of filled buffers for the blocking queue - private static final int FILLED_BUFFER_MAX_CAPACITY = 500; - - //Threshold for resetting buffer overflow condition - private static final int FILLED_BUFFER_OVERFLOW_RESET_THRESHOLD = 100; - - private String mDeviceName; - - //Handle to the USB bulk transfer device - private DeviceHandle mDeviceHandle; - - //Tuner format-specific byte to IQ float sample converter - private ISampleAdapter mSampleAdapter; - - //Byte array transfer buffers size in bytes - private int mBufferSize; - - private OverflowableTransferQueue mFilledBuffers; - private LinkedTransferQueue mAvailableTransfers = new LinkedTransferQueue<>(); - private LinkedTransferQueue mTransfersInProgress = new LinkedTransferQueue<>(); - private Listener mComplexBufferListener; - - private AtomicBoolean mRunning = new AtomicBoolean(); - - private ByteBuffer mLibUsbHandlerStatus = ByteBuffer.allocateDirect(4); - - private BufferDispatcher mBufferDispatcher = new BufferDispatcher(); - private ScheduledFuture mBufferDispatcherFuture; - - /** - * Manages stream of USB transfer buffers and converts buffers to complex buffer samples for distribution to - * any registered listeners. - * - * @param deviceName to use when logging information or errors - * @param deviceHandle to the USB bulk transfer device - * @param sampleAdapter specific to the tuner's byte buffer format for converting to floating point I/Q samples - * @param bufferSize in bytes. Should be a multiple of two: 65536, 131072 or 262144. - */ - public USBTransferProcessor(String deviceName, - DeviceHandle deviceHandle, - ISampleAdapter sampleAdapter, - int bufferSize) - { - mDeviceName = deviceName; - mDeviceHandle = deviceHandle; - mSampleAdapter = sampleAdapter; - mBufferSize = bufferSize; - - mFilledBuffers = new OverflowableTransferQueue<>(FILLED_BUFFER_MAX_CAPACITY, FILLED_BUFFER_OVERFLOW_RESET_THRESHOLD); - mFilledBuffers.setOverflowListener(new IOverflowListener() - { - @Override - public void sourceOverflow(boolean overflow) - { - if(overflow) - { - mLog.debug(mDeviceName + " - buffer overflow - temporary pause until processing catches up"); - } - else - { - mLog.debug(mDeviceName + " - buffer overflow cleared - resuming normal processing"); - } - } - }); - } - - /** - * Start USB transfer buffer processing. Subsequent calls to this method after started will be ignored. - */ - private void start() - { - if(mRunning.compareAndSet(false, true)) - { - prepareDeviceStart(); - - prepareTransfers(); - - while(!mAvailableTransfers.isEmpty()) - { - Transfer transfer = mAvailableTransfers.poll(); - - if(transfer != null) - { - int result = LibUsb.submitTransfer(transfer); - - if(result == LibUsb.SUCCESS) - { - mTransfersInProgress.add(transfer); - } - else if(result == LibUsb.ERROR_PIPE) - { - int resetResult = LibUsb.clearHalt(mDeviceHandle, USB_BULK_TRANSFER_ENDPOINT); - - if(resetResult == LibUsb.SUCCESS) - { - int resubmitResult = LibUsb.submitTransfer(transfer); - - if(resubmitResult == LibUsb.SUCCESS) - { - mTransfersInProgress.add(transfer); - } - else - { - mLog.error(mDeviceName + " - error resubmitting transfer after endpoint clear halt"); - } - } - else - { - mLog.error(mDeviceName + " - unable to clear device endpoint halt"); - } - } - else - { - //TODO: broadcast to each listener that this source has an error and is shutting down - mLog.error(mDeviceName + "- error submitting transfer [" + LibUsb.errorName(result) + "]"); - } - } - } - - //Start transferred buffer dispatcher - mBufferDispatcherFuture = ThreadPool.SCHEDULED.scheduleAtFixedRate(mBufferDispatcher, - 0, 11, TimeUnit.MILLISECONDS); - - //Register with LibUSB processor so that it auto-starts LibUSB processing - TunerManager.LIBUSB_TRANSFER_PROCESSOR.registerTransferProcessor(this); - } - } - - /** - * Stop USB transfer buffer processing. Subsequent calls to this method after stopped will be ignored. - */ - private void stop() - { - if(mRunning.compareAndSet(true, false)) - { - mBufferDispatcherFuture.cancel(true); - - mFilledBuffers.clear(); - - //Cancel the lib usb process timer - for(Transfer transfer : mTransfersInProgress) - { - LibUsb.cancelTransfer(transfer); - } - - - //Unregister from LibUSB processor so that it auto-stops LibUSB processing - TunerManager.LIBUSB_TRANSFER_PROCESSOR.unregisterTransferProcessor(this); - - //Directly invoke the timeout handler to ensure that our cancelled transfer buffers are flushed. - int result = LibUsb.handleEventsTimeoutCompleted(null, USB_TIMEOUT_MS, - mLibUsbHandlerStatus.asIntBuffer()); - - if(result != LibUsb.SUCCESS) - { - mLog.error(mDeviceName + " - error while cancelling transfer buffers during shutdown/pause - error code:" + result); - } - - mLibUsbHandlerStatus.rewind(); - - executeDeviceStop(); - } - } - - /** - * Allows sub-class implementations to execute any device-specific operations to prepare for starting USB transfers - */ - protected void prepareDeviceStart() - { - - } - - /** - * Allows sub-class implementations to execute any device-specific operations after stopping USB transfers - */ - protected void executeDeviceStop() - { - - } - - /** - * Sets the listener and auto-starts the buffer processor - */ - public void setListener(Listener listener) - { - mComplexBufferListener = listener; - start(); - } - - /** - * Auto-stops the buffer processor and removes the listener - */ - public void removeListener() - { - if(mComplexBufferListener != null) - { - stop(); - mComplexBufferListener = null; - } - } - - /** - * Prepares (allocates) a set of transfer buffers for use in transferring data from the USB device via the bulk - * interface. Since we're using direct allocation (native), buffers are retained and reused across multiple - * start/stop cycles. - */ - private void prepareTransfers() throws LibUsbException - { - while(mAvailableTransfers.size() < TRANSFER_BUFFER_POOL_SIZE) - { - Transfer transfer = LibUsb.allocTransfer(); - - if(transfer == null) - { - throw new LibUsbException("Couldn't allocate USB transfer buffer", LibUsb.ERROR_NO_MEM); - } - - final ByteBuffer buffer = ByteBuffer.allocateDirect(mBufferSize); - - LibUsb.fillBulkTransfer(transfer, mDeviceHandle, USB_BULK_TRANSFER_ENDPOINT, buffer, this, - "Buffer", USB_TIMEOUT_MS); - - mAvailableTransfers.add(transfer); - } - } - - /** - * Process a filled transfer buffer received back from the USB device - */ - @Override - public void processTransfer(Transfer transfer) - { - mTransfersInProgress.remove(transfer); - - switch(transfer.status()) - { - case LibUsb.TRANSFER_COMPLETED: - case LibUsb.TRANSFER_STALL: - case LibUsb.TRANSFER_TIMED_OUT: - if(transfer.actualLength() > 0) - { - ByteBuffer buffer = transfer.buffer(); - - byte[] data = new byte[transfer.actualLength()]; - buffer.get(data); - buffer.rewind(); - - if(mRunning.get()) - { - mFilledBuffers.offer(data); - } - } - break; - case LibUsb.TRANSFER_CANCELLED: - break; - default: - //Unexpected transfer error - mLog.error(mDeviceName + " - transfer error [" + getTransferStatus(transfer.status()) + - "] transferred actual: " + transfer.actualLength()); - } - - - if(mRunning.get()) - { - int result = LibUsb.submitTransfer(transfer); - - if(result == LibUsb.SUCCESS) - { - mTransfersInProgress.add(transfer); - } - else if(result == LibUsb.ERROR_PIPE) - { - int resetResult = LibUsb.clearHalt(mDeviceHandle, USB_BULK_TRANSFER_ENDPOINT); - - if(resetResult == LibUsb.SUCCESS) - { - int resubmitResult = LibUsb.submitTransfer(transfer); - - if(resubmitResult == LibUsb.SUCCESS) - { - mTransfersInProgress.add(transfer); - } - else - { - mLog.error(mDeviceName + " - error resubmitting transfer after endpoint clear halt"); - } - } - else - { - mLog.error(mDeviceName + " - unable to clear device endpoint halt"); - } - } - else - { - mAvailableTransfers.add(transfer); - mLog.error(mDeviceName + " - error submitting transfer [" + LibUsb.errorName(result) + "]"); - - if(mTransfersInProgress.isEmpty()) - { - mLog.warn(mDeviceName + " - all transfer buffer processing is stopped"); - //TODO: no transfers are in progress ... need to alert the user and the registered complex - //TODO: buffer listeners so that they can respond accordingly - } - } - } - else - { - //We're stopping - park the transfer buffers - if(!mAvailableTransfers.contains(transfer)) - { - mAvailableTransfers.add(transfer); - } - } - } - - /** - * Converts the USB transfer status number into a descriptive label - */ - public static String getTransferStatus(int status) - { - switch(status) - { - case 0: - return "TRANSFER COMPLETED (0)"; - case 1: - return "TRANSFER ERROR (1)"; - case 2: - return "TRANSFER TIMED OUT (2)"; - case 3: - return "TRANSFER CANCELLED (3)"; - case 4: - return "TRANSFER STALL (4)"; - case 5: - return "TRANSFER NO DEVICE (5)"; - case 6: - return "TRANSFER OVERFLOW (6)"; - default: - return "UNKNOWN TRANSFER STATUS (" + status + ")"; - } - } - - /** - * Fetches byte[] chunks from the raw sample buffer. Converts each byte - * array and broadcasts the array to all registered listeners - */ - public class BufferDispatcher implements Runnable - { - private List mBuffersToDispatch = new ArrayList<>(); - - @Override - public void run() - { - mFilledBuffers.drainTo(mBuffersToDispatch, 50); - - while(!mBuffersToDispatch.isEmpty()) - { - try - { - for(byte[] buffer : mBuffersToDispatch) - { - float[] complexSamples = mSampleAdapter.convert(buffer); - - if(mComplexBufferListener != null) - { - mComplexBufferListener.receive(new ComplexBuffer(complexSamples)); - } - } - } - catch(Exception e) - { - mLog.error(mDeviceName + " - error while dispatching complex IQ buffer samples", e); - } - - mBuffersToDispatch.clear(); - mFilledBuffers.drainTo(mBuffersToDispatch, 50); - } - } - } -} diff --git a/src/source/wave/ComplexWaveSource.java b/src/source/wave/ComplexWaveSource.java deleted file mode 100644 index 5c76c847..00000000 --- a/src/source/wave/ComplexWaveSource.java +++ /dev/null @@ -1,285 +0,0 @@ -/******************************************************************************* - * 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 source.wave; - -import source.heartbeat.Heartbeat; -import org.slf4j.Logger; -import org.slf4j.LoggerFactory; -import sample.ConversionUtils; -import sample.Listener; -import sample.complex.ComplexBuffer; -import source.ComplexSource; -import source.IControllableFileSource; -import source.IFrameLocationListener; -import source.SourceEvent; - -import javax.sound.sampled.AudioFormat; -import javax.sound.sampled.AudioInputStream; -import javax.sound.sampled.AudioSystem; -import javax.sound.sampled.UnsupportedAudioFileException; -import java.io.File; -import java.io.IOException; -import java.util.Arrays; -import java.util.concurrent.ScheduledExecutorService; - -public class ComplexWaveSource extends ComplexSource implements IControllableFileSource -{ - private final static Logger mLog = - LoggerFactory.getLogger(ComplexWaveSource.class); - - private IFrameLocationListener mFrameLocationListener; - private int mBytesPerFrame; - private int mFrameCounter = 0; - private long mFrequency = 0; - private Listener mListener; - private AudioInputStream mInputStream; - private File mFile; - - public ComplexWaveSource(File file) throws IOException - { - mFile = file; - } - - @Override - public void setSourceEventListener(Listener listener) - { - //Not implemented - } - - @Override - public void removeSourceEventListener() - { - //Not implemented - } - - @Override - public Listener getSourceEventListener() - { - //Not implemented - return null; - } - - @Override - public void reset() - { - stop(); - start(); - } - - @Override - public void start() - { - try - { - open(); - } - catch(IOException | UnsupportedAudioFileException e) - { - mLog.error("Error starting complex wave source"); - } - } - - @Override - public void stop() - { - try - { - close(); - } - catch(IOException e) - { - mLog.error("Error stopping complex wave source"); - } - } - - @Override - public long getFrameCount() throws IOException - { - // TODO Auto-generated method stub - return 0; - } - - @Override - public double getSampleRate() - { - if(mInputStream != null) - { - return mInputStream.getFormat().getSampleRate(); - } - - return 0; - } - - /** - * Returns the frequency set for this file. Normally returns zero, but - * the value can be set with setFrequency() method. - */ - public long getFrequency() - { - return mFrequency; - } - - /** - * Changes the value returned from getFrequency() for this source. - */ - public void setFrequency(long frequency) - { - mFrequency = frequency; - } - - /** - * Closes the source file - */ - public void close() throws IOException - { - if(mInputStream != null) - { - mInputStream.close(); - } - else - { - throw new IOException("Can't close wave source - was not opened"); - } - - mInputStream = null; - } - - /** - * Opens the source file for reading - */ - public void open() throws IOException, UnsupportedAudioFileException - { - if(mInputStream == null) - { - mInputStream = AudioSystem.getAudioInputStream(mFile); - } - else - { - throw new IOException("Can't open wave source - is already opened"); - } - - - AudioFormat format = mInputStream.getFormat(); - - mBytesPerFrame = format.getFrameSize(); - - if(format.getChannels() != 2 || format.getSampleSizeInBits() != 16) - { - throw new IOException("Unsupported Wave Format - EXPECTED: 2 " + - "channels 16-bit samples FOUND: " + - mInputStream.getFormat().getChannels() + " channels " + - mInputStream.getFormat().getSampleSizeInBits() + "-bit samples"); - } - - /* Broadcast that we're at frame location 0 */ - broadcast(0); - } - - /** - * Reads the number of frames and sends a buffer to the listener - */ - @Override - public void next(int frames) throws IOException - { - next(frames, true); - } - - /** - * Reads the number of frames and optionally sends the buffer to the listener - */ - public void next(int frames, boolean broadcast) throws IOException - { - if(mInputStream != null) - { - byte[] buffer = new byte[mBytesPerFrame * frames]; - - /* Fill the buffer with samples from the file */ - int samplesRead = mInputStream.read(buffer); - - mFrameCounter += samplesRead; - - broadcast(mFrameCounter); - - if(broadcast && mListener != null) - { - if(samplesRead < buffer.length) - { - buffer = Arrays.copyOf(buffer, samplesRead); - } - - float[] samples = ConversionUtils - .convertFromSigned16BitSamples(buffer); - - mListener.receive(new ComplexBuffer(samples)); - } - } - } - - /** - * Registers the listener to receive sample buffers as they are read from - * the wave file - */ - @Override - public void setListener(Listener listener) - { - mListener = listener; - } - - /** - * Unregisters the listener from receiving sample buffers - */ - public void removeListener(Listener listener) - { - mListener = null; - } - - @Override - public void dispose() - { - mListener = null; - } - - @Override - public File getFile() - { - return mFile; - } - - private void broadcast(int byteLocation) - { - int frameLocation = (int)(byteLocation / mBytesPerFrame); - - if(mFrameLocationListener != null) - { - mFrameLocationListener.frameLocationUpdated(frameLocation); - } - } - - @Override - public void setListener(IFrameLocationListener listener) - { - mFrameLocationListener = listener; - } - - @Override - public void removeListener(IFrameLocationListener listener) - { - mFrameLocationListener = null; - } -} diff --git a/src/source/wave/RealWaveSource.java b/src/source/wave/RealWaveSource.java deleted file mode 100644 index c0698e6a..00000000 --- a/src/source/wave/RealWaveSource.java +++ /dev/null @@ -1,290 +0,0 @@ -/******************************************************************************* - * SDR Trunk - * Copyright (C) 2014,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 - ******************************************************************************/ -package source.wave; - -import source.heartbeat.Heartbeat; -import org.slf4j.Logger; -import org.slf4j.LoggerFactory; -import sample.ConversionUtils; -import sample.Listener; -import sample.real.RealBuffer; -import source.IControllableFileSource; -import source.IFrameLocationListener; -import source.RealSource; -import source.SourceEvent; - -import javax.sound.sampled.AudioFormat; -import javax.sound.sampled.AudioInputStream; -import javax.sound.sampled.AudioSystem; -import javax.sound.sampled.UnsupportedAudioFileException; -import java.io.File; -import java.io.IOException; -import java.util.Arrays; -import java.util.concurrent.ScheduledExecutorService; - -public class RealWaveSource extends RealSource implements IControllableFileSource, AutoCloseable -{ - private final static Logger mLog = LoggerFactory.getLogger(RealWaveSource.class); - - private IFrameLocationListener mFrameLocationListener; - private int mBytesPerFrame; - private int mFrameCounter = 0; - private long mFrequency = 0; - private Listener mListener; - private AudioInputStream mInputStream; - private File mFile; - - public RealWaveSource(File file) throws IOException - { - mFile = file; - } - - @Override - public void setSourceEventListener(Listener listener) - { - //Not implemented - } - - @Override - public void removeSourceEventListener() - { - //Not implemented - } - - @Override - public Listener getSourceEventListener() - { - //Not implemented - return null; - } - - @Override - public void reset() - { - stop(); - start(); - } - - - @Override - public void start() - { - try - { - open(); - } - catch(IOException | UnsupportedAudioFileException e) - { - mLog.error("Error starting real wave source", e); - } - } - - - @Override - public void stop() - { - try - { - close(); - } - catch(IOException e) - { - mLog.error("Error stopping real wave source", e); - } - } - - - @Override - public long getFrameCount() throws IOException - { - return 0; - } - - @Override - public double getSampleRate() - { - if(mInputStream != null) - { - return mInputStream.getFormat().getSampleRate(); - } - - return 0; - } - - /** - * Returns the frequency set for this file. Normally returns zero, but - * the value can be set with setFrequency() method. - */ - public long getFrequency() - { - return mFrequency; - } - - /** - * Changes the value returned from getFrequency() for this source. - */ - public void setFrequency(long frequency) - { - mFrequency = frequency; - } - - /** - * Closes the source file - */ - public void close() throws IOException - { - if(mInputStream != null) - { - mInputStream.close(); - } - else - { - throw new IOException("Can't close wave source - was not opened"); - } - - mInputStream = null; - } - - /** - * Opens the source file for reading - */ - public void open() throws IOException, UnsupportedAudioFileException - { - if(mInputStream == null) - { - mInputStream = AudioSystem.getAudioInputStream(mFile); - } - else - { - throw new IOException("Can't open wave source - is already opened"); - } - - - AudioFormat format = mInputStream.getFormat(); - - mBytesPerFrame = format.getFrameSize(); - - if(format.getChannels() != 1 || format.getSampleSizeInBits() != 16) - { - throw new IOException("Unsupported Wave Format - EXPECTED: 1 " + - "channel 16-bit samples FOUND: " + - mInputStream.getFormat().getChannels() + " channels " + - mInputStream.getFormat().getSampleSizeInBits() + "-bit samples"); - } - - /* Broadcast that we're at frame location 0 */ - broadcast(0); - } - - /** - * Reads the number of frames and sends a buffer to the listener - */ - @Override - public void next(int frames) throws IOException - { - next(frames, true); - } - - /** - * Reads the number of frames and optionally sends the buffer to the listener - */ - public void next(int frames, boolean broadcast) throws IOException - { - if(mInputStream != null) - { - byte[] buffer = new byte[mBytesPerFrame * frames]; - - /* Fill the buffer with samples from the file */ - int samplesRead = mInputStream.read(buffer); - - mFrameCounter += samplesRead; - - broadcast(mFrameCounter); - - if(broadcast && mListener != null) - { - if(samplesRead < buffer.length) - { - if(samplesRead == -1) - { - throw new IOException("End of recording"); - } - - buffer = Arrays.copyOf(buffer, samplesRead); - } - - float[] samples = ConversionUtils - .convertFromSigned16BitSamples(buffer); - - mListener.receive(new RealBuffer(samples)); - } - } - } - - /** - * Registers the listener to receive sample buffers as they are read from - * the wave file - */ - @Override - public void setListener(Listener listener) - { - mListener = listener; - } - - /** - * Unregisters the listener from receiving sample buffers - */ - public void removeListener(Listener listener) - { - mListener = null; - } - - @Override - public void dispose() - { - mListener = null; - } - - @Override - public File getFile() - { - return mFile; - } - - private void broadcast(int byteLocation) - { - int frameLocation = (int)(byteLocation / mBytesPerFrame); - - if(mFrameLocationListener != null) - { - mFrameLocationListener.frameLocationUpdated(frameLocation); - } - } - - @Override - public void setListener(IFrameLocationListener listener) - { - mFrameLocationListener = listener; - } - - @Override - public void removeListener(IFrameLocationListener listener) - { - mFrameLocationListener = null; - } -} diff --git a/src/spectrum/ChannelSpectrumPanel.java b/src/spectrum/ChannelSpectrumPanel.java deleted file mode 100644 index 3e280c60..00000000 --- a/src/spectrum/ChannelSpectrumPanel.java +++ /dev/null @@ -1,489 +0,0 @@ -package spectrum; - -import java.awt.Component; -import java.awt.event.ComponentEvent; -import java.awt.event.ComponentListener; -import java.awt.event.MouseEvent; -import java.awt.event.MouseListener; -import java.awt.event.MouseMotionListener; -import java.util.concurrent.atomic.AtomicBoolean; - -import javax.swing.JLayeredPane; -import javax.swing.JMenu; -import javax.swing.JPanel; -import javax.swing.JPopupMenu; -import javax.swing.JSeparator; -import javax.swing.SwingUtilities; - -import module.ProcessingChain; -import net.miginfocom.swing.MigLayout; -import sample.Listener; -import sample.SampleType; -import sample.complex.ComplexBuffer; -import sample.real.RealBuffer; -import settings.ColorSetting.ColorSettingName; -import settings.ColorSettingMenuItem; -import settings.Setting; -import settings.SettingChangeListener; -import settings.SettingsManager; -import source.SourceEvent; -import source.SourceEvent.Event; -import spectrum.converter.DFTResultsConverter; -import spectrum.converter.RealDecibelConverter; -import spectrum.menu.AveragingItem; -import spectrum.menu.DFTSizeItem; -import spectrum.menu.FFTWindowTypeItem; -import spectrum.menu.FrameRateItem; -import spectrum.menu.SmoothingItem; -import spectrum.menu.SmoothingTypeItem; -import controller.channel.Channel; -import controller.channel.ChannelEvent; -import controller.channel.ChannelEventListener; -import controller.channel.ChannelProcessingManager; -import dsp.filter.Filters; -import dsp.filter.Window.WindowType; -import dsp.filter.halfband.real.HalfBandFilter_RB_RB; -import dsp.filter.smoothing.SmoothingFilter.SmoothingType; - -public class ChannelSpectrumPanel extends JPanel - implements ChannelEventListener, - Listener, - SettingChangeListener, - SpectralDisplayAdjuster -{ - private static final long serialVersionUID = 1L; - - private DFTProcessor mDFTProcessor = new DFTProcessor( SampleType.REAL ); - private DFTResultsConverter mDFTConverter = new RealDecibelConverter(); - private JLayeredPane mLayeredPane; - private SpectrumPanel mSpectrumPanel; - private ChannelOverlayPanel mOverlayPanel; - - private Channel mCurrentChannel; - - private int mSampleBufferSize = 2400; - - private HalfBandFilter_RB_RB mDecimatingFilter = new HalfBandFilter_RB_RB( - Filters.FIR_HALF_BAND_31T_ONE_EIGHTH_FCO.getCoefficients(), 1.0f, true ); - - private AtomicBoolean mEnabled = new AtomicBoolean(); - - private SettingsManager mSettingsManager; - private ChannelProcessingManager mChannelProcessingManager; - - public ChannelSpectrumPanel( SettingsManager settingsManager, - ChannelProcessingManager channelProcessingManager ) - { - mSettingsManager = settingsManager; - mChannelProcessingManager = channelProcessingManager; - - if( mSettingsManager != null ) - { - mSettingsManager.addListener( this ); - } - - mSpectrumPanel = new SpectrumPanel( mSettingsManager ); - mSpectrumPanel.setAveraging( 1 ); - - mOverlayPanel = new ChannelOverlayPanel( mSettingsManager ); - mDFTProcessor.addConverter( mDFTConverter ); - mDFTConverter.addListener( mSpectrumPanel ); - - /* Set the DFTProcessor to the decimated 24kHz sample rate */ - mDFTProcessor.process(SourceEvent.sampleRateChange(24000.0)); - - initGui(); - } - - public void dispose() - { - setEnabled( false ); - - mDFTProcessor.dispose(); - - if( mSettingsManager != null ) - { - mSettingsManager.removeListener( this ); - } - - mSettingsManager = null; - mCurrentChannel = null; - mDFTProcessor = null; - mSpectrumPanel = null; - } - - public void setFrameRate( int framesPerSecond ) - { - mSampleBufferSize = (int)( 48000 / framesPerSecond ); - - mDFTProcessor.setFrameRate( framesPerSecond ); - } - - private void initGui() - { - setLayout( new MigLayout( "insets 0 0 0 0 ", - "[grow,fill]", - "[grow,fill]") ); - - mLayeredPane = new JLayeredPane(); - mLayeredPane.addComponentListener( new ResizeListener() ); - - MouseEventProcessor mouser = new MouseEventProcessor(); - - mOverlayPanel.addMouseListener( mouser ); - mOverlayPanel.addMouseMotionListener( mouser ); - - mLayeredPane.add( mSpectrumPanel, new Integer( 0 ), 0 ); - mLayeredPane.add( mOverlayPanel, new Integer( 1 ), 0 ); - - add( mLayeredPane ); - } - - public void setEnabled( boolean enabled ) - { - if( enabled && mEnabled.compareAndSet( false, true ) ) - { - start(); - } - else if( !enabled && mEnabled.compareAndSet( true, false ) ) - { - stop(); - } - } - - @Override - @SuppressWarnings( "incomplete-switch" ) - public void channelChanged( ChannelEvent event ) - { - switch( event.getEvent() ) - { - case NOTIFICATION_SELECTION_CHANGE: - //ChannelSelectionManager ensures that only 1 channel can be - //selected and any previously selected channel will be first - //deselected before we get a new selection event - if( event.getChannel().isSelected() ) - { - if( mCurrentChannel != null ) - { - stop(); - mCurrentChannel = null; - } - - mCurrentChannel = event.getChannel(); - - if( mEnabled.get() ) - { - start(); - } - } - else - { - stop(); - mCurrentChannel = null; - } - break; - case NOTIFICATION_PROCESSING_STOP: - if( event.getChannel() == mCurrentChannel ) - { - if( mEnabled.get() ) - { - stop(); - } - - mCurrentChannel = null; - } - break; - } - } - - private void start() - { - if( mEnabled.get() && mCurrentChannel != null && mCurrentChannel.getEnabled() ) - { - ProcessingChain processingChain = mChannelProcessingManager - .getProcessingChain( mCurrentChannel ); - - if( processingChain != null ) - { - processingChain.addRealBufferListener( this ); - - mDFTProcessor.start(); - } - } - } - - private void stop() - { - if( mCurrentChannel != null && mCurrentChannel.getEnabled() ) - { - ProcessingChain processingChain = mChannelProcessingManager - .getProcessingChain( mCurrentChannel ); - - if( processingChain != null ) - { - processingChain.removeRealBufferListener( this ); - } - } - - mDFTProcessor.stop(); - - mSpectrumPanel.clearSpectrum(); - } - - @Override - public void settingChanged( Setting setting ) - { - if( mSpectrumPanel != null ) - { - mSpectrumPanel.settingChanged( setting ); - } - if( mOverlayPanel != null ) - { - mOverlayPanel.settingChanged( setting ); - } - } - - @Override - public void settingDeleted( Setting setting ) - { - if( mSpectrumPanel != null ) - { - mSpectrumPanel.settingDeleted( setting ); - } - - if( mOverlayPanel != null ) - { - mOverlayPanel.settingDeleted( setting ); - } - } - - @Override - public void receive( RealBuffer buffer ) - { - RealBuffer decimated = mDecimatingFilter.filter( buffer ); - - //Hack: we're placing real samples in a complex buffer that the DFT - //processor is expecting. - mDFTProcessor.receive( new ComplexBuffer( decimated.getSamples() ) ); - } - - /** - * Monitors the sizing of the layered pane and resizes the spectrum and - * channel panels whenever the layered pane is resized - */ - public class ResizeListener implements ComponentListener - { - @Override - public void componentResized( ComponentEvent e ) - { - Component c = e.getComponent(); - - mSpectrumPanel.setBounds( 0, 0, c.getWidth(), c.getHeight() ); - mOverlayPanel.setBounds( 0, 0, c.getWidth(), c.getHeight() ); - } - - @Override - public void componentHidden( ComponentEvent arg0 ) {} - @Override - public void componentMoved( ComponentEvent arg0 ) {} - @Override - public void componentShown( ComponentEvent arg0 ) {} - } - - /** - * Mouse event handler for the channel panel. - */ - public class MouseEventProcessor implements MouseMotionListener, MouseListener - { - @Override - public void mouseMoved( MouseEvent event ) - { - update( event ); - } - @Override - public void mouseDragged( MouseEvent event ) - { - update( event ); - } - - private void update( MouseEvent event ) - { - if( event.getComponent() == mOverlayPanel ) - { - mOverlayPanel.setCursorLocation( event.getPoint() ); - } - } - - @Override - public void mouseEntered( MouseEvent e ) - { - if( e.getComponent() == mOverlayPanel ) - { - mOverlayPanel.setCursorVisible( true ); - } - } - - @Override - public void mouseExited( MouseEvent e ) - { - mOverlayPanel.setCursorVisible( false ); - } - - /** - * Displays the context menu. - */ - @Override - public void mouseClicked( MouseEvent event ) - { - if( SwingUtilities.isRightMouseButton( event ) ) - { - JPopupMenu contextMenu = new JPopupMenu(); - - /** - * Color Menus - */ - JMenu colorMenu = new JMenu( "Color" ); - - colorMenu.add( new ColorSettingMenuItem( mSettingsManager, - ColorSettingName.SPECTRUM_CURSOR ) ); - - colorMenu.add( new ColorSettingMenuItem( mSettingsManager, - ColorSettingName.SPECTRUM_LINE ) ); - - colorMenu.add( new ColorSettingMenuItem( mSettingsManager, - ColorSettingName.SPECTRUM_BACKGROUND ) ); - - colorMenu.add( new ColorSettingMenuItem( mSettingsManager, - ColorSettingName.SPECTRUM_GRADIENT_BOTTOM ) ); - - colorMenu.add( new ColorSettingMenuItem( mSettingsManager, - ColorSettingName.SPECTRUM_GRADIENT_TOP ) ); - - contextMenu.add( colorMenu ); - - /** - * Display items: fft and frame rate - */ - JMenu displayMenu = new JMenu( "Display" ); - contextMenu.add( displayMenu ); - - /** - * Averaging menu - */ - JMenu averagingMenu = new JMenu( "Averaging" ); - averagingMenu.add( - new AveragingItem( ChannelSpectrumPanel.this, 2 ) ); - displayMenu.add( averagingMenu ); - - /** - * FFT width - */ - JMenu fftWidthMenu = new JMenu( "FFT Width" ); - displayMenu.add( fftWidthMenu ); - - for( DFTSize width: DFTSize.values() ) - { - fftWidthMenu.add( new DFTSizeItem( mDFTProcessor, width ) ); - } - - /** - * DFT Processor Frame Rate - */ - JMenu frameRateMenu = new JMenu( "Frame Rate" ); - displayMenu.add( frameRateMenu ); - - frameRateMenu.add( new FrameRateItem( mDFTProcessor, 14 ) ); - frameRateMenu.add( new FrameRateItem( mDFTProcessor, 16 ) ); - frameRateMenu.add( new FrameRateItem( mDFTProcessor, 18 ) ); - frameRateMenu.add( new FrameRateItem( mDFTProcessor, 20 ) ); - frameRateMenu.add( new FrameRateItem( mDFTProcessor, 25 ) ); - frameRateMenu.add( new FrameRateItem( mDFTProcessor, 30 ) ); - frameRateMenu.add( new FrameRateItem( mDFTProcessor, 40 ) ); - frameRateMenu.add( new FrameRateItem( mDFTProcessor, 50 ) ); - - /** - * FFT Window Type - */ - JMenu fftWindowType = new JMenu( "Window Type" ); - displayMenu.add( fftWindowType ); - - for( WindowType type: WindowType.values() ) - { - fftWindowType.add( - new FFTWindowTypeItem( mDFTProcessor, type ) ); - } - - /** - * Smoothing menu - */ - JMenu smoothingMenu = new JMenu( "Smoothing" ); - - if( mSpectrumPanel.getSmoothingType() != SmoothingType.NONE ) - { - smoothingMenu.add( new SmoothingItem( ChannelSpectrumPanel.this, 5 ) ); - smoothingMenu.add( new JSeparator() ); - } - - smoothingMenu.add( new SmoothingTypeItem( ChannelSpectrumPanel.this, SmoothingType.GAUSSIAN ) ); - smoothingMenu.add( new SmoothingTypeItem( ChannelSpectrumPanel.this, SmoothingType.TRIANGLE ) ); - smoothingMenu.add( new SmoothingTypeItem( ChannelSpectrumPanel.this, SmoothingType.RECTANGLE ) ); - smoothingMenu.add( new SmoothingTypeItem( ChannelSpectrumPanel.this, SmoothingType.NONE ) ); - - displayMenu.add( smoothingMenu ); - - if( contextMenu != null ) - { - contextMenu.show( mOverlayPanel, - event.getX(), - event.getY() ); - } - } - } - - @Override - public void mousePressed( MouseEvent e ) {} - @Override - public void mouseReleased( MouseEvent e ) {} - } - - @Override - public int getAveraging() - { - return mSpectrumPanel.getAveraging(); - } - - @Override - public void setAveraging( int averaging ) - { - mSpectrumPanel.setAveraging( averaging ); - } - - public void setSampleSize( double sampleSize ) - { - mSpectrumPanel.setSampleSize( sampleSize ); - } - - @Override - public int getSmoothing() - { - return mSpectrumPanel.getSmoothing(); - } - - @Override - public void setSmoothing( int smoothing ) - { - mSpectrumPanel.setSmoothing( smoothing ); - } - - @Override - public SmoothingType getSmoothingType() - { - return mSpectrumPanel.getSmoothingType(); - } - - @Override - public void setSmoothingType( SmoothingType type ) - { - mSpectrumPanel.setSmoothingType( type ); - } -} diff --git a/src/spectrum/DFTProcessor.java b/src/spectrum/DFTProcessor.java deleted file mode 100644 index 73515b87..00000000 --- a/src/spectrum/DFTProcessor.java +++ /dev/null @@ -1,482 +0,0 @@ -/******************************************************************************* - * 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 spectrum; - -import dsp.filter.Window; -import dsp.filter.Window.WindowType; -import org.jtransforms.fft.FloatFFT_1D; -import org.slf4j.Logger; -import org.slf4j.LoggerFactory; -import properties.SystemProperties; -import sample.Buffer; -import sample.Listener; -import sample.SampleType; -import sample.complex.ComplexBuffer; -import source.SourceEvent; -import source.ISourceEventProcessor; -import spectrum.converter.DFTResultsConverter; -import util.ThreadPool; - -import java.util.Arrays; -import java.util.Iterator; -import java.util.concurrent.ArrayBlockingQueue; -import java.util.concurrent.BlockingQueue; -import java.util.concurrent.CopyOnWriteArrayList; -import java.util.concurrent.ScheduledFuture; -import java.util.concurrent.TimeUnit; -import java.util.concurrent.atomic.AtomicBoolean; - -/** - * Processes both complex samples or float samples and dispatches a float array - * of DFT results, using configurable fft size and output dispatch timelines. - */ -public class DFTProcessor implements Listener, ISourceEventProcessor, IDFTWidthChangeProcessor -{ - private final static Logger mLog = LoggerFactory.getLogger(DFTProcessor.class); - - private CopyOnWriteArrayList mListeners = new CopyOnWriteArrayList(); - - //Fixed size sample queue. We set the size at 12 based on the highest - //sample rate tuner (10 MHz) producing 153 frames per second and the lowest - //DFT processing frame rate of 12 frames per second. This means that the - //maximum size needed is 153 / 12 = 11 frames per DFT processing iteration - //so we set it at 12. - private BlockingQueue mQueue = new ArrayBlockingQueue<>(12); - - private ScheduledFuture mProcessorTaskHandle; - - public static final String FRAME_RATE_PROPERTY = "spectral.display.frame.rate"; - - private DFTSize mDFTSize = DFTSize.FFT04096; - private DFTSize mNewDFTSize = DFTSize.FFT04096; - - private double[] mWindow; - - /* The Cosine and Hanning windows seem to offer the best spectral display - * with minimal bin leakage/smearing */ - private WindowType mWindowType = WindowType.BLACKMAN_HARRIS_7; - - private FloatFFT_1D mFFT = new FloatFFT_1D(mDFTSize.getSize()); - - private int mFrameRate; - private int mSampleRate; - private int mFFTFloatsPerFrame; - private float mNewFloatsPerFrame; - private float mNewFloatResidual; - private float[] mPreviousFrame = new float[8192]; - - private float[] mCurrentBuffer; - private int mCurrentBufferPointer = 0; - - private SampleType mSampleType; - - private AtomicBoolean mRunning = new AtomicBoolean(); - - public DFTProcessor(SampleType sampleType) - { - setSampleType(sampleType); - mFrameRate = SystemProperties.getInstance().get(FRAME_RATE_PROPERTY, 20); - calculateConsumptionRate(); - start(); - } - - public void dispose() - { - stop(); - - mListeners.clear(); - mQueue.clear(); - mWindow = null; - mCurrentBuffer = null; - } - - public WindowType getWindowType() - { - return mWindowType; - } - - public void setWindowType(WindowType windowType) - { - mWindowType = windowType; - - if(mSampleType == SampleType.COMPLEX) - { - mWindow = Window.getWindow(mWindowType, mDFTSize.getSize() * 2); - } - else - { - mWindow = Window.getWindow(mWindowType, mDFTSize.getSize()); - } - } - - /** - * Sets the processor mode to Float or Complex, depending on the sample - * types that will be delivered for processing - */ - public void setSampleType(SampleType type) - { - mSampleType = type; - setWindowType(mWindowType); - } - - public SampleType getSampleType() - { - return mSampleType; - } - - /** - * Queues an FFT size change request. The scheduled executor will apply - * the change when it runs. - */ - public void setDFTSize(DFTSize size) - { - mNewDFTSize = size; - } - - public DFTSize getDFTSize() - { - return mDFTSize; - } - - public int getFrameRate() - { - return mFrameRate; - } - - public void setFrameRate(int framesPerSecond) - { - //TODO: make sure frame rate & sample rate sample requirement doesn't - //expect overlap greater than the previous frame length - - if(framesPerSecond < 1 || framesPerSecond > 1000) - { - throw new IllegalArgumentException("DFTProcessor cannot run " - + "more than 1000 times per second -- requested setting:" - + framesPerSecond); - } - - mFrameRate = framesPerSecond; - - SystemProperties.getInstance().set(FRAME_RATE_PROPERTY, mFrameRate); - - calculateConsumptionRate(); - - restart(); - } - - public void start() - { - //Schedule the DFT to run calculations at a fixed rate - int initialDelay = 0; - int period = (int) (1000 / mFrameRate); - - mProcessorTaskHandle = ThreadPool.SCHEDULED.scheduleAtFixedRate(new DFTCalculationTask(), initialDelay, period, - TimeUnit.MILLISECONDS); - } - - public void stop() - { - //Cancel running DFT calculation task - if(mProcessorTaskHandle != null) - { - mProcessorTaskHandle.cancel(true); - } - } - - public void restart() - { - stop(); - start(); - } - - public int getCalculationsPerSecond() - { - return mFrameRate; - } - - /** - * Places the sample into a transfer queue for future processing. - */ - @Override - public void receive(ComplexBuffer sampleBuffer) - { - mQueue.offer(sampleBuffer); - } - - private void getNextBuffer() - { - mCurrentBuffer = null; - - try - { - Buffer buffer = mQueue.take(); - mCurrentBuffer = buffer.getSamples(); - } - catch(InterruptedException e) - { - mCurrentBuffer = null; - } - - mCurrentBufferPointer = 0; - } - - private float[] getSamples() - { - int remaining = (int) mFFTFloatsPerFrame; - - float[] currentFrame = new float[remaining]; - - int currentFramePointer = 0; - - float integralFloatsToConsume = mNewFloatsPerFrame + mNewFloatResidual; - - int newFloatsToConsumeThisFrame = (int) integralFloatsToConsume; - - mNewFloatResidual = integralFloatsToConsume - newFloatsToConsumeThisFrame; - - /* If the number of required floats for the fft is greater than the - * consumption rate per frame, we have to reach into the previous - * frame to makeup the difference. */ - if(newFloatsToConsumeThisFrame < remaining) - { - int previousFloatsRequired = remaining - newFloatsToConsumeThisFrame; - - System.arraycopy(mPreviousFrame, - mPreviousFrame.length - previousFloatsRequired, - currentFrame, - currentFramePointer, - previousFloatsRequired); - - remaining -= previousFloatsRequired; - currentFramePointer += previousFloatsRequired; - } - - /* Fill the rest of the buffer with new samples */ - while(mRunning.get() && remaining > 0) - { - if(mCurrentBuffer == null || - mCurrentBufferPointer >= mCurrentBuffer.length) - { - getNextBuffer(); - } - - /* If we don't have new samples to use, send the current frame with - * the remaining values as zero */ - if(mCurrentBuffer == null) - { - remaining = 0; - } - else - { - int samplesAvailable = mCurrentBuffer.length - mCurrentBufferPointer; - - while(remaining > 0 && samplesAvailable > 0) - { - currentFrame[currentFramePointer++] = - (float) mCurrentBuffer[mCurrentBufferPointer++]; - - samplesAvailable--; - remaining--; - newFloatsToConsumeThisFrame--; - } - } - } - - /* If the incoming float rate is greater than the fft consumption rate, - * then we have to purge some floats, otherwise, store the previous - * frame, because we have overlapping frames */ - if(newFloatsToConsumeThisFrame > 0) - { - purge(newFloatsToConsumeThisFrame); - } - else - { - mPreviousFrame = Arrays.copyOf(currentFrame, currentFrame.length); - } - - return currentFrame; - } - - private void calculate() - { - float[] samples = getSamples(); - - Window.apply(mWindow, samples); - - if(mSampleType == SampleType.REAL) - { - mFFT.realForward(samples); - } - else - { - mFFT.complexForward(samples); - } - - dispatch(samples); - } - - - private void purge(int samplesToPurge) - { - if(samplesToPurge <= 0) - { - throw new IllegalArgumentException("DFTProcessor - cannot purge " - + "negative sample amount"); - } - - while(mRunning.get() && samplesToPurge > 0) - { - if(mCurrentBuffer == null || - mCurrentBufferPointer >= mCurrentBuffer.length) - { - getNextBuffer(); - } - - if(mCurrentBuffer != null) - { - int samplesAvailable = mCurrentBuffer.length - mCurrentBufferPointer; - - if(samplesAvailable >= samplesToPurge) - { - mCurrentBufferPointer += samplesToPurge; - samplesToPurge = 0; - } - else - { - samplesToPurge -= samplesAvailable; - mCurrentBufferPointer = mCurrentBuffer.length; - } - } - else - { - samplesToPurge = 0; - } - } - } - - /** - * Takes a calculated DFT results set, reformats the data, and sends it - * out to all registered listeners. - */ - private void dispatch(float[] results) - { - Iterator it = mListeners.iterator(); - - while(it.hasNext()) - { - it.next().receive(results); - } - } - - public void addConverter(DFTResultsConverter listener) - { - mListeners.add(listener); - } - - public void removeConverter(DFTResultsConverter listener) - { - mListeners.remove(listener); - } - - private class DFTCalculationTask implements Runnable - { - @Override - public void run() - { - try - { - /* Only run if we're not currently running */ - if(mRunning.compareAndSet(false, true)) - { - checkFFTSize(); - - calculate(); - - mRunning.set(false); - } - } - catch(Exception e) - { - mLog.error("error during dft processor calculation task", e); - } - } - } - - /** - * Checks for a queued FFT width change request and applies it. This - * method will only be accessed by the scheduled executor that gains - * access to run a calculate method, thus providing thread safety. - */ - private void checkFFTSize() - { - if(mNewDFTSize.getSize() != mDFTSize.getSize()) - { - mDFTSize = mNewDFTSize; - - calculateConsumptionRate(); - - setWindowType(mWindowType); - - if(mSampleType == SampleType.COMPLEX) - { - mPreviousFrame = new float[mDFTSize.getSize() * 2]; - } - else - { - mPreviousFrame = new float[mDFTSize.getSize()]; - } - - mFFT = new FloatFFT_1D(mDFTSize.getSize()); - } - } - - public void clearBuffer() - { - mQueue.clear(); - } - - @Override - public void process(SourceEvent event) - { - switch(event.getEvent()) - { - case NOTIFICATION_SAMPLE_RATE_CHANGE: - mSampleRate = event.getValue().intValue(); - calculateConsumptionRate(); - break; - default: - break; - } - } - - /** - * - */ - private void calculateConsumptionRate() - { - mNewFloatResidual = 0.0f; - - mNewFloatsPerFrame = ((float) mSampleRate / (float) mFrameRate) * - (mSampleType == SampleType.COMPLEX ? 2.0f : 1.0f); - - mFFTFloatsPerFrame = (mSampleType == SampleType.COMPLEX ? - mDFTSize.getSize() * 2 : - mDFTSize.getSize()); - } -} diff --git a/src/spectrum/OverlayPanel.java b/src/spectrum/OverlayPanel.java deleted file mode 100644 index dc211bae..00000000 --- a/src/spectrum/OverlayPanel.java +++ /dev/null @@ -1,913 +0,0 @@ -/******************************************************************************* - * 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 spectrum; - -import controller.channel.Channel; -import controller.channel.Channel.ChannelType; -import controller.channel.ChannelEvent; -import controller.channel.ChannelEventListener; -import controller.channel.ChannelModel; -import org.slf4j.Logger; -import org.slf4j.LoggerFactory; -import settings.ColorSetting; -import settings.ColorSetting.ColorSettingName; -import settings.Setting; -import settings.SettingChangeListener; -import settings.SettingsManager; -import source.tuner.channel.TunerChannel; -import source.SourceEvent; -import source.ISourceEventProcessor; - -import javax.swing.*; -import java.awt.*; -import java.awt.event.ComponentEvent; -import java.awt.event.ComponentListener; -import java.awt.geom.Line2D; -import java.awt.geom.Rectangle2D; -import java.text.DecimalFormat; -import java.util.ArrayList; -import java.util.concurrent.CopyOnWriteArrayList; - -public class OverlayPanel extends JPanel implements ChannelEventListener, ISourceEventProcessor, - SettingChangeListener -{ - private static final long serialVersionUID = 1L; - - private final static Logger mLog = LoggerFactory.getLogger(OverlayPanel.class); - private final DecimalFormat PPM_FORMATTER = new DecimalFormat( "#.0" ); - - private final static RenderingHints RENDERING_HINTS = new RenderingHints(RenderingHints.KEY_ANTIALIASING, - RenderingHints.VALUE_ANTIALIAS_ON); - - static - { - RENDERING_HINTS.put(RenderingHints.KEY_RENDERING, RenderingHints.VALUE_RENDER_QUALITY); - } - - private final static BasicStroke DASHED_STROKE = new BasicStroke(0.8f, BasicStroke.CAP_BUTT, - BasicStroke.JOIN_MITER, 5.0f, new float[]{2.0f, 4.0f}, 0.0f); - - private static DecimalFormat CURSOR_FORMAT = new DecimalFormat("000.00000"); - private long mFrequency = 0; - private int mBandwidth = 0; - private Point mCursorLocation = new Point(0, 0); - private boolean mCursorVisible = false; - - private DFTSize mDFTSize = DFTSize.FFT04096; - private int mZoom = 0; - private int mDFTZoomWindowOffset = 0; - - /** - * Colors used by this component - */ - private Color mColorChannelConfig; - private Color mColorChannelConfigProcessing; - private Color mColorChannelConfigSelected; - private Color mColorSpectrumBackground; - private Color mColorSpectrumCursor; - private Color mColorSpectrumLine; - - //Currently visible/displayable channels - private CopyOnWriteArrayList mVisibleChannels = new CopyOnWriteArrayList(); - private ChannelDisplay mChannelDisplay = ChannelDisplay.ALL; - - //Defines the offset at the bottom of the spectral display to account for - //the frequency labels - private double mSpectrumInset = 20.0d; - private LabelSizeManager mLabelSizeMonitor = new LabelSizeManager(); - - private SettingsManager mSettingsManager; - private ChannelModel mChannelModel; - - /** - * Translucent overlay panel for displaying channel configurations, - * processing channels, selected channels, frequency labels and lines, and - * a cursor with a frequency readout. - */ - public OverlayPanel(SettingsManager settingsManager, ChannelModel channelModel) - { - mSettingsManager = settingsManager; - - if(mSettingsManager != null) - { - mSettingsManager.addListener(this); - } - - mChannelModel = channelModel; - - if(mChannelModel != null) - { - mChannelModel.addListener(this); - } - - addComponentListener(mLabelSizeMonitor); - - //Set the background transparent, so the spectrum display can be seen - setOpaque(false); - - //Fetch color settings from settings manager - setColors(); - } - - public void dispose() - { - if(mChannelModel != null) - { - mChannelModel.removeListener(this); - } - - mChannelModel = null; - - mVisibleChannels.clear(); - - if(mSettingsManager != null) - { - mSettingsManager.removeListener(this); - } - - mSettingsManager = null; - } - - /** - * Sets/changes the DFT bin size - */ - public void setDFTSize(DFTSize size) - { - mDFTSize = size; - } - - public ChannelDisplay getChannelDisplay() - { - return mChannelDisplay; - } - - public void setChannelDisplay(ChannelDisplay display) - { - mChannelDisplay = display; - } - - public void setCursorLocation(Point point) - { - mCursorLocation = point; - - repaint(); - } - - public void setCursorVisible(boolean visible) - { - mCursorVisible = visible; - - repaint(); - } - - /** - * Sets the current zoom level (2^zoom) - * - * 0 No Zoom - * 1 2x Zoom - * 2 4x Zoom - * 3 8x Zoom - * 4 16x Zoom - * 5 32x Zoom - * 6 64x Zoom - * - * @param zoom level, 0 - 6. - */ - public void setZoom(int zoom) - { - mZoom = zoom; - - mLabelSizeMonitor.update(); - } - - public void setZoomWindowOffset(int offset) - { - mDFTZoomWindowOffset = offset; - } - - /** - * Fetches the color settings from the settings manager - */ - private void setColors() - { - mColorChannelConfig = getColor(ColorSettingName.CHANNEL_CONFIG); - - mColorChannelConfigProcessing = getColor(ColorSettingName.CHANNEL_CONFIG_PROCESSING); - - mColorChannelConfigSelected = getColor(ColorSettingName.CHANNEL_CONFIG_SELECTED); - - mColorSpectrumCursor = getColor(ColorSettingName.SPECTRUM_CURSOR); - - mColorSpectrumLine = getColor(ColorSettingName.SPECTRUM_LINE); - - mColorSpectrumBackground = getColor(ColorSettingName.SPECTRUM_BACKGROUND); - } - - /** - * Fetches a named color setting from the settings manager. If the setting - * doesn't exist, creates the setting using the defaultColor - */ - private Color getColor(ColorSettingName name) - { - ColorSetting setting = mSettingsManager.getColorSetting(name); - - return setting.getColor(); - } - - /** - * Monitors for setting changes. Colors can be changed by external actions - * and will automatically update in this class - */ - @Override - public void settingChanged(Setting setting) - { - if(setting instanceof ColorSetting) - { - ColorSetting colorSetting = (ColorSetting)setting; - - switch(colorSetting.getColorSettingName()) - { - case CHANNEL_CONFIG: - mColorChannelConfig = colorSetting.getColor(); - break; - case CHANNEL_CONFIG_PROCESSING: - mColorChannelConfigProcessing = colorSetting.getColor(); - break; - case CHANNEL_CONFIG_SELECTED: - mColorChannelConfigSelected = colorSetting.getColor(); - break; - case SPECTRUM_BACKGROUND: - mColorSpectrumBackground = colorSetting.getColor(); - break; - case SPECTRUM_CURSOR: - mColorSpectrumCursor = colorSetting.getColor(); - break; - case SPECTRUM_LINE: - mColorSpectrumLine = colorSetting.getColor(); - break; - default: - break; - } - } - } - - /** - * Renders the channel configs, lines, labels, and cursor - */ - @Override - public void paintComponent(Graphics g) - { - super.paintComponent(g); - - Graphics2D graphics = (Graphics2D)g; - graphics.setBackground(mColorSpectrumBackground); - - graphics.setRenderingHints(RENDERING_HINTS); - - drawFrequencies(graphics); - drawChannels(graphics); - drawCursor(graphics); - } - - /** - * Draws a cursor on the panel, whenever the mouse is hovering over the - * panel - */ - private void drawCursor(Graphics2D graphics) - { - if(mCursorVisible) - { - drawFrequencyLine(graphics, mCursorLocation.x, mColorSpectrumCursor); - - String frequency = CURSOR_FORMAT.format(getFrequencyFromAxis(mCursorLocation.getX()) / 1E6D); - - FontMetrics fontMetrics = graphics.getFontMetrics(this.getFont()); - - Rectangle2D rect = fontMetrics.getStringBounds(frequency, graphics); - - if(mCursorLocation.y > rect.getHeight()) - { - graphics.drawString(frequency, mCursorLocation.x + 5, mCursorLocation.y); - } - - if(mZoom != 0) - { - graphics.drawString("Zoom: " + (int)Math.pow(2.0, mZoom) + "x", mCursorLocation.x + 17, - mCursorLocation.y + 11); - } - } - } - - /** - * Draws the frequency lines and labels every 10kHz - */ - private void drawFrequencies(Graphics2D graphics) - { - Stroke currentStroke = graphics.getStroke(); - - long minFrequency = getMinDisplayFrequency(); - long maxFrequency = getMaxDisplayFrequency(); - - //Frequency increments for label and tick spacing - int label = mLabelSizeMonitor.getLabelIncrement(graphics); - int major = mLabelSizeMonitor.getMajorTickIncrement(graphics); - int minor = mLabelSizeMonitor.getMinorTickIncrement(graphics); - - //Avoid divide by zero error - if(minor == 0) - { - minor = 1; - } - - //Adjust the start frequency to a multiple of the minor tick spacing - long frequency = minFrequency - (minFrequency % minor); - - while(frequency < maxFrequency) - { - if(frequency % label == 0) - { - drawFrequencyLineAndLabel(graphics, frequency); - } - else if(frequency % major == 0) - { - drawTickLine(graphics, frequency, true); - } - else - { - drawTickLine(graphics, frequency, false); - } - - frequency += minor; - } - } - - /** - * Draws a vertical line and a corresponding frequency label at the bottom - */ - private void drawFrequencyLineAndLabel(Graphics2D graphics, long frequency) - { - double xAxis = getAxisFromFrequency(frequency); - - drawFrequencyLine(graphics, xAxis, mColorSpectrumLine); - - drawTickLine(graphics, frequency, false); - - graphics.setColor(mColorSpectrumLine); - - drawFrequencyLabel(graphics, xAxis, frequency); - } - - /** - * Draws a vertical line at the xaxis - */ - private void drawTickLine(Graphics2D graphics, long frequency, boolean major) - { - graphics.setColor(mColorSpectrumLine); - - double xAxis = getAxisFromFrequency(frequency); - - double start = getSize().getHeight() - mSpectrumInset; - double end = start + (major ? 9.0d : 3.0d); - - graphics.draw(new Line2D.Double(xAxis, start, xAxis, end)); - } - - - /** - * Draws a vertical line at the xaxis - */ - private void drawFrequencyLine(Graphics2D graphics, double xaxis, Color color) - { - graphics.setColor(color); - - graphics.draw(new Line2D.Double(xaxis, 0.0d, xaxis, getSize().getHeight() - mSpectrumInset)); - } - - /** - * Draws a vertical line at the xaxis - */ - private void drawChannelCenterLine(Graphics2D graphics, double xaxis) - { - double height = getSize().getHeight() - mSpectrumInset; - - graphics.setColor(Color.LIGHT_GRAY); - - graphics.draw(new Line2D.Double(xaxis, height * 0.65d, xaxis, height - 1.0d)); - } - - /** - * Draws the Automatic Frequency Control (AFC) channel center offset - */ - private void drawAFC(Graphics2D graphics, double frequencyAxis, double errorAxis, double bandwidth, - int correction, long frequency) - { - double height = getSize().getHeight() - mSpectrumInset; - double verticalAxisTop = height * 0.88d; - double verticalAxisBottom = height * 0.98d; - - double halfBandwidth = bandwidth / 2.0; - double errorEdgeStart = errorAxis - halfBandwidth; - double errorEdgeStop = errorAxis + halfBandwidth; - - graphics.setColor(Color.YELLOW); - - //Horizontal line connecting frequency and error line - graphics.draw(new Line2D.Double(errorEdgeStart, verticalAxisBottom, errorEdgeStop, verticalAxisBottom)); - - //Vertical band edge lines - graphics.draw(new Line2D.Double(errorEdgeStart, verticalAxisTop, errorEdgeStart, verticalAxisBottom)); - graphics.draw(new Line2D.Double(errorEdgeStop, verticalAxisTop, errorEdgeStop, verticalAxisBottom)); - - double ppm = (double)correction / ((double)frequency / 1E6d); - - String label = "PPM " + PPM_FORMATTER.format(ppm) ; - - FontMetrics fontMetrics = graphics.getFontMetrics(this.getFont()); - - Rectangle2D rect = fontMetrics.getStringBounds(label, graphics); - - //Only render the correction value label if the spacing is large enough - if(rect.getWidth() <= bandwidth && rect.getHeight() * 5 <= height) - { - graphics.drawString(label, (float)(errorEdgeStart + 1.0), (float)(verticalAxisBottom - 2.0)); - } - } - - /** - * Returns the x-axis value corresponding to the frequency - */ - private double getAxisFromFrequency(long frequency) - { - double screenWidth = (double)getSize().getWidth(); - - double pixelsPerBin = screenWidth / (double)mDFTSize.getSize(); - - double pixelOffsetToMinDisplayFrequency = pixelsPerBin * 2.0d; - - //Calculate frequency offset from the min frequency - double frequencyOffset = (double)(frequency - getMinDisplayFrequency()); - - //Determine ratio of frequency offset to overall bandwidth - double ratio = frequencyOffset / (double)getDisplayBandwidth(); - - //Apply the ratio to the screen width minus 1 bin width - double screenOffset = screenWidth * ratio; - - return pixelOffsetToMinDisplayFrequency + screenOffset; - } - - /** - * Returns the frequency corresponding to the x-axis value using the current - * zoom level. - */ - public long getFrequencyFromAxis(double xAxis) - { - double width = getSize().getWidth(); - - double offset = xAxis / width; - - long frequency = getMinDisplayFrequency() + Math.round((double)getDisplayBandwidth() * offset); - - if(frequency > (getMaxFrequency())) - { - frequency = getMaxFrequency(); - } - - return frequency; - } - - /** - * Draws a frequency label at the x-axis position, at the bottom of the panel - */ - private void drawFrequencyLabel(Graphics2D graphics, double xaxis, long frequency) - { - String label = mLabelSizeMonitor.format(frequency); - - FontMetrics fontMetrics = graphics.getFontMetrics(this.getFont()); - - Rectangle2D rect = fontMetrics.getStringBounds(label, graphics); - - float xOffset = (float)rect.getWidth() / 2; - - graphics.drawString(label, (float)(xaxis - xOffset), (float)(getSize().getHeight() - 2.0f)); - } - - - /** - * Draws visible channel configs as translucent shaded frequency regions - */ - private void drawChannels(Graphics2D graphics) - { - for(Channel channel : mVisibleChannels) - { - if(mChannelDisplay == ChannelDisplay.ALL || - (mChannelDisplay == ChannelDisplay.ENABLED && channel.getEnabled())) - { - //Choose the correct background color to use - if(channel.isSelected()) - { - graphics.setColor(mColorChannelConfigSelected); - } - else if(channel.getEnabled()) - { - graphics.setColor(mColorChannelConfigProcessing); - } - else - { - graphics.setColor(mColorChannelConfig); - } - - TunerChannel tunerChannel = channel.getTunerChannel(); - - if(tunerChannel != null) - { - double xAxis = getAxisFromFrequency(tunerChannel.getFrequency()); - - double width = (double)(tunerChannel.getBandwidth()) / (double)getDisplayBandwidth() * getSize().getWidth(); - - Rectangle2D.Double box = - new Rectangle2D.Double(xAxis - (width / 2.0d), 0.0d, width, getSize().getHeight() - mSpectrumInset); - - //Fill the box with the correct color - graphics.fill(box); - - graphics.draw(box); - - //Change to the line color to render the channel name, etc. - graphics.setColor(mColorSpectrumLine); - - //Draw the labels starting at yAxis position 0 - double yAxis = 0; - - //Draw the system label and adjust the y-axis position - String system = channel.hasSystem() ? channel.getSystem() : " "; - - yAxis += drawLabel(graphics, system, this.getFont(), xAxis, yAxis, width); - - //Draw the site label and adjust the y-axis position - String site = channel.hasSite() ? channel.getSite() : " "; - - yAxis += drawLabel(graphics, site, this.getFont(), xAxis, yAxis, width); - - //Draw the channel label and adjust the y-axis position - yAxis += drawLabel(graphics, channel.getName(), this.getFont(), xAxis, yAxis, width); - - //Draw the decoder label - drawLabel(graphics, channel.getDecodeConfiguration().getDecoderType().getShortDisplayString(), - this.getFont(), xAxis, yAxis, width); - - long frequency = tunerChannel.getFrequency(); - - double frequencyAxis = getAxisFromFrequency(frequency); - - drawChannelCenterLine(graphics, frequencyAxis); - - /* Draw Automatic Frequency Control line */ - int correction = channel.getChannelFrequencyCorrection(); - - if(correction != 0) - { - long error = frequency + correction; - - drawAFC(graphics, frequencyAxis, getAxisFromFrequency(error), width, correction, - tunerChannel.getFrequency()); - } - } - } - } - } - - - /** - * Draws a textual label at the x/y position, clipping the end of the text - * to fit within the maxwidth value. - * - * @return height of the drawn label - */ - private double drawLabel(Graphics2D graphics, String text, Font font, double x, double baseY, double maxWidth) - { - FontMetrics fontMetrics = graphics.getFontMetrics(font); - - Rectangle2D label = fontMetrics.getStringBounds(text, graphics); - - double offset = label.getWidth() / 2.0d; - double y = baseY + label.getHeight(); - - /** - * If the label is wider than the max width, left justify the text and - * clip the end of it - */ - if(offset > (maxWidth / 2.0d)) - { - label.setRect(x - (maxWidth / 2.0d), y - label.getHeight(), maxWidth, label.getHeight()); - - graphics.setClip(label); - - graphics.drawString(text, (float)(x - (maxWidth / 2.0d)), (float)y); - - graphics.setClip(null); - } - else - { - graphics.drawString(text, (float)(x - offset), (float)y); - } - - return label.getHeight(); - } - - /** - * Frequency change event handler - */ - @Override - public void process(SourceEvent event) - { - switch(event.getEvent()) - { - case NOTIFICATION_SAMPLE_RATE_CHANGE: - mBandwidth = event.getValue().intValue(); - mLabelSizeMonitor.update(); - break; - case NOTIFICATION_FREQUENCY_CHANGE: - mFrequency = event.getValue().longValue(); - mLabelSizeMonitor.update(); - break; - default: - break; - } - - /** - * Reset the visible channels list - */ - mVisibleChannels.clear(); - mVisibleChannels.addAll(mChannelModel.getChannelsInFrequencyRange(getMinFrequency(), getMaxFrequency())); - } - - /** - * Channel change event handler - */ - @Override - @SuppressWarnings("incomplete-switch") - public void channelChanged(ChannelEvent event) - { - Channel channel = event.getChannel(); - - switch(event.getEvent()) - { - case NOTIFICATION_ADD: - case NOTIFICATION_PROCESSING_START: - if(!mVisibleChannels.contains(channel) && channel.isWithin(getMinFrequency(), getMaxFrequency())) - { - mVisibleChannels.add(channel); - } - break; - case NOTIFICATION_DELETE: - mVisibleChannels.remove(channel); - break; - case NOTIFICATION_PROCESSING_STOP: - if(channel.getChannelType() == ChannelType.TRAFFIC) - { - mVisibleChannels.remove(channel); - } - break; - case NOTIFICATION_CONFIGURATION_CHANGE: - if(mVisibleChannels.contains(channel) && !channel.isWithin(getMinFrequency(), getMaxFrequency())) - { - mVisibleChannels.remove(channel); - } - - if(!mVisibleChannels.contains(channel) && channel.isWithin(getMinFrequency(), getMaxFrequency())) - { - mVisibleChannels.add(channel); - } - break; - default: - break; - } - - repaint(); - } - - public int getBandwidth() - { - return mBandwidth; - } - - /** - * Currently displayed minimum frequency - */ - public long getMinFrequency() - { - return mFrequency - (mBandwidth / 2); - } - - /** - * Currently displayed maximum frequency - */ - public long getMaxFrequency() - { - return mFrequency + (mBandwidth / 2); - } - - public boolean containsFrequency(long frequency) - { - return Math.abs(mFrequency - frequency) <= (mBandwidth / 2); - } - - private long getMinDisplayFrequency() - { - double bandwidthPerBin = (double)mBandwidth / (double)(mDFTSize.getSize()); - - return getMinFrequency() + (int)((mDFTZoomWindowOffset) * bandwidthPerBin); - } - - private long getMaxDisplayFrequency() - { - return getMinDisplayFrequency() + getDisplayBandwidth(); - } - - private int getDisplayBandwidth() - { - if(mZoom != 0) - { - return mBandwidth / (int)Math.pow(2.0, mZoom); - } - - return mBandwidth; - } - - /** - * Returns a list of channel configs that contain the frequency within their - * min/max frequency settings. - */ - public ArrayList getChannelsAtFrequency(long frequency) - { - ArrayList configs = new ArrayList(); - - for(Channel config : mVisibleChannels) - { - TunerChannel channel = config.getTunerChannel(); - - if(channel != null && channel.getMinFrequency() <= frequency && channel.getMaxFrequency() >= frequency) - { - configs.add(config); - } - } - - return configs; - } - - @Override - public void settingDeleted(Setting setting) - { /* not implemented */ } - - /** - * Calculates correct spacing and format for frequency labels and major/minor - * tick lines based on current frequency, bandwidth, zoom and screen size. - */ - public class LabelSizeManager implements ComponentListener - { - private static final double LABEL_FILL_THRESHOLD = 0.5d; - - private DecimalFormat mFrequencyFormat = new DecimalFormat("0.0"); - - private boolean mUpdateRequired = true; - private int mLabelIncrement = 1; - private int mMajorTickIncrement = 1; - private int mMinorTickIncrement = 1; - - public String format(long frequency) - { - return mFrequencyFormat.format((double)frequency / 1E6D); - } - - private void setPrecision(int precision) - { - if(precision < 1) - { - precision = 1; - } - - if(precision > 5) - { - precision = 5; - } - - mFrequencyFormat.setMinimumFractionDigits(precision); - mFrequencyFormat.setMaximumFractionDigits(precision); - } - - private void update(Graphics2D graphics) - { - if(mUpdateRequired) - { - //Set maximum precision as a starting point - setPrecision(5); - - FontMetrics fontMetrics = - graphics.getFontMetrics(OverlayPanel.this.getFont()); - - int maxLabelWidth = fontMetrics.stringWidth(format(getMaxDisplayFrequency())); - - double maxLabels = ((double)OverlayPanel.this.getWidth() * LABEL_FILL_THRESHOLD) / (double)maxLabelWidth; - - //Calculate the next smallest base 10 value for the major increment - int power = (int)Math.log10((double)getDisplayBandwidth() / maxLabels); - - //Set the number of decimal places to display in frequency labels - int precision = 5 - power; - - int start = (int)Math.pow(10.0, power + 1); - - int minimum = (int)Math.pow(10.0, power); - - int labelIncrement = start; - - while(((double)getDisplayBandwidth() / (double)labelIncrement) < maxLabels && labelIncrement >= minimum) - { - labelIncrement /= 2; - precision++; - } - - if(labelIncrement == minimum) - { - precision = 5 - power; - } - - setPrecision(precision); - - mLabelIncrement = labelIncrement; - mMajorTickIncrement = labelIncrement / 2; - mMinorTickIncrement = labelIncrement / 10; - - mUpdateRequired = false; - } - } - - /** - * Forces the display to update the label and frequency display - * calculations - */ - public void update() - { - mUpdateRequired = true; - } - - public int getMajorTickIncrement(Graphics2D graphics) - { - //Check to see if a calculation update is scheduled - update(graphics); - - return mMajorTickIncrement; - } - - public int getMinorTickIncrement(Graphics2D graphics) - { - return mMinorTickIncrement; - } - - public int getLabelIncrement(Graphics2D graphics) - { - return mLabelIncrement; - } - - @Override - public void componentResized(ComponentEvent arg0) - { - update(); - } - - public void componentHidden(ComponentEvent arg0) - { - } - - public void componentMoved(ComponentEvent arg0) - { - } - - public void componentShown(ComponentEvent arg0) - { - } - - } - - public enum ChannelDisplay - { - ALL, ENABLED, NONE; - } -} diff --git a/src/spectrum/SpectralDisplayPanel.java b/src/spectrum/SpectralDisplayPanel.java deleted file mode 100644 index 46d79731..00000000 --- a/src/spectrum/SpectralDisplayPanel.java +++ /dev/null @@ -1,941 +0,0 @@ -/******************************************************************************* - * 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 spectrum; - -import com.jidesoft.swing.JideSplitPane; -import controller.channel.Channel; -import controller.channel.ChannelModel; -import controller.channel.ChannelProcessingManager; -import controller.channel.ChannelUtils; -import dsp.filter.Window.WindowType; -import dsp.filter.smoothing.SmoothingFilter.SmoothingType; -import module.decode.DecoderType; -import net.miginfocom.swing.MigLayout; -import org.slf4j.Logger; -import org.slf4j.LoggerFactory; -import properties.SystemProperties; -import sample.Listener; -import sample.SampleType; -import sample.complex.ComplexBuffer; -import settings.ColorSetting.ColorSettingName; -import settings.ColorSettingMenuItem; -import settings.SettingsManager; -import source.ISourceEventProcessor; -import source.SourceEvent; -import source.tuner.Tuner; -import spectrum.OverlayPanel.ChannelDisplay; -import spectrum.converter.ComplexDecibelConverter; -import spectrum.converter.DFTResultsConverter; -import spectrum.menu.AveragingItem; -import spectrum.menu.DFTSizeItem; -import spectrum.menu.FFTWindowTypeItem; -import spectrum.menu.FrameRateItem; -import spectrum.menu.SmoothingItem; -import spectrum.menu.SmoothingTypeItem; - -import javax.swing.*; -import javax.swing.event.ChangeEvent; -import javax.swing.event.ChangeListener; -import javax.swing.event.MouseInputAdapter; -import java.awt.*; -import java.awt.event.ActionEvent; -import java.awt.event.ActionListener; -import java.awt.event.ComponentEvent; -import java.awt.event.ComponentListener; -import java.awt.event.MouseEvent; -import java.awt.event.MouseWheelEvent; -import java.text.DecimalFormat; -import java.util.ArrayList; -import java.util.Hashtable; - -public class SpectralDisplayPanel extends JPanel implements Listener, ISourceEventProcessor, IDFTWidthChangeProcessor -{ - private static final long serialVersionUID = 1L; - - private final static Logger mLog = LoggerFactory.getLogger(SpectralDisplayPanel.class); - - private static DecimalFormat sCURSOR_FORMAT = new DecimalFormat("000.00000"); - - public static final String FFT_SIZE_PROPERTY = "spectral.display.dft.size"; - public static final int NO_ZOOM = 0; - public static final int MAX_ZOOM = 6; - - private DFTSize mDFTSize = DFTSize.FFT04096; - private int mZoom = 0; - private int mDFTZoomWindowOffset = 0; - - private JScrollPane mScrollPane; - private JLayeredPane mLayeredPanel; - private SpectrumPanel mSpectrumPanel; - private WaterfallPanel mWaterfallPanel; - private OverlayPanel mOverlayPanel; - private DFTProcessor mDFTProcessor; - private DFTResultsConverter mDFTConverter; - private ChannelModel mChannelModel; - private ChannelProcessingManager mChannelProcessingManager; - private SettingsManager mSettingsManager; - private Tuner mTuner; - - /** - * Spectral Display Panel provides a frequency component display with a - * historical waterfall display and a transparent overlay to show frequency, - * cursor and channel information. - * - * Mouse scrolling and zooming are supported and the waterfall display can - * be paused. - * - * Complex sample buffers are processed by a DFTProcessor and the output of - * the DFT is translated to decibels for display in the spectrum and - * waterfall components. - */ - public SpectralDisplayPanel(ChannelModel channelModel, - ChannelProcessingManager channelProcessingManager, - SettingsManager settingsManager) - { - mChannelModel = channelModel; - mChannelProcessingManager = channelProcessingManager; - mSettingsManager = settingsManager; - - mSpectrumPanel = new SpectrumPanel(mSettingsManager); - mOverlayPanel = new OverlayPanel(mSettingsManager, mChannelModel); - mWaterfallPanel = new WaterfallPanel(mSettingsManager); - - init(); - - loadSettings(); - } - - private void loadSettings() - { - SystemProperties properties = SystemProperties.getInstance(); - - String rawSize = properties.get(FFT_SIZE_PROPERTY, DFTSize.FFT04096.name()); - - DFTSize size = null; - - if(rawSize != null) - { - try - { - size = DFTSize.valueOf(rawSize); - } - catch(Exception e) - { - //Do nothing - } - } - - if(size == null) - { - size = DFTSize.FFT04096; - } - - setDFTSize(size, false); - } - - public void dispose() - { - /* De-register from receiving samples when the window closes */ - clearTuner(); - - mSettingsManager = null; - - mDFTProcessor.dispose(); - mDFTProcessor = null; - - mDFTConverter.dispose(); - mDFTConverter = null; - - mSpectrumPanel.dispose(); - mSpectrumPanel = null; - - mWaterfallPanel.dispose(); - mWaterfallPanel = null; - - mOverlayPanel.dispose(); - mOverlayPanel = null; - - - mTuner = null; - } - - /** - * Queues an FFT size change request. The scheduled executor will apply - * the change when it runs. - */ - public void setDFTSize(DFTSize size, boolean save) - { - mDFTProcessor.setDFTSize(size); - mOverlayPanel.setDFTSize(size); - mDFTSize = size; - - if(save) - { - SystemProperties.getInstance().set(FFT_SIZE_PROPERTY, size.name()); - } - - setZoom(0, 0, 0); - } - - public void setDFTSize(DFTSize size) - { - setDFTSize(size, true); - } - - @Override - public DFTSize getDFTSize() - { - return mDFTSize; - } - - public int getZoom() - { - return mZoom; - } - - /** - * Sets the current zoom level which will be 2 to the power of zoom (2^zoom) - * - * 0 No Zoom - * 1 2x Zoom - * 2 4x Zoom - * 3 8x Zoom - * 4 16x Zoom - * 5 32x Zoom - * 6 64x Zoom - * - * @param zoom level, 0 - 6. - * @param frequency under the mouse to maintain while zooming - * @param windowOffset where to maintain the frequency under the mouse - */ - public void setZoom(int zoom, long frequency, double windowOffset) - { - if(zoom < NO_ZOOM) - { - zoom = NO_ZOOM; - } - else if(zoom > MAX_ZOOM) - { - zoom = MAX_ZOOM; - } - - if(zoom != mZoom) - { - mZoom = zoom; - - //Calculate the bin offset that would place the reference frequency - //at the left edge of the zoom window. - double binOffsetToFrequency = getBinOffset(frequency); - - //Calculate the bin offset into the newly sized zoom window that - //would place the frequency in the same proportional window location - //that it was in the previous zoom size - double windowBinOffset = (double)getZoomWindowSizeInBins() * windowOffset; - - //Set the overall offset to place the reference frequency in the - //same location in the newly zoomed window - double offset = binOffsetToFrequency - windowBinOffset; - - mSpectrumPanel.setZoom(mZoom); - mOverlayPanel.setZoom(mZoom); - mWaterfallPanel.setZoom(mZoom); - - setZoomWindowOffset(offset); - } - } - - /** - * Sets the offset (in DFT bins) to the first bin that the zoom window displays - */ - public void setZoomWindowOffset(double offset) - { - if(offset < 0) - { - offset = 0; - } - - if(offset > (mDFTSize.getSize() - getZoomWindowSizeInBins())) - { - offset = mDFTSize.getSize() - getZoomWindowSizeInBins(); - } - - mDFTZoomWindowOffset = (int)offset; - - mSpectrumPanel.setZoomWindowOffset(mDFTZoomWindowOffset); - mOverlayPanel.setZoomWindowOffset(mDFTZoomWindowOffset); - mWaterfallPanel.setZoomWindowOffset(mDFTZoomWindowOffset); - } - - /** - * Calculates the size of the current zoom window in DFT bins - */ - private int getZoomWindowSizeInBins() - { - return mDFTSize.getSize() / getZoomMultiplier(); - } - - - public int getZoomMultiplier() - { - return (int)Math.pow(2.0, mZoom); - } - - /** - * Calculates the overall offset of the frequency from the current minimum - * frequency in terms of total FFT width - * - * @param frequency - * @return - */ - private double getBinOffset(long frequency) - { - double offset = 0.0; - - if(mOverlayPanel.containsFrequency(frequency)) - { - offset = (double)mDFTSize.getSize() * - ((double)(frequency - mOverlayPanel.getMinFrequency()) / - (double)mOverlayPanel.getBandwidth()); - } - - return offset; - } - - /** - * Overrides JComponent method to return false, since we have overlapping - * panels with the spectrum and channel panels - */ - public boolean isOptimizedDrawingEnabled() - { - return false; - } - - private void init() - { - setLayout(new MigLayout("insets 0 0 0 0", "[grow]", "[grow]")); - - /** - * The layered pane holds the overlapping spectrum and channel panels - * and manages the sizing of each panel with the resize listener - */ - mLayeredPanel = new JLayeredPane(); - mLayeredPanel.addComponentListener(new ResizeListener()); - - /** - * Create a mouse adapter to handle mouse events over the spectrum - * and waterfall panels - */ - MouseEventProcessor mouser = new MouseEventProcessor(); - - mOverlayPanel.addMouseListener(mouser); - mOverlayPanel.addMouseMotionListener(mouser); - mOverlayPanel.addMouseWheelListener(mouser); - - //Add the spectrum and channel panels to the layered panel - mLayeredPanel.add(mSpectrumPanel, new Integer(0), 0); - mLayeredPanel.add(mOverlayPanel, new Integer(1), 0); - - //Create the waterfall - mWaterfallPanel.addMouseListener(mouser); - mWaterfallPanel.addMouseMotionListener(mouser); - mWaterfallPanel.addMouseWheelListener(mouser); - - /* Attempt to set a 50/50 split preferred size for the split pane */ - double totalHeight = mLayeredPanel.getPreferredSize().getHeight() + - mWaterfallPanel.getPreferredSize().getHeight(); - - mLayeredPanel.setPreferredSize(new Dimension((int)mLayeredPanel - .getPreferredSize().getWidth(), (int)(totalHeight / 2.0d))); - - mWaterfallPanel.setPreferredSize(new Dimension((int)mWaterfallPanel - .getPreferredSize().getWidth(), (int)(totalHeight / 2.0d))); - - //Create the split pane to hold the layered pane and the waterfall - JideSplitPane splitPane = new JideSplitPane(JSplitPane.VERTICAL_SPLIT); - splitPane.setDividerSize(5); - splitPane.add(mLayeredPanel); - splitPane.add(mWaterfallPanel); - - mScrollPane = new JScrollPane(splitPane); - - add(mScrollPane, "grow"); - - /** - * Setup DFTProcessor to process samples and register the waterfall and - * spectrum panel to receive the processed dft results - */ - mDFTProcessor = new DFTProcessor(SampleType.COMPLEX); - mDFTConverter = new ComplexDecibelConverter(); - mDFTProcessor.addConverter(mDFTConverter); - - mDFTConverter.addListener((DFTResultsListener)mSpectrumPanel); - mDFTConverter.addListener((DFTResultsListener)mWaterfallPanel); - } - - /** - * Receives frequency change events -- primarily from tuner components. - */ - public void process(SourceEvent event) - { - mOverlayPanel.process(event); - mDFTProcessor.process(event); - } - - /** - * Complex sample buffer receive method - */ - @Override - public void receive(ComplexBuffer sampleBuffer) - { - mDFTProcessor.receive(sampleBuffer); - } - - /** - * Responds to tuner event by deregistering from the current - * complex sample buffer source and registering with the tuner argument. - */ - public void showTuner(Tuner tuner) - { - clearTuner(); - - mDFTProcessor.clearBuffer(); - - mTuner = tuner; - - if(mTuner != null) - { - //Register to receive frequency change events - mTuner.getTunerController().addListener(this); - - //Register the dft processor to receive samples from the tuner - mTuner.getSourceManager().addComplexBufferListener(mDFTProcessor); - - mSpectrumPanel.setSampleSize(mTuner.getSampleSize()); - - //Fire frequency and sample rate change events so that the spectrum - //and overlay panels can synchronize - process(SourceEvent.frequencyChange(mTuner.getTunerController().getFrequencyController().getFrequency())); - - process(SourceEvent.sampleRateChange(mTuner.getTunerController().getFrequencyController().getSampleRate())); - } - } - - /** - * Tuner de-selection cleanup method - */ - public void clearTuner() - { - if(mTuner != null) - { - //Deregister for frequency change events from the tuner - mTuner.getTunerController().removeListener(this); - - //Deregister the dft processor from receiving samples - mTuner.getTunerController().removeComplexBufferListener(mDFTProcessor); - mTuner = null; - } - } - - /** - * Monitors the sizing of the layered pane and resizes the spectrum and - * channel panels whenever the layered pane is resized - */ - public class ResizeListener implements ComponentListener - { - @Override - public void componentResized(ComponentEvent e) - { - Component c = e.getComponent(); - - mSpectrumPanel.setBounds(0, 0, c.getWidth(), c.getHeight()); - mOverlayPanel.setBounds(0, 0, c.getWidth(), c.getHeight()); - } - - @Override - public void componentHidden(ComponentEvent arg0) - { - } - - @Override - public void componentMoved(ComponentEvent arg0) - { - } - - @Override - public void componentShown(ComponentEvent arg0) - { - } - } - - /** - * Mouse event handler for the spectral display panel. - */ - public class MouseEventProcessor extends MouseInputAdapter - { - private int mDFTZoomWindowOffsetAtDragStart = 0; - private int mDragStartX = 0; - private double mPixelsPerBin; - - public MouseEventProcessor() - { - } - - @Override - public void mouseWheelMoved(MouseWheelEvent e) - { - int zoom = mZoom - e.getWheelRotation(); - - long frequency = mOverlayPanel.getFrequencyFromAxis(e.getX()); - - double windowOffset = (double)e.getX() / (double)getWidth(); - - setZoom(zoom, frequency, windowOffset); - } - - @Override - public void mouseMoved(MouseEvent event) - { - update(event); - } - - @Override - public void mouseDragged(MouseEvent event) - { - update(event); - - int dragDistance = mDragStartX - event.getX(); - - double binDistance = (double)dragDistance / mPixelsPerBin; - - int offset = (int)(mDFTZoomWindowOffsetAtDragStart + binDistance); - - if(offset < 0) - { - offset = 0; - } - - int maxOffset = mDFTSize.getSize() - (mDFTSize.getSize() / getZoomMultiplier()); - - if(offset > maxOffset) - { - offset = maxOffset; - } - - setZoomWindowOffset(offset); - } - - @Override - public void mousePressed(MouseEvent e) - { - mDragStartX = e.getX(); - - mDFTZoomWindowOffsetAtDragStart = mDFTZoomWindowOffset; - - mPixelsPerBin = (double)getWidth() / - ((double)(mDFTSize.getSize()) / - (double)getZoomMultiplier()); - } - - /** - * Updates the cursor display while the mouse is performing actions - */ - private void update(MouseEvent event) - { - if(event.getComponent() == mOverlayPanel) - { - mOverlayPanel.setCursorLocation(event.getPoint()); - } - else - { - mWaterfallPanel.setCursorLocation(event.getPoint()); - mWaterfallPanel.setCursorFrequency( - mOverlayPanel.getFrequencyFromAxis(event.getPoint().x)); - } - } - - @Override - public void mouseEntered(MouseEvent e) - { - if(e.getComponent() == mOverlayPanel) - { - mOverlayPanel.setCursorVisible(true); - } - else - { - mWaterfallPanel.setCursorVisible(true); - } - } - - @Override - public void mouseExited(MouseEvent e) - { - mOverlayPanel.setCursorVisible(false); - mWaterfallPanel.setCursorVisible(false); - } - - /** - * Displays the context menu. - */ - @Override - public void mouseClicked(MouseEvent event) - { - if(SwingUtilities.isRightMouseButton(event)) - { - JPopupMenu contextMenu = new JPopupMenu(); - - if(event.getComponent() == mWaterfallPanel) - { - contextMenu.add(new PauseItem(mWaterfallPanel, "Pause")); - } - - long frequency = - mOverlayPanel.getFrequencyFromAxis(event.getX()); - - if(event.getComponent() == mOverlayPanel) - { - ArrayList channels = - mOverlayPanel.getChannelsAtFrequency(frequency); - - for(Channel channel : channels) - { - JMenu channelMenu = ChannelUtils.getContextMenu(mChannelModel, - mChannelProcessingManager, channel, - SpectralDisplayPanel.this); - - if(channelMenu != null) - { - contextMenu.add(channelMenu); - } - } - - if(!channels.isEmpty()) - { - contextMenu.add(new JSeparator()); - } - - } - -// JMenu frequencyMenu = new JMenu( -// sCURSOR_FORMAT.format((float) frequency / 1000000.0f)); -// -// JMenu decoderMenu = new JMenu("Add Decoder"); -// -// for(DecoderType type : DecoderType.getPrimaryDecoders()) -// { -// decoderMenu.add(new DecoderItem(type, frequency)); -// } -// -// frequencyMenu.add(decoderMenu); -// -// contextMenu.add(frequencyMenu); -// -// contextMenu.add(new JSeparator()); - - /** - * Color Menus - */ - JMenu colorMenu = new JMenu("Color"); - - colorMenu.add(new ColorSettingMenuItem(mSettingsManager, - ColorSettingName.CHANNEL_CONFIG)); - - colorMenu.add(new ColorSettingMenuItem(mSettingsManager, - ColorSettingName.CHANNEL_CONFIG_PROCESSING)); - - colorMenu.add(new ColorSettingMenuItem(mSettingsManager, - ColorSettingName.CHANNEL_CONFIG_SELECTED)); - - colorMenu.add(new ColorSettingMenuItem(mSettingsManager, - ColorSettingName.SPECTRUM_CURSOR)); - - colorMenu.add(new ColorSettingMenuItem(mSettingsManager, - ColorSettingName.SPECTRUM_LINE)); - - colorMenu.add(new ColorSettingMenuItem(mSettingsManager, - ColorSettingName.SPECTRUM_BACKGROUND)); - - colorMenu.add(new ColorSettingMenuItem(mSettingsManager, - ColorSettingName.SPECTRUM_GRADIENT_BOTTOM)); - - colorMenu.add(new ColorSettingMenuItem(mSettingsManager, - ColorSettingName.SPECTRUM_GRADIENT_TOP)); - - contextMenu.add(colorMenu); - - /** - * Display items: fft and frame rate - */ - JMenu displayMenu = new JMenu("Display"); - contextMenu.add(displayMenu); - - if(event.getComponent() != mWaterfallPanel) - { - /** - * Averaging menu - */ - JMenu averagingMenu = new JMenu("Averaging"); - averagingMenu.add( - new AveragingItem(mSpectrumPanel, 4)); - displayMenu.add(averagingMenu); - - /** - * Channel Display setting menu - */ - JMenu channelDisplayMenu = new JMenu("Channel"); - - channelDisplayMenu.add(new ChannelDisplayItem( - mOverlayPanel, ChannelDisplay.ALL)); - channelDisplayMenu.add(new ChannelDisplayItem( - mOverlayPanel, ChannelDisplay.ENABLED)); - channelDisplayMenu.add(new ChannelDisplayItem( - mOverlayPanel, ChannelDisplay.NONE)); - - displayMenu.add(channelDisplayMenu); - } - - - /** - * FFT width - */ - JMenu fftWidthMenu = new JMenu("FFT Width"); - displayMenu.add(fftWidthMenu); - - for(DFTSize width : DFTSize.values()) - { - fftWidthMenu.add(new DFTSizeItem(SpectralDisplayPanel.this, width)); - } - - /** - * DFT Processor Frame Rate - */ - JMenu frameRateMenu = new JMenu("Frame Rate"); - displayMenu.add(frameRateMenu); - - frameRateMenu.add(new FrameRateItem(mDFTProcessor, 14)); - frameRateMenu.add(new FrameRateItem(mDFTProcessor, 16)); - frameRateMenu.add(new FrameRateItem(mDFTProcessor, 18)); - frameRateMenu.add(new FrameRateItem(mDFTProcessor, 20)); - frameRateMenu.add(new FrameRateItem(mDFTProcessor, 25)); - frameRateMenu.add(new FrameRateItem(mDFTProcessor, 30)); - frameRateMenu.add(new FrameRateItem(mDFTProcessor, 40)); - frameRateMenu.add(new FrameRateItem(mDFTProcessor, 50)); - - /** - * FFT Window Type - */ - JMenu fftWindowType = new JMenu("Window Type"); - displayMenu.add(fftWindowType); - - for(WindowType type : WindowType.values()) - { - fftWindowType.add( - new FFTWindowTypeItem(mDFTProcessor, type)); - } - - if(event.getComponent() != mWaterfallPanel) - { - /** - * Smoothing menu - */ - JMenu smoothingMenu = new JMenu("Smoothing"); - - if(mSpectrumPanel.getSmoothingType() != SmoothingType.NONE) - { - smoothingMenu.add(new SmoothingItem(mSpectrumPanel, 5)); - smoothingMenu.add(new JSeparator()); - } - smoothingMenu.add(new SmoothingTypeItem(mSpectrumPanel, SmoothingType.GAUSSIAN)); - smoothingMenu.add(new SmoothingTypeItem(mSpectrumPanel, SmoothingType.TRIANGLE)); - smoothingMenu.add(new SmoothingTypeItem(mSpectrumPanel, SmoothingType.RECTANGLE)); - smoothingMenu.add(new SmoothingTypeItem(mSpectrumPanel, SmoothingType.NONE)); - - displayMenu.add(smoothingMenu); - } - - /* - * Zoom menu - */ - JMenuItem zoomMenu = new JMenu("Zoom"); - - double windowOffset = (double)event.getX() / (double)getWidth(); - - zoomMenu.add(new ZoomItem(frequency, windowOffset)); - - contextMenu.add(zoomMenu); - - - if(contextMenu != null) - { - if(event.getComponent() == mOverlayPanel) - { - contextMenu.show(mOverlayPanel, - event.getX(), - event.getY()); - } - else - { - contextMenu.show(mWaterfallPanel, - event.getX(), - event.getY()); - } - } - } - } - } - - public class PauseItem extends JCheckBoxMenuItem - { - private static final long serialVersionUID = 1L; - - private Pausable mPausable; - - public PauseItem(Pausable pausable, String label) - { - super(label); - - final boolean paused = pausable.isPaused(); - - setSelected(paused); - - mPausable = pausable; - - addActionListener(new ActionListener() - { - @Override - public void actionPerformed(ActionEvent e) - { - EventQueue.invokeLater(new Runnable() - { - @Override - public void run() - { - mPausable.setPaused(!paused); - } - }); - } - }); - } - } - - public class ZoomItem extends JSlider - { - private static final long serialVersionUID = 1L; - - private long mFrequency; - private double mWindowOffset; - - public ZoomItem(long frequency, double windowOffset) - { - super(NO_ZOOM, MAX_ZOOM, mZoom); - - mFrequency = frequency; - mWindowOffset = windowOffset; - - Hashtable labels = new Hashtable<>(); - labels.put(new Integer(0), new JLabel("1x")); - labels.put(new Integer(1), new JLabel("2x")); - labels.put(new Integer(2), new JLabel("4x")); - labels.put(new Integer(3), new JLabel("8x")); - labels.put(new Integer(4), new JLabel("16x")); - labels.put(new Integer(5), new JLabel("32x")); - labels.put(new Integer(6), new JLabel("64x")); - - setLabelTable(labels); - - setMajorTickSpacing(1); - setMinorTickSpacing(1); - setPaintTicks(true); - setPaintLabels(true); - - this.addChangeListener(new ChangeListener() - { - @Override - public void stateChanged(ChangeEvent e) - { - setZoom(getValue(), mFrequency, mWindowOffset); - } - }); - } - } - - public class ChannelDisplayItem extends JCheckBoxMenuItem - { - private static final long serialVersionUID = 1L; - - private OverlayPanel mOverlayPanel; - private ChannelDisplay mChannelDisplay; - - public ChannelDisplayItem(OverlayPanel panel, ChannelDisplay display) - { - super(display.name()); - - mOverlayPanel = panel; - - mChannelDisplay = display; - - setSelected(mOverlayPanel.getChannelDisplay() == mChannelDisplay); - - addActionListener(new ActionListener() - { - @Override - public void actionPerformed(ActionEvent e) - { - EventQueue.invokeLater(new Runnable() - { - @Override - public void run() - { - mOverlayPanel.setChannelDisplay(mChannelDisplay); - } - }); - } - }); - } - } - - /** - * Context menu item to provide one-click access to starting a channel - * processing with the selected decoder - */ - public class DecoderItem extends JMenuItem - { - private static final long serialVersionUID = 1L; - - private ChannelModel mChannelModel; - private long mFrequency; - private DecoderType mDecoder; - - public DecoderItem(DecoderType type, long frequency) - { - super(type.getDisplayString()); - - mFrequency = frequency; - mDecoder = type; - - addActionListener(new ActionListener() - { - @Override - public void actionPerformed(ActionEvent e) - { - mChannelModel.createChannel(mDecoder, mFrequency); - } - }); - } - } -} From d2e7f008c8afa128a732b9b7bb8a8ab5ad040ccc Mon Sep 17 00:00:00 2001 From: Denny Date: Sun, 7 Jan 2018 07:46:41 -0500 Subject: [PATCH 39/87] WIP - implements tuner source manager to calculate and update the tuner's center frequency each time we allocate a new DDC/channel. Adds support for broadcasting channel count change source events so that the tuner can accurately convey sourced channel counts via tuner events. Completes changeover of polyphase channel manager to support arbitrary bandwidth channels. The new approach provides greater compatibility across any tuner sample rates. Likewise, all decoders will have to be updated to adapt to any channel sample rate, although channel bandwidths will be at least 12.5 kHz and likely no more than 25.0 kHz, with a 2x oversampling that produces channel sample rates in range 25.0 - 50.0 kHz. --- copyright_header.txt | 17 - .../filter/channelizer/ChannelCalculator.java | 8 +- .../ComplexPolyphaseChannelizerM2.java | 2 +- .../channelizer/PolyphaseChannelManager.java | 113 ++++- .../output/TwoChannelOutputProcessor.java | 2 +- .../github/dsheirer/source/SourceEvent.java | 32 +- .../github/dsheirer/source/tuner/Tuner.java | 104 ++--- .../source/tuner/TunerController.java | 231 +--------- .../dsheirer/source/tuner/TunerModel.java | 2 +- .../tuner/manager/AbstractSourceManager.java | 66 +-- .../tuner/manager/TunerSourceManager.java | 400 ++++++++++++++++-- 11 files changed, 613 insertions(+), 364 deletions(-) delete mode 100644 copyright_header.txt diff --git a/copyright_header.txt b/copyright_header.txt deleted file mode 100644 index 3eaea8b8..00000000 --- a/copyright_header.txt +++ /dev/null @@ -1,17 +0,0 @@ -/******************************************************************************* - * SDR Trunk - * Copyright (C) 2014-2016 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 - ******************************************************************************/ diff --git a/src/main/java/io/github/dsheirer/dsp/filter/channelizer/ChannelCalculator.java b/src/main/java/io/github/dsheirer/dsp/filter/channelizer/ChannelCalculator.java index 1210e45a..8565d3ed 100644 --- a/src/main/java/io/github/dsheirer/dsp/filter/channelizer/ChannelCalculator.java +++ b/src/main/java/io/github/dsheirer/dsp/filter/channelizer/ChannelCalculator.java @@ -185,7 +185,7 @@ public class ChannelCalculator */ public double getChannelSampleRate() { - return (double)getChannelBandwidth() * getOversampling(); + return getChannelBandwidth() * getOversampling(); } /** @@ -550,9 +550,9 @@ public class ChannelCalculator { mLog.debug("Starting ...."); - int channelBandwidth = 12500; - int sampleRate = 100000; //Airspy - int channelCount = sampleRate / channelBandwidth; + double channelBandwidth = 12500.0; + double sampleRate = 100000.0; //Airspy + int channelCount = (int)(sampleRate / channelBandwidth); double centerFrequency = 455000000; ChannelCalculator calculator = new ChannelCalculator(sampleRate, channelCount, centerFrequency, 2.0); diff --git a/src/main/java/io/github/dsheirer/dsp/filter/channelizer/ComplexPolyphaseChannelizerM2.java b/src/main/java/io/github/dsheirer/dsp/filter/channelizer/ComplexPolyphaseChannelizerM2.java index 3e0995dc..d15f0880 100644 --- a/src/main/java/io/github/dsheirer/dsp/filter/channelizer/ComplexPolyphaseChannelizerM2.java +++ b/src/main/java/io/github/dsheirer/dsp/filter/channelizer/ComplexPolyphaseChannelizerM2.java @@ -59,7 +59,7 @@ public class ComplexPolyphaseChannelizerM2 extends AbstractComplexPolyphaseChann if(channelCount % 2 != 0) { - throw new IllegalArgumentException("Channel count must be an even multiple of the oversample rate (2)"); + throw new IllegalArgumentException("Channel count must be an even multiple of the oversample rate (2x)"); } init(taps); diff --git a/src/main/java/io/github/dsheirer/dsp/filter/channelizer/PolyphaseChannelManager.java b/src/main/java/io/github/dsheirer/dsp/filter/channelizer/PolyphaseChannelManager.java index 2d673111..a7761217 100644 --- a/src/main/java/io/github/dsheirer/dsp/filter/channelizer/PolyphaseChannelManager.java +++ b/src/main/java/io/github/dsheirer/dsp/filter/channelizer/PolyphaseChannelManager.java @@ -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 @@ -19,26 +19,31 @@ import io.github.dsheirer.dsp.filter.channelizer.output.IPolyphaseChannelOutputP import io.github.dsheirer.dsp.filter.channelizer.output.OneChannelOutputProcessor; import io.github.dsheirer.dsp.filter.channelizer.output.TwoChannelOutputProcessor; import io.github.dsheirer.dsp.filter.design.FilterDesignException; +import io.github.dsheirer.sample.Broadcaster; import io.github.dsheirer.sample.Listener; import io.github.dsheirer.sample.complex.IComplexBufferProvider; -import io.github.dsheirer.source.Source; +import io.github.dsheirer.source.ISourceEventProcessor; import io.github.dsheirer.source.SourceEvent; import io.github.dsheirer.source.SourceException; +import io.github.dsheirer.source.tuner.TunerController; import io.github.dsheirer.source.tuner.channel.TunerChannel; +import io.github.dsheirer.source.tuner.channel.TunerChannelSource; import org.slf4j.Logger; import org.slf4j.LoggerFactory; import java.util.List; +import java.util.SortedSet; +import java.util.TreeSet; import java.util.concurrent.CopyOnWriteArrayList; -public class PolyphaseChannelManager implements Listener +public class PolyphaseChannelManager implements ISourceEventProcessor { private final static Logger mLog = LoggerFactory.getLogger(PolyphaseChannelManager.class); - private static final int CHANNEL_BANDWIDTH = 12500; + private static final double MINIMUM_CHANNEL_BANDWIDTH = 12500.0; private static final double CHANNEL_OVERSAMPLING = 2.0; - private static final int CHANNEL_SAMPLE_RATE = (int)(CHANNEL_BANDWIDTH * CHANNEL_OVERSAMPLING); private static final int POLYPHASE_FILTER_TAPS_PER_CHANNEL = 17; + private Broadcaster mSourceEventBroadcaster = new Broadcaster<>(); private IComplexBufferProvider mComplexBufferProvider; private List mChannelSources = new CopyOnWriteArrayList<>(); private ChannelCalculator mChannelCalculator; @@ -61,7 +66,7 @@ public class PolyphaseChannelManager implements Listener * @param frequency of the provided complex buffer samples * @param sampleRate of the provided complex buffer samples */ - public PolyphaseChannelManager(IComplexBufferProvider complexBufferProvider, long frequency, int sampleRate) + public PolyphaseChannelManager(IComplexBufferProvider complexBufferProvider, long frequency, double sampleRate) { if(complexBufferProvider == null) { @@ -70,14 +75,13 @@ public class PolyphaseChannelManager implements Listener mComplexBufferProvider = complexBufferProvider; - //Ensure that sample rate is a multiple of channel sample rate since polyphase channelizer is M2 - if(sampleRate % (CHANNEL_SAMPLE_RATE) != 0) - { - throw new IllegalArgumentException("Tuner sample rate [" + sampleRate + "] must be an even multiple of " + - CHANNEL_SAMPLE_RATE + " Hz"); - } + int channelCount = (int)(sampleRate / MINIMUM_CHANNEL_BANDWIDTH); - int channelCount = sampleRate / CHANNEL_BANDWIDTH; + //Ensure channel count is an even integer since we're using a 2x oversampling polyphase channelizer + if(channelCount % 2 != 0) + { + channelCount--; + } mChannelCalculator = new ChannelCalculator(sampleRate, channelCount, frequency, CHANNEL_OVERSAMPLING); @@ -86,13 +90,37 @@ public class PolyphaseChannelManager implements Listener updateChannelizer(sampleRate, null); } + /** + * Creates a polyphase channel manager for the tuner controller + */ + public PolyphaseChannelManager(TunerController tunerController) + { + this(tunerController, tunerController.getFrequency(), tunerController.getSampleRate()); + } + + /** + * Current channel sample rate which is (2 * channel bandwidth). + */ + public double getChannelSampleRate() + { + return mChannelCalculator.getChannelSampleRate(); + } + + /** + * Current channel bandwidth/spacing. + */ + public double getChannelBandwidth() + { + return mChannelCalculator.getChannelBandwidth(); + } + /** * Provides a Digital Drop Channel (DDC) for the specified tuner channel or returns null if the channel can't be * sourced due to the current center frequency and/or sample rate. * @param tunerChannel specifying center frequency and bandwidth. * @return source or null. */ - public Source getChannel(TunerChannel tunerChannel) + public TunerChannelSource getChannel(TunerChannel tunerChannel) { PolyphaseChannelSource channelSource = null; @@ -105,7 +133,7 @@ public class PolyphaseChannelManager implements Listener if(outputProcessor != null) { channelSource = new PolyphaseChannelSource(tunerChannel, outputProcessor, mChannelSourceEventListener, - CHANNEL_SAMPLE_RATE); + mChannelCalculator.getChannelSampleRate()); mChannelSources.add(channelSource); } @@ -131,9 +159,9 @@ public class PolyphaseChannelManager implements Listener switch(indexes.size()) { case 1: - return new OneChannelOutputProcessor(CHANNEL_SAMPLE_RATE, indexes); + return new OneChannelOutputProcessor(mChannelCalculator.getChannelSampleRate(), indexes); case 2: - return new TwoChannelOutputProcessor(CHANNEL_SAMPLE_RATE, indexes); + return new TwoChannelOutputProcessor(mChannelCalculator.getChannelSampleRate(), indexes); 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()); @@ -152,6 +180,7 @@ public class PolyphaseChannelManager implements Listener synchronized(mBufferProcessor) { mPolyphaseChannelizer.addChannel(channelSource); + mSourceEventBroadcaster.broadcast(SourceEvent.channelCountChange(mChannelSources.size())); if(mPolyphaseChannelizer.getRegisteredChannelCount() == 1) { @@ -172,6 +201,7 @@ public class PolyphaseChannelManager implements Listener synchronized(mBufferProcessor) { mChannelSources.remove(channelSource); + mSourceEventBroadcaster.broadcast(SourceEvent.channelCountChange(mChannelSources.size())); if(mPolyphaseChannelizer.getRegisteredChannelCount() == 0) { @@ -182,11 +212,11 @@ public class PolyphaseChannelManager implements Listener } /** - * Implements the Listener interface for receiving notifications of frequency and sample rate + * Implements the ISourceEventProcessor interface for receiving notifications of frequency and sample rate * changes from the tuner. */ @Override - public void receive(SourceEvent sourceEvent) + public void process(SourceEvent sourceEvent) { switch(sourceEvent.getEvent()) { @@ -199,6 +229,9 @@ public class PolyphaseChannelManager implements Listener //Update the channelizer (and the output processors) for the new sample rate updateChannelizer(sourceEvent.getValue().intValue(), sourceEvent); break; + default: + mLog.info("Received an unrecognized source event: " + sourceEvent.getEvent()); + break; } } @@ -210,7 +243,7 @@ public class PolyphaseChannelManager implements Listener * event will be passed to all output processors to ensure the change is fully promulgated before restarting the * buffer processor if it was already running. */ - private void updateChannelizer(int sampleRate, SourceEvent sourceEvent) + private void updateChannelizer(double sampleRate, SourceEvent sourceEvent) { mChannelCalculator.setSampleRate(sampleRate); @@ -322,6 +355,46 @@ public class PolyphaseChannelManager implements Listener } } + /** + * Sorted set of currently sourced tuner channels being provided by this channel manager. The set is ordered by + * frequency (low to high). + */ + public SortedSet getTunerChannels() + { + SortedSet tunerChannels = new TreeSet<>(); + + for(PolyphaseChannelSource channelSource: mChannelSources) + { + tunerChannels.add(channelSource.getTunerChannel()); + } + + return tunerChannels; + } + + /** + * Count of currently sourced tuner channels + */ + public int getTunerChannelCount() + { + return mChannelSources.size(); + } + + /** + * Adds the listener to receive source events + */ + public void addSourceEventListener(Listener listener) + { + mSourceEventBroadcaster.addListener(listener); + } + + /** + * Removes the listener from receiving source events + */ + public void removeSourceEventListener(Listener listener) + { + mSourceEventBroadcaster.removeListener(listener); + } + /** * Internal class for handling requests for start/stop sample stream from polyphase channel sources */ diff --git a/src/main/java/io/github/dsheirer/dsp/filter/channelizer/output/TwoChannelOutputProcessor.java b/src/main/java/io/github/dsheirer/dsp/filter/channelizer/output/TwoChannelOutputProcessor.java index 350567a3..23c3e120 100644 --- a/src/main/java/io/github/dsheirer/dsp/filter/channelizer/output/TwoChannelOutputProcessor.java +++ b/src/main/java/io/github/dsheirer/dsp/filter/channelizer/output/TwoChannelOutputProcessor.java @@ -38,7 +38,7 @@ public class TwoChannelOutputProcessor extends ChannelOutputProcessor * @param sampleRate of the output sample stream. * @param channelIndexes containing two channel indices. */ - public TwoChannelOutputProcessor(int sampleRate, List channelIndexes) + public TwoChannelOutputProcessor(double sampleRate, List channelIndexes) { //Set the frequency correction oscillator to 2 x output sample rate since we'll be correcting the frequency //after synthesizing both input channels diff --git a/src/main/java/io/github/dsheirer/source/SourceEvent.java b/src/main/java/io/github/dsheirer/source/SourceEvent.java index 8f9b2857..53ef609f 100644 --- a/src/main/java/io/github/dsheirer/source/SourceEvent.java +++ b/src/main/java/io/github/dsheirer/source/SourceEvent.java @@ -18,15 +18,17 @@ ******************************************************************************/ package io.github.dsheirer.source; +import java.util.EnumSet; + public class SourceEvent { public enum Event { + NOTIFICATION_CHANNEL_COUNT_CHANGE, NOTIFICATION_CHANNEL_FREQUENCY_CORRECTION_CHANGE, NOTIFICATION_CHANNEL_SAMPLE_RATE_CHANGE, NOTIFICATION_FREQUENCY_CHANGE, NOTIFICATION_FREQUENCY_CORRECTION_CHANGE, - NOTIFICATION_POLYPHASE_CHANNEL_COUNT_CHANGE, NOTIFICATION_SAMPLE_RATE_CHANGE, REQUEST_CHANNEL_FREQUENCY_CORRECTION_CHANGE, @@ -34,6 +36,11 @@ public class SourceEvent REQUEST_SOURCE_DISPOSE, REQUEST_START_SAMPLE_STREAM, REQUEST_STOP_SAMPLE_STREAM; + + public static EnumSet NOTIFICATION_EVENTS = + EnumSet.range(NOTIFICATION_CHANNEL_COUNT_CHANGE, NOTIFICATION_SAMPLE_RATE_CHANGE); + public static EnumSet REQUEST_EVENTS = + EnumSet.range(NOTIFICATION_CHANNEL_COUNT_CHANGE, NOTIFICATION_SAMPLE_RATE_CHANGE); } private Event mEvent; @@ -66,6 +73,22 @@ public class SourceEvent return mEvent; } + /** + * Indicates if this is a notification event + */ + public boolean isNotificationEvent() + { + return Event.NOTIFICATION_EVENTS.contains(mEvent); + } + + /** + * Indicates if this is a request event + */ + public boolean isRequestEvent() + { + return Event.REQUEST_EVENTS.contains(mEvent); + } + /** * Value associated with the event. */ @@ -152,7 +175,7 @@ public class SourceEvent */ public static SourceEvent channelCountChange(int channelCount) { - return new SourceEvent(Event.NOTIFICATION_POLYPHASE_CHANNEL_COUNT_CHANGE, channelCount); + return new SourceEvent(Event.NOTIFICATION_CHANNEL_COUNT_CHANGE, channelCount); } /** @@ -199,4 +222,9 @@ public class SourceEvent { return new SourceEvent(Event.REQUEST_SOURCE_DISPOSE, source); } + + public String toString() + { + return "SOURCE EVENT:" + mEvent + " VALUE:" + (mValue != null ? mValue : "(empty)"); + } } diff --git a/src/main/java/io/github/dsheirer/source/tuner/Tuner.java b/src/main/java/io/github/dsheirer/source/tuner/Tuner.java index 70e02d71..d9a19e90 100644 --- a/src/main/java/io/github/dsheirer/source/tuner/Tuner.java +++ b/src/main/java/io/github/dsheirer/source/tuner/Tuner.java @@ -1,19 +1,17 @@ /******************************************************************************* - * SDR Trunk - * Copyright (C) 2014-2016 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 + * 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 + * ******************************************************************************/ package io.github.dsheirer.source.tuner; @@ -24,20 +22,17 @@ import io.github.dsheirer.source.SourceEvent; import io.github.dsheirer.source.tuner.TunerEvent.Event; import io.github.dsheirer.source.tuner.manager.AbstractSourceManager; import io.github.dsheirer.source.tuner.manager.TunerSourceManager; -import org.slf4j.Logger; -import org.slf4j.LoggerFactory; /** - * Tuner - provides tuner channel sources, representing a channel frequency + * Tuner provides an interface to a software or hardware tuner controller that provides I/Q sample data coupled with an + * abstract source manager to provide access to Digital Drop Channel (DDC) resources. */ -public abstract class Tuner +public abstract class Tuner implements ISourceEventProcessor { - private final static Logger mLog = LoggerFactory.getLogger(Tuner.class); - - protected Broadcaster mTunerEventBroadcaster = new Broadcaster<>(); + private Broadcaster mTunerEventBroadcaster = new Broadcaster<>(); private TunerController mTunerController; + private AbstractSourceManager mSourceManager; private String mName; - private AbstractSourceManager mTunerSourceManager; /** * Abstract tuner class. @@ -48,28 +43,31 @@ public abstract class Tuner { mName = name; mTunerController = tunerController; + //Register to receive frequency and sample rate change notifications + mTunerController.addListener(this::process); - mTunerSourceManager = new TunerSourceManager(mTunerController); + mSourceManager = new TunerSourceManager(mTunerController); + //Register to receive channel count change notifications + mSourceManager.addSourceEventListener(this::process); + } - //Rebroadcast frequency and sample rate change events as tuner events - mTunerController.addListener(new ISourceEventProcessor() + @Override + public void process(SourceEvent event) + { + switch(event.getEvent()) { - @Override - public void process(SourceEvent event) - { - switch(event.getEvent()) - { - case NOTIFICATION_FREQUENCY_CHANGE: - broadcast(new TunerEvent(Tuner.this, Event.FREQUENCY)); - break; - case NOTIFICATION_SAMPLE_RATE_CHANGE: - broadcast(new TunerEvent(Tuner.this, Event.SAMPLE_RATE)); - break; - default: - break; - } - } - }); + case NOTIFICATION_CHANNEL_COUNT_CHANGE: + broadcast(new TunerEvent(Tuner.this, Event.CHANNEL_COUNT)); + break; + case NOTIFICATION_FREQUENCY_CHANGE: + broadcast(new TunerEvent(Tuner.this, Event.FREQUENCY)); + break; + case NOTIFICATION_SAMPLE_RATE_CHANGE: + broadcast(new TunerEvent(Tuner.this, Event.SAMPLE_RATE)); + break; + default: + break; + } } /** @@ -77,14 +75,20 @@ public abstract class Tuner */ public AbstractSourceManager getSourceManager() { - return mTunerSourceManager; + return mSourceManager; } + /** + * Tuner controller. + */ public TunerController getTunerController() { return mTunerController; } + /** + * Name for this tuner + */ public String toString() { return mName; @@ -94,19 +98,19 @@ public abstract class Tuner { } + /** + * Sets the name for this tuner + */ public void setName(String name) { mName = name; } /** - * Unique identifier for this tuner, used to lookup a tuner configuration - * from the settings manager. + * Unique identifier for this tuner, used to lookup a tuner configuration from the settings manager. * - * @return - unique identifier like a serial number, or a usb bus location - * or ip address and port. Return some form of unique identification that - * allows this tuner to be identified from among several of the same types - * of tuners. + * @return - unique identifier like a serial number, or a usb bus location or ip address and port. Return some + * form of unique identification that allows this tuner to be identified from among the same types of tuners. */ public abstract String getUniqueID(); @@ -154,7 +158,7 @@ public abstract class Tuner /** * Broadcasts the tuner change event */ - public void broadcast(TunerEvent tunerEvent) + protected void broadcast(TunerEvent tunerEvent) { mTunerEventBroadcaster.broadcast(tunerEvent); } diff --git a/src/main/java/io/github/dsheirer/source/tuner/TunerController.java b/src/main/java/io/github/dsheirer/source/tuner/TunerController.java index 0076862f..688ee442 100644 --- a/src/main/java/io/github/dsheirer/source/tuner/TunerController.java +++ b/src/main/java/io/github/dsheirer/source/tuner/TunerController.java @@ -26,29 +26,19 @@ import io.github.dsheirer.source.ISourceEventProcessor; import io.github.dsheirer.source.SourceEvent; import io.github.dsheirer.source.SourceEventListenerToProcessorAdapter; import io.github.dsheirer.source.SourceException; -import io.github.dsheirer.source.tuner.channel.CICTunerChannelSource; -import io.github.dsheirer.source.tuner.channel.TunerChannel; -import io.github.dsheirer.source.tuner.channel.TunerChannelSource; import io.github.dsheirer.source.tuner.configuration.TunerConfiguration; import io.github.dsheirer.source.tuner.frequency.FrequencyController; import io.github.dsheirer.source.tuner.frequency.FrequencyController.Tunable; import org.slf4j.Logger; import org.slf4j.LoggerFactory; -import java.util.SortedSet; -import java.util.concurrent.ConcurrentSkipListSet; -import java.util.concurrent.RejectedExecutionException; - public abstract class TunerController implements Tunable, ISourceEventProcessor, ISourceEventListener, IComplexBufferProvider, Listener { private final static Logger mLog = LoggerFactory.getLogger(TunerController.class); protected Broadcaster mSampleBroadcaster = new Broadcaster<>(); - - /* List of currently tuned channels being served to demod channels */ - private SortedSet mTunedChannels = new ConcurrentSkipListSet<>(); protected FrequencyController mFrequencyController; - private int mMiddleUnusable; + private int mMiddleUnusableHalfBandwidth; private double mUsableBandwidthPercentage; private Listener mSourceEventListener; @@ -58,15 +48,15 @@ public abstract class TunerController implements Tunable, ISourceEventProcessor, * * @param minimumFrequency minimum uncorrected tunable frequency * @param maximumFrequency maximum uncorrected tunable frequency - * @param middleUnusable is the +/- value center DC spike to avoid for channels + * @param middleUnusableHalfBandwidth is the +/- value center DC spike to avoid for channels * @param usableBandwidth percentage of usable bandwidth relative to space at the extreme ends of the spectrum * @throws SourceException - for any issues related to constructing the * class, tuning a frequency, or setting the bandwidth */ - public TunerController(long minimumFrequency, long maximumFrequency, int middleUnusable, double usableBandwidth) + public TunerController(long minimumFrequency, long maximumFrequency, int middleUnusableHalfBandwidth, double usableBandwidth) { mFrequencyController = new FrequencyController(this, minimumFrequency, maximumFrequency, 0.0d); - mMiddleUnusable = middleUnusable; + mMiddleUnusableHalfBandwidth = middleUnusableHalfBandwidth; mUsableBandwidthPercentage = usableBandwidth; mSourceEventListener = new SourceEventListenerToProcessorAdapter(this); } @@ -182,218 +172,39 @@ public abstract class TunerController implements Tunable, ISourceEventProcessor, } /** - * Indicates if channel along with all of the other currently sourced - * channels can fit within the tunable bandwidth. + * Total bandwidth of the middle unusable bandwidth region. This value is used to avoid a central DC spike + * present in some tuners. */ - private boolean canTune(TunerChannel channel) + public int getMiddleUnusableHalfBandwidth() { - //Make sure we're within the tunable frequency range of this tuner - if(getMinFrequency() < channel.getMinFrequency() && getMaxFrequency() > channel.getMaxFrequency()) - { - //If this is the first lock, then we're good - if(mTunedChannels.isEmpty()) - { - return true; - } - else - { - int usableBandwidth = getUsableBandwidth(); - long minLockedFrequency = mTunedChannels.first().getMinFrequency(); - long maxLockedFrequency = mTunedChannels.last().getMaxFrequency(); - - //Requested channel is within current locked channel frequency range - if(minLockedFrequency <= channel.getMinFrequency() && channel.getMaxFrequency() <= maxLockedFrequency) - { - return true; - } - - //Requested channel is higher than min locked frequency - if(channel.getMaxFrequency() > minLockedFrequency && - channel.getMaxFrequency() - minLockedFrequency <= usableBandwidth) - { - return true; - } - - //Requested channel is lower than the max locked frequency - if(channel.getMinFrequency() <= maxLockedFrequency && - maxLockedFrequency - channel.getMinFrequency() <= usableBandwidth) - { - return true; - } - } - } - - return false; + return mMiddleUnusableHalfBandwidth; } /** - * Constructs a digital drop channel (DDC) as a tuner channel from from the tuner, or returns null if the channel - * cannot be sourced from the tuner. - * - * This controller can't provide a channel if the channel, along with the current set of sourced channels, can't be - * accomodated within the current tuner bandwidth, or if a center frequency cannot be calculated that will ensure - * all of the channels can be accomodated and any defined central DC spike avoided. - * - * @param tuner to source the channel from - * @param channel with defined center frequency and bandwidth - * @return fully constructed tuner channel or null - * @throws RejectedExecutionException if the decimation processor has an error + * Indicates if this tuner controller has a middle unusable bandwidth region. */ - public TunerChannelSource getChannel(Tuner tuner, TunerChannel channel) throws RejectedExecutionException + public boolean hasMiddleUnusableBandwidth() { - TunerChannelSource source = null; - - if(canTune(channel)) - { - try - { - mTunedChannels.add(channel); - - if(requiresLOUpdate(channel)) - { - updateLOFrequency(); - } - - source = new CICTunerChannelSource(tuner, channel); - } - catch(SourceException se) - { - mTunedChannels.remove(channel); - source = null; - } - } - - return source; - } - - public int getChannelCount() - { - return mTunedChannels.size(); - } - - /** - * Indicates if the tuner's LO frequency must be updated in order to accommodate the tuner channel - */ - private boolean requiresLOUpdate(TunerChannel channel) throws SourceException - { - boolean isBelow = channel.getMinFrequency() < getMinTunedFrequency(); - boolean isAbove = channel.getMaxFrequency() > getMaxTunedFrequency(); - boolean overlapsCenter = mMiddleUnusable > 0 && - channel.overlaps(getFrequency() - mMiddleUnusable, getFrequency() + mMiddleUnusable); - - return isAbove || isBelow || overlapsCenter; - } - - /** - * Releases the currently sourced tuner channel from this tuner and shuts down the tuner if no other sources exist. - */ - public void releaseChannel(TunerChannelSource tunerChannelSource) - { - if(tunerChannelSource != null) - { - mTunedChannels.remove(tunerChannelSource.getTunerChannel()); - } - } - - /** - * Sets the Local Oscillator frequency to accomodate the current set of tuned channels. - * - * If there is only a single tuned channel, it is placed immediately to the right of the usable bandwidth right - * of the central DC spike. - * - * Otherwise, it places the highest channel frequency at the upper end of the tuner bandwidth, and then iteratively - * moves the center frequency higher until all channels fit within the bandwidth and none of the channels overlap - * any defined central DC spike unusable region. If a center tune frequency cannot be calculated, throw an - * exception so that the most recently added channel can be removed. - * - * Note: the tuned frequency is not changed until a legitimate new frequency can be calculated. If an exception - * is thrown, the current frequency is retained, so that the recently added channel can be removed and all other - * channels can continue as previously arranged. - * - * @throws SourceException if the set of tuner channels, including a recently added channel, cannot be tuned - * within the current tuner bandwidth and any central DC spike unusable region. - */ - private void updateLOFrequency() throws SourceException - { - long candidateFrequency; - - boolean isValidCandidateFrequency = true; - - //If there is only 1 channel set the center frequency so that the channel is positioned to the right of center - if(mTunedChannels.size() == 1) - { - candidateFrequency = mTunedChannels.first().getMinFrequency() - mMiddleUnusable + 1; - } - else - { - long minLockedFrequency = mTunedChannels.first().getMinFrequency(); - long maxLockedFrequency = mTunedChannels.last().getMaxFrequency(); - - //Start by placing the highest frequency channel at the high end of the spectrum - candidateFrequency = maxLockedFrequency - (getUsableBandwidth() / 2); - - //Iterate the channels and make sure that none of them overlap the center DC spike buffer, if one exists - if(mMiddleUnusable > 0) - { - boolean processingRequired = true; - - while(isValidCandidateFrequency && processingRequired) - { - processingRequired = false; - - long minAvoid = candidateFrequency - mMiddleUnusable; - long maxAvoid = candidateFrequency + mMiddleUnusable; - - //If any of the center channel(s) overlap the central DC spike avoid area, we'll iteratively - //increase the tuned frequency causing the set of channels to move left in the tuned bandwidth until - //we either find a good center tune frequency, or we walk the lowest frequency channel out of the - //minimum tuned range, in which case we'll throw an exception indicating we don't have a solution. - for(TunerChannel channel : mTunedChannels) - { - if(channel.overlaps(minAvoid, maxAvoid)) - { - //Calculate a tuned frequency adjustment that places this overlapping channel just to the - //left of the central DC spike avoid zone - long adjustment = channel.getMaxFrequency() - minAvoid + 1; - - //If the candidate frequency doesn't push the lowest channel out of bounds, make adjustment - if(candidateFrequency + adjustment - (getUsableBandwidth() / 2) <= minLockedFrequency) - { - candidateFrequency += adjustment; - processingRequired = true; - } - //Otherwise, punt and indicate that we can't find a center frequency or add the channel - else - { - isValidCandidateFrequency = false; - } - - //break out of the for/each loop, so that we can start over again with all of the channels - break; - } - } - } - } - } - - if(isValidCandidateFrequency) - { - mFrequencyController.setFrequency(candidateFrequency); - } - else - { - throw new SourceException("Couldn't calculate viable center frequency from set of tuner channels"); - } + return mMiddleUnusableHalfBandwidth != 0; } /** * Usable bandwidth - total bandwidth minus the unusable space at either end of the spectrum. */ - private int getUsableBandwidth() + public int getUsableBandwidth() { return (int)(getBandwidth() * mUsableBandwidthPercentage); } + /** + * Usable half bandwidth - total bandwidth minus unusable space at either end of the spectrum, minus the blocked + * center bandwidth. + */ + public int getUsableHalfBandwidth() + { + return (int)(getUsableBandwidth() / 2) - mMiddleUnusableHalfBandwidth; + } + /** * Sets the listener to be notified any time that the tuner changes frequency * or bandwidth/sample rate. diff --git a/src/main/java/io/github/dsheirer/source/tuner/TunerModel.java b/src/main/java/io/github/dsheirer/source/tuner/TunerModel.java index a0735a24..919f32d6 100644 --- a/src/main/java/io/github/dsheirer/source/tuner/TunerModel.java +++ b/src/main/java/io/github/dsheirer/source/tuner/TunerModel.java @@ -235,7 +235,7 @@ public class TunerModel extends AbstractTableModel implements Listener +public abstract class AbstractSourceManager implements IComplexBufferProvider, ISourceEventProcessor { - protected Broadcaster mComplexBufferBroadcaster = new Broadcaster<>(); + private Broadcaster mComplexBufferBroadcaster = new Broadcaster<>(); + private Broadcaster mSourceEventBroadcaster = new Broadcaster<>(); /** - * Indicates if this source manager can tune (ie provide a source for) the specified tuner channel + * Sorted set of tuner channels being sourced by this source manager. Set is ordered by frequency lowest to highest */ - public abstract boolean canTune(TunerChannel tunerChannel); + public abstract SortedSet getTunerChannels(); /** - * List of tuner channels being sourced by this source manager + * Count of tuner channels being sourced by this source manager. + * @return */ - public abstract List getChannels(); + public abstract int getTunerChannelCount(); /** * Obtains a source for the tuner channel or returns null if the channel cannot be sourced by this tuner. - * Note: use the canTune() method to check if a tuner channel can be sourced/tuned prior to accessing this method. + * + * Note: you MUST invoke start() on the obtained source to start the sample flow and invoke stop() to release all + * resources allocated for the tuner channel source. * * @param tunerChannel for requested source * @return tuner channel source or null */ public abstract TunerChannelSource getSource(TunerChannel tunerChannel); - - /** - * Releases the source and all related resources - */ - public abstract void releaseSource(TunerChannelSource source); - - /** - * Adds the listener to receive source events from this source manager - * - * @param sourceEventListener to add - */ - public abstract void addSourceEventListener(Listener sourceEventListener); - - /** - * Removes the listener from receiving source events from this source manager - * @param sourceEventListener to remove - */ - public abstract void removeSourceEventListener(Listener sourceEventListener); - /** * Adds the listener to receive complex buffer samples */ @@ -105,4 +91,28 @@ public abstract class AbstractSourceManager implements IComplexBufferProvider, L { mComplexBufferBroadcaster.broadcast(complexBuffer); } + + /** + * Adds a listener to receive source events + */ + public void addSourceEventListener(Listener listener) + { + mSourceEventBroadcaster.addListener(listener); + } + + /** + * Remove the listener from receiving source events + */ + public void removeSourceEventListener(Listener listener) + { + mSourceEventBroadcaster.removeListener(listener); + } + + /** + * Broadcasts the source event to any registered listeners + */ + protected void broadcast(SourceEvent sourceEvent) + { + mSourceEventBroadcaster.broadcast(sourceEvent); + } } diff --git a/src/main/java/io/github/dsheirer/source/tuner/manager/TunerSourceManager.java b/src/main/java/io/github/dsheirer/source/tuner/manager/TunerSourceManager.java index 8b7c913d..9276f649 100644 --- a/src/main/java/io/github/dsheirer/source/tuner/manager/TunerSourceManager.java +++ b/src/main/java/io/github/dsheirer/source/tuner/manager/TunerSourceManager.java @@ -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 @@ -16,65 +16,405 @@ package io.github.dsheirer.source.tuner.manager; import io.github.dsheirer.dsp.filter.channelizer.PolyphaseChannelManager; -import io.github.dsheirer.sample.Listener; import io.github.dsheirer.source.SourceEvent; +import io.github.dsheirer.source.SourceException; import io.github.dsheirer.source.tuner.TunerController; import io.github.dsheirer.source.tuner.channel.TunerChannel; import io.github.dsheirer.source.tuner.channel.TunerChannelSource; +import org.slf4j.Logger; +import org.slf4j.LoggerFactory; -import java.util.List; +import java.util.SortedSet; public class TunerSourceManager extends AbstractSourceManager { + private final static Logger mLog = LoggerFactory.getLogger(TunerSourceManager.class); private PolyphaseChannelManager mPolyphaseChannelManager; private TunerController mTunerController; + /** + * TunerSourceManager is responsible for managing the tuner's center tuned frequency and providing access to + * polyphase tuner channel sources (ie DDCs). This class is responsible for determining IF a requested channel can + * be provided and then adjusting the center frequency and provisioning a DDC Polyphase Tuner channel source. + * + * @param tunerController with a center tuned frequency that will be managed by this instance + */ public TunerSourceManager(TunerController tunerController) { mTunerController = tunerController; + //Register to receive frequency and sample rate notifications + mTunerController.addListener(this); + + mPolyphaseChannelManager = new PolyphaseChannelManager(tunerController); + //Register to receive channel count change notifications for rebroadcasting + mPolyphaseChannelManager.addSourceEventListener(this::process); } - //TODO: broadcast a tuner event each time we add a new channel to the polyphase manager - - @Override - public boolean canTune(TunerChannel tunerChannel) + /** + * Indicates if the channel min/max frequencies are within the tunable frequency range of the tuner controller + * + * @param tunerChannel to check + * @return true if the channel is within the frequency range of the tuner controller + */ + private boolean isTunable(TunerChannel tunerChannel) { + return mTunerController.canTune(tunerChannel.getMinFrequency()) && + mTunerController.canTune(tunerChannel.getMaxFrequency()); + } + + /** + * Indicates if the list of tuner channels can be tuned by the tuner controller within the current bandwidth and + * tuner center-blocked frequency range limitations. + * + * @param tunerChannels that are being evaluated + * @return true if the set of channels fit within the bandwidth and center blocked frequency band of the tuner + */ + private boolean canTune(SortedSet tunerChannels) + { + //If there is only one channel, check to ensure it fits the current bandwidth or half-bandwidth + if(tunerChannels.size() == 1) + { + if(mTunerController.hasMiddleUnusableBandwidth()) + { + return tunerChannels.first().getBandwidth() < mTunerController.getUsableHalfBandwidth(); + } + else + { + return tunerChannels.first().getBandwidth() < mTunerController.getUsableBandwidth(); + } + } + else + { + //Check that the total bandwidth of the channel set is within the usable bandwidth of the tuner + int tunerUsableBandwidth = mTunerController.getUsableBandwidth(); + + long channelSetBandwidth = tunerChannels.last().getMaxFrequency() - tunerChannels.first().getMinFrequency(); + + if(channelSetBandwidth <= tunerUsableBandwidth) + { + //If the tuner doesn't have a center DC spike blocked region, we're good + if(!mTunerController.hasMiddleUnusableBandwidth()) + { + return true; + } + + //Check to see if the channel set fits within the usable half bandwidth of the tuner + int tunerUsableHalfBandwidth = mTunerController.getUsableHalfBandwidth(); + + if(channelSetBandwidth < tunerUsableHalfBandwidth) + { + return true; + } + + //At this point we're within the tuner's total usable bandwidth, but there is a central DC spike blocked + //region. See if we can notionally partition the channels into left/right usable half bandwidth regions + TunerChannel firstRightChannel = null; + + for(TunerChannel tunerChannel: tunerChannels) + { + if(firstRightChannel == null) + { + long leftSideBandwidth = tunerChannel.getMaxFrequency() - tunerChannels.first().getMinFrequency(); + + if(leftSideBandwidth > tunerUsableHalfBandwidth) + { + //Ensure this first right-partition channel fits within the half bandwidth + if(tunerChannel.getBandwidth() > tunerUsableHalfBandwidth) + { + return false; + } + + firstRightChannel = tunerChannel; + } + } + else + { + long rightSideBandwidth = tunerChannel.getMaxFrequency() - firstRightChannel.getMinFrequency(); + + //If the right side partition's bandwidth exceeds the half bandwidth, we're done + if(rightSideBandwidth > tunerUsableHalfBandwidth) + { + return false; + } + } + } + + //All channels can be partitioned into notional left/right partition sets - we're good + return true; + } + } + return false; } - @Override - public List getChannels() + /** + * Calculates the optimal center tune frequency for the set of currently sourced tuner channels. Note: the returned + * frequency will only be different from the current center frequency argument if a change is required in order + * to fit all of the sourced tuner channels, or to better optimize the channels for alignment with the current + * polyphase channelizer channel partitioning. + * + * @param channels that are currently sourced by this source manager + * @param currentCenterFrequency of the tuner. + * @return optimal center tuned frequency for the set of currently sourced tuner channels. + * @throws IllegalArgumentException if a center frequency cannot be determined for the set of tuner channels + */ + private long getCenterFrequency(SortedSet channels, long currentCenterFrequency) + throws IllegalArgumentException { - return null; + if(channels.isEmpty()) + { + return currentCenterFrequency; + } + + long bestIntegralFrequency = getIntegralFrequency(channels); + + //See if we can reuse the current frequency + if(isIntegralSpacing(currentCenterFrequency, bestIntegralFrequency) && + isValidCenterFrequency(channels, currentCenterFrequency)) + { + return currentCenterFrequency; + } + + int integralHalfChannelCount = (int)(mTunerController.getUsableHalfBandwidth() / + mPolyphaseChannelManager.getChannelBandwidth()) + 1; + + int integralHalfBandwidth = (int)(integralHalfChannelCount * mPolyphaseChannelManager.getChannelBandwidth()); + + long minimumFrequency = channels.last().getFrequency() - integralHalfBandwidth; + long testFrequency = channels.first().getFrequency() + integralHalfBandwidth; + + if(isValidCenterFrequency(channels,testFrequency)) + { + return testFrequency; + } + + while(testFrequency >= minimumFrequency) + { + testFrequency -= (long)mPolyphaseChannelManager.getChannelBandwidth(); + + if(isValidCenterFrequency(channels, testFrequency)) + { + return testFrequency; + } + } + + //If we can't find an integral center frequency, choose a frequency that will fit the channels + minimumFrequency = channels.last().getFrequency() - mTunerController.getUsableHalfBandwidth(); + testFrequency = channels.first().getMinFrequency() + mTunerController.getUsableHalfBandwidth(); + + if(isValidCenterFrequency(channels,testFrequency)) + { + return testFrequency; + } + + while(testFrequency >= minimumFrequency) + { + testFrequency--; + + if(isValidCenterFrequency(channels, testFrequency)) + { + return testFrequency; + } + } + + throw new IllegalArgumentException("Can't calculate valid center frequency for the channel set"); } + /** + * Indicates if the candidate center frequency is valid to ensure that all of the channels fit within the tuner's + * bandwidth and avoid any center DC spike blocked frequency region + * + * @param tunerChannels to test for the candidate center frequency + * @param centerFrequency to test + * @return true if the candidate center frequency is valid for the set of channels + */ + private boolean isValidCenterFrequency(SortedSet tunerChannels, long centerFrequency) + { + for(TunerChannel tunerChannel: tunerChannels) + { + if(!isValidCenterFrequency(tunerChannel, centerFrequency)) + { + return false; + } + } + + return true; + } + + /** + * Indicates if the channel will fit within the tuner's bandwidth if the tuner were using the candidate center + * frequency and that the channel does not overlap an unusable center region that would be centered about the + * candidate center frequency + * + * @param tunerChannel to test for fit + * @param centerFrequency to test against + * @return true if the channel will fit when the tuner uses the specified center frequency + */ + private boolean isValidCenterFrequency(TunerChannel tunerChannel, long centerFrequency) + { + boolean fits = (tunerChannel.getMinFrequency() >= centerFrequency - mTunerController.getUsableHalfBandwidth()) && + (tunerChannel.getMaxFrequency() <= centerFrequency + mTunerController.getUsableHalfBandwidth()); + + if(fits && mTunerController.hasMiddleUnusableBandwidth()) + { + fits = !tunerChannel.overlaps(centerFrequency - mTunerController.getMiddleUnusableHalfBandwidth(), + centerFrequency + mTunerController.getMiddleUnusableHalfBandwidth()); + } + + return fits; + } + + /** + * Indicates if the two frequencies have a spacing that is an integral multiple of the channel bandwidth. + */ + private boolean isIntegralSpacing(long frequencyA, long frequencyB) + { + double delta = Math.abs(frequencyA - frequencyB); + return delta % mPolyphaseChannelManager.getChannelBandwidth() <= 1.0; + } + + /** + * Identifies the tuner channel frequency that has the best fit as an integral multiple of all of the other tuner + * channels. Using a polyphase channelizer to split each of the channels at a spacing of the channel sample rate, + * best fit is defined as the lowest absolute miss distance for each channel from the integral centers + * for each of the channels in the polyphase channelizer. + * + * @param channels containing a subset of channels that are spaced at integral multiples of the channel sample rate + * @return best channel containing the highest subset of matching, integrally spaced channels in the set, or null + * if there are no channels that are integrally spaced. + */ + private long getIntegralFrequency(SortedSet channels) + { + if(channels.isEmpty()) + { + throw new IllegalArgumentException("Channels cannot be empty"); + } + + long bestIntegralFrequency = channels.first().getFrequency(); + + if(channels.size() > 1) + { + double bestScore = Double.MAX_VALUE; + + for(TunerChannel firstChannel: channels) + { + double score = 0.0; + + for(TunerChannel secondChannel: channels) + { + if(firstChannel != secondChannel) + { + double delta = Math.abs(firstChannel.getFrequency() - secondChannel.getFrequency()) % + mPolyphaseChannelManager.getChannelBandwidth(); + + score += delta; + } + } + + if(score < bestScore) + { + bestScore = score; + bestIntegralFrequency = firstChannel.getFrequency(); + } + } + } + + return bestIntegralFrequency; + } + + /** + * List of tuner channels currently being sourced by this source manager + */ + @Override + public SortedSet getTunerChannels() + { + return mPolyphaseChannelManager.getTunerChannels(); + } + + /** + * Count of tuner channels currently being sourced by this source manager + */ + @Override + public int getTunerChannelCount() + { + return mPolyphaseChannelManager.getTunerChannelCount(); + } + + /** + * Allocates a tuner channel source for the tuner channel. + * + * @param tunerChannel for requested source + * @return allocated DDC tuner channel source, or null if the channel cannot be provided by this source manager + */ @Override public TunerChannelSource getSource(TunerChannel tunerChannel) { + if(isTunable(tunerChannel)) + { + //Get a new set of currently tuned channels + SortedSet tunerChannels = getTunerChannels(); + + //Add the requested channel to the list + tunerChannels.add(tunerChannel); + + if(canTune(tunerChannels)) + { + long currentCenterFrequency = mTunerController.getFrequency(); + long updatedCenterFrequency = 0; + + //Attempt to adjust the center frequency before we allocate the channel + try + { + updatedCenterFrequency = getCenterFrequency(tunerChannels, currentCenterFrequency); + + if(updatedCenterFrequency != currentCenterFrequency && updatedCenterFrequency != 0) + { + mTunerController.setFrequency(updatedCenterFrequency); + } + + //If we're successful to here, allocate the channel + return mPolyphaseChannelManager.getChannel(tunerChannel); + } + catch(SourceException se) + { + //Tuner controller threw an error trying to tune to the updated frequency + mLog.error("Error while updating tuner controller with new center frequency [" + + updatedCenterFrequency + "] - unable to allocate new tuner channel", se); + } + catch(IllegalArgumentException iae) + { + //Center frequency calculation failed + mLog.error("Couldn't calculate new tuner center frequency for new tuner channel [" + + tunerChannel + "] unable to allocate new tuner channel", iae); + } + } + } + return null; } + /** + * Processes source events received from the tuner by simply passing them on to the embedded polyphase channel + * manager + */ @Override - public void releaseSource(TunerChannelSource source) + public void process(SourceEvent sourceEvent) { - - } - - @Override - public void addSourceEventListener(Listener sourceEventListener) - { - - } - - @Override - public void removeSourceEventListener(Listener sourceEventListener) - { - - } - - @Override - public void receive(SourceEvent sourceEvent) - { - + switch(sourceEvent.getEvent()) + { + case NOTIFICATION_CHANNEL_COUNT_CHANGE: + //Rebroadcast this event to any registered listeners (ie tuner and tuner controller) + broadcast(sourceEvent); + break; + case NOTIFICATION_FREQUENCY_CHANGE: + mPolyphaseChannelManager.process(sourceEvent); + break; + case NOTIFICATION_SAMPLE_RATE_CHANGE: + mPolyphaseChannelManager.process(sourceEvent); + break; + default: + mLog.info("Received an unrecognized source event: " + sourceEvent); + break; + } } } From 05fa5c25fd323f32c5302fdc9c3ee4e49cb9bc0a Mon Sep 17 00:00:00 2001 From: Denny Date: Sat, 13 Jan 2018 09:10:57 -0500 Subject: [PATCH 40/87] #39 WIP - adds support for timestamped complex buffers so that decoded messages and events can be accurately timestamped, and more importantly, allocated traffic channels can request to be pre-loaded with buffered samples to ensure that traffic channels don't miss any samples that are currently missed due processing latency inherent in current buffer sizing. This should reduce the clipping of starting messages or voice on the traffic channels. Restructures processing path for tuner frequency and sample rate change events. The tuner now embeds these events in the sequence of buffers that are generated immediately after the event occurs since those are the buffers that will be impacted by the change. Previously, the change event would be broadcast throughout the system and would impact any buffered/cached buffers. This also reduces the number of source event change listeners that have to be managed. Note: this change is not yet implemented in all tuners ... only in the TestTuner implementation. Updates the polyphase channelizer to now buffer channel results and then dispatch the buffer to the channel output processors to improve efficiency of using float arrays versus single value complex float processing. --- .../AbstractComplexPolyphaseChannelizer.java | 6 +- .../filter/channelizer/ChannelizerViewer.java | 9 +- .../ComplexPolyphaseChannelizerM2.java | 114 +++++++----- .../channelizer/PolyphaseChannelManager.java | 139 +++++++++------ .../PolyphaseChannelResultsBuffer.java | 166 ++++++++++++++++++ .../channelizer/PolyphaseChannelSource.java | 30 ++-- .../channelizer/SampleTimestampManager.java | 83 +++++++++ .../output/ChannelOutputProcessor.java | 62 +++---- .../IPolyphaseChannelOutputProcessor.java | 11 +- .../output/OneChannelOutputProcessor.java | 30 ++-- .../output/TwoChannelOutputProcessor.java | 20 +-- .../github/dsheirer/dsp/mixer/Oscillator.java | 18 ++ .../io/github/dsheirer/sample/Buffer.java | 91 ++++++++-- .../sample/complex/ComplexBuffer.java | 71 ++++++-- .../complex/TimestampedBufferAssembler.java | 132 ++++++++++++++ .../complex/TimestampedComplexBuffer.java | 88 ++++++++++ .../dsheirer/sample/real/RealBuffer.java | 57 ++++-- .../github/dsheirer/source/SourceEvent.java | 15 +- .../source/tuner/TunerController.java | 7 +- .../source/tuner/channel/TunerChannel.java | 6 +- .../tuner/channel/TunerChannelSource.java | 4 +- .../tuner/manager/TunerSourceManager.java | 52 ++++-- .../source/tuner/test/SampleGenerator.java | 17 +- .../dsheirer/source/tuner/test/TestTuner.java | 2 +- .../tuner/test/TestTunerController.java | 9 +- 25 files changed, 962 insertions(+), 277 deletions(-) create mode 100644 src/main/java/io/github/dsheirer/dsp/filter/channelizer/PolyphaseChannelResultsBuffer.java create mode 100644 src/main/java/io/github/dsheirer/dsp/filter/channelizer/SampleTimestampManager.java create mode 100644 src/main/java/io/github/dsheirer/sample/complex/TimestampedBufferAssembler.java create mode 100644 src/main/java/io/github/dsheirer/sample/complex/TimestampedComplexBuffer.java diff --git a/src/main/java/io/github/dsheirer/dsp/filter/channelizer/AbstractComplexPolyphaseChannelizer.java b/src/main/java/io/github/dsheirer/dsp/filter/channelizer/AbstractComplexPolyphaseChannelizer.java index 85b2e832..25d0de82 100644 --- a/src/main/java/io/github/dsheirer/dsp/filter/channelizer/AbstractComplexPolyphaseChannelizer.java +++ b/src/main/java/io/github/dsheirer/dsp/filter/channelizer/AbstractComplexPolyphaseChannelizer.java @@ -90,13 +90,13 @@ public abstract class AbstractComplexPolyphaseChannelizer implements Listener polyphaseIndexes = mChannelCalculator.getChannelIndexes(tunerChannel); + mLog.debug("Getting polyphase channel for: " + tunerChannel + " indexes:" + polyphaseIndexes); IPolyphaseChannelOutputProcessor outputProcessor = getOutputProcessor(polyphaseIndexes); @@ -180,7 +183,7 @@ public class PolyphaseChannelManager implements ISourceEventProcessor synchronized(mBufferProcessor) { mPolyphaseChannelizer.addChannel(channelSource); - mSourceEventBroadcaster.broadcast(SourceEvent.channelCountChange(mChannelSources.size())); + mSourceEventBroadcaster.broadcast(SourceEvent.channelCountChange(getTunerChannelCount())); if(mPolyphaseChannelizer.getRegisteredChannelCount() == 1) { @@ -201,7 +204,7 @@ public class PolyphaseChannelManager implements ISourceEventProcessor synchronized(mBufferProcessor) { mChannelSources.remove(channelSource); - mSourceEventBroadcaster.broadcast(SourceEvent.channelCountChange(mChannelSources.size())); + mSourceEventBroadcaster.broadcast(SourceEvent.channelCountChange(getTunerChannelCount())); if(mPolyphaseChannelizer.getRegisteredChannelCount() == 0) { @@ -209,52 +212,33 @@ public class PolyphaseChannelManager implements ISourceEventProcessor mBufferProcessor.stop(); } } - } - /** - * Implements the ISourceEventProcessor interface for receiving notifications of frequency and sample rate - * changes from the tuner. - */ - @Override - public void process(SourceEvent sourceEvent) - { - switch(sourceEvent.getEvent()) + try { - case NOTIFICATION_FREQUENCY_CHANGE: - //Update the index calculator and each of the output processors to update each channel frequency - mChannelCalculator.setCenterFrequency(sourceEvent.getValue().longValue()); - updateOutputProcessors(sourceEvent); - break; - case NOTIFICATION_SAMPLE_RATE_CHANGE: - //Update the channelizer (and the output processors) for the new sample rate - updateChannelizer(sourceEvent.getValue().intValue(), sourceEvent); - break; - default: - mLog.info("Received an unrecognized source event: " + sourceEvent.getEvent()); - break; + //Broadcast a stop sample stream notification in case this was a forced-stop so consumers are aware + channelSource.process(SourceEvent.stopSampleStreamNotification(channelSource)); + } + catch(SourceException se) + { + //Do nothing } } /** * Creates or updates the channelizer to process the incoming sample rate and updates any channel processors. * + * Note: this method should only be invoked on the mBufferProcessor thread or prior to starting the mBufferProcessor. + * Sample rate source events will normally arrive via the incoming complex buffer stream from the mBufferProcessor + * and will be handled as they arrive. + * * @param sampleRate to use for the channelizer - * @param sourceEvent containing a notification of sample rate change (optional - can be null). This source - * event will be passed to all output processors to ensure the change is fully promulgated before restarting the - * buffer processor if it was already running. */ - private void updateChannelizer(double sampleRate, SourceEvent sourceEvent) + private void updateChannelizerSampleRate(double sampleRate) { - mChannelCalculator.setSampleRate(sampleRate); - - //Lock on the buffer processor to block any channel create/start/stop requests while we configure - synchronized(mBufferProcessor) + if(mPolyphaseChannelizer == null || Math.abs(mPolyphaseChannelizer.getSampleRate() - sampleRate) > 1.0) { - //Get current processor state so we can restart it after we update the channelizer - boolean bufferProcessorWasRunning = mBufferProcessor.isRunning(); - - //Stop will be ignored if it is not currently running - mBufferProcessor.stop(); + mLog.debug("Creating or Updating Channelizer for sample rate " + sampleRate); + mChannelCalculator.setSampleRate(sampleRate); try { @@ -262,8 +246,6 @@ public class PolyphaseChannelManager implements ISourceEventProcessor { mPolyphaseChannelizer = new ComplexPolyphaseChannelizerM2(sampleRate, POLYPHASE_FILTER_TAPS_PER_CHANNEL); - - mBufferProcessor.setListener(mPolyphaseChannelizer); } else { @@ -279,16 +261,23 @@ public class PolyphaseChannelManager implements ISourceEventProcessor mLog.error("Could not create filter for polyphase channelizer for sample rate [" + sampleRate + "]", fde); } - if(sourceEvent != null && sourceEvent.getEvent() == SourceEvent.Event.NOTIFICATION_SAMPLE_RATE_CHANGE) - { - updateOutputProcessors(sourceEvent); - } - - if(bufferProcessorWasRunning) - { - mBufferProcessor.start(); - } + //Broadcast the new channel sample rate + double updatedChannelSampleRate = mChannelCalculator.getChannelSampleRate(); + updateOutputProcessors(SourceEvent.sampleRateChange(updatedChannelSampleRate)); } + else + { + mLog.debug("Request to update current sample rate ignored: " + sampleRate); + } + } + + private void updateChannelizerCenterFrequency(long frequency) + { + mChannelCalculator.setCenterFrequency(frequency); + + mLog.debug("Updating output processors - center frequency is now: " + frequency); + + updateOutputProcessors(null); } /** @@ -310,6 +299,7 @@ public class PolyphaseChannelManager implements ISourceEventProcessor { try { +//TODO: this notification event should go to the channelizer to be embedded in the channel buffer stream channelSource.process(sourceEvent); } catch(SourceException se) @@ -331,6 +321,8 @@ public class PolyphaseChannelManager implements ISourceEventProcessor { try { + //If a change in sample rate or center frequency makes this channel no longer viable, then the channel + //calculator will throw an IllegalArgException ... handled below List indexes = mChannelCalculator.getChannelIndexes(channelSource.getTunerChannel()); long centerFrequency = mChannelCalculator.getCenterFrequencyForIndexes(indexes); @@ -338,11 +330,13 @@ 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 indexes to " + indexes + " for: " + channelSource.getTunerChannel()); channelSource.getPolyphaseChannelOutputProcessor().setPolyphaseChannelIndices(indexes); channelSource.setFrequency(centerFrequency); } else { + mLog.debug("Creating new output processor for indexes " + indexes); channelSource.setPolyphaseChannelOutputProcessor(getOutputProcessor(indexes), centerFrequency); } } @@ -433,4 +427,49 @@ public class PolyphaseChannelManager implements ISourceEventProcessor } } } + + /** + * Processes the incoming buffer stream from the provider to detect frequency and sample rate change source events + * and update the channelizer configuration as necessary. + */ + public class BufferSourceEventMonitor implements Listener + { + @Override + public void receive(ComplexBuffer complexBuffer) + { + if(complexBuffer.hasSourceEvent()) + { + process(complexBuffer.getSourceEvent()); + } + + if(mPolyphaseChannelizer != null) + { + mPolyphaseChannelizer.receive(complexBuffer); + } + } + + /** + * Processes frequency and sample rate change events that are embedded in the complex buffer stream from the + * tuner or sample source + */ + public void process(SourceEvent sourceEvent) + { + switch(sourceEvent.getEvent()) + { + case NOTIFICATION_FREQUENCY_CHANGE: + //Update the index calculator and each of the output processors to update each channel frequency + mChannelCalculator.setCenterFrequency(sourceEvent.getValue().longValue()); + mLog.debug("Updating output processors - center frequency is now: " + sourceEvent.getValue().longValue()); + updateOutputProcessors(sourceEvent); + break; + case NOTIFICATION_SAMPLE_RATE_CHANGE: + //Update the channelizer (and the output processors) for the new sample rate + updateChannelizerSampleRate(sourceEvent.getValue().intValue()); + break; + default: + mLog.info("Received an unrecognized source event: " + sourceEvent.getEvent()); + break; + } + } + } } \ No newline at end of file diff --git a/src/main/java/io/github/dsheirer/dsp/filter/channelizer/PolyphaseChannelResultsBuffer.java b/src/main/java/io/github/dsheirer/dsp/filter/channelizer/PolyphaseChannelResultsBuffer.java new file mode 100644 index 00000000..f0ccc96c --- /dev/null +++ b/src/main/java/io/github/dsheirer/dsp/filter/channelizer/PolyphaseChannelResultsBuffer.java @@ -0,0 +1,166 @@ +/******************************************************************************* + * 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 + * + ******************************************************************************/ +package io.github.dsheirer.dsp.filter.channelizer; + +import java.util.Arrays; + +public class PolyphaseChannelResultsBuffer +{ + private long mTimestamp; + private int mPointer = 0; + private int mMaxSize; + private float[][] mChannelResults; + + /** + * Buffer containing an array of polyphase channelizer complex channel results. + * @param timestamp reference relative to the first channel results. + * @param maxSize of channel results to store in this buffer + */ + public PolyphaseChannelResultsBuffer(long timestamp, int maxSize) + { + mTimestamp = timestamp; + mMaxSize = maxSize; + } + + /** + * Reference timestamp relative to the first channel results contained in this buffer. + * @return reference timestamp in milliseconds since epoch + */ + public long getTimestamp() + { + return mTimestamp; + } + + /** + * Indicates if this buffer is full + */ + public boolean isFull() + { + return mPointer >= mMaxSize; + } + + /** + * Indicates if this buffer is empty + */ + public boolean isEmpty() + { + return mPointer == 0; + } + + /** + * Adds the channel results to this buffer + * @param channelResults to add + * @throws IllegalArgumentException if the buffer is full or if the size of the channel results array is a different + * length than any of the preceeding channel results that were added to this buffer + */ + public void add(float[] channelResults) throws IllegalArgumentException + { + if(mPointer >= mMaxSize) + { + throw new IllegalArgumentException("Buffer is full - cannot add new channel results"); + } + if(mPointer == 0) + { + mChannelResults = new float[mMaxSize][channelResults.length]; + mChannelResults[mPointer++] = channelResults; + } + else + { + int currentLength = mChannelResults[0].length; + + if(channelResults.length != currentLength) + { + throw new IllegalArgumentException("Channel results length [" + channelResults.length + "] cannot be " + + "a different length than the channel results that are already contained in this buffer [" + + currentLength + "]"); + } + + mChannelResults[mPointer++] = channelResults; + } + } + + /** + * Channel results array contained in this buffer + */ + public float[][] getChannelResults() + { + if(mPointer == mMaxSize) + { + return mChannelResults; + } + else + { + return Arrays.copyOf(mChannelResults, mPointer); + } + } + + /** + * Extracts a single I/Q interleaved sample buffer from the channel results + * @param iChannelIndex to extract for the inphase sample. The q index is assumed to be one greater than i. + * @return float[] containing interleaved I and Q samples + * @throws IllegalArgumentException if this buffer doesn't contain any samples or if the requested channel is + * not within the channel range of the contained channel results. + */ + public float[] getChannel(int iChannelIndex) + { + if(isEmpty()) + { + throw new IllegalArgumentException("Buffer is empty - cannot extract channel [" + iChannelIndex + "]"); + } + if(!isValidChannelIndex(iChannelIndex)) + { + throw new IllegalArgumentException("Channel [" + iChannelIndex + "] is not valid for the contained channel " + + "results -- max channel is " + getMaxChannelIndex()); + } + + float[] samples = new float[mChannelResults.length * 2]; + int pointer = 0; + + int qChannelIndex = iChannelIndex + 1; + + for(float[] channelResults: mChannelResults) + { + samples[pointer++] = channelResults[iChannelIndex]; + samples[pointer++] = channelResults[qChannelIndex]; + } + + return samples; + } + + /** + * Indicates if the channel argument is valid for the contained channel results. + * @param channelIndex number (0 to N) to check + * @return false if the buffer is empty or if the channel is greater than the contained channel results + */ + private boolean isValidChannelIndex(int channelIndex) + { + return channelIndex <= getMaxChannelIndex(); + } + + /** + * Maximum channel index represented in the channel results contained in this buffer. + * @return max channel number, or 0 if the buffer is empty + */ + private int getMaxChannelIndex() + { + if(isEmpty()) + { + return 0; + } + + return mChannelResults[0].length; + } +} diff --git a/src/main/java/io/github/dsheirer/dsp/filter/channelizer/PolyphaseChannelSource.java b/src/main/java/io/github/dsheirer/dsp/filter/channelizer/PolyphaseChannelSource.java index e2985c2a..2b5145ef 100644 --- a/src/main/java/io/github/dsheirer/dsp/filter/channelizer/PolyphaseChannelSource.java +++ b/src/main/java/io/github/dsheirer/dsp/filter/channelizer/PolyphaseChannelSource.java @@ -1,7 +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 @@ -12,14 +11,14 @@ * * You should have received a copy of the GNU General Public License along with this program. * If not, see - * ********************************************************************************************************************* + * ******************************************************************************/ package io.github.dsheirer.dsp.filter.channelizer; import io.github.dsheirer.dsp.filter.channelizer.output.IPolyphaseChannelOutputProcessor; import io.github.dsheirer.sample.Listener; import io.github.dsheirer.sample.complex.ComplexBuffer; -import io.github.dsheirer.sample.complex.ComplexToComplexBufferAssembler; +import io.github.dsheirer.sample.complex.TimestampedBufferAssembler; import io.github.dsheirer.source.SourceEvent; import io.github.dsheirer.source.tuner.channel.TunerChannel; import io.github.dsheirer.source.tuner.channel.TunerChannelSource; @@ -30,7 +29,7 @@ public class PolyphaseChannelSource extends TunerChannelSource { private final static Logger mLog = LoggerFactory.getLogger(PolyphaseChannelSource.class); - private ComplexToComplexBufferAssembler mComplexBufferAssembler = new ComplexToComplexBufferAssembler(2500); + private TimestampedBufferAssembler mTimestampedBufferAssembler; private IPolyphaseChannelOutputProcessor mPolyphaseChannelOutputProcessor; private double mChannelSampleRate; private long mCenterFrequency; @@ -52,6 +51,7 @@ public class PolyphaseChannelSource extends TunerChannelSource super(producerSourceEventListener, tunerChannel); mPolyphaseChannelOutputProcessor = outputProcessor; mChannelSampleRate = channelSampleRate; + mTimestampedBufferAssembler = new TimestampedBufferAssembler(2500, mChannelSampleRate); } /** @@ -60,7 +60,7 @@ public class PolyphaseChannelSource extends TunerChannelSource @Override public void setListener(final Listener listener) { - mComplexBufferAssembler.setListener(listener); + mTimestampedBufferAssembler.setListener(listener); } /** @@ -69,7 +69,7 @@ public class PolyphaseChannelSource extends TunerChannelSource @Override public void removeListener(Listener listener) { - mComplexBufferAssembler.setListener(null); + mTimestampedBufferAssembler.setListener(null); } /** @@ -108,7 +108,7 @@ public class PolyphaseChannelSource extends TunerChannelSource //Fully process the residual channel results buffer of the previous channel output processor if(existingProcessor != null) { - existingProcessor.processChannelResults(mComplexBufferAssembler); + existingProcessor.processChannelResults(mTimestampedBufferAssembler); } //Finally, setup the frequency offset for the output processor and reset the frequency correction value @@ -121,14 +121,15 @@ public class PolyphaseChannelSource extends TunerChannelSource } /** - * Primary method for receiving channel results output from a polyphase channelizer. The results array will be + * Primary method for receiving channel results output from a polyphase channelizer. The results buffer will be * queued for processing to extract the target channel samples, process them for frequency correction and/or * channel aggregation, and dispatch the results to the downstream sample listener/consumer. * - * @param channelResultsBuffer containing the polyphase channelizer output for a single complex sample period. + * @param channelResultsBuffer containing an array of polyphase channelizer outputs. */ - public void receiveChannelResults(float[] channelResultsBuffer) + public void receiveChannelResults(PolyphaseChannelResultsBuffer channelResultsBuffer) { + mTimestampedBufferAssembler.updateTimestamp(channelResultsBuffer.getTimestamp()); mPolyphaseChannelOutputProcessor.receiveChannelResults(channelResultsBuffer); } @@ -186,16 +187,19 @@ public class PolyphaseChannelSource extends TunerChannelSource protected void setFrequency(long frequency) { mCenterFrequency = frequency; + + //Set frequency correction to zero to trigger an update to the mixer and allow downstream monitors to + //recalculate the frequency error correction again setFrequencyCorrection(0); } @Override protected void processSamples() { - //Lock on the output processor so that it can't be changed out in the middle of processing + //Lock on the output processor so that it can't be changed in the middle of processing synchronized(mPolyphaseChannelOutputProcessor) { - mPolyphaseChannelOutputProcessor.processChannelResults(mComplexBufferAssembler); + mPolyphaseChannelOutputProcessor.processChannelResults(mTimestampedBufferAssembler); } } diff --git a/src/main/java/io/github/dsheirer/dsp/filter/channelizer/SampleTimestampManager.java b/src/main/java/io/github/dsheirer/dsp/filter/channelizer/SampleTimestampManager.java new file mode 100644 index 00000000..38c8a943 --- /dev/null +++ b/src/main/java/io/github/dsheirer/dsp/filter/channelizer/SampleTimestampManager.java @@ -0,0 +1,83 @@ +/******************************************************************************* + * 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 + * + ******************************************************************************/ +package io.github.dsheirer.dsp.filter.channelizer; + +public class SampleTimestampManager +{ + private long mReferenceTimestamp = System.currentTimeMillis(); + private double mSamplesPerMilliSecond; + private long mSampleCount; + + /** + * Tracks the number of samples produced relative to a reference timestamp to derive a current reference timestamp + * for use in generating sample buffers. + * + * @param sampleRate per second + */ + public SampleTimestampManager(double sampleRate) + { + } + + /** + * Updates the internal reference timestamp and resets the sample counter to zero. + * + * @param referenceTimestamp in milli-seconds since epoch + */ + public void setReferenceTimestamp(long referenceTimestamp) + { + mReferenceTimestamp = referenceTimestamp; + mSampleCount = 0; + } + + /** + * Updates the sample rate that is used in calculating the reference timestamp + * + * @param sampleRate + */ + public void setSampleRate(double sampleRate) + { + //Convert to samples per milli-second for compatibility with reference timestamp + mSamplesPerMilliSecond = sampleRate / 1000.0d; + } + + /** + * Increments the internal count of dispatched samples to use in calculating an accurate reference timestamp. + */ + public void increment() + { + mSampleCount++; + } + + /** + * Increments the internal count of dispatched samples to use in calculating an accurate reference timestamp. + * + * @param value to add to the sample count + */ + public void increment(int value) + { + mSampleCount += value; + } + + public long getCurrentTimestamp() + { + if(mSampleCount > 0) + { + return mReferenceTimestamp + (long)((double)mSampleCount / mSamplesPerMilliSecond); + } + + return mReferenceTimestamp; + } +} diff --git a/src/main/java/io/github/dsheirer/dsp/filter/channelizer/output/ChannelOutputProcessor.java b/src/main/java/io/github/dsheirer/dsp/filter/channelizer/output/ChannelOutputProcessor.java index 272f69d0..b699edae 100644 --- a/src/main/java/io/github/dsheirer/dsp/filter/channelizer/output/ChannelOutputProcessor.java +++ b/src/main/java/io/github/dsheirer/dsp/filter/channelizer/output/ChannelOutputProcessor.java @@ -18,10 +18,10 @@ ******************************************************************************/ package io.github.dsheirer.dsp.filter.channelizer.output; +import io.github.dsheirer.dsp.filter.channelizer.PolyphaseChannelResultsBuffer; import io.github.dsheirer.dsp.mixer.Oscillator; import io.github.dsheirer.sample.OverflowableTransferQueue; -import io.github.dsheirer.sample.complex.Complex; -import io.github.dsheirer.sample.complex.ComplexSampleListener; +import io.github.dsheirer.sample.complex.TimestampedBufferAssembler; import io.github.dsheirer.sample.real.IOverflowListener; import io.github.dsheirer.source.Source; import org.slf4j.Logger; @@ -34,8 +34,8 @@ public abstract class ChannelOutputProcessor implements IPolyphaseChannelOutputP { private final static Logger mLog = LoggerFactory.getLogger(ChannelOutputProcessor.class); - private OverflowableTransferQueue mChannelResultsQueue; - private List mChannelResultsToProcess = new ArrayList<>(); + private OverflowableTransferQueue mChannelResultsQueue; + private List mChannelResultsToProcess = new ArrayList<>(); private int mMaxResultsToProcess; private int mInputChannelCount; @@ -71,26 +71,34 @@ public abstract class ChannelOutputProcessor implements IPolyphaseChannelOutputP mFrequencyCorrectionEnabled = (frequencyOffset != 0); } + /** + * Oscillator/mixer to use for frequency correction against incominb sample stream + */ + protected Oscillator getFrequencyCorrectionMixer() + { + return mFrequencyCorrectionMixer; + } + @Override - public void receiveChannelResults(float[] channelResults) + public void receiveChannelResults(PolyphaseChannelResultsBuffer channelResults) { mChannelResultsQueue.offer(channelResults); } /** * Processes all enqueued polyphase channelizer results until the internal queue is empty - * @param listener to receive the processed channel results + * @param timestampedBufferAssembler to receive the processed channel results */ @Override - public void processChannelResults(ComplexSampleListener listener) + public void processChannelResults(TimestampedBufferAssembler timestampedBufferAssembler) { try { - int count = mChannelResultsQueue.drainTo(mChannelResultsToProcess, mMaxResultsToProcess); + int toProcess = mChannelResultsQueue.drainTo(mChannelResultsToProcess, mMaxResultsToProcess); - if(count > 0) + if(toProcess > 0) { - process(mChannelResultsToProcess, listener); + process(mChannelResultsToProcess, timestampedBufferAssembler); } } catch(Throwable throwable) @@ -104,9 +112,10 @@ public abstract class ChannelOutputProcessor implements IPolyphaseChannelOutputP /** * Sub-class implementation to process one polyphase channelizer result array. * @param channelResults to process - * @param listener to receive the channel complex sample + * @param timestampedBufferAssembler to receive the processed channelizer results */ - public abstract void process(List channelResults, ComplexSampleListener listener); + public abstract void process(List channelResults, + TimestampedBufferAssembler timestampedBufferAssembler); /** @@ -131,34 +140,6 @@ public abstract class ChannelOutputProcessor implements IPolyphaseChannelOutputP return mInputChannelCount; } - /** - * Provides frequency correction of the inphase component or simply returns the inphase argument value when no - * frequency correction value has been specified. - * - * @param inphase value of the sample to translate/correct - * @param quadrature value of the sample to translate/correct - * @return corrected inphase value - */ - protected float getFrequencyCorrectedInphase(float inphase, float quadrature) - { - return Complex.multiplyInphase(inphase, quadrature, mFrequencyCorrectionMixer.inphase(), - mFrequencyCorrectionMixer.quadrature()); - } - - /** - * Provides frequency correction of the quadrature component or simply returns the quadrature argument value when no - * frequency correction value has been applied, - * - * @param inphase value of the sample to translate/correct - * @param quadrature value of the sample to translate/correct - * @return translated inphase value - */ - protected float getFrequencyCorrectedQuadrature(float inphase, float quadrature) - { - return Complex.multiplyQuadrature(inphase, quadrature, mFrequencyCorrectionMixer.inphase(), - mFrequencyCorrectionMixer.quadrature()); - } - /** * Indicates if this channel has a frequency correction offset */ @@ -166,5 +147,4 @@ public abstract class ChannelOutputProcessor implements IPolyphaseChannelOutputP { return mFrequencyCorrectionEnabled; } - } diff --git a/src/main/java/io/github/dsheirer/dsp/filter/channelizer/output/IPolyphaseChannelOutputProcessor.java b/src/main/java/io/github/dsheirer/dsp/filter/channelizer/output/IPolyphaseChannelOutputProcessor.java index ce56168a..fe71eba8 100644 --- a/src/main/java/io/github/dsheirer/dsp/filter/channelizer/output/IPolyphaseChannelOutputProcessor.java +++ b/src/main/java/io/github/dsheirer/dsp/filter/channelizer/output/IPolyphaseChannelOutputProcessor.java @@ -18,7 +18,8 @@ ******************************************************************************/ package io.github.dsheirer.dsp.filter.channelizer.output; -import io.github.dsheirer.sample.complex.ComplexSampleListener; +import io.github.dsheirer.dsp.filter.channelizer.PolyphaseChannelResultsBuffer; +import io.github.dsheirer.sample.complex.TimestampedBufferAssembler; import io.github.dsheirer.source.Source; import java.util.List; @@ -27,16 +28,16 @@ public interface IPolyphaseChannelOutputProcessor { /** * Receive and enqueue output results from the polyphase analysis channelizer - * @param channelResults to enqueue + * @param channelResultsBuffer to enqueue */ - void receiveChannelResults(float[] channelResults); + void receiveChannelResults(PolyphaseChannelResultsBuffer channelResultsBuffer); /** * Process the channel output channel results queue and deliver the output to the listener * - * @param listener to receive the processed channel results + * @param timestampedBufferAssembler to receive the processed channel results */ - void processChannelResults(ComplexSampleListener listener); + void processChannelResults(TimestampedBufferAssembler timestampedBufferAssembler); /** * Sets the desired frequency offset from center. The samples will be mixed with an oscillator set to this offset diff --git a/src/main/java/io/github/dsheirer/dsp/filter/channelizer/output/OneChannelOutputProcessor.java b/src/main/java/io/github/dsheirer/dsp/filter/channelizer/output/OneChannelOutputProcessor.java index e8adb854..d1c1f687 100644 --- a/src/main/java/io/github/dsheirer/dsp/filter/channelizer/output/OneChannelOutputProcessor.java +++ b/src/main/java/io/github/dsheirer/dsp/filter/channelizer/output/OneChannelOutputProcessor.java @@ -18,7 +18,8 @@ ******************************************************************************/ package io.github.dsheirer.dsp.filter.channelizer.output; -import io.github.dsheirer.sample.complex.ComplexSampleListener; +import io.github.dsheirer.dsp.filter.channelizer.PolyphaseChannelResultsBuffer; +import io.github.dsheirer.sample.complex.TimestampedBufferAssembler; import org.slf4j.Logger; import org.slf4j.LoggerFactory; @@ -67,29 +68,28 @@ public class OneChannelOutputProcessor extends ChannelOutputProcessor * extracted frequency-corrected channel I/Q sample set to the complex sample listener. * * @param channelResults to process containing a list of channel array of I/Q sample pairs (I0,Q0,I1,Q1...In,Qn) - * @param listener to receive the extracted, frequency-translated channel results + * @param timestampedBufferAssembler to receive the extracted, frequency-translated channel results */ @Override - public void process(List channelResults, ComplexSampleListener listener) + public void process(List channelResults, + TimestampedBufferAssembler timestampedBufferAssembler) { - for(float[] channels: channelResults) + for(PolyphaseChannelResultsBuffer channelResultsBuffer: channelResults) { - if(channels.length < mChannelOffset + 1) + try { - throw new IllegalArgumentException("Polyphase channelizer output channels array is not large enough to " + - "cover this single channel output processor with channel offset [" + mChannelOffset + "]"); - } + float[] samples = channelResultsBuffer.getChannel(mChannelOffset); - float i = channels[mChannelOffset]; - float q = channels[mChannelOffset + 1]; + if(hasFrequencyCorrection()) + { + getFrequencyCorrectionMixer().mixComplex(samples); + } - if(hasFrequencyCorrection()) - { - listener.receive(getFrequencyCorrectedInphase(i, q), getFrequencyCorrectedQuadrature(i, q)); + timestampedBufferAssembler.receive(samples); } - else + catch(IllegalArgumentException iae) { - listener.receive(i, q); + mLog.error("Error extracting channel samples from polyphase channel results buffer"); } } } diff --git a/src/main/java/io/github/dsheirer/dsp/filter/channelizer/output/TwoChannelOutputProcessor.java b/src/main/java/io/github/dsheirer/dsp/filter/channelizer/output/TwoChannelOutputProcessor.java index 23c3e120..3e904d4d 100644 --- a/src/main/java/io/github/dsheirer/dsp/filter/channelizer/output/TwoChannelOutputProcessor.java +++ b/src/main/java/io/github/dsheirer/dsp/filter/channelizer/output/TwoChannelOutputProcessor.java @@ -18,7 +18,8 @@ ******************************************************************************/ package io.github.dsheirer.dsp.filter.channelizer.output; -import io.github.dsheirer.sample.complex.ComplexSampleListener; +import io.github.dsheirer.dsp.filter.channelizer.PolyphaseChannelResultsBuffer; +import io.github.dsheirer.sample.complex.TimestampedBufferAssembler; import org.slf4j.Logger; import org.slf4j.LoggerFactory; @@ -70,22 +71,13 @@ public class TwoChannelOutputProcessor extends ChannelOutputProcessor * Extract the channel from the channel results array, apply frequency translation, and deliver the * extracted frequency-corrected channel I/Q sample set to the complex sample listener. * - * @param channelResults to process containing an array of channel I/Q sample pairs (I0,Q0,I1,Q1...In,Qn) - * @param listener to receive the extracted, frequency-translated channel results + * @param channelResultsBuffers to process containing an array of channel I/Q sample pairs (I0,Q0,I1,Q1...In,Qn) + * @param timestampedBufferAssembler to receive the extracted, frequency-translated channel results */ @Override - public void process(List channelResults, ComplexSampleListener listener) + public void process(List channelResultsBuffers, + TimestampedBufferAssembler timestampedBufferAssembler) { mLog.debug("I got channel results - doing nothing !!"); -// if(channels.length < mChannelOffset + 1) -// { -// throw new IllegalArgumentException("Polyphase channelizer output channels array is not large enough to " + -// "cover this single channel output processor with channel offset [" + mChannelOffset + "]"); -// } -// -// float i = channels[mChannelOffset]; -// float q = channels[mChannelOffset + 1]; - -// listener.receive(getFrequencyCorrectedInphase(i, q), getFrequencyCorrectedQuadrature(i, q)); } } diff --git a/src/main/java/io/github/dsheirer/dsp/mixer/Oscillator.java b/src/main/java/io/github/dsheirer/dsp/mixer/Oscillator.java index 3099a5bf..a5354ded 100644 --- a/src/main/java/io/github/dsheirer/dsp/mixer/Oscillator.java +++ b/src/main/java/io/github/dsheirer/dsp/mixer/Oscillator.java @@ -149,4 +149,22 @@ public class Oscillator 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(); + } + } } diff --git a/src/main/java/io/github/dsheirer/sample/Buffer.java b/src/main/java/io/github/dsheirer/sample/Buffer.java index 62197e04..61d9fe8e 100644 --- a/src/main/java/io/github/dsheirer/sample/Buffer.java +++ b/src/main/java/io/github/dsheirer/sample/Buffer.java @@ -1,24 +1,83 @@ +/******************************************************************************* + * 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 + * + ******************************************************************************/ package io.github.dsheirer.sample; +import io.github.dsheirer.source.SourceEvent; + public class Buffer { - protected float[] mSamples; + private float[] mSamples; + private SourceEvent mSourceEvent; - public Buffer( float[] samples ) - { - mSamples = samples; - } + /** + * Buffer containing float sample data and a source event that should be processed prior to processing the + * sample data. + * + * @param samples data + * @param sourceEvent indicating a change to the sample data that should be processed first + */ + public Buffer(float[] samples, SourceEvent sourceEvent) + { + mSamples = samples; + mSourceEvent = sourceEvent; + } - public float[] getSamples() - { - return mSamples; - } + /** + * Buffer containing float sample data + * @param samples + */ + public Buffer(float[] samples) + { + mSamples = samples; + } - /** - * Cleanup method to nullify all data and references - */ - public void dispose() - { - mSamples = null; - } + /** + * Float sample array + */ + public float[] getSamples() + { + return mSamples; + } + + /** + * Optional source event that should be processed before processing the included samples. Use the hasSourceEvent() + * method to check for the existence of an event before accessing. + * + * @return optional source event or null + */ + public SourceEvent getSourceEvent() + { + return mSourceEvent; + } + + /** + * Indicates if this buffer contains an optional source event that should be processed prior to processing the + * included float samples array. + */ + public boolean hasSourceEvent() + { + return mSourceEvent != null; + } + + /** + * Cleanup method to nullify all data and references + */ + public void dispose() + { + mSamples = null; + mSourceEvent = null; + } } \ No newline at end of file diff --git a/src/main/java/io/github/dsheirer/sample/complex/ComplexBuffer.java b/src/main/java/io/github/dsheirer/sample/complex/ComplexBuffer.java index 336b61e8..bfd8971e 100644 --- a/src/main/java/io/github/dsheirer/sample/complex/ComplexBuffer.java +++ b/src/main/java/io/github/dsheirer/sample/complex/ComplexBuffer.java @@ -1,26 +1,61 @@ +/******************************************************************************* + * 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 + * + ******************************************************************************/ package io.github.dsheirer.sample.complex; import io.github.dsheirer.sample.Buffer; - -import java.util.Arrays; +import io.github.dsheirer.source.SourceEvent; public class ComplexBuffer extends Buffer { - /** - * Wrapper around float array containing interleaved I/Q samples - */ - public ComplexBuffer( float[] samples ) - { - super( samples ); - } + //Default timestamp - we use a static variable here so that there is only ever 1 instance in memory + private static final long DEFAULT_TIMESTAMP = 0; - /** - * Creates a deep copy of the buffer - */ - public ComplexBuffer copyOf() - { - float[] copy = Arrays.copyOf( mSamples, mSamples.length ); - - return new ComplexBuffer( copy ); - } + /** + * Wrapper around float array containing interleaved I/Q samples and an optional source event that should be + * processed prior to processing the sample data. + */ + public ComplexBuffer(float[] samples, SourceEvent sourceEvent) + { + super(samples, sourceEvent); + } + /** + * Wrapper around float array containing interleaved I/Q samples + */ + public ComplexBuffer(float[] samples) + { + super(samples); + } + + + /** + * Reference timestamp for the first sample in this buffer. This functionality is intended to be implemented by a + * subclass implementation. The default value is 0. + * + * @return reference timestamp or a value of 0 if hasTimestamp() indicates false. + */ + public long getTimestamp() + { + return DEFAULT_TIMESTAMP; + } + + /** + * Indicates if this buffer has a timestamp that references the first sample in the buffer. + */ + public boolean hasTimestamp() + { + return false; + } } diff --git a/src/main/java/io/github/dsheirer/sample/complex/TimestampedBufferAssembler.java b/src/main/java/io/github/dsheirer/sample/complex/TimestampedBufferAssembler.java new file mode 100644 index 00000000..fbc7cfed --- /dev/null +++ b/src/main/java/io/github/dsheirer/sample/complex/TimestampedBufferAssembler.java @@ -0,0 +1,132 @@ +/******************************************************************************* + * 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 + * + ******************************************************************************/ +package io.github.dsheirer.sample.complex; + +import io.github.dsheirer.dsp.filter.channelizer.SampleTimestampManager; +import io.github.dsheirer.sample.Listener; +import org.apache.commons.lang3.Validate; + +import java.nio.FloatBuffer; + +/** + * Assembles complex float samples into a ComplexBuffer containing an array of floats. Monitors incoming sample count + * to accurately assign a relative timestamp to each assembled buffer. + */ +public class TimestampedBufferAssembler +{ + private SampleTimestampManager mTimestampManager; + private FloatBuffer mBuffer; + private long mCurrentBufferTimestamp; + private Listener mListener; + + public TimestampedBufferAssembler(int bufferSize, double sampleRate) + { + Validate.isTrue(bufferSize % 2 == 0); + mBuffer = FloatBuffer.allocate(bufferSize); + mTimestampManager = new SampleTimestampManager(sampleRate); + } + + /** + * Updates the reference timestamp to use for all subsequent samples that are assembled. This timestamp should + * identify the relative timestamp of the first/next sample that is received by this assembler. + * @param timestamp to use relative to the incoming sample arrays. + */ + public void updateTimestamp(long timestamp) + { + mTimestampManager.setReferenceTimestamp(timestamp); + + if(mBuffer.position() == 0) + { + mCurrentBufferTimestamp = timestamp; + } + } + + public void dispose() + { + mListener = null; + mBuffer = null; + } + + /** + * Adds the samples to this assembler. As each buffer is assembled, it will be dispatched to the registered + * listener. + * + * @param samples to assemble + */ + public void receive(float[] samples) + { + if(mBuffer.remaining() >= samples.length) + { + mBuffer.put(samples); + mTimestampManager.increment(samples.length / 2); + + if(!mBuffer.hasRemaining()) + { + flush(); + } + } + else + { + int offset = 0; + + while(offset < samples.length) + { + int toCopy = mBuffer.remaining(); + + if((samples.length - offset) < toCopy) + { + toCopy = samples.length - offset; + } + + mBuffer.put(samples, offset, toCopy); + + offset += toCopy; + mTimestampManager.increment(toCopy / 2); + + if(!mBuffer.hasRemaining()) + { + flush(); + } + } + } + } + + /** + * Flushes the current buffer contents to the registered listener + */ + public void flush() + { + if(mListener != null && mBuffer.position() > 0) + { + float[] samples = new float[mBuffer.position()]; + mBuffer.rewind(); + mBuffer.get(samples); + + mListener.receive(new TimestampedComplexBuffer(samples, mCurrentBufferTimestamp)); + } + + mBuffer.rewind(); + mCurrentBufferTimestamp = mTimestampManager.getCurrentTimestamp(); + } + + /** + * Sets the listener to receive the output from this buffer assembler. + */ + public void setListener(Listener listener) + { + mListener = listener; + } +} diff --git a/src/main/java/io/github/dsheirer/sample/complex/TimestampedComplexBuffer.java b/src/main/java/io/github/dsheirer/sample/complex/TimestampedComplexBuffer.java new file mode 100644 index 00000000..ac40103d --- /dev/null +++ b/src/main/java/io/github/dsheirer/sample/complex/TimestampedComplexBuffer.java @@ -0,0 +1,88 @@ +/******************************************************************************* + * 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 + * + ******************************************************************************/ +package io.github.dsheirer.sample.complex; + +import io.github.dsheirer.source.SourceEvent; + +public class TimestampedComplexBuffer extends ComplexBuffer +{ + private long mTimestamp; + + /** + * Constructs a timestamped complex buffer using the specified time in milliseconds. Also includes a source event + * that should be processed prior to processing the sample data. + * + * @param samples of data + * @param sourceEvent - optional source event to embed with this buffer + * @param timestamp relative to the first sample in the sample array + */ + public TimestampedComplexBuffer(float[] samples, SourceEvent sourceEvent, long timestamp) + { + super(samples, sourceEvent); + mTimestamp = timestamp; + } + + /** + * Constructs a timestamped complex buffer using the specified time in milliseconds. + * + * @param samples of data + */ + public TimestampedComplexBuffer(float[] samples, long timestamp) + { + this(samples, null, timestamp); + } + + /** + * Constructs a timestamped complex buffer using the current system time in milliseconds. Also includes a source + * event that should be processed prior to processing the sample data. + * + * @param samples of data + */ + public TimestampedComplexBuffer(float[] samples, SourceEvent sourceEvent) + { + this(samples, sourceEvent, System.currentTimeMillis()); + } + + /** + * Constructs a timestamped complex buffer using the current system time in milliseconds. + * + * @param samples of data + */ + public TimestampedComplexBuffer(float[] samples) + { + this(samples, null, System.currentTimeMillis()); + } + + /** + * Reference timestamp for this complex buffer. Timestamp is relative to the first sample in the buffer. + * @return timestamp as milliseconds since epoch + */ + @Override + public long getTimestamp() + { + return mTimestamp; + } + + /** + * Indicates that this complex buffer has a reference timestamp + * @return true + */ + @Override + public boolean hasTimestamp() + { + return true; + } +} diff --git a/src/main/java/io/github/dsheirer/sample/real/RealBuffer.java b/src/main/java/io/github/dsheirer/sample/real/RealBuffer.java index 5631a9bd..3628165e 100644 --- a/src/main/java/io/github/dsheirer/sample/real/RealBuffer.java +++ b/src/main/java/io/github/dsheirer/sample/real/RealBuffer.java @@ -1,26 +1,51 @@ +/******************************************************************************* + * 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 + * + ******************************************************************************/ package io.github.dsheirer.sample.real; import io.github.dsheirer.sample.Buffer; +import io.github.dsheirer.source.SourceEvent; import java.util.Arrays; public class RealBuffer extends Buffer { - /** - * Wrapper around float array containing real float samples - */ - public RealBuffer( float[] samples ) - { - super( samples ); - } + /** + * Wrapper around float array containing real float samples and an optional source event that should be processed + * prior to processing the sample array data. + */ + public RealBuffer(float[] samples, SourceEvent sourceEvent) + { + super(samples, sourceEvent); + } - /** - * Creates a deep copy of the buffer - */ - public RealBuffer copyOf() - { - float[] copy = Arrays.copyOf( mSamples, mSamples.length ); - - return new RealBuffer( copy ); - } + /** + * Wrapper around float array containing real float samples + */ + public RealBuffer(float[] samples) + { + super(samples); + } + + /** + * Creates a deep copy of the buffer and a shallow (reference) copy of the source event + */ + public RealBuffer copyOf() + { + float[] copy = Arrays.copyOf(getSamples(), getSamples().length); + + return new RealBuffer(copy, getSourceEvent()); + } } diff --git a/src/main/java/io/github/dsheirer/source/SourceEvent.java b/src/main/java/io/github/dsheirer/source/SourceEvent.java index 53ef609f..a5c23087 100644 --- a/src/main/java/io/github/dsheirer/source/SourceEvent.java +++ b/src/main/java/io/github/dsheirer/source/SourceEvent.java @@ -30,6 +30,7 @@ public class SourceEvent NOTIFICATION_FREQUENCY_CHANGE, NOTIFICATION_FREQUENCY_CORRECTION_CHANGE, NOTIFICATION_SAMPLE_RATE_CHANGE, + NOTIFICATION_STOP_SAMPLE_STREAM, REQUEST_CHANNEL_FREQUENCY_CORRECTION_CHANGE, REQUEST_FREQUENCY_CHANGE, @@ -38,7 +39,7 @@ public class SourceEvent REQUEST_STOP_SAMPLE_STREAM; public static EnumSet NOTIFICATION_EVENTS = - EnumSet.range(NOTIFICATION_CHANNEL_COUNT_CHANGE, NOTIFICATION_SAMPLE_RATE_CHANGE); + EnumSet.range(NOTIFICATION_CHANNEL_COUNT_CHANGE, NOTIFICATION_STOP_SAMPLE_STREAM); public static EnumSet REQUEST_EVENTS = EnumSet.range(NOTIFICATION_CHANNEL_COUNT_CHANGE, NOTIFICATION_SAMPLE_RATE_CHANGE); } @@ -201,7 +202,7 @@ public class SourceEvent /** * Creates a new start sample stream request event */ - public static SourceEvent startSampleStream(Source source) + public static SourceEvent startSampleStreamRequest(Source source) { return new SourceEvent(Event.REQUEST_START_SAMPLE_STREAM, source); } @@ -209,11 +210,19 @@ public class SourceEvent /** * Creates a new stop sample stream request event */ - public static SourceEvent stopSampleStream(Source source) + public static SourceEvent stopSampleStreamRequest(Source source) { return new SourceEvent(Event.REQUEST_STOP_SAMPLE_STREAM, source); } + /** + * Creates a new stop sample stream request event + */ + public static SourceEvent stopSampleStreamNotification(Source source) + { + return new SourceEvent(Event.NOTIFICATION_STOP_SAMPLE_STREAM, source); + } + /** * Creates a request to dispose of the source. This is normally used for TunerChannelSource to notify the * parent source manager that the channel is no longer needed diff --git a/src/main/java/io/github/dsheirer/source/tuner/TunerController.java b/src/main/java/io/github/dsheirer/source/tuner/TunerController.java index 688ee442..36b177e9 100644 --- a/src/main/java/io/github/dsheirer/source/tuner/TunerController.java +++ b/src/main/java/io/github/dsheirer/source/tuner/TunerController.java @@ -197,12 +197,13 @@ public abstract class TunerController implements Tunable, ISourceEventProcessor, } /** - * Usable half bandwidth - total bandwidth minus unusable space at either end of the spectrum, minus the blocked - * center bandwidth. + * Usable half bandwidth - total bandwidth minus unusable space at either end of the spectrum. + * + * Note: this does not account for any DC spike protected frequency region at the center of the tuner */ public int getUsableHalfBandwidth() { - return (int)(getUsableBandwidth() / 2) - mMiddleUnusableHalfBandwidth; + return (int)(getUsableBandwidth() / 2); } /** diff --git a/src/main/java/io/github/dsheirer/source/tuner/channel/TunerChannel.java b/src/main/java/io/github/dsheirer/source/tuner/channel/TunerChannel.java index 1c2e5424..0fb0bd1c 100644 --- a/src/main/java/io/github/dsheirer/source/tuner/channel/TunerChannel.java +++ b/src/main/java/io/github/dsheirer/source/tuner/channel/TunerChannel.java @@ -77,10 +77,8 @@ public class TunerChannel implements Comparable { StringBuilder sb = new StringBuilder(); - sb.append("Channel: "); - sb.append(getMinFrequency()); - sb.append("-"); - sb.append(getMaxFrequency()); + sb.append("Channel:").append(getFrequency()).append(" (").append(getMinFrequency()); + sb.append("-").append(getMaxFrequency()).append(")"); return sb.toString(); } diff --git a/src/main/java/io/github/dsheirer/source/tuner/channel/TunerChannelSource.java b/src/main/java/io/github/dsheirer/source/tuner/channel/TunerChannelSource.java index 96386b45..217f7409 100644 --- a/src/main/java/io/github/dsheirer/source/tuner/channel/TunerChannelSource.java +++ b/src/main/java/io/github/dsheirer/source/tuner/channel/TunerChannelSource.java @@ -122,7 +122,7 @@ public abstract class TunerChannelSource extends ComplexSource implements ISourc if(mProducerSourceEventListener != null) { - mProducerSourceEventListener.receive(SourceEvent.startSampleStream(this)); + mProducerSourceEventListener.receive(SourceEvent.startSampleStreamRequest(this)); } mBufferProcessor.start(); @@ -136,7 +136,7 @@ public abstract class TunerChannelSource extends ComplexSource implements ISourc { if(mProducerSourceEventListener != null) { - mProducerSourceEventListener.receive(SourceEvent.stopSampleStream(this)); + mProducerSourceEventListener.receive(SourceEvent.stopSampleStreamRequest(this)); mProducerSourceEventListener.receive(SourceEvent.sourceDisposeRequest(this)); } diff --git a/src/main/java/io/github/dsheirer/source/tuner/manager/TunerSourceManager.java b/src/main/java/io/github/dsheirer/source/tuner/manager/TunerSourceManager.java index 9276f649..2a5be07e 100644 --- a/src/main/java/io/github/dsheirer/source/tuner/manager/TunerSourceManager.java +++ b/src/main/java/io/github/dsheirer/source/tuner/manager/TunerSourceManager.java @@ -42,8 +42,6 @@ public class TunerSourceManager extends AbstractSourceManager public TunerSourceManager(TunerController tunerController) { mTunerController = tunerController; - //Register to receive frequency and sample rate notifications - mTunerController.addListener(this); mPolyphaseChannelManager = new PolyphaseChannelManager(tunerController); //Register to receive channel count change notifications for rebroadcasting @@ -166,6 +164,13 @@ public class TunerSourceManager extends AbstractSourceManager return currentCenterFrequency; } + long channelSetBandwidth = getChannelSetBandwidth(channels); + + if(channelSetBandwidth > mTunerController.getUsableBandwidth()) + { + throw new IllegalArgumentException("Channel set bandwidth is greater than tuner's available bandwidth"); + } + long bestIntegralFrequency = getIntegralFrequency(channels); //See if we can reuse the current frequency @@ -175,32 +180,42 @@ public class TunerSourceManager extends AbstractSourceManager return currentCenterFrequency; } - int integralHalfChannelCount = (int)(mTunerController.getUsableHalfBandwidth() / - mPolyphaseChannelManager.getChannelBandwidth()) + 1; + //Start by placing the first channel at the minimum location within the tuner bandwidth + double usableHalfBandwidth = mTunerController.getUsableHalfBandwidth(); + double startFrequency = channels.first().getMinFrequency() + usableHalfBandwidth; - int integralHalfBandwidth = (int)(integralHalfChannelCount * mPolyphaseChannelManager.getChannelBandwidth()); + //Align the test frequency to the next greater integral frequency placing to place the first channel at the + // minimum extreme of the available bandwidth as a starting location + double delta = Math.abs(startFrequency - bestIntegralFrequency); + double adjustment = delta % mPolyphaseChannelManager.getChannelBandwidth(); - long minimumFrequency = channels.last().getFrequency() - integralHalfBandwidth; - long testFrequency = channels.first().getFrequency() + integralHalfBandwidth; + startFrequency += adjustment; - if(isValidCenterFrequency(channels,testFrequency)) + long availableTestBandwidth = mTunerController.getUsableBandwidth() - channelSetBandwidth; + int availableTestChannels = (int)(availableTestBandwidth / mPolyphaseChannelManager.getChannelBandwidth()) + 1; + + if(isValidCenterFrequency(channels,(long)startFrequency)) { - return testFrequency; + return (long)startFrequency; } - while(testFrequency >= minimumFrequency) + int currentChannel = 1; + + while(currentChannel <= availableTestChannels) { - testFrequency -= (long)mPolyphaseChannelManager.getChannelBandwidth(); + long testFrequency = (long)(startFrequency - (currentChannel * mPolyphaseChannelManager.getChannelBandwidth())); if(isValidCenterFrequency(channels, testFrequency)) { return testFrequency; } + + currentChannel++; } //If we can't find an integral center frequency, choose a frequency that will fit the channels - minimumFrequency = channels.last().getFrequency() - mTunerController.getUsableHalfBandwidth(); - testFrequency = channels.first().getMinFrequency() + mTunerController.getUsableHalfBandwidth(); + long testFrequency = channels.first().getMinFrequency() + mTunerController.getUsableHalfBandwidth(); + long minimumFrequency = testFrequency - availableTestBandwidth; if(isValidCenterFrequency(channels,testFrequency)) { @@ -220,6 +235,11 @@ public class TunerSourceManager extends AbstractSourceManager throw new IllegalArgumentException("Can't calculate valid center frequency for the channel set"); } + private long getChannelSetBandwidth(SortedSet channels) + { + return channels.last().getMaxFrequency() - channels.first().getMinFrequency(); + } + /** * Indicates if the candidate center frequency is valid to ensure that all of the channels fit within the tuner's * bandwidth and avoid any center DC spike blocked frequency region @@ -406,12 +426,6 @@ public class TunerSourceManager extends AbstractSourceManager //Rebroadcast this event to any registered listeners (ie tuner and tuner controller) broadcast(sourceEvent); break; - case NOTIFICATION_FREQUENCY_CHANGE: - mPolyphaseChannelManager.process(sourceEvent); - break; - case NOTIFICATION_SAMPLE_RATE_CHANGE: - mPolyphaseChannelManager.process(sourceEvent); - break; default: mLog.info("Received an unrecognized source event: " + sourceEvent); break; diff --git a/src/main/java/io/github/dsheirer/source/tuner/test/SampleGenerator.java b/src/main/java/io/github/dsheirer/source/tuner/test/SampleGenerator.java index b08403eb..51d157b5 100644 --- a/src/main/java/io/github/dsheirer/source/tuner/test/SampleGenerator.java +++ b/src/main/java/io/github/dsheirer/source/tuner/test/SampleGenerator.java @@ -21,10 +21,14 @@ package io.github.dsheirer.source.tuner.test; import io.github.dsheirer.dsp.mixer.LowPhaseNoiseOscillator; import io.github.dsheirer.sample.Listener; import io.github.dsheirer.sample.complex.ComplexBuffer; +import io.github.dsheirer.sample.complex.TimestampedComplexBuffer; +import io.github.dsheirer.source.SourceEvent; import io.github.dsheirer.util.ThreadPool; import org.slf4j.Logger; import org.slf4j.LoggerFactory; +import java.util.Queue; +import java.util.concurrent.ConcurrentLinkedQueue; import java.util.concurrent.ScheduledFuture; import java.util.concurrent.TimeUnit; @@ -38,6 +42,7 @@ public class SampleGenerator private long mInterval; private int mSamplesPerInterval; private ScheduledFuture mScheduledFuture; + private Queue mSourceEventQueue = new ConcurrentLinkedQueue(); /** * Generates complex sample buffers at the specified sample rate with a unity gain tone at the specified @@ -79,6 +84,16 @@ public class SampleGenerator this(sampleRate, frequency, interval, 0); } + /** + * Queues the source event for inclusion in the complex buffers generated by this sample generator. Source events + * are queued FIFO and embedded one at a time in the subsequent buffers produced by this generator. + * @param sourceEvent to queue + */ + public void queue(SourceEvent sourceEvent) + { + mSourceEventQueue.offer(sourceEvent); + } + /** * Updates the number of complex samples generated per interval */ @@ -170,7 +185,7 @@ public class SampleGenerator if(mListener != null) { float[] samples = mOscillator.generate(mSamplesPerInterval); - mListener.receive(new ComplexBuffer(samples)); + mListener.receive(new TimestampedComplexBuffer(samples, mSourceEventQueue.poll())); if(mSweepUpdateInterval != 0) { diff --git a/src/main/java/io/github/dsheirer/source/tuner/test/TestTuner.java b/src/main/java/io/github/dsheirer/source/tuner/test/TestTuner.java index fa918b56..4a81a60b 100644 --- a/src/main/java/io/github/dsheirer/source/tuner/test/TestTuner.java +++ b/src/main/java/io/github/dsheirer/source/tuner/test/TestTuner.java @@ -47,7 +47,7 @@ public class TestTuner extends Tuner */ public TestTunerController getTunerController() { - return (TestTunerController)getTunerController(); + return (TestTunerController)super.getTunerController(); } @Override diff --git a/src/main/java/io/github/dsheirer/source/tuner/test/TestTunerController.java b/src/main/java/io/github/dsheirer/source/tuner/test/TestTunerController.java index 0c94dd5d..bac4b008 100644 --- a/src/main/java/io/github/dsheirer/source/tuner/test/TestTunerController.java +++ b/src/main/java/io/github/dsheirer/source/tuner/test/TestTunerController.java @@ -20,6 +20,7 @@ package io.github.dsheirer.source.tuner.test; import io.github.dsheirer.sample.Listener; import io.github.dsheirer.sample.complex.ComplexBuffer; +import io.github.dsheirer.source.SourceEvent; import io.github.dsheirer.source.SourceException; import io.github.dsheirer.source.tuner.TunerController; import io.github.dsheirer.source.tuner.configuration.TunerConfiguration; @@ -33,9 +34,9 @@ public class TestTunerController extends TunerController public static final long MINIMUM_FREQUENCY = 1000000l; //1 MHz public static final long MAXIMUM_FREQUENCY = 10000000000l; //10 GHz public static final long DEFAULT_CENTER_FREQUENCY = 100000000; //100 MHz - public static final int DEFAULT_SAMPLE_RATE = 50000; //25 kHz - public static final int DC_NOISE_BANDWIDTH = 5000; // +/-5 kHz - public static final double USABLE_BANDWIDTH = 0.90; //90% usable bandwidth - 5% unusable on either end + public static final int DEFAULT_SAMPLE_RATE = 100000; //25 kHz + public static final int DC_NOISE_BANDWIDTH = 0; // +/-5 kHz + public static final double USABLE_BANDWIDTH = 1.00; //100% usable bandwidth public static final int SPECTRAL_FRAME_RATE = 20; public static final long SAMPLE_GENERATION_INTERVAL = 1000 / SPECTRAL_FRAME_RATE; @@ -129,6 +130,7 @@ public class TestTunerController extends TunerController { long frequency = toneFrequency - mReferenceFrequency; mSampleGenerator.setFrequency(frequency); + mSampleGenerator.queue(SourceEvent.frequencyChange(frequency)); } /** @@ -155,5 +157,6 @@ public class TestTunerController extends TunerController public void setSampleRate(int sampleRate) throws SourceException { mFrequencyController.setSampleRate(sampleRate); + mSampleGenerator.queue(SourceEvent.sampleRateChange(sampleRate)); } } From d4761be00a1921a912caf78c33decdc1511b0cba Mon Sep 17 00:00:00 2001 From: Denny Date: Sat, 13 Jan 2018 11:43:26 -0500 Subject: [PATCH 41/87] #39 WIP - rolls back embedding source events in buffers. The channel calculator must be updated immediately any time the tuner's center frequency changes so that a channel can be allocated immediately. So, I changed source event handling to two parts where the channel calculator is immediately updated and the channelizer configuration is deferred so that it can happen on the buffer processor execution thread and we can avoid any sync() locking on the channelizer or polyphase channel sources. --- .../ComplexPolyphaseChannelizer.java | 171 ------------------ .../channelizer/PolyphaseChannelManager.java | 116 +++++++----- .../io/github/dsheirer/sample/Buffer.java | 37 ---- .../sample/complex/ComplexBuffer.java | 9 - .../complex/TimestampedComplexBuffer.java | 31 +--- .../dsheirer/sample/real/RealBuffer.java | 12 +- .../tuner/manager/TunerSourceManager.java | 3 +- .../source/tuner/test/SampleGenerator.java | 16 +- .../tuner/test/TestTunerController.java | 5 +- 9 files changed, 80 insertions(+), 320 deletions(-) delete mode 100644 src/main/java/io/github/dsheirer/dsp/filter/channelizer/ComplexPolyphaseChannelizer.java diff --git a/src/main/java/io/github/dsheirer/dsp/filter/channelizer/ComplexPolyphaseChannelizer.java b/src/main/java/io/github/dsheirer/dsp/filter/channelizer/ComplexPolyphaseChannelizer.java deleted file mode 100644 index 9ab818b9..00000000 --- a/src/main/java/io/github/dsheirer/dsp/filter/channelizer/ComplexPolyphaseChannelizer.java +++ /dev/null @@ -1,171 +0,0 @@ -/******************************************************************************* - * 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 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.fir.real.RealFIRFilter; -import io.github.dsheirer.dsp.mixer.Oscillator; -import io.github.dsheirer.sample.complex.ComplexBuffer; -import org.jtransforms.fft.FloatFFT_1D; -import org.slf4j.Logger; -import org.slf4j.LoggerFactory; - -public class ComplexPolyphaseChannelizer extends AbstractComplexPolyphaseChannelizer -{ - private final static Logger mLog = LoggerFactory.getLogger(ComplexPolyphaseChannelizer.class); - - private RealFIRFilter[] mInphaseFilters; - private RealFIRFilter[] mQuadratureFilters; - private int mFilterPointer = 0; - private float[] mFilteredSamples; - private FloatFFT_1D mFFT; - - /** - * Maximally Decimated Polyphase Filter Bank (MDPFB) channelizer that divides the input frequency band into - * equal bandwidth channels. - * - * @param coefficients of a prototype low-pass filter designed for the inbound sample rate with a cutoff frequency - * equal to the channel bandwidth (sample rate / filters). If you need to synthesize (combine two or more - * filter outputs) a new bandwidth signal from the outputs of this filter, then the filter should be designed - * as a nyquist filter with -6 dB attenuation at the channel bandwidth cutoff frequency - * - * @param channelCount - number of channels to output. - * @param channelSampleRate - channel sample rate (after decimation) - */ - public ComplexPolyphaseChannelizer(float[] coefficients, int channelCount, int channelSampleRate) - { - super(channelCount, channelSampleRate); - - initFilters(coefficients); - } - - @Override - public void receive(ComplexBuffer complexBuffer) - { - float[] samples = complexBuffer.getSamples(); - - for(int x = 0; x < samples.length; x += 2) - { - filter(samples[x], samples[x + 1]); - } - } - - protected void filter(float inphase, float quadrature) - { - int index = (getChannelCount() - mFilterPointer - 1) * 2; - - mFilteredSamples[index] = mInphaseFilters[mFilterPointer].filter(inphase); - mFilteredSamples[index + 1] = mQuadratureFilters[mFilterPointer].filter(quadrature); - - if(mFilterPointer == 0) - { - calculate(); - mFilterPointer = getChannelCount() - 1; - } - else - { - mFilterPointer--; - } - } - - private void calculate() - { - float[] samples = new float[getChannelCount() * 2]; - System.arraycopy(mFilteredSamples, 0, samples, 0, mFilteredSamples.length); - - //FFT is executed in-place where the output overwrites the input - mFFT.complexForward(samples); - - dispatch(samples); - } - - /** - * Distributes the filter taps to each of the polyphase filters - * - * @param taps - */ - private void initFilters(float[] taps) - { - int tapCount = (int)Math.ceil((double)taps.length / (double)getChannelCount()); - - float[][] coefficients = new float[getChannelCount()][tapCount]; - - for(int tap = 0; tap < tapCount; tap++) - { - for(int filter = 0; filter < getChannelCount(); filter++) - { - int index = tap * getChannelCount() + filter; - - if(index < taps.length) - { - coefficients[filter][tap] = taps[index]; - } - } - } - - mInphaseFilters = new RealFIRFilter[getChannelCount()]; - mQuadratureFilters = new RealFIRFilter[getChannelCount()]; - - for(int filter = 0; filter < getChannelCount(); filter++) - { - mInphaseFilters[filter] = new RealFIRFilter(coefficients[filter], 1.0f); - mQuadratureFilters[filter] = new RealFIRFilter(coefficients[filter], 1.0f); - } - - mFilteredSamples = new float[getChannelCount() * 2]; - mFFT = new FloatFFT_1D(getChannelCount()); - mFilterPointer = getChannelCount() -1; - } - - public static void main(String[] args) - { - mLog.debug("Starting ..."); - - int symbolRate = 4800; - int samplesPerSymbol = 2; - int channels = 800; - int channelBandwidth = 12500; - int sampleRate = channels * channelBandwidth; - - float[] taps = FilterFactory.getLowPass( 10000000, 12500, 5600, Window.WindowType.BLACKMAN_HARRIS_7 ); - - int channelCenter = (int)(12500 * 1.5); - - StringBuilder sb = new StringBuilder(); - sb.append("\nPolyphase TunerChannelizer\n"); - sb.append("Sample Rate:" + sampleRate + " Channels:" + channels + " Channel Rate:" + channelBandwidth + "\n"); - sb.append("Tap Count:" + taps.length + "\n"); - sb.append("Channel:" + channelCenter); - - mLog.debug(sb.toString()); - - ComplexPolyphaseChannelizer channelizer = new ComplexPolyphaseChannelizer(taps, 8, channelBandwidth); - - Oscillator oscillator = new Oscillator(channelCenter, 2 * sampleRate); - - for(int x = 0; x < 2000; x++) - { - channelizer.filter(oscillator.getComplex().inphase(), oscillator.getComplex().quadrature()); - oscillator.rotate(); - } - - mLog.debug("Finished!"); - } -} diff --git a/src/main/java/io/github/dsheirer/dsp/filter/channelizer/PolyphaseChannelManager.java b/src/main/java/io/github/dsheirer/dsp/filter/channelizer/PolyphaseChannelManager.java index 7774050a..81ed8b03 100644 --- a/src/main/java/io/github/dsheirer/dsp/filter/channelizer/PolyphaseChannelManager.java +++ b/src/main/java/io/github/dsheirer/dsp/filter/channelizer/PolyphaseChannelManager.java @@ -23,6 +23,7 @@ import io.github.dsheirer.sample.Broadcaster; import io.github.dsheirer.sample.Listener; import io.github.dsheirer.sample.complex.ComplexBuffer; import io.github.dsheirer.sample.complex.IComplexBufferProvider; +import io.github.dsheirer.source.ISourceEventProcessor; import io.github.dsheirer.source.SourceEvent; import io.github.dsheirer.source.SourceException; import io.github.dsheirer.source.tuner.TunerController; @@ -32,11 +33,13 @@ import org.slf4j.Logger; import org.slf4j.LoggerFactory; import java.util.List; +import java.util.Queue; import java.util.SortedSet; import java.util.TreeSet; +import java.util.concurrent.ConcurrentLinkedQueue; import java.util.concurrent.CopyOnWriteArrayList; -public class PolyphaseChannelManager +public class PolyphaseChannelManager implements ISourceEventProcessor { private final static Logger mLog = LoggerFactory.getLogger(PolyphaseChannelManager.class); private static final double MINIMUM_CHANNEL_BANDWIDTH = 12500.0; @@ -224,6 +227,40 @@ public class PolyphaseChannelManager } } + /** + * Process source events received from the source (ie tuner controller) for frequency and sample rate change + * notifications. + * @param sourceEvent to process + * @throws SourceException + */ + @Override + public void process(SourceEvent sourceEvent) throws SourceException + { + switch(sourceEvent.getEvent()) + { + case NOTIFICATION_FREQUENCY_CHANGE: + //Update channel calculator immediately so that channels can be allocated + mLog.debug("Updating channel calculator to frequency: " + sourceEvent.getValue()); + mChannelCalculator.setCenterFrequency(sourceEvent.getValue().longValue()); + + //Defer channelizer configuration changes to be handled on the buffer processor thread + mBufferSourceEventMonitor.receive(sourceEvent); + break; + case NOTIFICATION_SAMPLE_RATE_CHANGE: + //Update channel calculator immediately so that channels can be allocated + mLog.debug("Updating channel calculator to sample rate: " + sourceEvent.getValue()); + mChannelCalculator.setSampleRate(sourceEvent.getValue().doubleValue()); + + //Defer channelizer configuration changes to be handled on the buffer processor thread + mBufferSourceEventMonitor.receive(sourceEvent); + break; + default: + mLog.info("Unrecognized source event: " + sourceEvent.toString()); + break; + + } + } + /** * Creates or updates the channelizer to process the incoming sample rate and updates any channel processors. * @@ -235,10 +272,9 @@ public class PolyphaseChannelManager */ private void updateChannelizerSampleRate(double sampleRate) { - if(mPolyphaseChannelizer == null || Math.abs(mPolyphaseChannelizer.getSampleRate() - sampleRate) > 1.0) + if(mPolyphaseChannelizer == null || Math.abs(mPolyphaseChannelizer.getSampleRate() - sampleRate) > 0.5) { mLog.debug("Creating or Updating Channelizer for sample rate " + sampleRate); - mChannelCalculator.setSampleRate(sampleRate); try { @@ -271,21 +307,12 @@ public class PolyphaseChannelManager } } - private void updateChannelizerCenterFrequency(long frequency) - { - mChannelCalculator.setCenterFrequency(frequency); - - mLog.debug("Updating output processors - center frequency is now: " + frequency); - - updateOutputProcessors(null); - } - /** * Updates each of the output processors for any changes in the tuner's center frequency or sample rate, which * would cause the output processors to change the polyphase channelizer results channel(s) that the processor is * consuming * - * @param sourceEvent that requires an update to the output processors + * @param sourceEvent (optional-can be null) to broadcast to each output processor following the update */ private void updateOutputProcessors(SourceEvent sourceEvent) { @@ -293,13 +320,11 @@ public class PolyphaseChannelManager { updateOutputProcessor(channelSource); - //Notify the channel source that the frequency or sample rate has changed so that it can reset - //any frequency correction value. + //Send the non-null source event to each channel source if(sourceEvent != null) { try { -//TODO: this notification event should go to the channelizer to be embedded in the channel buffer stream channelSource.process(sourceEvent); } catch(SourceException se) @@ -429,17 +454,44 @@ public class PolyphaseChannelManager } /** - * Processes the incoming buffer stream from the provider to detect frequency and sample rate change source events - * and update the channelizer configuration as necessary. + * Processes the incoming buffer stream from the provider and transfers the buffers to the polyphase channelizer. + * + * This monitor incorporates a queue for receiving source events that affect configuration of the channelizer so + * that they can be processed on the buffer processor calling thread, avoiding unnecessary locks on the channelizer + * and/or the channel sources and output processors. */ public class BufferSourceEventMonitor implements Listener { + private Queue mQueuedSourceEvents = new ConcurrentLinkedQueue<>(); + + /** + * Queues the source event for deferred execution on the buffer processing thread. + * @param event that affects configuration of the channelizer (frequency or sample rate change events) + */ + public void receive(SourceEvent event) + { + mQueuedSourceEvents.offer(event); + } + @Override public void receive(ComplexBuffer complexBuffer) { - if(complexBuffer.hasSourceEvent()) + //Process any queued source events before processing the buffer + SourceEvent queuedSourceEvent = mQueuedSourceEvents.poll(); + + while(queuedSourceEvent != null) { - process(complexBuffer.getSourceEvent()); + switch(queuedSourceEvent.getEvent()) + { + case NOTIFICATION_FREQUENCY_CHANGE: + updateOutputProcessors(queuedSourceEvent); + break; + case NOTIFICATION_SAMPLE_RATE_CHANGE: + updateChannelizerSampleRate(queuedSourceEvent.getValue().intValue()); + break; + } + + queuedSourceEvent = mQueuedSourceEvents.poll(); } if(mPolyphaseChannelizer != null) @@ -447,29 +499,5 @@ public class PolyphaseChannelManager mPolyphaseChannelizer.receive(complexBuffer); } } - - /** - * Processes frequency and sample rate change events that are embedded in the complex buffer stream from the - * tuner or sample source - */ - public void process(SourceEvent sourceEvent) - { - switch(sourceEvent.getEvent()) - { - case NOTIFICATION_FREQUENCY_CHANGE: - //Update the index calculator and each of the output processors to update each channel frequency - mChannelCalculator.setCenterFrequency(sourceEvent.getValue().longValue()); - mLog.debug("Updating output processors - center frequency is now: " + sourceEvent.getValue().longValue()); - updateOutputProcessors(sourceEvent); - break; - case NOTIFICATION_SAMPLE_RATE_CHANGE: - //Update the channelizer (and the output processors) for the new sample rate - updateChannelizerSampleRate(sourceEvent.getValue().intValue()); - break; - default: - mLog.info("Received an unrecognized source event: " + sourceEvent.getEvent()); - break; - } - } } } \ No newline at end of file diff --git a/src/main/java/io/github/dsheirer/sample/Buffer.java b/src/main/java/io/github/dsheirer/sample/Buffer.java index 61d9fe8e..bb1f6aa4 100644 --- a/src/main/java/io/github/dsheirer/sample/Buffer.java +++ b/src/main/java/io/github/dsheirer/sample/Buffer.java @@ -15,25 +15,9 @@ ******************************************************************************/ package io.github.dsheirer.sample; -import io.github.dsheirer.source.SourceEvent; - public class Buffer { private float[] mSamples; - private SourceEvent mSourceEvent; - - /** - * Buffer containing float sample data and a source event that should be processed prior to processing the - * sample data. - * - * @param samples data - * @param sourceEvent indicating a change to the sample data that should be processed first - */ - public Buffer(float[] samples, SourceEvent sourceEvent) - { - mSamples = samples; - mSourceEvent = sourceEvent; - } /** * Buffer containing float sample data @@ -52,32 +36,11 @@ public class Buffer return mSamples; } - /** - * Optional source event that should be processed before processing the included samples. Use the hasSourceEvent() - * method to check for the existence of an event before accessing. - * - * @return optional source event or null - */ - public SourceEvent getSourceEvent() - { - return mSourceEvent; - } - - /** - * Indicates if this buffer contains an optional source event that should be processed prior to processing the - * included float samples array. - */ - public boolean hasSourceEvent() - { - return mSourceEvent != null; - } - /** * Cleanup method to nullify all data and references */ public void dispose() { mSamples = null; - mSourceEvent = null; } } \ No newline at end of file diff --git a/src/main/java/io/github/dsheirer/sample/complex/ComplexBuffer.java b/src/main/java/io/github/dsheirer/sample/complex/ComplexBuffer.java index bfd8971e..884a6f20 100644 --- a/src/main/java/io/github/dsheirer/sample/complex/ComplexBuffer.java +++ b/src/main/java/io/github/dsheirer/sample/complex/ComplexBuffer.java @@ -16,21 +16,12 @@ package io.github.dsheirer.sample.complex; import io.github.dsheirer.sample.Buffer; -import io.github.dsheirer.source.SourceEvent; public class ComplexBuffer extends Buffer { //Default timestamp - we use a static variable here so that there is only ever 1 instance in memory private static final long DEFAULT_TIMESTAMP = 0; - /** - * Wrapper around float array containing interleaved I/Q samples and an optional source event that should be - * processed prior to processing the sample data. - */ - public ComplexBuffer(float[] samples, SourceEvent sourceEvent) - { - super(samples, sourceEvent); - } /** * Wrapper around float array containing interleaved I/Q samples */ diff --git a/src/main/java/io/github/dsheirer/sample/complex/TimestampedComplexBuffer.java b/src/main/java/io/github/dsheirer/sample/complex/TimestampedComplexBuffer.java index ac40103d..9e453ec7 100644 --- a/src/main/java/io/github/dsheirer/sample/complex/TimestampedComplexBuffer.java +++ b/src/main/java/io/github/dsheirer/sample/complex/TimestampedComplexBuffer.java @@ -15,26 +15,10 @@ ******************************************************************************/ package io.github.dsheirer.sample.complex; -import io.github.dsheirer.source.SourceEvent; - public class TimestampedComplexBuffer extends ComplexBuffer { private long mTimestamp; - /** - * Constructs a timestamped complex buffer using the specified time in milliseconds. Also includes a source event - * that should be processed prior to processing the sample data. - * - * @param samples of data - * @param sourceEvent - optional source event to embed with this buffer - * @param timestamp relative to the first sample in the sample array - */ - public TimestampedComplexBuffer(float[] samples, SourceEvent sourceEvent, long timestamp) - { - super(samples, sourceEvent); - mTimestamp = timestamp; - } - /** * Constructs a timestamped complex buffer using the specified time in milliseconds. * @@ -42,18 +26,9 @@ public class TimestampedComplexBuffer extends ComplexBuffer */ public TimestampedComplexBuffer(float[] samples, long timestamp) { - this(samples, null, timestamp); - } + super(samples); - /** - * Constructs a timestamped complex buffer using the current system time in milliseconds. Also includes a source - * event that should be processed prior to processing the sample data. - * - * @param samples of data - */ - public TimestampedComplexBuffer(float[] samples, SourceEvent sourceEvent) - { - this(samples, sourceEvent, System.currentTimeMillis()); + mTimestamp = timestamp; } /** @@ -63,7 +38,7 @@ public class TimestampedComplexBuffer extends ComplexBuffer */ public TimestampedComplexBuffer(float[] samples) { - this(samples, null, System.currentTimeMillis()); + this(samples, System.currentTimeMillis()); } /** diff --git a/src/main/java/io/github/dsheirer/sample/real/RealBuffer.java b/src/main/java/io/github/dsheirer/sample/real/RealBuffer.java index 3628165e..7050e1bf 100644 --- a/src/main/java/io/github/dsheirer/sample/real/RealBuffer.java +++ b/src/main/java/io/github/dsheirer/sample/real/RealBuffer.java @@ -16,21 +16,11 @@ package io.github.dsheirer.sample.real; import io.github.dsheirer.sample.Buffer; -import io.github.dsheirer.source.SourceEvent; import java.util.Arrays; public class RealBuffer extends Buffer { - /** - * Wrapper around float array containing real float samples and an optional source event that should be processed - * prior to processing the sample array data. - */ - public RealBuffer(float[] samples, SourceEvent sourceEvent) - { - super(samples, sourceEvent); - } - /** * Wrapper around float array containing real float samples */ @@ -46,6 +36,6 @@ public class RealBuffer extends Buffer { float[] copy = Arrays.copyOf(getSamples(), getSamples().length); - return new RealBuffer(copy, getSourceEvent()); + return new RealBuffer(copy); } } diff --git a/src/main/java/io/github/dsheirer/source/tuner/manager/TunerSourceManager.java b/src/main/java/io/github/dsheirer/source/tuner/manager/TunerSourceManager.java index 2a5be07e..f3c659f6 100644 --- a/src/main/java/io/github/dsheirer/source/tuner/manager/TunerSourceManager.java +++ b/src/main/java/io/github/dsheirer/source/tuner/manager/TunerSourceManager.java @@ -46,6 +46,7 @@ public class TunerSourceManager extends AbstractSourceManager mPolyphaseChannelManager = new PolyphaseChannelManager(tunerController); //Register to receive channel count change notifications for rebroadcasting mPolyphaseChannelManager.addSourceEventListener(this::process); + mTunerController.addListener(mPolyphaseChannelManager); } /** @@ -427,7 +428,7 @@ public class TunerSourceManager extends AbstractSourceManager broadcast(sourceEvent); break; default: - mLog.info("Received an unrecognized source event: " + sourceEvent); + mLog.info("Unrecognized source event: " + sourceEvent); break; } } diff --git a/src/main/java/io/github/dsheirer/source/tuner/test/SampleGenerator.java b/src/main/java/io/github/dsheirer/source/tuner/test/SampleGenerator.java index 51d157b5..11cf4f14 100644 --- a/src/main/java/io/github/dsheirer/source/tuner/test/SampleGenerator.java +++ b/src/main/java/io/github/dsheirer/source/tuner/test/SampleGenerator.java @@ -22,13 +22,10 @@ import io.github.dsheirer.dsp.mixer.LowPhaseNoiseOscillator; import io.github.dsheirer.sample.Listener; import io.github.dsheirer.sample.complex.ComplexBuffer; import io.github.dsheirer.sample.complex.TimestampedComplexBuffer; -import io.github.dsheirer.source.SourceEvent; import io.github.dsheirer.util.ThreadPool; import org.slf4j.Logger; import org.slf4j.LoggerFactory; -import java.util.Queue; -import java.util.concurrent.ConcurrentLinkedQueue; import java.util.concurrent.ScheduledFuture; import java.util.concurrent.TimeUnit; @@ -42,7 +39,6 @@ public class SampleGenerator private long mInterval; private int mSamplesPerInterval; private ScheduledFuture mScheduledFuture; - private Queue mSourceEventQueue = new ConcurrentLinkedQueue(); /** * Generates complex sample buffers at the specified sample rate with a unity gain tone at the specified @@ -84,16 +80,6 @@ public class SampleGenerator this(sampleRate, frequency, interval, 0); } - /** - * Queues the source event for inclusion in the complex buffers generated by this sample generator. Source events - * are queued FIFO and embedded one at a time in the subsequent buffers produced by this generator. - * @param sourceEvent to queue - */ - public void queue(SourceEvent sourceEvent) - { - mSourceEventQueue.offer(sourceEvent); - } - /** * Updates the number of complex samples generated per interval */ @@ -185,7 +171,7 @@ public class SampleGenerator if(mListener != null) { float[] samples = mOscillator.generate(mSamplesPerInterval); - mListener.receive(new TimestampedComplexBuffer(samples, mSourceEventQueue.poll())); + mListener.receive(new TimestampedComplexBuffer(samples)); if(mSweepUpdateInterval != 0) { diff --git a/src/main/java/io/github/dsheirer/source/tuner/test/TestTunerController.java b/src/main/java/io/github/dsheirer/source/tuner/test/TestTunerController.java index bac4b008..4561d0c4 100644 --- a/src/main/java/io/github/dsheirer/source/tuner/test/TestTunerController.java +++ b/src/main/java/io/github/dsheirer/source/tuner/test/TestTunerController.java @@ -20,7 +20,6 @@ package io.github.dsheirer.source.tuner.test; import io.github.dsheirer.sample.Listener; import io.github.dsheirer.sample.complex.ComplexBuffer; -import io.github.dsheirer.source.SourceEvent; import io.github.dsheirer.source.SourceException; import io.github.dsheirer.source.tuner.TunerController; import io.github.dsheirer.source.tuner.configuration.TunerConfiguration; @@ -56,7 +55,7 @@ public class TestTunerController extends TunerController try { - mLog.debug("Setting frequency ..."); + mLog.debug("Setting frequency to:" + DEFAULT_CENTER_FREQUENCY); mFrequencyController.setFrequency(DEFAULT_CENTER_FREQUENCY); mLog.debug("Setting sample rate of freq controller to: " + DEFAULT_SAMPLE_RATE); mFrequencyController.setSampleRate(DEFAULT_SAMPLE_RATE); @@ -130,7 +129,6 @@ public class TestTunerController extends TunerController { long frequency = toneFrequency - mReferenceFrequency; mSampleGenerator.setFrequency(frequency); - mSampleGenerator.queue(SourceEvent.frequencyChange(frequency)); } /** @@ -157,6 +155,5 @@ public class TestTunerController extends TunerController public void setSampleRate(int sampleRate) throws SourceException { mFrequencyController.setSampleRate(sampleRate); - mSampleGenerator.queue(SourceEvent.sampleRateChange(sampleRate)); } } From a7a0c26b380141f710e5543de0fc61159cea90e9 Mon Sep 17 00:00:00 2001 From: Denny Date: Sun, 14 Jan 2018 08:40:34 -0500 Subject: [PATCH 42/87] #39 WIP - changes for testing of the channelizer viewer. --- .../filter/channelizer/ChannelizerViewer.java | 2 +- .../ComplexPolyphaseChannelizerM2.java | 10 ++--- .../channelizer/PolyphaseChannelManager.java | 42 ++++++++++++------- .../channelizer/ScheduledBufferProcessor.java | 27 ++++++++---- 4 files changed, 51 insertions(+), 30 deletions(-) diff --git a/src/main/java/io/github/dsheirer/dsp/filter/channelizer/ChannelizerViewer.java b/src/main/java/io/github/dsheirer/dsp/filter/channelizer/ChannelizerViewer.java index 3f4b2619..f3ffb5ef 100644 --- a/src/main/java/io/github/dsheirer/dsp/filter/channelizer/ChannelizerViewer.java +++ b/src/main/java/io/github/dsheirer/dsp/filter/channelizer/ChannelizerViewer.java @@ -194,7 +194,7 @@ public class ChannelizerViewer extends JFrame long baseFrequency = 100006250; //100.006250 MHz - for(int x = 0; x < mChannelCount; x++) + for(int x = 0; x < mChannelCount - 1; x++) { long frequency = baseFrequency + (x * CHANNEL_BANDWIDTH); diff --git a/src/main/java/io/github/dsheirer/dsp/filter/channelizer/ComplexPolyphaseChannelizerM2.java b/src/main/java/io/github/dsheirer/dsp/filter/channelizer/ComplexPolyphaseChannelizerM2.java index c8745de8..8b60fc86 100644 --- a/src/main/java/io/github/dsheirer/dsp/filter/channelizer/ComplexPolyphaseChannelizerM2.java +++ b/src/main/java/io/github/dsheirer/dsp/filter/channelizer/ComplexPolyphaseChannelizerM2.java @@ -40,7 +40,7 @@ public class ComplexPolyphaseChannelizerM2 extends AbstractComplexPolyphaseChann private int mBlockSize; private FloatFFT_1D mFFT; private PolyphaseChannelResultsBuffer mChannelResultsBuffer; - private int mChannelResultsBufferSize; + private int mChannelResultsBufferSize = 2500; private SampleTimestampManager mTimestampManager; /** @@ -55,14 +55,11 @@ public class ComplexPolyphaseChannelizerM2 extends AbstractComplexPolyphaseChann * @param sampleRate of the incoming sample stream * @param channelCount - number of filters/channels to output. Since this filter bank oversamples each filter * output, this number must be even (divisible by the 2x oversample rate). - * @param bufferSize for the channel results buffers produced by this channelizer */ - public ComplexPolyphaseChannelizerM2(float[] taps, int sampleRate, int channelCount, int bufferSize) + public ComplexPolyphaseChannelizerM2(float[] taps, int sampleRate, int channelCount) { super(sampleRate, channelCount); - mChannelResultsBufferSize = bufferSize; - if(channelCount % 2 != 0) { throw new IllegalArgumentException("Channel count must be an even multiple of the over-sample rate (2x)"); @@ -92,6 +89,9 @@ public class ComplexPolyphaseChannelizerM2 extends AbstractComplexPolyphaseChann float[] filterTaps = FilterFactory.getSincChannelizer(getChannelSampleRate(), getChannelCount(), tapsPerFilter, WindowType.BLACKMAN_HARRIS_7, true); + double oversampledChannelSampleRate = getChannelSampleRate() * 2.0; + mTimestampManager = new SampleTimestampManager(oversampledChannelSampleRate); + init(filterTaps); } diff --git a/src/main/java/io/github/dsheirer/dsp/filter/channelizer/PolyphaseChannelManager.java b/src/main/java/io/github/dsheirer/dsp/filter/channelizer/PolyphaseChannelManager.java index 81ed8b03..bf2d03ae 100644 --- a/src/main/java/io/github/dsheirer/dsp/filter/channelizer/PolyphaseChannelManager.java +++ b/src/main/java/io/github/dsheirer/dsp/filter/channelizer/PolyphaseChannelManager.java @@ -470,33 +470,45 @@ public class PolyphaseChannelManager implements ISourceEventProcessor */ public void receive(SourceEvent event) { + mLog.debug("Got a source event: " + event.toString()); mQueuedSourceEvents.offer(event); } @Override public void receive(ComplexBuffer complexBuffer) { - //Process any queued source events before processing the buffer - SourceEvent queuedSourceEvent = mQueuedSourceEvents.poll(); - - while(queuedSourceEvent != null) + try { - switch(queuedSourceEvent.getEvent()) + //Process any queued source events before processing the buffer + SourceEvent queuedSourceEvent = mQueuedSourceEvents.poll(); + + while(queuedSourceEvent != null) { - case NOTIFICATION_FREQUENCY_CHANGE: - updateOutputProcessors(queuedSourceEvent); - break; - case NOTIFICATION_SAMPLE_RATE_CHANGE: - updateChannelizerSampleRate(queuedSourceEvent.getValue().intValue()); - break; + switch(queuedSourceEvent.getEvent()) + { + case NOTIFICATION_FREQUENCY_CHANGE: + mLog.debug("******Frequency change event"); + updateOutputProcessors(queuedSourceEvent); + break; + case NOTIFICATION_SAMPLE_RATE_CHANGE: + mLog.debug("******Sample Rate change event"); + updateChannelizerSampleRate(queuedSourceEvent.getValue().intValue()); + break; + } + + mLog.debug("****** Source event processed ... continuing"); + + queuedSourceEvent = mQueuedSourceEvents.poll(); } - queuedSourceEvent = mQueuedSourceEvents.poll(); + if(mPolyphaseChannelizer != null) + { + mPolyphaseChannelizer.receive(complexBuffer); + } } - - if(mPolyphaseChannelizer != null) + catch(Throwable throwable) { - mPolyphaseChannelizer.receive(complexBuffer); + mLog.error("Error", throwable); } } } diff --git a/src/main/java/io/github/dsheirer/dsp/filter/channelizer/ScheduledBufferProcessor.java b/src/main/java/io/github/dsheirer/dsp/filter/channelizer/ScheduledBufferProcessor.java index f9b3563f..2604961e 100644 --- a/src/main/java/io/github/dsheirer/dsp/filter/channelizer/ScheduledBufferProcessor.java +++ b/src/main/java/io/github/dsheirer/dsp/filter/channelizer/ScheduledBufferProcessor.java @@ -152,17 +152,26 @@ public class ScheduledBufferProcessor implements Listener @Override public void run() { - mQueue.drainTo(mBuffers, mMaxBuffersPerInterval); - - if(mListener != null) + try { - for(E buffer: mBuffers) - { - mListener.receive(buffer); - } - } + mQueue.drainTo(mBuffers, mMaxBuffersPerInterval); - mBuffers.clear(); + mLog.debug("Dispatching buffers: " + mBuffers.size()); + if(mListener != null) + { + for(E buffer: mBuffers) + { + mListener.receive(buffer); + } + } + mLog.debug("Finished Dispatching buffers"); + + mBuffers.clear(); + } + catch(Throwable throwable) + { + mLog.error("Error while dispatching buffers to listeners", throwable); + } } } } From 5ae3cb38e116b2ae6afee62c4629181e6009cffa Mon Sep 17 00:00:00 2001 From: Denny Date: Mon, 15 Jan 2018 05:03:03 -0500 Subject: [PATCH 43/87] Merge master and re-apply the source event structures to the DFT processor. --- .../io/github/dsheirer/spectrum/DFTProcessor.java | 12 ++++++------ 1 file changed, 6 insertions(+), 6 deletions(-) diff --git a/src/main/java/io/github/dsheirer/spectrum/DFTProcessor.java b/src/main/java/io/github/dsheirer/spectrum/DFTProcessor.java index 6e32e222..01b3ee1f 100644 --- a/src/main/java/io/github/dsheirer/spectrum/DFTProcessor.java +++ b/src/main/java/io/github/dsheirer/spectrum/DFTProcessor.java @@ -21,8 +21,8 @@ import io.github.dsheirer.properties.SystemProperties; import io.github.dsheirer.sample.Listener; import io.github.dsheirer.sample.SampleType; import io.github.dsheirer.sample.complex.ComplexBuffer; -import io.github.dsheirer.source.tuner.frequency.FrequencyChangeEvent; -import io.github.dsheirer.source.tuner.frequency.IFrequencyChangeProcessor; +import io.github.dsheirer.source.ISourceEventProcessor; +import io.github.dsheirer.source.SourceEvent; import io.github.dsheirer.spectrum.converter.DFTResultsConverter; import io.github.dsheirer.util.ThreadPool; import org.jtransforms.fft.FloatFFT_1D; @@ -40,15 +40,15 @@ import java.util.concurrent.atomic.AtomicBoolean; * Processes both complex samples or float samples and dispatches a float array of DFT results, using configurable fft * size and output dispatch timelines. */ -public class DFTProcessor implements Listener, IFrequencyChangeProcessor, IDFTWidthChangeProcessor +public class DFTProcessor implements Listener, ISourceEventProcessor, IDFTWidthChangeProcessor { private static final Logger mLog = LoggerFactory.getLogger(DFTProcessor.class); private static final int BUFFER_QUEUE_MAX_SIZE = 12; private static final int BUFFER_QUEUE_OVERFLOW_RESET_THRESHOLD = 6; private static final String FRAME_RATE_PROPERTY = "spectral.display.frame.rate"; - //The Cosine and Hanning windows seem to offer the best spectral display with minimal bin leakage/smearing - private WindowType mWindowType = Window.WindowType.HANNING; + //The Cosine and Hann windows seem to offer the best spectral display with minimal bin leakage/smearing + private WindowType mWindowType = WindowType.HANN; private double[] mWindow; private DFTSize mDFTSize = DFTSize.FFT04096; private DFTSize mNewDFTSize = DFTSize.FFT04096; @@ -289,7 +289,7 @@ public class DFTProcessor implements Listener, IFrequencyChangePr } @Override - public void frequencyChanged(FrequencyChangeEvent event) + public void process(SourceEvent event) { switch(event.getEvent()) { From 32ab0598e89f27b3d01048e7a37ffc3b05533525 Mon Sep 17 00:00:00 2001 From: Denny Date: Mon, 15 Jan 2018 05:34:36 -0500 Subject: [PATCH 44/87] Updates sample generator and test tuner controller to support multiple complex buffer listeners. Removes complex buffer broadcast support from the source manager and renames it to channel source manager to convey that it's only responsible for providing channels from the tuner. --- .../filter/channelizer/ChannelizerViewer.java | 4 +- .../channelizer/ScheduledBufferProcessor.java | 2 - .../github/dsheirer/source/tuner/Tuner.java | 18 +++--- .../dsheirer/source/tuner/TunerModel.java | 4 +- ...Manager.java => ChannelSourceManager.java} | 44 +------------- ...er.java => TunerChannelSourceManager.java} | 8 +-- .../source/tuner/test/SampleGenerator.java | 58 ++++++++++++------- .../tuner/test/TestTunerController.java | 20 +------ .../spectrum/SpectralDisplayPanel.java | 2 +- 9 files changed, 60 insertions(+), 100 deletions(-) rename src/main/java/io/github/dsheirer/source/tuner/manager/{AbstractSourceManager.java => ChannelSourceManager.java} (66%) rename src/main/java/io/github/dsheirer/source/tuner/manager/{TunerSourceManager.java => TunerChannelSourceManager.java} (98%) diff --git a/src/main/java/io/github/dsheirer/dsp/filter/channelizer/ChannelizerViewer.java b/src/main/java/io/github/dsheirer/dsp/filter/channelizer/ChannelizerViewer.java index f3ffb5ef..33832fe3 100644 --- a/src/main/java/io/github/dsheirer/dsp/filter/channelizer/ChannelizerViewer.java +++ b/src/main/java/io/github/dsheirer/dsp/filter/channelizer/ChannelizerViewer.java @@ -115,7 +115,7 @@ public class ChannelizerViewer extends JFrame mTestTuner.getTunerController().getSampleRate()); mPrimarySpectrumPanel.setPreferredSize(new Dimension(1000, 200)); mPrimarySpectrumPanel.setDFTSize(mMainPanelDFTSize); - mTestTuner.getSourceManager().addComplexBufferListener(mPrimarySpectrumPanel); + mTestTuner.getTunerController().addComplexBufferListener(mPrimarySpectrumPanel); } return mPrimarySpectrumPanel; @@ -272,7 +272,7 @@ public class ChannelizerViewer extends JFrame mFrequency = frequency; TunerChannel tunerChannel = new TunerChannel(frequency, bandwidth); - mSource = mTestTuner.getSourceManager().getSource(tunerChannel); + mSource = mTestTuner.getChannelSourceManager().getSource(tunerChannel); if(mSource != null) { diff --git a/src/main/java/io/github/dsheirer/dsp/filter/channelizer/ScheduledBufferProcessor.java b/src/main/java/io/github/dsheirer/dsp/filter/channelizer/ScheduledBufferProcessor.java index 2604961e..f9b0ab0c 100644 --- a/src/main/java/io/github/dsheirer/dsp/filter/channelizer/ScheduledBufferProcessor.java +++ b/src/main/java/io/github/dsheirer/dsp/filter/channelizer/ScheduledBufferProcessor.java @@ -156,7 +156,6 @@ public class ScheduledBufferProcessor implements Listener { mQueue.drainTo(mBuffers, mMaxBuffersPerInterval); - mLog.debug("Dispatching buffers: " + mBuffers.size()); if(mListener != null) { for(E buffer: mBuffers) @@ -164,7 +163,6 @@ public class ScheduledBufferProcessor implements Listener mListener.receive(buffer); } } - mLog.debug("Finished Dispatching buffers"); mBuffers.clear(); } diff --git a/src/main/java/io/github/dsheirer/source/tuner/Tuner.java b/src/main/java/io/github/dsheirer/source/tuner/Tuner.java index d9a19e90..66c30bf7 100644 --- a/src/main/java/io/github/dsheirer/source/tuner/Tuner.java +++ b/src/main/java/io/github/dsheirer/source/tuner/Tuner.java @@ -20,18 +20,18 @@ import io.github.dsheirer.sample.Listener; import io.github.dsheirer.source.ISourceEventProcessor; import io.github.dsheirer.source.SourceEvent; import io.github.dsheirer.source.tuner.TunerEvent.Event; -import io.github.dsheirer.source.tuner.manager.AbstractSourceManager; -import io.github.dsheirer.source.tuner.manager.TunerSourceManager; +import io.github.dsheirer.source.tuner.manager.ChannelSourceManager; +import io.github.dsheirer.source.tuner.manager.TunerChannelSourceManager; /** - * Tuner provides an interface to a software or hardware tuner controller that provides I/Q sample data coupled with an - * abstract source manager to provide access to Digital Drop Channel (DDC) resources. + * Tuner provides an interface to a software or hardware tuner controller that provides I/Q sample data coupled with a + * channel source manager to provide access to Digital Drop Channel (DDC) resources. */ public abstract class Tuner implements ISourceEventProcessor { private Broadcaster mTunerEventBroadcaster = new Broadcaster<>(); + private ChannelSourceManager mChannelSourceManager; private TunerController mTunerController; - private AbstractSourceManager mSourceManager; private String mName; /** @@ -46,9 +46,9 @@ public abstract class Tuner implements ISourceEventProcessor //Register to receive frequency and sample rate change notifications mTunerController.addListener(this::process); - mSourceManager = new TunerSourceManager(mTunerController); + mChannelSourceManager = new TunerChannelSourceManager(mTunerController); //Register to receive channel count change notifications - mSourceManager.addSourceEventListener(this::process); + mChannelSourceManager.addSourceEventListener(this::process); } @Override @@ -73,9 +73,9 @@ public abstract class Tuner implements ISourceEventProcessor /** * Source Manager. Provides access to registering for complex buffer samples and source event notifications. */ - public AbstractSourceManager getSourceManager() + public ChannelSourceManager getChannelSourceManager() { - return mSourceManager; + return mChannelSourceManager; } /** diff --git a/src/main/java/io/github/dsheirer/source/tuner/TunerModel.java b/src/main/java/io/github/dsheirer/source/tuner/TunerModel.java index 919f32d6..9c00915e 100644 --- a/src/main/java/io/github/dsheirer/source/tuner/TunerModel.java +++ b/src/main/java/io/github/dsheirer/source/tuner/TunerModel.java @@ -235,7 +235,7 @@ public class TunerModel extends AbstractTableModel implements Listener mComplexBufferBroadcaster = new Broadcaster<>(); private Broadcaster mSourceEventBroadcaster = new Broadcaster<>(); /** @@ -57,41 +54,6 @@ public abstract class AbstractSourceManager implements IComplexBufferProvider, I */ public abstract TunerChannelSource getSource(TunerChannel tunerChannel); - /** - * Adds the listener to receive complex buffer samples - */ - @Override - public void addComplexBufferListener(Listener listener) - { - mComplexBufferBroadcaster.addListener(listener); - } - - /** - * Removes the listener from receiving complex buffer samples - */ - @Override - public void removeComplexBufferListener(Listener listener) - { - mComplexBufferBroadcaster.removeListener(listener); - } - - /** - * Indicates if there are any complex buffer listeners registered with this source manager - */ - @Override - public boolean hasComplexBufferListeners() - { - return mComplexBufferBroadcaster.hasListeners(); - } - - /** - * Broadcasts the buffer to any registered listeners - */ - protected void broadcast(ComplexBuffer complexBuffer) - { - mComplexBufferBroadcaster.broadcast(complexBuffer); - } - /** * Adds a listener to receive source events */ diff --git a/src/main/java/io/github/dsheirer/source/tuner/manager/TunerSourceManager.java b/src/main/java/io/github/dsheirer/source/tuner/manager/TunerChannelSourceManager.java similarity index 98% rename from src/main/java/io/github/dsheirer/source/tuner/manager/TunerSourceManager.java rename to src/main/java/io/github/dsheirer/source/tuner/manager/TunerChannelSourceManager.java index f3c659f6..e6159a0b 100644 --- a/src/main/java/io/github/dsheirer/source/tuner/manager/TunerSourceManager.java +++ b/src/main/java/io/github/dsheirer/source/tuner/manager/TunerChannelSourceManager.java @@ -26,20 +26,20 @@ import org.slf4j.LoggerFactory; import java.util.SortedSet; -public class TunerSourceManager extends AbstractSourceManager +public class TunerChannelSourceManager extends ChannelSourceManager { - private final static Logger mLog = LoggerFactory.getLogger(TunerSourceManager.class); + private final static Logger mLog = LoggerFactory.getLogger(TunerChannelSourceManager.class); private PolyphaseChannelManager mPolyphaseChannelManager; private TunerController mTunerController; /** - * TunerSourceManager is responsible for managing the tuner's center tuned frequency and providing access to + * TunerChannelSourceManager is responsible for managing the tuner's center tuned frequency and providing access to * polyphase tuner channel sources (ie DDCs). This class is responsible for determining IF a requested channel can * be provided and then adjusting the center frequency and provisioning a DDC Polyphase Tuner channel source. * * @param tunerController with a center tuned frequency that will be managed by this instance */ - public TunerSourceManager(TunerController tunerController) + public TunerChannelSourceManager(TunerController tunerController) { mTunerController = tunerController; diff --git a/src/main/java/io/github/dsheirer/source/tuner/test/SampleGenerator.java b/src/main/java/io/github/dsheirer/source/tuner/test/SampleGenerator.java index 11cf4f14..fd6d322a 100644 --- a/src/main/java/io/github/dsheirer/source/tuner/test/SampleGenerator.java +++ b/src/main/java/io/github/dsheirer/source/tuner/test/SampleGenerator.java @@ -1,24 +1,22 @@ /******************************************************************************* - * 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 + * You should have received a copy of the GNU General Public License along with this program. + * If not, see * ******************************************************************************/ package io.github.dsheirer.source.tuner.test; import io.github.dsheirer.dsp.mixer.LowPhaseNoiseOscillator; +import io.github.dsheirer.sample.Broadcaster; import io.github.dsheirer.sample.Listener; import io.github.dsheirer.sample.complex.ComplexBuffer; import io.github.dsheirer.sample.complex.TimestampedComplexBuffer; @@ -33,9 +31,9 @@ public class SampleGenerator { private final static Logger mLog = LoggerFactory.getLogger(SampleGenerator.class); + private Broadcaster mComplexBufferBroadcaster = new Broadcaster<>(); private LowPhaseNoiseOscillator mOscillator; private int mSweepUpdateInterval; - private Listener mListener; private long mInterval; private int mSamplesPerInterval; private ScheduledFuture mScheduledFuture; @@ -93,7 +91,7 @@ public class SampleGenerator /** * Starts the generator producing samples */ - public void start() + private void start() { if(mScheduledFuture == null) { @@ -109,7 +107,7 @@ public class SampleGenerator /** * Stops the generator from producing samples */ - public void stop() + private void stop() { if(mScheduledFuture != null) { @@ -123,14 +121,31 @@ public class SampleGenerator } /** - * Registers the listener to receive samples once the generator starts. Note: invoke this method with null - * argument to de-register the listener. + * Registers the listener to receive samples and auto-starts the generator if this is the first listener. * * @param listener to receive complex sample buffers */ - public void setListener(Listener listener) + public void addListener(Listener listener) { - mListener = listener; + mComplexBufferBroadcaster.addListener(listener); + + if(mComplexBufferBroadcaster.getListenerCount() == 1) + { + start(); + } + } + + /** + * Removes the listener and stops the sample generator if there are no more listeners. + */ + public void removeListener(Listener listener) + { + mComplexBufferBroadcaster.removeListener(listener); + + if(mComplexBufferBroadcaster.getListenerCount() == 0) + { + stop(); + } } /** @@ -168,10 +183,11 @@ public class SampleGenerator @Override public void run() { - if(mListener != null) + if(mComplexBufferBroadcaster.hasListeners()) { float[] samples = mOscillator.generate(mSamplesPerInterval); - mListener.receive(new TimestampedComplexBuffer(samples)); + + mComplexBufferBroadcaster.receive(new TimestampedComplexBuffer(samples)); if(mSweepUpdateInterval != 0) { diff --git a/src/main/java/io/github/dsheirer/source/tuner/test/TestTunerController.java b/src/main/java/io/github/dsheirer/source/tuner/test/TestTunerController.java index 4561d0c4..9867f2c2 100644 --- a/src/main/java/io/github/dsheirer/source/tuner/test/TestTunerController.java +++ b/src/main/java/io/github/dsheirer/source/tuner/test/TestTunerController.java @@ -66,32 +66,16 @@ public class TestTunerController extends TunerController } } - /** - * Starts the tuner controller producing samples that will be delivered to the specified listener - */ - public void start(Listener listener) - { - } - @Override public void addComplexBufferListener(Listener listener) { - mSampleGenerator.setListener(listener); - mSampleGenerator.start(); + mSampleGenerator.addListener(listener); } @Override public void removeComplexBufferListener(Listener listener) { - mSampleGenerator.stop(); - mSampleGenerator.setListener(null); - } - - /** - * Stops the tuner controller sample generator and deregisters the listener - */ - public void stop() - { + mSampleGenerator.removeListener(listener); } @Override diff --git a/src/main/java/io/github/dsheirer/spectrum/SpectralDisplayPanel.java b/src/main/java/io/github/dsheirer/spectrum/SpectralDisplayPanel.java index c9719429..a5032815 100644 --- a/src/main/java/io/github/dsheirer/spectrum/SpectralDisplayPanel.java +++ b/src/main/java/io/github/dsheirer/spectrum/SpectralDisplayPanel.java @@ -435,7 +435,7 @@ public class SpectralDisplayPanel extends JPanel implements Listener Date: Fri, 19 Jan 2018 12:44:31 -0500 Subject: [PATCH 45/87] Polyphase Channelizer Viewer is fully functional again, now using the TestTuner and TestTunerController implementations. This implementation only has support for channels that are completely centered within a channelizer output. Two output slice stitching to form a composite channel will be implemented next. Initial profiling indicates that the FFT function within the polyphase channelizer is now the dominant processing workload - 28% cpu loading when channelizing a 10 MHz sample rate input into 800 channels running at 25.0 kHz each. This is ideal for offloading to an OpenCL kernel and running on a GPU. --- .../filter/channelizer/ChannelizerViewer.java | 141 ++++++++++++++---- .../ComplexPolyphaseChannelizerM2.java | 18 ++- .../channelizer/PolyphaseChannelManager.java | 9 -- .../PolyphaseChannelResultsBuffer.java | 13 +- .../channelizer/ScheduledBufferProcessor.java | 9 -- .../output/OneChannelOutputProcessor.java | 1 - .../source/tuner/TunerController.java | 4 +- .../source/tuner/test/SampleGenerator.java | 67 ++++++--- .../tuner/test/TestTunerController.java | 48 +++--- 9 files changed, 208 insertions(+), 102 deletions(-) diff --git a/src/main/java/io/github/dsheirer/dsp/filter/channelizer/ChannelizerViewer.java b/src/main/java/io/github/dsheirer/dsp/filter/channelizer/ChannelizerViewer.java index 33832fe3..b37e82ff 100644 --- a/src/main/java/io/github/dsheirer/dsp/filter/channelizer/ChannelizerViewer.java +++ b/src/main/java/io/github/dsheirer/dsp/filter/channelizer/ChannelizerViewer.java @@ -23,6 +23,7 @@ import io.github.dsheirer.settings.SettingsManager; import io.github.dsheirer.source.ISourceEventProcessor; import io.github.dsheirer.source.SourceEvent; import io.github.dsheirer.source.SourceException; +import io.github.dsheirer.source.tuner.Tuner; import io.github.dsheirer.source.tuner.channel.TunerChannel; import io.github.dsheirer.source.tuner.channel.TunerChannelSource; import io.github.dsheirer.source.tuner.configuration.TunerConfigurationModel; @@ -44,6 +45,8 @@ import javax.swing.event.ChangeEvent; import javax.swing.event.ChangeListener; import java.awt.Dimension; import java.awt.EventQueue; +import java.util.ArrayList; +import java.util.List; public class ChannelizerViewer extends JFrame { @@ -55,13 +58,14 @@ public class ChannelizerViewer extends JFrame private SettingsManager mSettingsManager = new SettingsManager(new TunerConfigurationModel()); private JPanel mPrimaryPanel; private JPanel mControlPanel; + private JLabel mToneFrequencyLabel; private PrimarySpectrumPanel mPrimarySpectrumPanel; private ChannelArrayPanel mChannelPanel; private int mChannelCount; private int mChannelsPerRow; - private long mToneFrequency; + private long mBaseFrequency = 100000000; //100 MHz private DFTSize mMainPanelDFTSize = DFTSize.FFT32768; - private DFTSize mChannelPanelDFTSize = DFTSize.FFT01024; + private DFTSize mChannelPanelDFTSize = DFTSize.FFT04096; private TestTuner mTestTuner; /** @@ -76,16 +80,12 @@ public class ChannelizerViewer extends JFrame mChannelCount = (int)(mTestTuner.getTunerController().getUsableBandwidth() / CHANNEL_BANDWIDTH); mChannelsPerRow = channelsPerRow; - mToneFrequency = mTestTuner.getTunerController().getFrequency() + 19200; - - mTestTuner.getTunerController().setToneFrequency(mToneFrequency); - init(); } private void init() { - setTitle("TunerChannelizer Viewer"); + setTitle("Polyphase Channelizer Viewer"); setSize(1000, 800); setDefaultCloseOperation(JFrame.EXIT_ON_CLOSE); setLayout(new MigLayout("insets 0 0 0 0", "[grow,fill]", "[grow,fill]")); @@ -129,11 +129,11 @@ public class ChannelizerViewer extends JFrame mControlPanel.setLayout(new MigLayout("insets 0 0 0 0", "", "")); mControlPanel.add(new JLabel("Tone:"), "align left"); - int maximumFrequencyOffset = (int)(mTestTuner.getTunerController().getSampleRate() / 2); - long minimumFrequency = mTestTuner.getTunerController().getFrequency() - maximumFrequencyOffset; - long maximumFrequency = mTestTuner.getTunerController().getFrequency() + maximumFrequencyOffset; + long minimumFrequency = -(long)mTestTuner.getTunerController().getSampleRate() / 2; + long maximumFrequency = (long)mTestTuner.getTunerController().getSampleRate() / 2; + long toneFrequency = 0; - SpinnerNumberModel model = new SpinnerNumberModel(mToneFrequency, minimumFrequency, maximumFrequency, + SpinnerNumberModel model = new SpinnerNumberModel(toneFrequency, minimumFrequency, maximumFrequency, 100 ); model.addChangeListener(new ChangeListener() @@ -141,8 +141,9 @@ public class ChannelizerViewer extends JFrame @Override public void stateChanged(ChangeEvent e) { - int toneFrequency = model.getNumber().intValue(); + long toneFrequency = model.getNumber().longValue(); mTestTuner.getTunerController().setToneFrequency(toneFrequency); + mToneFrequencyLabel.setText(String.valueOf(getToneFrequency())); } }); @@ -151,15 +152,21 @@ public class ChannelizerViewer extends JFrame mControlPanel.add(spinner); mControlPanel.add(new JLabel("Hz")); - mControlPanel.add(new JLabel("Sample Rate: " + mTestTuner.getTunerController().getSampleRate() + " Hz"), "push,align right"); + mControlPanel.add(new JLabel("Frequency:"), "push,align right"); + mToneFrequencyLabel = new JLabel(String.valueOf(getToneFrequency())); + mControlPanel.add(mToneFrequencyLabel, "push,align left"); - mControlPanel.add(new JLabel("Max: " + (mTestTuner.getTunerController().getSampleRate() / 2) + " Hz"), "push,align right"); mControlPanel.add(new JLabel("Channels: " + mChannelCount), "push,align right"); } return mControlPanel; } + private long getToneFrequency() + { + return mTestTuner.getTunerController().getFrequency() + mTestTuner.getTunerController().getToneFrequency(); + } + private ChannelArrayPanel getChannelArrayPanel() { if(mChannelPanel == null) @@ -192,7 +199,7 @@ public class ChannelizerViewer extends JFrame int channelToLog = -1; - long baseFrequency = 100006250; //100.006250 MHz + long baseFrequency = mBaseFrequency + (CHANNEL_BANDWIDTH / 2); for(int x = 0; x < mChannelCount - 1; x++) { @@ -215,6 +222,33 @@ public class ChannelizerViewer extends JFrame } } + /** + * Returns a list of tuner channels that will fit within the tuner's bandwidth, minus a half channel each at the + * lower and upper ends of the spectrum. + * + * @param tuner to create channels for + * @return list of contiguous channels filling the tuner bandwidth + */ + public static List getTunerChannels(Tuner tuner) + { + List tunerChannels = new ArrayList<>(); + + long baseFrequency = tuner.getTunerController().getFrequency(); + baseFrequency -= tuner.getTunerController().getSampleRate() / 2; + baseFrequency += (CHANNEL_BANDWIDTH / 2); + + int channelCount = (int)(tuner.getTunerController().getSampleRate() / CHANNEL_BANDWIDTH) - 1; + + for(int x = 0; x < channelCount; x++) + { + long frequency = baseFrequency + (x * CHANNEL_BANDWIDTH); + TunerChannel tunerChannel = new TunerChannel(frequency, CHANNEL_BANDWIDTH); + tunerChannels.add(tunerChannel); + } + + return tunerChannels; + } + public class PrimarySpectrumPanel extends JPanel implements Listener, ISourceEventProcessor { private DFTProcessor mDFTProcessor = new DFTProcessor(SampleType.COMPLEX); @@ -225,6 +259,7 @@ public class ChannelizerViewer extends JFrame { setLayout(new MigLayout("insets 0 0 0 0", "[grow,fill]", "[grow,fill]")); mSpectrumPanel = new SpectrumPanel(settingsManager); + mSpectrumPanel.setSampleSize(26); add(mSpectrumPanel); mDFTProcessor.addConverter(mComplexDecibelConverter); @@ -256,21 +291,20 @@ public class ChannelizerViewer extends JFrame private DFTProcessor mDFTProcessor = new DFTProcessor(SampleType.COMPLEX); private ComplexDecibelConverter mComplexDecibelConverter = new ComplexDecibelConverter(); private SpectrumPanel mSpectrumPanel; - private long mFrequency; public ChannelPanel(SettingsManager settingsManager, double sampleRate, long frequency, int bandwidth, boolean enableLogging) { - setLayout(new MigLayout("insets 0 0 0 0", "[grow,fill]", "[grow,fill]")); + setLayout(new MigLayout("insets 0 0 0 0", "[center,grow,fill]", "[grow,fill][]")); mSpectrumPanel = new SpectrumPanel(settingsManager); - add(mSpectrumPanel); + mSpectrumPanel.setSampleSize(28); + add(mSpectrumPanel, "wrap"); + add(new JLabel("Center:" + frequency)); mDFTProcessor.addConverter(mComplexDecibelConverter); mDFTProcessor.process(SourceEvent.sampleRateChange(sampleRate)); mComplexDecibelConverter.addListener(mSpectrumPanel); - mFrequency = frequency; - TunerChannel tunerChannel = new TunerChannel(frequency, bandwidth); mSource = mTestTuner.getChannelSourceManager().getSource(tunerChannel); @@ -326,18 +360,69 @@ public class ChannelizerViewer extends JFrame public static void main(String[] args) { - int channelsPerRow = 4; + boolean useGUI = true; - final ChannelizerViewer frame = new ChannelizerViewer(channelsPerRow); - - EventQueue.invokeLater(new Runnable() + if(useGUI) { - @Override - public void run() + int channelsPerRow = 4; + + final ChannelizerViewer frame = new ChannelizerViewer(channelsPerRow); + + EventQueue.invokeLater(new Runnable() { - frame.setVisible(true); + @Override + public void run() + { + frame.setVisible(true); + } + }); + } + else + { + TestTuner tuner = new TestTuner(); + + List tunerChannels = getTunerChannels(tuner); + + List sources = new ArrayList<>(); + + int maxSourceCount = 30; + int sourceCount = 0; + for(TunerChannel tunerChannel: tunerChannels) + { + if(sourceCount < maxSourceCount) + { + TunerChannelSource source = tuner.getChannelSourceManager().getSource(tunerChannel); + + if(source != null) + { + sources.add(source); + sourceCount++; + } + else + { + mLog.debug("Couldn't get source for: " + tunerChannel); + } + } } - }); + + mLog.debug("Starting [" + sources.size() + "] tuner channel sources"); + + for(TunerChannelSource tunerChannelSource: sources) + { + tunerChannelSource.setListener(new Listener() + { + @Override + public void receive(ComplexBuffer complexBuffer) + { + //Do nothing + } + }); + + tunerChannelSource.start(); + } + + while(true); + } } } \ No newline at end of file diff --git a/src/main/java/io/github/dsheirer/dsp/filter/channelizer/ComplexPolyphaseChannelizerM2.java b/src/main/java/io/github/dsheirer/dsp/filter/channelizer/ComplexPolyphaseChannelizerM2.java index 8b60fc86..63ec72db 100644 --- a/src/main/java/io/github/dsheirer/dsp/filter/channelizer/ComplexPolyphaseChannelizerM2.java +++ b/src/main/java/io/github/dsheirer/dsp/filter/channelizer/ComplexPolyphaseChannelizerM2.java @@ -197,11 +197,16 @@ public class ComplexPolyphaseChannelizerM2 extends AbstractComplexPolyphaseChann int half = getChannelCount() / 2; + int outputOffset = getChannelCount() - half - 1; + int outputIndex; + int sampleIndex; + int filterIndex; + for(int channel = 0; channel < half; channel++) { - int outputIndex = 2 * (getChannelCount() - channel - 1); - int sampleIndex = 2 * channel; - int filterIndex = channel + half; + outputIndex = 2 * (outputOffset - channel); + sampleIndex = 2 * channel; + filterIndex = channel + half; processed[outputIndex] = filter(sampleIndex, mCoefficients[filterIndex], sampleIndexMap); processed[outputIndex + 1] = filter(sampleIndex + 1, mCoefficients[filterIndex], sampleIndexMap); @@ -209,12 +214,13 @@ public class ComplexPolyphaseChannelizerM2 extends AbstractComplexPolyphaseChann //The second half of the sample buffer was loaded using the previous column pointer - adjust for that now sampleIndexMap = mSampleIndexMap[(mColumnPointer == (mSamples.length - 1) ? 0 : mColumnPointer + 1)]; + outputOffset = getChannelCount() + half - 1; for(int channel = half; channel < getChannelCount(); channel++) { - int outputIndex = 2 * (getChannelCount() - channel - 1); - int sampleIndex = 2 * channel; - int filterIndex = channel - half; + outputIndex = 2 * (outputOffset - channel); + sampleIndex = 2 * channel; + filterIndex = channel - half; processed[outputIndex] = filter(sampleIndex, mCoefficients[filterIndex], sampleIndexMap); processed[outputIndex + 1] = filter(sampleIndex + 1, mCoefficients[filterIndex], sampleIndexMap); diff --git a/src/main/java/io/github/dsheirer/dsp/filter/channelizer/PolyphaseChannelManager.java b/src/main/java/io/github/dsheirer/dsp/filter/channelizer/PolyphaseChannelManager.java index bf2d03ae..e0fc7eed 100644 --- a/src/main/java/io/github/dsheirer/dsp/filter/channelizer/PolyphaseChannelManager.java +++ b/src/main/java/io/github/dsheirer/dsp/filter/channelizer/PolyphaseChannelManager.java @@ -132,7 +132,6 @@ public class PolyphaseChannelManager implements ISourceEventProcessor try { List polyphaseIndexes = mChannelCalculator.getChannelIndexes(tunerChannel); - mLog.debug("Getting polyphase channel for: " + tunerChannel + " indexes:" + polyphaseIndexes); IPolyphaseChannelOutputProcessor outputProcessor = getOutputProcessor(polyphaseIndexes); @@ -240,7 +239,6 @@ public class PolyphaseChannelManager implements ISourceEventProcessor { case NOTIFICATION_FREQUENCY_CHANGE: //Update channel calculator immediately so that channels can be allocated - mLog.debug("Updating channel calculator to frequency: " + sourceEvent.getValue()); mChannelCalculator.setCenterFrequency(sourceEvent.getValue().longValue()); //Defer channelizer configuration changes to be handled on the buffer processor thread @@ -248,7 +246,6 @@ public class PolyphaseChannelManager implements ISourceEventProcessor break; case NOTIFICATION_SAMPLE_RATE_CHANGE: //Update channel calculator immediately so that channels can be allocated - mLog.debug("Updating channel calculator to sample rate: " + sourceEvent.getValue()); mChannelCalculator.setSampleRate(sourceEvent.getValue().doubleValue()); //Defer channelizer configuration changes to be handled on the buffer processor thread @@ -355,13 +352,11 @@ 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 indexes to " + indexes + " for: " + channelSource.getTunerChannel()); channelSource.getPolyphaseChannelOutputProcessor().setPolyphaseChannelIndices(indexes); channelSource.setFrequency(centerFrequency); } else { - mLog.debug("Creating new output processor for indexes " + indexes); channelSource.setPolyphaseChannelOutputProcessor(getOutputProcessor(indexes), centerFrequency); } } @@ -487,17 +482,13 @@ public class PolyphaseChannelManager implements ISourceEventProcessor switch(queuedSourceEvent.getEvent()) { case NOTIFICATION_FREQUENCY_CHANGE: - mLog.debug("******Frequency change event"); updateOutputProcessors(queuedSourceEvent); break; case NOTIFICATION_SAMPLE_RATE_CHANGE: - mLog.debug("******Sample Rate change event"); updateChannelizerSampleRate(queuedSourceEvent.getValue().intValue()); break; } - mLog.debug("****** Source event processed ... continuing"); - queuedSourceEvent = mQueuedSourceEvents.poll(); } diff --git a/src/main/java/io/github/dsheirer/dsp/filter/channelizer/PolyphaseChannelResultsBuffer.java b/src/main/java/io/github/dsheirer/dsp/filter/channelizer/PolyphaseChannelResultsBuffer.java index f0ccc96c..ce46030c 100644 --- a/src/main/java/io/github/dsheirer/dsp/filter/channelizer/PolyphaseChannelResultsBuffer.java +++ b/src/main/java/io/github/dsheirer/dsp/filter/channelizer/PolyphaseChannelResultsBuffer.java @@ -72,10 +72,10 @@ public class PolyphaseChannelResultsBuffer { throw new IllegalArgumentException("Buffer is full - cannot add new channel results"); } + if(mPointer == 0) { mChannelResults = new float[mMaxSize][channelResults.length]; - mChannelResults[mPointer++] = channelResults; } else { @@ -87,9 +87,9 @@ public class PolyphaseChannelResultsBuffer "a different length than the channel results that are already contained in this buffer [" + currentLength + "]"); } - - mChannelResults[mPointer++] = channelResults; } + + mChannelResults[mPointer++] = channelResults; } /** @@ -97,10 +97,12 @@ public class PolyphaseChannelResultsBuffer */ public float[][] getChannelResults() { + //If this buffer is full, return all of the channel results if(mPointer == mMaxSize) { return mChannelResults; } + //Otherwise, truncate the results to return else { return Arrays.copyOf(mChannelResults, mPointer); @@ -120,6 +122,7 @@ public class PolyphaseChannelResultsBuffer { throw new IllegalArgumentException("Buffer is empty - cannot extract channel [" + iChannelIndex + "]"); } + if(!isValidChannelIndex(iChannelIndex)) { throw new IllegalArgumentException("Channel [" + iChannelIndex + "] is not valid for the contained channel " + @@ -131,8 +134,10 @@ public class PolyphaseChannelResultsBuffer int qChannelIndex = iChannelIndex + 1; - for(float[] channelResults: mChannelResults) + for(int x = 0; x < mPointer; x++) { + float[] channelResults = mChannelResults[x]; + samples[pointer++] = channelResults[iChannelIndex]; samples[pointer++] = channelResults[qChannelIndex]; } diff --git a/src/main/java/io/github/dsheirer/dsp/filter/channelizer/ScheduledBufferProcessor.java b/src/main/java/io/github/dsheirer/dsp/filter/channelizer/ScheduledBufferProcessor.java index f9b0ab0c..57c3b9fc 100644 --- a/src/main/java/io/github/dsheirer/dsp/filter/channelizer/ScheduledBufferProcessor.java +++ b/src/main/java/io/github/dsheirer/dsp/filter/channelizer/ScheduledBufferProcessor.java @@ -60,15 +60,6 @@ public class ScheduledBufferProcessor implements Listener public ScheduledBufferProcessor(int maximumSize, int resetThreshold, long distributionInterval, int maxBuffersPerInterval) { mQueue = new OverflowableTransferQueue<>(maximumSize, resetThreshold); - - mQueue.setOverflowListener(new IOverflowListener() - { - @Override - public void sourceOverflow(boolean overflow) - { - mLog.error("Overflow state changed - overflow:" + overflow); - } - }); mDistributionInterval = distributionInterval; mMaxBuffersPerInterval = maxBuffersPerInterval; } diff --git a/src/main/java/io/github/dsheirer/dsp/filter/channelizer/output/OneChannelOutputProcessor.java b/src/main/java/io/github/dsheirer/dsp/filter/channelizer/output/OneChannelOutputProcessor.java index d1c1f687..eaff4feb 100644 --- a/src/main/java/io/github/dsheirer/dsp/filter/channelizer/output/OneChannelOutputProcessor.java +++ b/src/main/java/io/github/dsheirer/dsp/filter/channelizer/output/OneChannelOutputProcessor.java @@ -42,7 +42,6 @@ public class OneChannelOutputProcessor extends ChannelOutputProcessor { super(1, sampleRate); setPolyphaseChannelIndices(channelIndexes); - mLog.debug("Sample Rate:" + sampleRate + " PolyPhase Channel Indexes:" + channelIndexes); } /** diff --git a/src/main/java/io/github/dsheirer/source/tuner/TunerController.java b/src/main/java/io/github/dsheirer/source/tuner/TunerController.java index 36b177e9..5a7b4abf 100644 --- a/src/main/java/io/github/dsheirer/source/tuner/TunerController.java +++ b/src/main/java/io/github/dsheirer/source/tuner/TunerController.java @@ -161,12 +161,12 @@ public abstract class TunerController implements Tunable, ISourceEventProcessor, return mFrequencyController.getMaximumFrequency(); } - private long getMinTunedFrequency() throws SourceException + public long getMinTunedFrequency() throws SourceException { return mFrequencyController.getFrequency() - (getUsableBandwidth() / 2); } - private long getMaxTunedFrequency() throws SourceException + public long getMaxTunedFrequency() throws SourceException { return mFrequencyController.getFrequency() + (getUsableBandwidth() / 2); } diff --git a/src/main/java/io/github/dsheirer/source/tuner/test/SampleGenerator.java b/src/main/java/io/github/dsheirer/source/tuner/test/SampleGenerator.java index fd6d322a..c7a4147a 100644 --- a/src/main/java/io/github/dsheirer/source/tuner/test/SampleGenerator.java +++ b/src/main/java/io/github/dsheirer/source/tuner/test/SampleGenerator.java @@ -36,6 +36,7 @@ public class SampleGenerator private int mSweepUpdateInterval; private long mInterval; private int mSamplesPerInterval; + private boolean mReuseBuffers; private ScheduledFuture mScheduledFuture; /** @@ -49,17 +50,17 @@ public class SampleGenerator * @param sweepUpdateRate to turn on sweeping of the tone frequency where the frequency is incremented by the * rate specified at each interval, resetting to 1 Hz after reaching/exceeding the highest frequency. */ - public SampleGenerator(int sampleRate, long frequency, long interval, int sweepUpdateRate) + public SampleGenerator(int sampleRate, long frequency, long interval, int sweepUpdateRate, boolean reuseBuffers) { if(Math.abs(sweepUpdateRate) >= sampleRate) { throw new IllegalArgumentException("Sweep update rate cannot be greater than sample rate"); } - mLog.debug("Sample Generator - Sample Rate:" + sampleRate + " Frequency:" + frequency); mOscillator = new LowPhaseNoiseOscillator(sampleRate, frequency); mInterval = interval; mSweepUpdateInterval = sweepUpdateRate; + mReuseBuffers = reuseBuffers; updateSamplesPerInterval(); } @@ -73,9 +74,9 @@ public class SampleGenerator * @param frequency of the tone produced by the generator * @param interval in milliseconds for generating samples */ - public SampleGenerator(int sampleRate, long frequency, long interval) + public SampleGenerator(int sampleRate, long frequency, long interval, boolean reuseBuffers) { - this(sampleRate, frequency, interval, 0); + this(sampleRate, frequency, interval, 0, reuseBuffers); } /** @@ -84,8 +85,6 @@ public class SampleGenerator private void updateSamplesPerInterval() { mSamplesPerInterval = (int)((double)mOscillator.getSampleRate() * ((double)mInterval / 1000.0)); - - mLog.debug("Sample Generator - " + mSamplesPerInterval + " samples every " + mInterval + "ms"); } /** @@ -175,35 +174,65 @@ public class SampleGenerator updateSamplesPerInterval(); } + public double getSampleRate() + { + return mOscillator.getSampleRate(); + } + /** * Generates a complex sample buffer and distributes the buffer to a registered listener */ public class Generator implements Runnable { + private int mTriggerInterval = 0; + private long mOscillatorFrequency = 1; + private ComplexBuffer mReusableBuffer; + @Override public void run() { if(mComplexBufferBroadcaster.hasListeners()) { - float[] samples = mOscillator.generate(mSamplesPerInterval); + if(mReuseBuffers) + { + if(mReusableBuffer == null || + mReusableBuffer.getSamples().length != mSamplesPerInterval || + mOscillatorFrequency != mOscillator.getFrequency()) + { + mOscillatorFrequency = mOscillator.getFrequency(); + mReusableBuffer = new TimestampedComplexBuffer(mOscillator.generate(mSamplesPerInterval)); + } - mComplexBufferBroadcaster.receive(new TimestampedComplexBuffer(samples)); + mComplexBufferBroadcaster.receive(mReusableBuffer); + } + else + { + float[] samples = mOscillator.generate(mSamplesPerInterval); + mComplexBufferBroadcaster.receive(new TimestampedComplexBuffer(samples)); + } if(mSweepUpdateInterval != 0) { - long updatedFrequency = mOscillator.getFrequency() + mSweepUpdateInterval; + mTriggerInterval++; - if(updatedFrequency > mOscillator.getSampleRate() / 2) + if(mTriggerInterval >= 10) { - mOscillator.setFrequency(mOscillator.getSampleRate() / -2); - } - else if(updatedFrequency < mOscillator.getSampleRate() / -2) - { - mOscillator.setFrequency(mOscillator.getSampleRate() / 2); - } - else - { - mOscillator.setFrequency(updatedFrequency); + mTriggerInterval = 0; + + long updatedFrequency = mOscillator.getFrequency() + mSweepUpdateInterval; + + if(updatedFrequency > mOscillator.getSampleRate() / 2) + { + mOscillator.setFrequency(mOscillator.getSampleRate() / -2); + } + else if(updatedFrequency < mOscillator.getSampleRate() / -2) + { + mOscillator.setFrequency(mOscillator.getSampleRate() / 2); + } + else + { + mOscillator.setFrequency(updatedFrequency); + } } } } diff --git a/src/main/java/io/github/dsheirer/source/tuner/test/TestTunerController.java b/src/main/java/io/github/dsheirer/source/tuner/test/TestTunerController.java index 9867f2c2..b9e6f3c8 100644 --- a/src/main/java/io/github/dsheirer/source/tuner/test/TestTunerController.java +++ b/src/main/java/io/github/dsheirer/source/tuner/test/TestTunerController.java @@ -30,17 +30,18 @@ public class TestTunerController extends TunerController { private final static Logger mLog = LoggerFactory.getLogger(TestTunerController.class); - public static final long MINIMUM_FREQUENCY = 1000000l; //1 MHz - public static final long MAXIMUM_FREQUENCY = 10000000000l; //10 GHz - public static final long DEFAULT_CENTER_FREQUENCY = 100000000; //100 MHz - public static final int DEFAULT_SAMPLE_RATE = 100000; //25 kHz - public static final int DC_NOISE_BANDWIDTH = 0; // +/-5 kHz - public static final double USABLE_BANDWIDTH = 1.00; //100% usable bandwidth + 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 DC_NOISE_BANDWIDTH = 0; + public static final double USABLE_BANDWIDTH = 1.00; + public static final int SPECTRAL_FRAME_RATE = 20; public static final long SAMPLE_GENERATION_INTERVAL = 1000 / SPECTRAL_FRAME_RATE; private SampleGenerator mSampleGenerator; - private long mReferenceFrequency = DEFAULT_CENTER_FREQUENCY; + private boolean mReuseBuffers = true; + private long mFrequency = 100000000l; /** * Tuner controller testing implementation. @@ -50,15 +51,14 @@ public class TestTunerController extends TunerController super(MINIMUM_FREQUENCY, MAXIMUM_FREQUENCY, DC_NOISE_BANDWIDTH, USABLE_BANDWIDTH); int sweepRate = 0; //Hz per interval - - mSampleGenerator = new SampleGenerator(DEFAULT_SAMPLE_RATE, 12500, SAMPLE_GENERATION_INTERVAL, sweepRate); + long initialToneFrequency = SAMPLE_RATE / 2 + 100; + mSampleGenerator = new SampleGenerator(SAMPLE_RATE, initialToneFrequency, SAMPLE_GENERATION_INTERVAL, + sweepRate, mReuseBuffers); try { - mLog.debug("Setting frequency to:" + DEFAULT_CENTER_FREQUENCY); - mFrequencyController.setFrequency(DEFAULT_CENTER_FREQUENCY); - mLog.debug("Setting sample rate of freq controller to: " + DEFAULT_SAMPLE_RATE); - mFrequencyController.setSampleRate(DEFAULT_SAMPLE_RATE); + mFrequencyController.setFrequency(mFrequency); + mFrequencyController.setSampleRate(SAMPLE_RATE); } catch(Exception e) { @@ -91,7 +91,7 @@ public class TestTunerController extends TunerController @Override public long getTunedFrequency() throws SourceException { - return mReferenceFrequency; + return mFrequency; } /** @@ -102,26 +102,26 @@ public class TestTunerController extends TunerController @Override public void setTunedFrequency(long frequency) throws SourceException { - mReferenceFrequency = frequency; + mFrequency = frequency; } /** - * Sets the tone frequency for the sample generator. The tone frequency is offset from the center tuned frequency. - * @param toneFrequency + * Sets the tone output of the frequency generator + * @param frequency in the range: 0 <> Sample Rate */ - public void setToneFrequency(long toneFrequency) + public void setToneFrequency(long frequency) { - long frequency = toneFrequency - mReferenceFrequency; mSampleGenerator.setFrequency(frequency); } /** - * Current tone frequency for this test controller. - * @return + * Frequency of the tone being generated + * + * @return tone frequency in range: 0 <> Sample Rate */ public long getToneFrequency() { - return mReferenceFrequency + mSampleGenerator.getFrequency(); + return mSampleGenerator.getFrequency(); } /** @@ -130,7 +130,7 @@ public class TestTunerController extends TunerController @Override public double getCurrentSampleRate() { - return mFrequencyController.getSampleRate(); + return mSampleGenerator.getSampleRate(); } /** @@ -138,6 +138,6 @@ public class TestTunerController extends TunerController */ public void setSampleRate(int sampleRate) throws SourceException { - mFrequencyController.setSampleRate(sampleRate); + mSampleGenerator.setSampleRate(sampleRate); } } From 0cf964fa9845d954a86f07bc8521a2fc9eaee9cf Mon Sep 17 00:00:00 2001 From: Denny Date: Sat, 20 Jan 2018 04:26:53 -0500 Subject: [PATCH 46/87] Updates test tuner to cleanup the buffer reuse settings --- .../filter/channelizer/ChannelizerViewer.java | 4 +++- .../source/tuner/test/SampleGenerator.java | 22 +++++++++++++++---- .../tuner/test/TestTunerController.java | 14 ++++++++++-- 3 files changed, 33 insertions(+), 7 deletions(-) diff --git a/src/main/java/io/github/dsheirer/dsp/filter/channelizer/ChannelizerViewer.java b/src/main/java/io/github/dsheirer/dsp/filter/channelizer/ChannelizerViewer.java index b37e82ff..2a5cb42d 100644 --- a/src/main/java/io/github/dsheirer/dsp/filter/channelizer/ChannelizerViewer.java +++ b/src/main/java/io/github/dsheirer/dsp/filter/channelizer/ChannelizerViewer.java @@ -381,6 +381,9 @@ public class ChannelizerViewer extends JFrame { TestTuner tuner = new TestTuner(); + //Turn on buffer reuse since we're simply profiling data flow/throughput + tuner.getTunerController().setReuseBuffers(true); + List tunerChannels = getTunerChannels(tuner); List sources = new ArrayList<>(); @@ -424,5 +427,4 @@ public class ChannelizerViewer extends JFrame while(true); } } - } \ No newline at end of file diff --git a/src/main/java/io/github/dsheirer/source/tuner/test/SampleGenerator.java b/src/main/java/io/github/dsheirer/source/tuner/test/SampleGenerator.java index c7a4147a..76e7642b 100644 --- a/src/main/java/io/github/dsheirer/source/tuner/test/SampleGenerator.java +++ b/src/main/java/io/github/dsheirer/source/tuner/test/SampleGenerator.java @@ -50,7 +50,7 @@ public class SampleGenerator * @param sweepUpdateRate to turn on sweeping of the tone frequency where the frequency is incremented by the * rate specified at each interval, resetting to 1 Hz after reaching/exceeding the highest frequency. */ - public SampleGenerator(int sampleRate, long frequency, long interval, int sweepUpdateRate, boolean reuseBuffers) + public SampleGenerator(int sampleRate, long frequency, long interval, int sweepUpdateRate) { if(Math.abs(sweepUpdateRate) >= sampleRate) { @@ -60,7 +60,6 @@ public class SampleGenerator mOscillator = new LowPhaseNoiseOscillator(sampleRate, frequency); mInterval = interval; mSweepUpdateInterval = sweepUpdateRate; - mReuseBuffers = reuseBuffers; updateSamplesPerInterval(); } @@ -74,9 +73,24 @@ public class SampleGenerator * @param frequency of the tone produced by the generator * @param interval in milliseconds for generating samples */ - public SampleGenerator(int sampleRate, long frequency, long interval, boolean reuseBuffers) + public SampleGenerator(int sampleRate, long frequency, long interval) { - this(sampleRate, frequency, interval, 0, reuseBuffers); + this(sampleRate, frequency, interval, 0); + } + + /** + * Instructs the sample generator to reuse the same sample buffer each time instead of generating new samples for + * each and every buffer. This is useful when you simply want to generate data to test or profile data flows and + * are not concerned about the content of the buffers. + * + * The generator will create a new buffer of data each time that it detects that the sample size changes or when + * the tone frequency changes. + * + * @param reuseBuffers set to true to turn on buffer reuse + */ + public void setReuseBuffers(boolean reuseBuffers) + { + mReuseBuffers = reuseBuffers; } /** diff --git a/src/main/java/io/github/dsheirer/source/tuner/test/TestTunerController.java b/src/main/java/io/github/dsheirer/source/tuner/test/TestTunerController.java index b9e6f3c8..e8f980ba 100644 --- a/src/main/java/io/github/dsheirer/source/tuner/test/TestTunerController.java +++ b/src/main/java/io/github/dsheirer/source/tuner/test/TestTunerController.java @@ -40,7 +40,6 @@ public class TestTunerController extends TunerController public static final long SAMPLE_GENERATION_INTERVAL = 1000 / SPECTRAL_FRAME_RATE; private SampleGenerator mSampleGenerator; - private boolean mReuseBuffers = true; private long mFrequency = 100000000l; /** @@ -53,7 +52,7 @@ public class TestTunerController extends TunerController int sweepRate = 0; //Hz per interval long initialToneFrequency = SAMPLE_RATE / 2 + 100; mSampleGenerator = new SampleGenerator(SAMPLE_RATE, initialToneFrequency, SAMPLE_GENERATION_INTERVAL, - sweepRate, mReuseBuffers); + sweepRate); try { @@ -66,6 +65,17 @@ public class TestTunerController extends TunerController } } + /** + * Sets the sample generator to reuse buffers instead of generating new buffers each time. Enable this setting + * when you simply want to test/profile buffer data flows for consumer processes. + * + * @param reuseBuffers set to true to turn on buffer reuse + */ + public void setReuseBuffers(boolean reuseBuffers) + { + mSampleGenerator.setReuseBuffers(reuseBuffers); + } + @Override public void addComplexBufferListener(Listener listener) { From e7f7d27a1fdd04383fd77e0e65b64028cf6f6624 Mon Sep 17 00:00:00 2001 From: Denny Date: Sat, 27 Jan 2018 04:41:01 -0500 Subject: [PATCH 47/87] Updates the Oscillator and LowPhaseNoiseOscillator classes to share a common interface and abstract class. --- .idea/dictionaries/denny.xml | 2 + .../dsheirer/audio/invert/AudioAdapter.java | 5 +- .../dsheirer/audio/invert/AudioInverter.java | 5 +- .../dsheirer/buffer/CircularBuffer.java | 172 +++++----- ...larBuffer.java => RealCircularBuffer.java} | 4 +- .../dsheirer/dsp/filter/FilterFactory.java | 47 ++- .../filter/channelizer/ChannelCalculator.java | 16 +- .../filter/channelizer/ChannelizerViewer.java | 2 +- .../ComplexPolyphaseChannelizerM2.java | 4 +- .../channelizer/PolyphaseChannelManager.java | 61 +++- .../channelizer/PolyphaseChannelSource.java | 1 + .../filter/channelizer/SynthesizerViewer.java | 303 ++++++++++++++++++ .../channelizer/TwoChannelSynthesizerM2.java | 172 ++++++++++ .../output/ChannelOutputProcessor.java | 5 +- .../IPolyphaseChannelOutputProcessor.java | 6 + .../output/OneChannelOutputProcessor.java | 6 + .../output/TwoChannelOutputProcessor.java | 67 +++- .../dsp/filter/design/FilterView.java | 287 +++++++++-------- .../dsp/filter/design/FilterViewer.java | 29 +- .../filter/fir/complex/ComplexFIRFilter.java | 161 +++++++--- .../dsp/filter/fir/real/RealFIRFilter.java | 191 ++++++----- .../PolyphaseFIRDecimatingFilter_RB.java | 11 +- .../dsp/gain/AutomaticGainControl.java | 6 +- .../gain/ComplexFeedForwardGainControl.java | 6 +- .../dsp/mixer/AbstractOscillator.java | 143 +++++++++ .../dsheirer/dsp/mixer/IOscillator.java | 95 ++++++ .../dsp/mixer/LowPhaseNoiseOscillator.java | 169 ++-------- .../github/dsheirer/dsp/mixer/Oscillator.java | 146 ++------- .../tuner/channel/CICTunerChannelSource.java | 5 +- .../tuner/channel/TunerChannelSource.java | 1 + .../source/tuner/test/SampleGenerator.java | 15 +- .../tuner/test/TestTunerController.java | 2 +- src/main/resources/FilterView.css | 6 +- 33 files changed, 1471 insertions(+), 680 deletions(-) rename src/main/java/io/github/dsheirer/buffer/{FloatCircularBuffer.java => RealCircularBuffer.java} (97%) create mode 100644 src/main/java/io/github/dsheirer/dsp/filter/channelizer/SynthesizerViewer.java create mode 100644 src/main/java/io/github/dsheirer/dsp/filter/channelizer/TwoChannelSynthesizerM2.java create mode 100644 src/main/java/io/github/dsheirer/dsp/mixer/AbstractOscillator.java create mode 100644 src/main/java/io/github/dsheirer/dsp/mixer/IOscillator.java diff --git a/.idea/dictionaries/denny.xml b/.idea/dictionaries/denny.xml index 015bac87..30d94b58 100644 --- a/.idea/dictionaries/denny.xml +++ b/.idea/dictionaries/denny.xml @@ -5,6 +5,8 @@ broadcastify esnid fleetsync + heterdyned + heterodynes icecast ident nutall diff --git a/src/main/java/io/github/dsheirer/audio/invert/AudioAdapter.java b/src/main/java/io/github/dsheirer/audio/invert/AudioAdapter.java index 090c002c..d3e1d269 100644 --- a/src/main/java/io/github/dsheirer/audio/invert/AudioAdapter.java +++ b/src/main/java/io/github/dsheirer/audio/invert/AudioAdapter.java @@ -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; diff --git a/src/main/java/io/github/dsheirer/audio/invert/AudioInverter.java b/src/main/java/io/github/dsheirer/audio/invert/AudioInverter.java index a892b23c..63b6b6d9 100644 --- a/src/main/java/io/github/dsheirer/audio/invert/AudioInverter.java +++ b/src/main/java/io/github/dsheirer/audio/invert/AudioInverter.java @@ -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(); } diff --git a/src/main/java/io/github/dsheirer/buffer/CircularBuffer.java b/src/main/java/io/github/dsheirer/buffer/CircularBuffer.java index fd92b870..86ff4b0e 100644 --- a/src/main/java/io/github/dsheirer/buffer/CircularBuffer.java +++ b/src/main/java/io/github/dsheirer/buffer/CircularBuffer.java @@ -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 + * + ******************************************************************************/ package io.github.dsheirer.buffer; import io.github.dsheirer.sample.Listener; @@ -8,87 +23,86 @@ import java.util.List; public class CircularBuffer implements Listener { - private ArrayList mElements; - private Listener mListener; - private int mPointer; - private int mSize; + private List mElements; + private Listener 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(); - } + /** + * 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(); + } - /** - * 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 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 listener ) - { - mListener = listener; - } + return null; + } + + public int getSize() + { + return mSize; + } + + public List 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 listener) + { + mListener = listener; + } } diff --git a/src/main/java/io/github/dsheirer/buffer/FloatCircularBuffer.java b/src/main/java/io/github/dsheirer/buffer/RealCircularBuffer.java similarity index 97% rename from src/main/java/io/github/dsheirer/buffer/FloatCircularBuffer.java rename to src/main/java/io/github/dsheirer/buffer/RealCircularBuffer.java index 855a7e2a..f4274c8a 100644 --- a/src/main/java/io/github/dsheirer/buffer/FloatCircularBuffer.java +++ b/src/main/java/io/github/dsheirer/buffer/RealCircularBuffer.java @@ -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]; } diff --git a/src/main/java/io/github/dsheirer/dsp/filter/FilterFactory.java b/src/main/java/io/github/dsheirer/dsp/filter/FilterFactory.java index dddf3690..bafa24fc 100644 --- a/src/main/java/io/github/dsheirer/dsp/filter/FilterFactory.java +++ b/src/main/java/io/github/dsheirer/dsp/filter/FilterFactory.java @@ -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; } diff --git a/src/main/java/io/github/dsheirer/dsp/filter/channelizer/ChannelCalculator.java b/src/main/java/io/github/dsheirer/dsp/filter/channelizer/ChannelCalculator.java index 8565d3ed..0f387d8b 100644 --- a/src/main/java/io/github/dsheirer/dsp/filter/channelizer/ChannelCalculator.java +++ b/src/main/java/io/github/dsheirer/dsp/filter/channelizer/ChannelCalculator.java @@ -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); } + } /** diff --git a/src/main/java/io/github/dsheirer/dsp/filter/channelizer/ChannelizerViewer.java b/src/main/java/io/github/dsheirer/dsp/filter/channelizer/ChannelizerViewer.java index 2a5cb42d..008e7dc2 100644 --- a/src/main/java/io/github/dsheirer/dsp/filter/channelizer/ChannelizerViewer.java +++ b/src/main/java/io/github/dsheirer/dsp/filter/channelizer/ChannelizerViewer.java @@ -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); diff --git a/src/main/java/io/github/dsheirer/dsp/filter/channelizer/ComplexPolyphaseChannelizerM2.java b/src/main/java/io/github/dsheirer/dsp/filter/channelizer/ComplexPolyphaseChannelizerM2.java index 63ec72db..6e62c8c6 100644 --- a/src/main/java/io/github/dsheirer/dsp/filter/channelizer/ComplexPolyphaseChannelizerM2.java +++ b/src/main/java/io/github/dsheirer/dsp/filter/channelizer/ComplexPolyphaseChannelizerM2.java @@ -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); diff --git a/src/main/java/io/github/dsheirer/dsp/filter/channelizer/PolyphaseChannelManager.java b/src/main/java/io/github/dsheirer/dsp/filter/channelizer/PolyphaseChannelManager.java index e0fc7eed..57de3a4e 100644 --- a/src/main/java/io/github/dsheirer/dsp/filter/channelizer/PolyphaseChannelManager.java +++ b/src/main/java/io/github/dsheirer/dsp/filter/channelizer/PolyphaseChannelManager.java @@ -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 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()); diff --git a/src/main/java/io/github/dsheirer/dsp/filter/channelizer/PolyphaseChannelSource.java b/src/main/java/io/github/dsheirer/dsp/filter/channelizer/PolyphaseChannelSource.java index 2b5145ef..3d2bcab4 100644 --- a/src/main/java/io/github/dsheirer/dsp/filter/channelizer/PolyphaseChannelSource.java +++ b/src/main/java/io/github/dsheirer/dsp/filter/channelizer/PolyphaseChannelSource.java @@ -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 diff --git a/src/main/java/io/github/dsheirer/dsp/filter/channelizer/SynthesizerViewer.java b/src/main/java/io/github/dsheirer/dsp/filter/channelizer/SynthesizerViewer.java new file mode 100644 index 00000000..9352c5eb --- /dev/null +++ b/src/main/java/io/github/dsheirer/dsp/filter/channelizer/SynthesizerViewer.java @@ -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 + * + ******************************************************************************/ +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 + { + 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 + { + 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(); + }); + } +} \ No newline at end of file diff --git a/src/main/java/io/github/dsheirer/dsp/filter/channelizer/TwoChannelSynthesizerM2.java b/src/main/java/io/github/dsheirer/dsp/filter/channelizer/TwoChannelSynthesizerM2.java new file mode 100644 index 00000000..093f682b --- /dev/null +++ b/src/main/java/io/github/dsheirer/dsp/filter/channelizer/TwoChannelSynthesizerM2.java @@ -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 + * + ******************************************************************************/ +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!!"); + } +} diff --git a/src/main/java/io/github/dsheirer/dsp/filter/channelizer/output/ChannelOutputProcessor.java b/src/main/java/io/github/dsheirer/dsp/filter/channelizer/output/ChannelOutputProcessor.java index b699edae..4afb805d 100644 --- a/src/main/java/io/github/dsheirer/dsp/filter/channelizer/output/ChannelOutputProcessor.java +++ b/src/main/java/io/github/dsheirer/dsp/filter/channelizer/output/ChannelOutputProcessor.java @@ -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; } diff --git a/src/main/java/io/github/dsheirer/dsp/filter/channelizer/output/IPolyphaseChannelOutputProcessor.java b/src/main/java/io/github/dsheirer/dsp/filter/channelizer/output/IPolyphaseChannelOutputProcessor.java index fe71eba8..916ae095 100644 --- a/src/main/java/io/github/dsheirer/dsp/filter/channelizer/output/IPolyphaseChannelOutputProcessor.java +++ b/src/main/java/io/github/dsheirer/dsp/filter/channelizer/output/IPolyphaseChannelOutputProcessor.java @@ -59,6 +59,12 @@ public interface IPolyphaseChannelOutputProcessor */ void setPolyphaseChannelIndices(List 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 diff --git a/src/main/java/io/github/dsheirer/dsp/filter/channelizer/output/OneChannelOutputProcessor.java b/src/main/java/io/github/dsheirer/dsp/filter/channelizer/output/OneChannelOutputProcessor.java index eaff4feb..e6f52d3f 100644 --- a/src/main/java/io/github/dsheirer/dsp/filter/channelizer/output/OneChannelOutputProcessor.java +++ b/src/main/java/io/github/dsheirer/dsp/filter/channelizer/output/OneChannelOutputProcessor.java @@ -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. diff --git a/src/main/java/io/github/dsheirer/dsp/filter/channelizer/output/TwoChannelOutputProcessor.java b/src/main/java/io/github/dsheirer/dsp/filter/channelizer/output/TwoChannelOutputProcessor.java index 3e904d4d..cc58584b 100644 --- a/src/main/java/io/github/dsheirer/dsp/filter/channelizer/output/TwoChannelOutputProcessor.java +++ b/src/main/java/io/github/dsheirer/dsp/filter/channelizer/output/TwoChannelOutputProcessor.java @@ -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 + * You should have received a copy of the GNU General Public License along with this program. + * If not, see * ******************************************************************************/ 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 channelIndexes) + public TwoChannelOutputProcessor(double sampleRate, List 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 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); + } } } diff --git a/src/main/java/io/github/dsheirer/dsp/filter/design/FilterView.java b/src/main/java/io/github/dsheirer/dsp/filter/design/FilterView.java index edaa4289..f0ee3b78 100644 --- a/src/main/java/io/github/dsheirer/dsp/filter/design/FilterView.java +++ b/src/main/java/io/github/dsheirer/dsp/filter/design/FilterView.java @@ -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 + * + ******************************************************************************/ 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 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( 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 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(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> list = mImpulseChart.getData(); - list.add( series ); - } - - public void setDFTTaps( float[] taps ) - { - double smallest = 0.0; - - final XYChart.Series 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( x, taps[ x ] ) ); - } - - ObservableList> 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 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(x, taps[x])); + } + + ObservableList> 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; } } diff --git a/src/main/java/io/github/dsheirer/dsp/filter/design/FilterViewer.java b/src/main/java/io/github/dsheirer/dsp/filter/design/FilterViewer.java index e2e7d9ca..b7e26b4a 100644 --- a/src/main/java/io/github/dsheirer/dsp/filter/design/FilterViewer.java +++ b/src/main/java/io/github/dsheirer/dsp/filter/design/FilterViewer.java @@ -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; } } diff --git a/src/main/java/io/github/dsheirer/dsp/filter/fir/complex/ComplexFIRFilter.java b/src/main/java/io/github/dsheirer/dsp/filter/fir/complex/ComplexFIRFilter.java index 8190b9a1..11e6955e 100644 --- a/src/main/java/io/github/dsheirer/dsp/filter/fir/complex/ComplexFIRFilter.java +++ b/src/main/java/io/github/dsheirer/dsp/filter/fir/complex/ComplexFIRFilter.java @@ -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 + * + ******************************************************************************/ 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(); + } } diff --git a/src/main/java/io/github/dsheirer/dsp/filter/fir/real/RealFIRFilter.java b/src/main/java/io/github/dsheirer/dsp/filter/fir/real/RealFIRFilter.java index d582dfa1..ec019bdc 100644 --- a/src/main/java/io/github/dsheirer/dsp/filter/fir/real/RealFIRFilter.java +++ b/src/main/java/io/github/dsheirer/dsp/filter/fir/real/RealFIRFilter.java @@ -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 + * 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 + * ******************************************************************************/ 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; + } + } + } } \ No newline at end of file diff --git a/src/main/java/io/github/dsheirer/dsp/filter/polyphase/PolyphaseFIRDecimatingFilter_RB.java b/src/main/java/io/github/dsheirer/dsp/filter/polyphase/PolyphaseFIRDecimatingFilter_RB.java index 0012b2c9..34a12cb7 100644 --- a/src/main/java/io/github/dsheirer/dsp/filter/polyphase/PolyphaseFIRDecimatingFilter_RB.java +++ b/src/main/java/io/github/dsheirer/dsp/filter/polyphase/PolyphaseFIRDecimatingFilter_RB.java @@ -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); } diff --git a/src/main/java/io/github/dsheirer/dsp/gain/AutomaticGainControl.java b/src/main/java/io/github/dsheirer/dsp/gain/AutomaticGainControl.java index 77e4d7f7..4d2ba36d 100644 --- a/src/main/java/io/github/dsheirer/dsp/gain/AutomaticGainControl.java +++ b/src/main/java/io/github/dsheirer/dsp/gain/AutomaticGainControl.java @@ -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 ) ); diff --git a/src/main/java/io/github/dsheirer/dsp/gain/ComplexFeedForwardGainControl.java b/src/main/java/io/github/dsheirer/dsp/gain/ComplexFeedForwardGainControl.java index d1ca98a1..4334e214 100644 --- a/src/main/java/io/github/dsheirer/dsp/gain/ComplexFeedForwardGainControl.java +++ b/src/main/java/io/github/dsheirer/dsp/gain/ComplexFeedForwardGainControl.java @@ -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() diff --git a/src/main/java/io/github/dsheirer/dsp/mixer/AbstractOscillator.java b/src/main/java/io/github/dsheirer/dsp/mixer/AbstractOscillator.java new file mode 100644 index 00000000..6969ad78 --- /dev/null +++ b/src/main/java/io/github/dsheirer/dsp/mixer/AbstractOscillator.java @@ -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 + * + ******************************************************************************/ +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; + } +} diff --git a/src/main/java/io/github/dsheirer/dsp/mixer/IOscillator.java b/src/main/java/io/github/dsheirer/dsp/mixer/IOscillator.java new file mode 100644 index 00000000..b9e23c08 --- /dev/null +++ b/src/main/java/io/github/dsheirer/dsp/mixer/IOscillator.java @@ -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 + * + ******************************************************************************/ +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); +} diff --git a/src/main/java/io/github/dsheirer/dsp/mixer/LowPhaseNoiseOscillator.java b/src/main/java/io/github/dsheirer/dsp/mixer/LowPhaseNoiseOscillator.java index bc33df62..86d4b786 100644 --- a/src/main/java/io/github/dsheirer/dsp/mixer/LowPhaseNoiseOscillator.java +++ b/src/main/java/io/github/dsheirer/dsp/mixer/LowPhaseNoiseOscillator.java @@ -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 + * You should have received a copy of the GNU General Public License along with this program. + * If not, see * ******************************************************************************/ 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"); - - } } diff --git a/src/main/java/io/github/dsheirer/dsp/mixer/Oscillator.java b/src/main/java/io/github/dsheirer/dsp/mixer/Oscillator.java index a5354ded..3e87a0ba 100644 --- a/src/main/java/io/github/dsheirer/dsp/mixer/Oscillator.java +++ b/src/main/java/io/github/dsheirer/dsp/mixer/Oscillator.java @@ -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 + * You should have received a copy of the GNU General Public License along with this program. + * If not, see * ******************************************************************************/ 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); } } diff --git a/src/main/java/io/github/dsheirer/source/tuner/channel/CICTunerChannelSource.java b/src/main/java/io/github/dsheirer/source/tuner/channel/CICTunerChannelSource.java index 0e3625bd..ddd74ab4 100644 --- a/src/main/java/io/github/dsheirer/source/tuner/channel/CICTunerChannelSource.java +++ b/src/main/java/io/github/dsheirer/source/tuner/channel/CICTunerChannelSource.java @@ -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 mBuffer; - private Oscillator mFrequencyCorrectionMixer; + private IOscillator mFrequencyCorrectionMixer; private ComplexPrimeCICDecimate mDecimationFilter; private Listener mListener; private List mSampleBuffers = new ArrayList(); diff --git a/src/main/java/io/github/dsheirer/source/tuner/channel/TunerChannelSource.java b/src/main/java/io/github/dsheirer/source/tuner/channel/TunerChannelSource.java index 217f7409..7137b611 100644 --- a/src/main/java/io/github/dsheirer/source/tuner/channel/TunerChannelSource.java +++ b/src/main/java/io/github/dsheirer/source/tuner/channel/TunerChannelSource.java @@ -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: diff --git a/src/main/java/io/github/dsheirer/source/tuner/test/SampleGenerator.java b/src/main/java/io/github/dsheirer/source/tuner/test/SampleGenerator.java index 76e7642b..b4a09a40 100644 --- a/src/main/java/io/github/dsheirer/source/tuner/test/SampleGenerator.java +++ b/src/main/java/io/github/dsheirer/source/tuner/test/SampleGenerator.java @@ -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 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) { diff --git a/src/main/java/io/github/dsheirer/source/tuner/test/TestTunerController.java b/src/main/java/io/github/dsheirer/source/tuner/test/TestTunerController.java index e8f980ba..bedb5bfd 100644 --- a/src/main/java/io/github/dsheirer/source/tuner/test/TestTunerController.java +++ b/src/main/java/io/github/dsheirer/source/tuner/test/TestTunerController.java @@ -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; diff --git a/src/main/resources/FilterView.css b/src/main/resources/FilterView.css index eed6bc12..3577be48 100644 --- a/src/main/resources/FilterView.css +++ b/src/main/resources/FilterView.css @@ -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%; From 5a03a0038ce0d0cadb09295a6db74037900a4546 Mon Sep 17 00:00:00 2001 From: Denny Date: Sat, 27 Jan 2018 15:37:26 -0500 Subject: [PATCH 48/87] WIP - Adds FS/4 up and down converter classes. --- .../filter/channelizer/ChannelizerViewer.java | 177 ++++++++++++++++-- .../ComplexPolyphaseChannelizerM2.java | 6 +- .../channelizer/PolyphaseChannelManager.java | 29 ++- .../filter/channelizer/SynthesizerViewer.java | 11 ++ .../output/ChannelOutputProcessor.java | 4 +- .../output/TwoChannelOutputProcessor.java | 10 +- .../dsp/filter/dc/BufferDCFilter.java | 70 +++++++ .../dsp/filter/dc/DCRemovalFilter_CB.java | 71 ------- .../dsp/filter/dc/DCRemovalFilter_R_R.java | 54 ------ .../dsp/mixer/AbstractOscillator.java | 4 + .../dsheirer/dsp/mixer/FS4DownConverter.java | 73 ++++++++ .../dsheirer/dsp/mixer/FS4UpConverter.java | 73 ++++++++ .../dsp/mixer/LowPhaseNoiseOscillator.java | 27 ++- .../tuner/manager/ChannelSourceManager.java | 2 + .../manager/TunerChannelSourceManager.java | 7 + .../dsheirer/spectrum/DFTProcessor.java | 10 + 16 files changed, 477 insertions(+), 151 deletions(-) create mode 100644 src/main/java/io/github/dsheirer/dsp/filter/dc/BufferDCFilter.java delete mode 100644 src/main/java/io/github/dsheirer/dsp/filter/dc/DCRemovalFilter_CB.java delete mode 100644 src/main/java/io/github/dsheirer/dsp/filter/dc/DCRemovalFilter_R_R.java create mode 100644 src/main/java/io/github/dsheirer/dsp/mixer/FS4DownConverter.java create mode 100644 src/main/java/io/github/dsheirer/dsp/mixer/FS4UpConverter.java diff --git a/src/main/java/io/github/dsheirer/dsp/filter/channelizer/ChannelizerViewer.java b/src/main/java/io/github/dsheirer/dsp/filter/channelizer/ChannelizerViewer.java index 008e7dc2..fba3a9da 100644 --- a/src/main/java/io/github/dsheirer/dsp/filter/channelizer/ChannelizerViewer.java +++ b/src/main/java/io/github/dsheirer/dsp/filter/channelizer/ChannelizerViewer.java @@ -40,12 +40,16 @@ import javax.swing.JFrame; import javax.swing.JLabel; import javax.swing.JPanel; import javax.swing.JSpinner; +import javax.swing.JToggleButton; import javax.swing.SpinnerNumberModel; import javax.swing.event.ChangeEvent; import javax.swing.event.ChangeListener; import java.awt.Dimension; import java.awt.EventQueue; +import java.awt.event.ActionEvent; +import java.awt.event.ActionListener; import java.util.ArrayList; +import java.util.Arrays; import java.util.List; public class ChannelizerViewer extends JFrame @@ -61,6 +65,7 @@ public class ChannelizerViewer extends JFrame private JLabel mToneFrequencyLabel; private PrimarySpectrumPanel mPrimarySpectrumPanel; private ChannelArrayPanel mChannelPanel; + private DiscreteIndexChannelArrayPanel mDiscreteIndexChannelPanel; private int mChannelCount; private int mChannelsPerRow; private long mBaseFrequency = 100000000; //100 MHz @@ -98,10 +103,11 @@ 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][grow,fill]")); mPrimaryPanel.add(getSpectrumPanel(), "wrap"); mPrimaryPanel.add(getControlPanel(), "wrap"); - mPrimaryPanel.add(getChannelArrayPanel()); + mPrimaryPanel.add(getChannelArrayPanel(), "wrap"); + mPrimaryPanel.add(getDiscreteIndexChannelPanel()); } return mPrimaryPanel; @@ -177,6 +183,16 @@ public class ChannelizerViewer extends JFrame return mChannelPanel; } + private DiscreteIndexChannelArrayPanel getDiscreteIndexChannelPanel() + { + if(mDiscreteIndexChannelPanel == null) + { + mDiscreteIndexChannelPanel = new DiscreteIndexChannelArrayPanel(); + } + + return mDiscreteIndexChannelPanel; + } + public class ChannelArrayPanel extends JPanel { public ChannelArrayPanel() @@ -222,6 +238,41 @@ public class ChannelizerViewer extends JFrame } } + public class DiscreteIndexChannelArrayPanel extends JPanel + { + public DiscreteIndexChannelArrayPanel() + { + int bufferSize = CHANNEL_BANDWIDTH / CHANNEL_FFT_FRAME_RATE; + if(bufferSize % 2 == 1) + { + bufferSize++; + } + + init(); + } + + private void init() + { + setLayout(new MigLayout("insets 0 0 0 0", "fill", "fill")); + + for(int x = 0; x < mChannelCount; x++) + { + TunerChannelSource source = mTestTuner.getChannelSourceManager().getSpecialSource(x); + DiscreteChannelPanel channelPanel = new DiscreteChannelPanel(mSettingsManager, source, x); + channelPanel.setDFTSize(mChannelPanelDFTSize); + + if(x % mChannelsPerRow == mChannelsPerRow - 1) + { + add(channelPanel, "grow,push,wrap 2px"); + } + else + { + add(channelPanel, "grow,push"); + } + } + } + } + /** * Returns a list of tuner channels that will fit within the tuner's bandwidth, minus a half channel each at the * lower and upper ends of the spectrum. @@ -291,18 +342,30 @@ public class ChannelizerViewer extends JFrame private DFTProcessor mDFTProcessor = new DFTProcessor(SampleType.COMPLEX); private ComplexDecibelConverter mComplexDecibelConverter = new ComplexDecibelConverter(); private SpectrumPanel mSpectrumPanel; + private JToggleButton mLoggingButton; + private boolean mLoggingEnabled; public ChannelPanel(SettingsManager settingsManager, double sampleRate, long frequency, int bandwidth, boolean enableLogging) { - setLayout(new MigLayout("insets 0 0 0 0", "[center,grow,fill]", "[grow,fill][]")); + setLayout(new MigLayout("insets 0 0 0 0", "[center,grow,fill][]", "[grow,fill][]")); mSpectrumPanel = new SpectrumPanel(settingsManager); mSpectrumPanel.setSampleSize(28); - add(mSpectrumPanel, "wrap"); + add(mSpectrumPanel, "span"); add(new JLabel("Center:" + frequency)); + mLoggingButton = new JToggleButton("Logging"); + mLoggingButton.addActionListener(new ActionListener() + { + @Override + public void actionPerformed(ActionEvent e) + { + mLoggingEnabled = mLoggingButton.isSelected(); + } + }); + add(mLoggingButton); + mDFTProcessor.addConverter(mComplexDecibelConverter); mDFTProcessor.process(SourceEvent.sampleRateChange(sampleRate)); - mComplexDecibelConverter.addListener(mSpectrumPanel); TunerChannel tunerChannel = new TunerChannel(frequency, bandwidth); @@ -310,21 +373,19 @@ public class ChannelizerViewer extends JFrame if(mSource != null) { - if(enableLogging) + mSource.setListener(new Listener() { - mSource.setListener(new Listener() + @Override + public void receive(ComplexBuffer complexBuffer) { - @Override - public void receive(ComplexBuffer complexBuffer) + if(mLoggingEnabled) { - mDFTProcessor.receive(complexBuffer); + mLog.debug("Samples:" + Arrays.toString(complexBuffer.getSamples())); } - }); - } - else - { - mSource.setListener(mDFTProcessor); - } + + mDFTProcessor.receive(complexBuffer); + } + }); mSource.start(); } @@ -358,13 +419,95 @@ public class ChannelizerViewer extends JFrame } } + public class DiscreteChannelPanel extends JPanel implements Listener, ISourceEventProcessor + { + private TunerChannelSource mSource; + private DFTProcessor mDFTProcessor = new DFTProcessor(SampleType.COMPLEX); + private ComplexDecibelConverter mComplexDecibelConverter = new ComplexDecibelConverter(); + private SpectrumPanel mSpectrumPanel; + private JToggleButton mLoggingButton; + private boolean mLoggingEnabled; + + public DiscreteChannelPanel(SettingsManager settingsManager, TunerChannelSource source, int index) + { + mSource = source; + setLayout(new MigLayout("insets 0 0 0 0", "[center,grow,fill][]", "[grow,fill][]")); + mSpectrumPanel = new SpectrumPanel(settingsManager); + mSpectrumPanel.setSampleSize(28); + add(mSpectrumPanel, "span"); + add(new JLabel("Index:" + index)); + + mLoggingButton = new JToggleButton("Logging"); + mLoggingButton.addActionListener(new ActionListener() + { + @Override + public void actionPerformed(ActionEvent e) + { + mLoggingEnabled = mLoggingButton.isSelected(); + } + }); + add(mLoggingButton); + + mDFTProcessor.addConverter(mComplexDecibelConverter); + mDFTProcessor.process(SourceEvent.sampleRateChange(25000.0)); + mComplexDecibelConverter.addListener(mSpectrumPanel); + + if(mSource != null) + { + mSource.setListener(new Listener() + { + @Override + public void receive(ComplexBuffer complexBuffer) + { + if(mLoggingEnabled) + { + mLog.debug("Samples:" + Arrays.toString(complexBuffer.getSamples())); + } + + mDFTProcessor.receive(complexBuffer); + } + }); + + mSource.start(); + } + else + { + mLog.error("Couldn't get a source from the tuner for index: " + index); + } + } + + public TunerChannelSource getSource() + { + return mSource; + } + + public void setDFTSize(DFTSize dftSize) + { + mDFTProcessor.setDFTSize(dftSize); + } + + @Override + public void receive(ComplexBuffer complexBuffer) + { + mLog.debug("Got a buffer for the channel dft processor"); + mDFTProcessor.receive(complexBuffer); + } + + @Override + public void process(SourceEvent event) throws SourceException + { + mLog.debug("Source Event! Add handler support for this to channelizer viewer"); + } + } + + public static void main(String[] args) { boolean useGUI = true; if(useGUI) { - int channelsPerRow = 4; + int channelsPerRow = 8; final ChannelizerViewer frame = new ChannelizerViewer(channelsPerRow); diff --git a/src/main/java/io/github/dsheirer/dsp/filter/channelizer/ComplexPolyphaseChannelizerM2.java b/src/main/java/io/github/dsheirer/dsp/filter/channelizer/ComplexPolyphaseChannelizerM2.java index 6e62c8c6..662bbdbc 100644 --- a/src/main/java/io/github/dsheirer/dsp/filter/channelizer/ComplexPolyphaseChannelizerM2.java +++ b/src/main/java/io/github/dsheirer/dsp/filter/channelizer/ComplexPolyphaseChannelizerM2.java @@ -197,7 +197,8 @@ public class ComplexPolyphaseChannelizerM2 extends AbstractComplexPolyphaseChann int half = getChannelCount() / 2; - int outputOffset = getChannelCount() - half - 1; +// int outputOffset = getChannelCount() - half - 1; + int outputOffset = getChannelCount() - 1; int outputIndex; int sampleIndex; int filterIndex; @@ -214,7 +215,8 @@ public class ComplexPolyphaseChannelizerM2 extends AbstractComplexPolyphaseChann //The second half of the sample buffer was loaded using the previous column pointer - adjust for that now sampleIndexMap = mSampleIndexMap[(mColumnPointer == (mSamples.length - 1) ? 0 : mColumnPointer + 1)]; - outputOffset = getChannelCount() + half - 1; +// outputOffset = getChannelCount() + half - 1; + outputOffset = getChannelCount() - 1; for(int channel = half; channel < getChannelCount(); channel++) { diff --git a/src/main/java/io/github/dsheirer/dsp/filter/channelizer/PolyphaseChannelManager.java b/src/main/java/io/github/dsheirer/dsp/filter/channelizer/PolyphaseChannelManager.java index 57de3a4e..cae8b91a 100644 --- a/src/main/java/io/github/dsheirer/dsp/filter/channelizer/PolyphaseChannelManager.java +++ b/src/main/java/io/github/dsheirer/dsp/filter/channelizer/PolyphaseChannelManager.java @@ -34,6 +34,7 @@ import io.github.dsheirer.source.tuner.channel.TunerChannelSource; import org.slf4j.Logger; import org.slf4j.LoggerFactory; +import java.util.ArrayList; import java.util.HashMap; import java.util.List; import java.util.Map; @@ -158,6 +159,32 @@ public class PolyphaseChannelManager implements ISourceEventProcessor return channelSource; } + public TunerChannelSource getSpecialChannel(int index) + { + PolyphaseChannelSource channelSource = null; + + try + { + List polyphaseIndexes = new ArrayList<>(); + polyphaseIndexes.add(index); + + IPolyphaseChannelOutputProcessor outputProcessor = getOutputProcessor(polyphaseIndexes); + + TunerChannel tunerChannel = new TunerChannel(100000000, 12500); + + if(outputProcessor != null) + { + channelSource = new PolyphaseChannelSource(tunerChannel, outputProcessor, mChannelSourceEventListener, + mChannelCalculator.getChannelSampleRate()); + } + } + catch(IllegalArgumentException iae) + { + } + + return channelSource; + } + /** * Creates a processor to process the channelizer channel indexes into a composite output stream providing * channelized complex sample buffers to a registered source listener. @@ -456,7 +483,7 @@ public class PolyphaseChannelManager implements ISourceEventProcessor if(taps == null) { - taps = FilterFactory.getSincM2Synthesizer(mChannelCalculator.getChannelBandwidth(), channels, + taps = FilterFactory.getSincM2Synthesizer(mChannelCalculator.getChannelSampleRate(), channels, POLYPHASE_FILTER_TAPS_PER_CHANNEL, Window.WindowType.BLACKMAN_HARRIS_7, true); mOutputProcessorFilters.put(channels, taps); diff --git a/src/main/java/io/github/dsheirer/dsp/filter/channelizer/SynthesizerViewer.java b/src/main/java/io/github/dsheirer/dsp/filter/channelizer/SynthesizerViewer.java index 9352c5eb..33e728bd 100644 --- a/src/main/java/io/github/dsheirer/dsp/filter/channelizer/SynthesizerViewer.java +++ b/src/main/java/io/github/dsheirer/dsp/filter/channelizer/SynthesizerViewer.java @@ -23,6 +23,7 @@ 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.sample.real.IOverflowListener; import io.github.dsheirer.settings.SettingsManager; import io.github.dsheirer.source.SourceEvent; import io.github.dsheirer.source.tuner.configuration.TunerConfigurationModel; @@ -155,6 +156,15 @@ public class SynthesizerViewer extends JFrame mDFTProcessor.process(SourceEvent.sampleRateChange(CHANNEL_SAMPLE_RATE)); mDFTProcessor.setFrameRate(CHANNEL_FFT_FRAME_RATE); mComplexDecibelConverter.addListener(mSpectrumPanel); + + mDFTProcessor.setOverflowListener(new IOverflowListener() + { + @Override + public void sourceOverflow(boolean overflow) + { + mLog.debug("Buffer " + (overflow ? "overflow" : "reset")); + } + }); } public void setDFTSize(DFTSize dftSize) @@ -174,6 +184,7 @@ public class SynthesizerViewer extends JFrame private DFTProcessor mDFTProcessor = new DFTProcessor(SampleType.COMPLEX); private ComplexDecibelConverter mComplexDecibelConverter = new ComplexDecibelConverter(); private SpectrumPanel mSpectrumPanel; + private boolean mLoggingEnabled = false; public ChannelPanel(SettingsManager settingsManager, ChannelControlPanel channelControlPanel) { diff --git a/src/main/java/io/github/dsheirer/dsp/filter/channelizer/output/ChannelOutputProcessor.java b/src/main/java/io/github/dsheirer/dsp/filter/channelizer/output/ChannelOutputProcessor.java index 4afb805d..f280efbb 100644 --- a/src/main/java/io/github/dsheirer/dsp/filter/channelizer/output/ChannelOutputProcessor.java +++ b/src/main/java/io/github/dsheirer/dsp/filter/channelizer/output/ChannelOutputProcessor.java @@ -68,7 +68,9 @@ public abstract class ChannelOutputProcessor implements IPolyphaseChannelOutputP @Override public void setFrequencyOffset(long frequencyOffset) { - mFrequencyCorrectionMixer.setFrequency(frequencyOffset); + long offset = frequencyOffset; + mLog.debug("Setting frequency offset to: " + offset); +// mFrequencyCorrectionMixer.setFrequency(offset); mFrequencyCorrectionEnabled = (frequencyOffset != 0); } diff --git a/src/main/java/io/github/dsheirer/dsp/filter/channelizer/output/TwoChannelOutputProcessor.java b/src/main/java/io/github/dsheirer/dsp/filter/channelizer/output/TwoChannelOutputProcessor.java index cc58584b..1ca55983 100644 --- a/src/main/java/io/github/dsheirer/dsp/filter/channelizer/output/TwoChannelOutputProcessor.java +++ b/src/main/java/io/github/dsheirer/dsp/filter/channelizer/output/TwoChannelOutputProcessor.java @@ -19,6 +19,7 @@ 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.dsp.mixer.FS4DownConverter; import io.github.dsheirer.sample.complex.TimestampedBufferAssembler; import org.slf4j.Logger; import org.slf4j.LoggerFactory; @@ -30,6 +31,7 @@ public class TwoChannelOutputProcessor extends ChannelOutputProcessor private ComplexFIRFilter mLowPassFilter = new ComplexFIRFilter(Filters.HALF_BAND_FILTER_47T.getCoefficients(), 1.0f); private TwoChannelSynthesizerM2 mSynthesizer; + private FS4DownConverter mFS4DownConverter = new FS4DownConverter(); private final static Logger mLog = LoggerFactory.getLogger(TwoChannelOutputProcessor.class); @@ -59,8 +61,7 @@ public class TwoChannelOutputProcessor extends ChannelOutputProcessor @Override public void setFrequencyOffset(long frequencyOffset) { -// super.setFrequencyOffset(frequencyOffset); - mLog.debug("**Ignoring** Setting frequency offset [" + frequencyOffset + "] for channel offsets [" + mChannelOffset1 + "," + mChannelOffset2 + "]"); + super.setFrequencyOffset(frequencyOffset); } @@ -109,8 +110,11 @@ public class TwoChannelOutputProcessor extends ChannelOutputProcessor //Join the two channels using the synthesizer float[] synthesized = mSynthesizer.process(channel1, channel2); + //The synthesized channels are centered at +FS/4 ... downconvert to center the spectrum + mFS4DownConverter.mixComplex(synthesized); + //Apply frequency correction to center the signal of interest within the synthesized channel - getFrequencyCorrectionMixer().mixComplex(synthesized); +// getFrequencyCorrectionMixer().mixComplex(synthesized); //Apply low-pass filtering to attenuate neighboring channel(s) // synthesized = mLowPassFilter.filter(synthesized); diff --git a/src/main/java/io/github/dsheirer/dsp/filter/dc/BufferDCFilter.java b/src/main/java/io/github/dsheirer/dsp/filter/dc/BufferDCFilter.java new file mode 100644 index 00000000..bf229d4a --- /dev/null +++ b/src/main/java/io/github/dsheirer/dsp/filter/dc/BufferDCFilter.java @@ -0,0 +1,70 @@ +/******************************************************************************* + * 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 + * + ******************************************************************************/ +package io.github.dsheirer.dsp.filter.dc; + +import org.slf4j.Logger; +import org.slf4j.LoggerFactory; + +public class BufferDCFilter +{ + private final static Logger mLog = LoggerFactory.getLogger(BufferDCFilter.class); + + /** + * Calculates the DC offset present in the samples and subtracts the offset from all samples. + */ + public static void filterReal(float[] samples) + { + double accumulator = 0.0; + + for(int x = 0; x < samples.length; x++) + { + accumulator += samples[x]; + } + + float adjustment = (float)(accumulator / (double)samples.length); + + for(int x = 0; x < samples.length; x++) + { + samples[x] = samples[x] - adjustment; + } + } + + /** + * Calculates the DC offset present in the samples and subtracts the offset from all samples. + */ + public static void filterComplex(float[] samples) + { + double iAccumulator = 0.0; + double qAccumulator = 0.0; + + for(int x = 0; x < samples.length; x += 2) + { + iAccumulator += samples[x]; + qAccumulator += samples[x + 1]; + } + + double half = (double)samples.length / 2.0; + + float iAdjustment = (float)(iAccumulator / half); + float qAdjustment = (float)(qAccumulator / half); + + for(int x = 0; x < samples.length; x += 2) + { + samples[x] = samples[x] - iAdjustment; + samples[x + 1] = samples[x + 1] - qAdjustment; + } + } +} diff --git a/src/main/java/io/github/dsheirer/dsp/filter/dc/DCRemovalFilter_CB.java b/src/main/java/io/github/dsheirer/dsp/filter/dc/DCRemovalFilter_CB.java deleted file mode 100644 index 28deb686..00000000 --- a/src/main/java/io/github/dsheirer/dsp/filter/dc/DCRemovalFilter_CB.java +++ /dev/null @@ -1,71 +0,0 @@ -/******************************************************************************* - * SDR Trunk - * Copyright (C) 2014 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 io.github.dsheirer.dsp.filter.dc; - -import io.github.dsheirer.sample.Listener; -import io.github.dsheirer.sample.complex.ComplexBuffer; - -public class DCRemovalFilter_CB implements Listener -{ - private Listener mListener; - private DCRemovalFilter mIFilter; - private DCRemovalFilter mQFilter; - - public DCRemovalFilter_CB( float ratio ) - { - mIFilter = new DCRemovalFilter( ratio ); - mQFilter = new DCRemovalFilter( ratio ); - } - - public void dispose() - { - mListener = null; - } - - public float[] filter( float[] samples ) - { - for( int x = 0; x < samples.length; x += 2 ) - { - samples[ x ] = mIFilter.filter( samples[ x ] ); - samples[ x + 1 ] = mQFilter.filter( samples[ x + 1 ] ); - } - - return samples; - } - - public ComplexBuffer filter( ComplexBuffer buffer ) - { - filter( buffer.getSamples() ); - - return buffer; - } - - @Override - public void receive( ComplexBuffer buffer ) - { - if( mListener != null ) - { - mListener.receive( filter( buffer ) ); - } - } - - public void setListener( Listener listener ) - { - mListener = listener; - } -} diff --git a/src/main/java/io/github/dsheirer/dsp/filter/dc/DCRemovalFilter_R_R.java b/src/main/java/io/github/dsheirer/dsp/filter/dc/DCRemovalFilter_R_R.java deleted file mode 100644 index ee5c8c53..00000000 --- a/src/main/java/io/github/dsheirer/dsp/filter/dc/DCRemovalFilter_R_R.java +++ /dev/null @@ -1,54 +0,0 @@ -/******************************************************************************* - * SDR Trunk - * Copyright (C) 2014 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 io.github.dsheirer.dsp.filter.dc; - -import io.github.dsheirer.sample.real.RealSampleListener; - -public class DCRemovalFilter_R_R extends DCRemovalFilter implements RealSampleListener -{ - RealSampleListener mListener; - - public DCRemovalFilter_R_R( float ratio ) - { - super( ratio ); - } - - public void dispose() - { - mListener = null; - } - - @Override - public void receive( float sample ) - { - if( mListener != null ) - { - mListener.receive( filter( sample ) ); - } - } - - public void setListener( RealSampleListener listener ) - { - mListener = listener; - } - - public void removeListener() - { - mListener = null; - } -} diff --git a/src/main/java/io/github/dsheirer/dsp/mixer/AbstractOscillator.java b/src/main/java/io/github/dsheirer/dsp/mixer/AbstractOscillator.java index 6969ad78..b0432b2e 100644 --- a/src/main/java/io/github/dsheirer/dsp/mixer/AbstractOscillator.java +++ b/src/main/java/io/github/dsheirer/dsp/mixer/AbstractOscillator.java @@ -79,6 +79,10 @@ public abstract class AbstractOscillator implements IOscillator return mEnabled; } + /** + * Commands sub-class implementations to update internal structures to account for changes in frequency or + * sample rate. The boolean reset indicator optionally commands a reset of the current I/Q values. + */ protected abstract void update(); /** diff --git a/src/main/java/io/github/dsheirer/dsp/mixer/FS4DownConverter.java b/src/main/java/io/github/dsheirer/dsp/mixer/FS4DownConverter.java new file mode 100644 index 00000000..5c70593e --- /dev/null +++ b/src/main/java/io/github/dsheirer/dsp/mixer/FS4DownConverter.java @@ -0,0 +1,73 @@ +/******************************************************************************* + * 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 + * + ******************************************************************************/ +package io.github.dsheirer.dsp.mixer; + +import org.slf4j.Logger; +import org.slf4j.LoggerFactory; + +public class FS4DownConverter +{ + private final static Logger mLog = LoggerFactory.getLogger(FS4DownConverter.class); + + private int mPointer = 0; + + /** + * Performs complex down-sampling by one-fourth (-FS/4) of the sample rate using simple value assignments. + * + * See: Digital Signal Processing 3e, Lyons, p.674-675 + */ + public FS4DownConverter() + { + } + + public float[] mixComplex(float[] samples) + { + float real; + + for(int x = 0; x < samples.length; x += 2) + { + switch(mPointer) + { + case 0: + //no-op + break; + case 1: + real = samples[x]; + samples[x] = samples[x + 1]; + samples[x + 1] = -real; + break; + case 2: + samples[x] = -samples[x]; + samples[x + 1] = -samples[x + 1]; + break; + case 3: + real = samples[x]; + samples[x] = -samples[x + 1]; + samples[x + 1] = real; + break; + } + + mPointer++; + + if(mPointer >= 4) + { + mPointer = 0; + } + } + + return samples; + } +} diff --git a/src/main/java/io/github/dsheirer/dsp/mixer/FS4UpConverter.java b/src/main/java/io/github/dsheirer/dsp/mixer/FS4UpConverter.java new file mode 100644 index 00000000..c57abaaa --- /dev/null +++ b/src/main/java/io/github/dsheirer/dsp/mixer/FS4UpConverter.java @@ -0,0 +1,73 @@ +/******************************************************************************* + * 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 + * + ******************************************************************************/ +package io.github.dsheirer.dsp.mixer; + +import org.slf4j.Logger; +import org.slf4j.LoggerFactory; + +public class FS4UpConverter +{ + private final static Logger mLog = LoggerFactory.getLogger(FS4UpConverter.class); + + private int mPointer = 0; + + /** + * Performs complex up-sampling by one-fourth (+FS/4) of the sample rate using simple value assignments. + * + * See: Digital Signal Processing 3e, Lyons, p.674-675 + */ + public FS4UpConverter() + { + } + + public float[] mixComplex(float[] samples) + { + float real; + + for(int x = 0; x < samples.length; x += 2) + { + switch(mPointer) + { + case 0: + //no-op + break; + case 1: + real = samples[x]; + samples[x] = -samples[x + 1]; + samples[x + 1] = real; + break; + case 2: + samples[x] = -samples[x]; + samples[x + 1] = -samples[x + 1]; + break; + case 3: + real = samples[x]; + samples[x] = samples[x + 1]; + samples[x + 1] = -real; + break; + } + + mPointer++; + + if(mPointer >= 4) + { + mPointer = 0; + } + } + + return samples; + } +} diff --git a/src/main/java/io/github/dsheirer/dsp/mixer/LowPhaseNoiseOscillator.java b/src/main/java/io/github/dsheirer/dsp/mixer/LowPhaseNoiseOscillator.java index 86d4b786..76171c27 100644 --- a/src/main/java/io/github/dsheirer/dsp/mixer/LowPhaseNoiseOscillator.java +++ b/src/main/java/io/github/dsheirer/dsp/mixer/LowPhaseNoiseOscillator.java @@ -31,6 +31,7 @@ public class LowPhaseNoiseOscillator extends AbstractOscillator private double mCosineAngle; private double mSineAngle; private double mGain = 1.0; + private boolean mReset; /** * Ultra-low phase noise complex oscillator as described in Digital Signal Processing 3e, Lyons, p.786 @@ -45,6 +46,19 @@ public class LowPhaseNoiseOscillator extends AbstractOscillator update(); } + /** + * Sets the frequency of the tone generated by this oscillator. + */ + @Override + public void setFrequency(double frequency) + { + //This oscillator does not recover when the current frequency is equal to exactly half the sample rate, so we + //have to set a flag to reset the I/Q structures when changing the frequency from exactly half the sample rate. + mReset = getFrequency() < 0.01 || Math.abs(getFrequency()) - (getSampleRate() / 2) < 1.0; + + super.setFrequency(frequency); + } + @Override public float quadrature() { @@ -64,8 +78,17 @@ public class LowPhaseNoiseOscillator extends AbstractOscillator { double anglePerSample = TWO_PI * getFrequency() / getSampleRate(); - mCosineAngle = (float)Math.cos(anglePerSample); - mSineAngle = (float)Math.sin(anglePerSample); + mCosineAngle = Math.cos(anglePerSample); + mSineAngle = Math.sin(anglePerSample); + + if(mReset) + { + mInphase = 1.0; + mQuadrature = 0.0; + mPreviousInphase = 1.0; + mPreviousQuadrature = 0.0; + mReset = false; + } } /** diff --git a/src/main/java/io/github/dsheirer/source/tuner/manager/ChannelSourceManager.java b/src/main/java/io/github/dsheirer/source/tuner/manager/ChannelSourceManager.java index 3a3550df..9280ac4b 100644 --- a/src/main/java/io/github/dsheirer/source/tuner/manager/ChannelSourceManager.java +++ b/src/main/java/io/github/dsheirer/source/tuner/manager/ChannelSourceManager.java @@ -54,6 +54,8 @@ public abstract class ChannelSourceManager implements ISourceEventProcessor */ public abstract TunerChannelSource getSource(TunerChannel tunerChannel); + public abstract TunerChannelSource getSpecialSource(int index); + /** * Adds a listener to receive source events */ diff --git a/src/main/java/io/github/dsheirer/source/tuner/manager/TunerChannelSourceManager.java b/src/main/java/io/github/dsheirer/source/tuner/manager/TunerChannelSourceManager.java index e6159a0b..510aba97 100644 --- a/src/main/java/io/github/dsheirer/source/tuner/manager/TunerChannelSourceManager.java +++ b/src/main/java/io/github/dsheirer/source/tuner/manager/TunerChannelSourceManager.java @@ -49,6 +49,13 @@ public class TunerChannelSourceManager extends ChannelSourceManager mTunerController.addListener(mPolyphaseChannelManager); } + @Override + public TunerChannelSource getSpecialSource(int index) + { + //TODO: remove this debug method + return mPolyphaseChannelManager.getSpecialChannel(index); + } + /** * Indicates if the channel min/max frequencies are within the tunable frequency range of the tuner controller * diff --git a/src/main/java/io/github/dsheirer/spectrum/DFTProcessor.java b/src/main/java/io/github/dsheirer/spectrum/DFTProcessor.java index 01b3ee1f..fa0cb3f3 100644 --- a/src/main/java/io/github/dsheirer/spectrum/DFTProcessor.java +++ b/src/main/java/io/github/dsheirer/spectrum/DFTProcessor.java @@ -21,6 +21,7 @@ import io.github.dsheirer.properties.SystemProperties; import io.github.dsheirer.sample.Listener; import io.github.dsheirer.sample.SampleType; import io.github.dsheirer.sample.complex.ComplexBuffer; +import io.github.dsheirer.sample.real.IOverflowListener; import io.github.dsheirer.source.ISourceEventProcessor; import io.github.dsheirer.source.SourceEvent; import io.github.dsheirer.spectrum.converter.DFTResultsConverter; @@ -83,6 +84,15 @@ public class DFTProcessor implements Listener, ISourceEventProces mWindow = null; } + /** + * Sets the listener to receive buffer overflow/reset indications + * @param listener + */ + public void setOverflowListener(IOverflowListener listener) + { + mOverflowableBufferStream.setOverflowListener(listener); + } + public WindowType getWindowType() { return mWindowType; From 673c8a24236b153547367f312dc23402058a600a Mon Sep 17 00:00:00 2001 From: Denny Date: Sat, 27 Jan 2018 15:57:22 -0500 Subject: [PATCH 49/87] WIP - two-channel output processor is correctly synthesizing a channel and translating it by -FS/4 to correctly center the synthesized channel. The low-pass filter is currently disabled to better troubleshoot/view the state of the synthesized channel. It appears that the synthesizer is not correctly joining the channel ... instead of down-sampling and then synthesizing the two channels, it appears to be synthesizing and then down-sampling. The synthesized channel has a null in the middle which would render it unusable. --- .../dsp/filter/channelizer/ChannelCalculator.java | 14 ++++++++++---- .../channelizer/output/ChannelOutputProcessor.java | 7 +++---- 2 files changed, 13 insertions(+), 8 deletions(-) diff --git a/src/main/java/io/github/dsheirer/dsp/filter/channelizer/ChannelCalculator.java b/src/main/java/io/github/dsheirer/dsp/filter/channelizer/ChannelCalculator.java index 0f387d8b..a33b2f2b 100644 --- a/src/main/java/io/github/dsheirer/dsp/filter/channelizer/ChannelCalculator.java +++ b/src/main/java/io/github/dsheirer/dsp/filter/channelizer/ChannelCalculator.java @@ -521,15 +521,21 @@ public class ChannelCalculator int index = indexes.get(centerIndex); - if(index == getWrapAroundIndex()) + if(indexes.size() % 2 == 0) { - return (long)getIndexCenterFrequency(indexes.get(centerIndex), IndexBoundaryPolicy.ADJUST_NEGATIVE); + return (long)getIndexMaximumFrequency(index, IndexBoundaryPolicy.ADJUST_NEGATIVE); } else { - return (long)getIndexCenterFrequency(indexes.get(centerIndex), IndexBoundaryPolicy.ADJUST_POSITIVE); + if(index == getWrapAroundIndex()) + { + return (long)getIndexCenterFrequency(index, IndexBoundaryPolicy.ADJUST_NEGATIVE); + } + else + { + return (long)getIndexCenterFrequency(index, IndexBoundaryPolicy.ADJUST_POSITIVE); + } } - } /** diff --git a/src/main/java/io/github/dsheirer/dsp/filter/channelizer/output/ChannelOutputProcessor.java b/src/main/java/io/github/dsheirer/dsp/filter/channelizer/output/ChannelOutputProcessor.java index f280efbb..c75205a4 100644 --- a/src/main/java/io/github/dsheirer/dsp/filter/channelizer/output/ChannelOutputProcessor.java +++ b/src/main/java/io/github/dsheirer/dsp/filter/channelizer/output/ChannelOutputProcessor.java @@ -68,14 +68,13 @@ public abstract class ChannelOutputProcessor implements IPolyphaseChannelOutputP @Override public void setFrequencyOffset(long frequencyOffset) { - long offset = frequencyOffset; - mLog.debug("Setting frequency offset to: " + offset); -// mFrequencyCorrectionMixer.setFrequency(offset); + mLog.debug("Setting frequency offset to: " + frequencyOffset); + mFrequencyCorrectionMixer.setFrequency(frequencyOffset); mFrequencyCorrectionEnabled = (frequencyOffset != 0); } /** - * Oscillator/mixer to use for frequency correction against incominb sample stream + * Oscillator/mixer to use for frequency correction against incoming sample stream */ protected IOscillator getFrequencyCorrectionMixer() { From e78d2339ffea554096ab415953ff12a6a57aedff Mon Sep 17 00:00:00 2001 From: Denny Date: Sat, 27 Jan 2018 16:27:58 -0500 Subject: [PATCH 50/87] WIP - aligns the SynthesizerViewer configuration with that of the ChannelizerViewer so that we can compare the two-channel synthesis operations. The SynthesizerViewer is successfully down-sampling and synthesizing two 25.0 kHz channels into a combined 25.0 kHz channel with no noticeable drop/nulling in the joined region. However, the channelizer viewer's synthesized channels definitely have a null in the joined region. I suspect that the channelizer is not quite where it needs to be and am going to start experimenting there. --- .../dsp/filter/channelizer/ChannelizerViewer.java | 2 +- .../filter/channelizer/PolyphaseChannelManager.java | 4 ++-- .../dsp/filter/channelizer/SynthesizerViewer.java | 12 +++++++----- 3 files changed, 10 insertions(+), 8 deletions(-) diff --git a/src/main/java/io/github/dsheirer/dsp/filter/channelizer/ChannelizerViewer.java b/src/main/java/io/github/dsheirer/dsp/filter/channelizer/ChannelizerViewer.java index fba3a9da..94ce0f31 100644 --- a/src/main/java/io/github/dsheirer/dsp/filter/channelizer/ChannelizerViewer.java +++ b/src/main/java/io/github/dsheirer/dsp/filter/channelizer/ChannelizerViewer.java @@ -349,7 +349,7 @@ public class ChannelizerViewer extends JFrame { setLayout(new MigLayout("insets 0 0 0 0", "[center,grow,fill][]", "[grow,fill][]")); mSpectrumPanel = new SpectrumPanel(settingsManager); - mSpectrumPanel.setSampleSize(28); + mSpectrumPanel.setSampleSize(22); add(mSpectrumPanel, "span"); add(new JLabel("Center:" + frequency)); diff --git a/src/main/java/io/github/dsheirer/dsp/filter/channelizer/PolyphaseChannelManager.java b/src/main/java/io/github/dsheirer/dsp/filter/channelizer/PolyphaseChannelManager.java index cae8b91a..be617313 100644 --- a/src/main/java/io/github/dsheirer/dsp/filter/channelizer/PolyphaseChannelManager.java +++ b/src/main/java/io/github/dsheirer/dsp/filter/channelizer/PolyphaseChannelManager.java @@ -49,7 +49,7 @@ public class PolyphaseChannelManager implements ISourceEventProcessor private final static Logger mLog = LoggerFactory.getLogger(PolyphaseChannelManager.class); private static final double MINIMUM_CHANNEL_BANDWIDTH = 12500.0; private static final double CHANNEL_OVERSAMPLING = 2.0; - private static final int POLYPHASE_FILTER_TAPS_PER_CHANNEL = 17; + private static final int POLYPHASE_FILTER_TAPS_PER_CHANNEL = 19; private Broadcaster mSourceEventBroadcaster = new Broadcaster<>(); private IComplexBufferProvider mComplexBufferProvider; @@ -483,7 +483,7 @@ public class PolyphaseChannelManager implements ISourceEventProcessor if(taps == null) { - taps = FilterFactory.getSincM2Synthesizer(mChannelCalculator.getChannelSampleRate(), channels, + taps = FilterFactory.getSincM2Synthesizer(mChannelCalculator.getChannelBandwidth(), channels, POLYPHASE_FILTER_TAPS_PER_CHANNEL, Window.WindowType.BLACKMAN_HARRIS_7, true); mOutputProcessorFilters.put(channels, taps); diff --git a/src/main/java/io/github/dsheirer/dsp/filter/channelizer/SynthesizerViewer.java b/src/main/java/io/github/dsheirer/dsp/filter/channelizer/SynthesizerViewer.java index 33e728bd..94de48c7 100644 --- a/src/main/java/io/github/dsheirer/dsp/filter/channelizer/SynthesizerViewer.java +++ b/src/main/java/io/github/dsheirer/dsp/filter/channelizer/SynthesizerViewer.java @@ -18,6 +18,7 @@ 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.FS4DownConverter; import io.github.dsheirer.dsp.mixer.IOscillator; import io.github.dsheirer.dsp.mixer.LowPhaseNoiseOscillator; import io.github.dsheirer.sample.Listener; @@ -149,7 +150,7 @@ public class SynthesizerViewer extends JFrame { setLayout(new MigLayout("insets 0 0 0 0", "[grow,fill]", "[grow,fill]")); mSpectrumPanel = new SpectrumPanel(settingsManager); - mSpectrumPanel.setSampleSize(12); + mSpectrumPanel.setSampleSize(22); add(mSpectrumPanel); mDFTProcessor.addConverter(mComplexDecibelConverter); @@ -190,7 +191,7 @@ public class SynthesizerViewer extends JFrame { setLayout(new MigLayout("insets 0 0 0 0", "[grow,fill]", "[grow,fill][]")); mSpectrumPanel = new SpectrumPanel(settingsManager); - mSpectrumPanel.setSampleSize(12); + mSpectrumPanel.setSampleSize(22); add(mSpectrumPanel, "wrap"); add(channelControlPanel); @@ -235,12 +236,11 @@ public class SynthesizerViewer extends JFrame 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); + private IOscillator mOscillator = new LowPhaseNoiseOscillator(DEFAULT_FREQUENCY, CHANNEL_SAMPLE_RATE); public ChannelControlPanel() { @@ -272,7 +272,8 @@ public class SynthesizerViewer extends JFrame public class DataGenerationManager implements Runnable { private TwoChannelSynthesizerM2 mSynthesizer; - private int mSamplesPerCycle = 25000 / DATA_GENERATOR_FRAME_RATE; + private FS4DownConverter mFS4DownConverter = new FS4DownConverter(); + private int mSamplesPerCycle = CHANNEL_SAMPLE_RATE / DATA_GENERATOR_FRAME_RATE; public DataGenerationManager() { @@ -295,6 +296,7 @@ public class SynthesizerViewer extends JFrame float[] channel2 = getChannel2ControlPanel().getOscillator().generateComplex(mSamplesPerCycle); float[] synthesized = mSynthesizer.process(channel1, channel2); + mFS4DownConverter.mixComplex(synthesized); getChannel1Panel().receive(new ComplexBuffer(channel1)); getChannel2Panel().receive(new ComplexBuffer(channel2)); From fcd3c2fd46bb7ddd8715e4ea2d77a26227e526ee Mon Sep 17 00:00:00 2001 From: Denny Date: Sun, 28 Jan 2018 10:52:53 -0500 Subject: [PATCH 51/87] [WIP] channelizer and 1/2 channel output processors along with the 2 channel synthesizer ... all appear to be functional. --- .../dsheirer/dsp/filter/FilterFactory.java | 2 +- .../filter/channelizer/ChannelizerViewer.java | 20 ++++++++-------- .../channelizer/PolyphaseChannelManager.java | 2 +- .../output/TwoChannelOutputProcessor.java | 12 ++-------- .../dsp/mixer/LowPhaseNoiseOscillator.java | 23 ------------------- .../tuner/test/TestTunerController.java | 2 +- 6 files changed, 15 insertions(+), 46 deletions(-) diff --git a/src/main/java/io/github/dsheirer/dsp/filter/FilterFactory.java b/src/main/java/io/github/dsheirer/dsp/filter/FilterFactory.java index bafa24fc..440400f9 100644 --- a/src/main/java/io/github/dsheirer/dsp/filter/FilterFactory.java +++ b/src/main/java/io/github/dsheirer/dsp/filter/FilterFactory.java @@ -740,7 +740,7 @@ public class FilterFactory public static float[] getSincM2Channelizer(double channelBandwidth, int channels, int tapsPerChannel, Window.WindowType windowType, boolean logResults) throws FilterDesignException { - return getSincFilter(channelBandwidth * channels * 2.0, channelBandwidth, + return getSincFilter(channelBandwidth * channels, channelBandwidth, channels, tapsPerChannel, windowType,logResults); } diff --git a/src/main/java/io/github/dsheirer/dsp/filter/channelizer/ChannelizerViewer.java b/src/main/java/io/github/dsheirer/dsp/filter/channelizer/ChannelizerViewer.java index 94ce0f31..8b1f8d8d 100644 --- a/src/main/java/io/github/dsheirer/dsp/filter/channelizer/ChannelizerViewer.java +++ b/src/main/java/io/github/dsheirer/dsp/filter/channelizer/ChannelizerViewer.java @@ -91,7 +91,7 @@ public class ChannelizerViewer extends JFrame private void init() { setTitle("Polyphase Channelizer Viewer"); - setSize(1000, 800); + setSize(1200, 800); setDefaultCloseOperation(JFrame.EXIT_ON_CLOSE); setLayout(new MigLayout("insets 0 0 0 0", "[grow,fill]", "[grow,fill]")); setLocationRelativeTo(null); @@ -119,7 +119,7 @@ public class ChannelizerViewer extends JFrame { mPrimarySpectrumPanel = new PrimarySpectrumPanel(mSettingsManager, mTestTuner.getTunerController().getSampleRate()); - mPrimarySpectrumPanel.setPreferredSize(new Dimension(1000, 200)); + mPrimarySpectrumPanel.setPreferredSize(new Dimension(1200, 200)); mPrimarySpectrumPanel.setDFTSize(mMainPanelDFTSize); mTestTuner.getTunerController().addComplexBufferListener(mPrimarySpectrumPanel); } @@ -135,8 +135,8 @@ public class ChannelizerViewer extends JFrame mControlPanel.setLayout(new MigLayout("insets 0 0 0 0", "", "")); mControlPanel.add(new JLabel("Tone:"), "align left"); - long minimumFrequency = -(long)mTestTuner.getTunerController().getSampleRate() / 2; - long maximumFrequency = (long)mTestTuner.getTunerController().getSampleRate() / 2; + long minimumFrequency = -(long)mTestTuner.getTunerController().getSampleRate() / 2 + 1; + long maximumFrequency = (long)mTestTuner.getTunerController().getSampleRate() / 2 - 1; long toneFrequency = 0; SpinnerNumberModel model = new SpinnerNumberModel(toneFrequency, minimumFrequency, maximumFrequency, @@ -310,7 +310,7 @@ public class ChannelizerViewer extends JFrame { setLayout(new MigLayout("insets 0 0 0 0", "[grow,fill]", "[grow,fill]")); mSpectrumPanel = new SpectrumPanel(settingsManager); - mSpectrumPanel.setSampleSize(26); + mSpectrumPanel.setSampleSize(16); add(mSpectrumPanel); mDFTProcessor.addConverter(mComplexDecibelConverter); @@ -349,7 +349,7 @@ public class ChannelizerViewer extends JFrame { setLayout(new MigLayout("insets 0 0 0 0", "[center,grow,fill][]", "[grow,fill][]")); mSpectrumPanel = new SpectrumPanel(settingsManager); - mSpectrumPanel.setSampleSize(22); + mSpectrumPanel.setSampleSize(16); add(mSpectrumPanel, "span"); add(new JLabel("Center:" + frequency)); @@ -362,7 +362,7 @@ public class ChannelizerViewer extends JFrame mLoggingEnabled = mLoggingButton.isSelected(); } }); - add(mLoggingButton); +// add(mLoggingButton); mDFTProcessor.addConverter(mComplexDecibelConverter); mDFTProcessor.process(SourceEvent.sampleRateChange(sampleRate)); @@ -433,7 +433,7 @@ public class ChannelizerViewer extends JFrame mSource = source; setLayout(new MigLayout("insets 0 0 0 0", "[center,grow,fill][]", "[grow,fill][]")); mSpectrumPanel = new SpectrumPanel(settingsManager); - mSpectrumPanel.setSampleSize(28); + mSpectrumPanel.setSampleSize(16); add(mSpectrumPanel, "span"); add(new JLabel("Index:" + index)); @@ -446,7 +446,7 @@ public class ChannelizerViewer extends JFrame mLoggingEnabled = mLoggingButton.isSelected(); } }); - add(mLoggingButton); +// add(mLoggingButton); mDFTProcessor.addConverter(mComplexDecibelConverter); mDFTProcessor.process(SourceEvent.sampleRateChange(25000.0)); @@ -507,7 +507,7 @@ public class ChannelizerViewer extends JFrame if(useGUI) { - int channelsPerRow = 8; + int channelsPerRow = 16; final ChannelizerViewer frame = new ChannelizerViewer(channelsPerRow); diff --git a/src/main/java/io/github/dsheirer/dsp/filter/channelizer/PolyphaseChannelManager.java b/src/main/java/io/github/dsheirer/dsp/filter/channelizer/PolyphaseChannelManager.java index be617313..ccc13d1d 100644 --- a/src/main/java/io/github/dsheirer/dsp/filter/channelizer/PolyphaseChannelManager.java +++ b/src/main/java/io/github/dsheirer/dsp/filter/channelizer/PolyphaseChannelManager.java @@ -49,7 +49,7 @@ public class PolyphaseChannelManager implements ISourceEventProcessor private final static Logger mLog = LoggerFactory.getLogger(PolyphaseChannelManager.class); private static final double MINIMUM_CHANNEL_BANDWIDTH = 12500.0; private static final double CHANNEL_OVERSAMPLING = 2.0; - private static final int POLYPHASE_FILTER_TAPS_PER_CHANNEL = 19; + private static final int POLYPHASE_FILTER_TAPS_PER_CHANNEL = 17; private Broadcaster mSourceEventBroadcaster = new Broadcaster<>(); private IComplexBufferProvider mComplexBufferProvider; diff --git a/src/main/java/io/github/dsheirer/dsp/filter/channelizer/output/TwoChannelOutputProcessor.java b/src/main/java/io/github/dsheirer/dsp/filter/channelizer/output/TwoChannelOutputProcessor.java index 1ca55983..7b30e2ed 100644 --- a/src/main/java/io/github/dsheirer/dsp/filter/channelizer/output/TwoChannelOutputProcessor.java +++ b/src/main/java/io/github/dsheirer/dsp/filter/channelizer/output/TwoChannelOutputProcessor.java @@ -15,10 +15,8 @@ ******************************************************************************/ 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.dsp.mixer.FS4DownConverter; import io.github.dsheirer.sample.complex.TimestampedBufferAssembler; import org.slf4j.Logger; @@ -28,8 +26,6 @@ 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 FS4DownConverter mFS4DownConverter = new FS4DownConverter(); @@ -113,13 +109,9 @@ public class TwoChannelOutputProcessor extends ChannelOutputProcessor //The synthesized channels are centered at +FS/4 ... downconvert to center the spectrum mFS4DownConverter.mixComplex(synthesized); - //Apply frequency correction to center the signal of interest within the synthesized channel -// getFrequencyCorrectionMixer().mixComplex(synthesized); + //Apply offset and 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); } } diff --git a/src/main/java/io/github/dsheirer/dsp/mixer/LowPhaseNoiseOscillator.java b/src/main/java/io/github/dsheirer/dsp/mixer/LowPhaseNoiseOscillator.java index 76171c27..09c772d0 100644 --- a/src/main/java/io/github/dsheirer/dsp/mixer/LowPhaseNoiseOscillator.java +++ b/src/main/java/io/github/dsheirer/dsp/mixer/LowPhaseNoiseOscillator.java @@ -31,7 +31,6 @@ public class LowPhaseNoiseOscillator extends AbstractOscillator private double mCosineAngle; private double mSineAngle; private double mGain = 1.0; - private boolean mReset; /** * Ultra-low phase noise complex oscillator as described in Digital Signal Processing 3e, Lyons, p.786 @@ -46,19 +45,6 @@ public class LowPhaseNoiseOscillator extends AbstractOscillator update(); } - /** - * Sets the frequency of the tone generated by this oscillator. - */ - @Override - public void setFrequency(double frequency) - { - //This oscillator does not recover when the current frequency is equal to exactly half the sample rate, so we - //have to set a flag to reset the I/Q structures when changing the frequency from exactly half the sample rate. - mReset = getFrequency() < 0.01 || Math.abs(getFrequency()) - (getSampleRate() / 2) < 1.0; - - super.setFrequency(frequency); - } - @Override public float quadrature() { @@ -80,15 +66,6 @@ public class LowPhaseNoiseOscillator extends AbstractOscillator mCosineAngle = Math.cos(anglePerSample); mSineAngle = Math.sin(anglePerSample); - - if(mReset) - { - mInphase = 1.0; - mQuadrature = 0.0; - mPreviousInphase = 1.0; - mPreviousQuadrature = 0.0; - mReset = false; - } } /** diff --git a/src/main/java/io/github/dsheirer/source/tuner/test/TestTunerController.java b/src/main/java/io/github/dsheirer/source/tuner/test/TestTunerController.java index bedb5bfd..c9dac7ce 100644 --- a/src/main/java/io/github/dsheirer/source/tuner/test/TestTunerController.java +++ b/src/main/java/io/github/dsheirer/source/tuner/test/TestTunerController.java @@ -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 = 50000; + public static final int SAMPLE_RATE = 800000; public static final int DC_NOISE_BANDWIDTH = 0; public static final double USABLE_BANDWIDTH = 1.00; From f2f2c92afeb9f553f4e92a25c00cf8d4bd8ea95f Mon Sep 17 00:00:00 2001 From: Denny Date: Sat, 3 Feb 2018 10:05:16 -0500 Subject: [PATCH 52/87] [WIP] Implements reusable sample buffers across all components. This produces a significant reduction in memory usage, especially for tuners like the airspy and hackrf that produce 10/20 MSPS sample rates. This is observed to be around 66% reduction in peak memory usage and provides a more stable memory utilization across time reducing stress on the garbage collector. Use of these reusable memory buffers will be pushed further down into the processing chain modules to ensure we keep new object creation to the lowest levels possible. --- .idea/libraries/imports.xml | 33 -- .idea/misc.xml | 2 +- .../AbstractComplexPolyphaseChannelizer.java | 4 +- .../filter/channelizer/ChannelizerViewer.java | 61 +-- .../ComplexPolyphaseChannelizerM2.java | 35 +- .../channelizer/PolyphaseChannelManager.java | 26 +- .../channelizer/PolyphaseChannelSource.java | 22 +- .../channelizer/ScheduledBufferProcessor.java | 34 +- .../ScheduledReusableBufferProcessor.java | 45 ++ .../filter/channelizer/SynthesizerViewer.java | 34 +- .../output/ChannelOutputProcessor.java | 12 +- .../IPolyphaseChannelOutputProcessor.java | 6 +- .../output/OneChannelOutputProcessor.java | 8 +- .../output/TwoChannelOutputProcessor.java | 8 +- .../dsp/mixer/AbstractOscillator.java | 34 ++ .../dsheirer/dsp/mixer/IOscillator.java | 11 + .../io/github/dsheirer/module/Module.java | 3 +- .../dsheirer/module/ProcessingChain.java | 58 +-- .../module/decode/p25/P25_LSMDecoder.java | 374 +++++++++-------- .../demodulate/am/AMDemodulatorModule.java | 54 ++- .../demodulate/fm/FMDemodulatorModule.java | 24 +- .../wave/ComplexBufferWaveRecorder.java | 335 +++++++-------- .../github/dsheirer/sample/Broadcaster.java | 2 +- .../io/github/dsheirer/sample/Buffer.java | 2 +- .../sample/OverflowableTransferQueue.java | 21 +- .../sample/adapter/ComplexShortAdapter.java | 58 +++ .../sample/adapter/IComplexSampleAdapter.java | 28 ++ .../sample/complex/ComplexBuffer.java | 23 -- ...erflowableReusableBufferTransferQueue.java | 69 ++++ .../github/dsheirer/source/ComplexSource.java | 19 +- .../dsheirer/source/mixer/ComplexMixer.java | 348 ++++++++-------- .../dsheirer/source/recording/Recording.java | 12 +- .../source/tuner/ITunerChannelProvider.java | 42 -- .../source/tuner/TunerController.java | 26 +- .../dsheirer/source/tuner/TunerManager.java | 5 +- ...dapter.java => AirspySampleConverter.java} | 116 +++--- .../tuner/airspy/AirspyTunerController.java | 10 +- .../tuner/channel/CICTunerChannelSource.java | 22 +- .../tuner/channel/TunerChannelSource.java | 4 +- .../source/tuner/fcd/FCDTunerController.java | 20 +- .../tuner/hackrf/HackRFTunerController.java | 7 +- .../tuner/rtl/RTL2832TunerController.java | 36 +- .../tuner/rtl/e4k/E4KTunerController.java | 2 +- .../source/tuner/test/SampleGenerator.java | 50 +-- .../dsheirer/source/tuner/test/TestTuner.java | 3 - .../tuner/test/TestTunerController.java | 21 +- .../USBMasterProcessor.java} | 50 +-- .../tuner/usb/USBTransferProcessor.java | 389 +++++++++--------- .../source/tuner/usb/USBTunerController.java | 14 +- .../usb/converter/ByteSampleConverter.java | 80 ++++ .../usb/converter/NativeBufferConverter.java | 64 +++ .../source/wave/ComplexWaveSource.java | 37 +- .../spectrum/ChannelSpectrumPanel.java | 8 +- .../dsheirer/spectrum/DFTProcessor.java | 15 +- .../spectrum/OverflowableBufferStream.java | 45 +- .../spectrum/SpectralDisplayPanel.java | 14 +- 56 files changed, 1630 insertions(+), 1255 deletions(-) create mode 100644 src/main/java/io/github/dsheirer/dsp/filter/channelizer/ScheduledReusableBufferProcessor.java create mode 100644 src/main/java/io/github/dsheirer/sample/adapter/ComplexShortAdapter.java create mode 100644 src/main/java/io/github/dsheirer/sample/adapter/IComplexSampleAdapter.java create mode 100644 src/main/java/io/github/dsheirer/sample/complex/reusable/OverflowableReusableBufferTransferQueue.java delete mode 100644 src/main/java/io/github/dsheirer/source/tuner/ITunerChannelProvider.java rename src/main/java/io/github/dsheirer/source/tuner/airspy/{AirspySampleAdapter.java => AirspySampleConverter.java} (54%) rename src/main/java/io/github/dsheirer/source/tuner/{LibUSBTransferProcessor.java => usb/USBMasterProcessor.java} (68%) create mode 100644 src/main/java/io/github/dsheirer/source/tuner/usb/converter/ByteSampleConverter.java create mode 100644 src/main/java/io/github/dsheirer/source/tuner/usb/converter/NativeBufferConverter.java diff --git a/.idea/libraries/imports.xml b/.idea/libraries/imports.xml index 3be82b35..e97bf8ec 100644 --- a/.idea/libraries/imports.xml +++ b/.idea/libraries/imports.xml @@ -1,41 +1,8 @@ - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - diff --git a/.idea/misc.xml b/.idea/misc.xml index 05483570..d5d79e0c 100644 --- a/.idea/misc.xml +++ b/.idea/misc.xml @@ -1,6 +1,6 @@ - + \ No newline at end of file diff --git a/src/main/java/io/github/dsheirer/dsp/filter/channelizer/AbstractComplexPolyphaseChannelizer.java b/src/main/java/io/github/dsheirer/dsp/filter/channelizer/AbstractComplexPolyphaseChannelizer.java index 25d0de82..cbffb3f7 100644 --- a/src/main/java/io/github/dsheirer/dsp/filter/channelizer/AbstractComplexPolyphaseChannelizer.java +++ b/src/main/java/io/github/dsheirer/dsp/filter/channelizer/AbstractComplexPolyphaseChannelizer.java @@ -20,7 +20,7 @@ package io.github.dsheirer.dsp.filter.channelizer; import io.github.dsheirer.sample.Broadcaster; import io.github.dsheirer.sample.Listener; -import io.github.dsheirer.sample.complex.ComplexBuffer; +import io.github.dsheirer.sample.complex.reusable.ReusableComplexBuffer; import io.github.dsheirer.source.ISourceEventListener; import io.github.dsheirer.source.SourceEvent; import org.slf4j.Logger; @@ -29,7 +29,7 @@ import org.slf4j.LoggerFactory; import java.util.List; import java.util.concurrent.CopyOnWriteArrayList; -public abstract class AbstractComplexPolyphaseChannelizer implements Listener, ISourceEventListener +public abstract class AbstractComplexPolyphaseChannelizer implements Listener, ISourceEventListener { private final static Logger mLog = LoggerFactory.getLogger(AbstractComplexPolyphaseChannelizer.class); diff --git a/src/main/java/io/github/dsheirer/dsp/filter/channelizer/ChannelizerViewer.java b/src/main/java/io/github/dsheirer/dsp/filter/channelizer/ChannelizerViewer.java index 8b1f8d8d..283a83c2 100644 --- a/src/main/java/io/github/dsheirer/dsp/filter/channelizer/ChannelizerViewer.java +++ b/src/main/java/io/github/dsheirer/dsp/filter/channelizer/ChannelizerViewer.java @@ -18,7 +18,7 @@ package io.github.dsheirer.dsp.filter.channelizer; import io.github.dsheirer.sample.Listener; import io.github.dsheirer.sample.SampleType; -import io.github.dsheirer.sample.complex.ComplexBuffer; +import io.github.dsheirer.sample.complex.reusable.ReusableComplexBuffer; import io.github.dsheirer.settings.SettingsManager; import io.github.dsheirer.source.ISourceEventProcessor; import io.github.dsheirer.source.SourceEvent; @@ -121,7 +121,7 @@ public class ChannelizerViewer extends JFrame mTestTuner.getTunerController().getSampleRate()); mPrimarySpectrumPanel.setPreferredSize(new Dimension(1200, 200)); mPrimarySpectrumPanel.setDFTSize(mMainPanelDFTSize); - mTestTuner.getTunerController().addComplexBufferListener(mPrimarySpectrumPanel); + mTestTuner.getTunerController().addBufferListener(mPrimarySpectrumPanel); } return mPrimarySpectrumPanel; @@ -140,7 +140,7 @@ public class ChannelizerViewer extends JFrame long toneFrequency = 0; SpinnerNumberModel model = new SpinnerNumberModel(toneFrequency, minimumFrequency, maximumFrequency, - 100 ); + 100); model.addChangeListener(new ChangeListener() { @@ -300,7 +300,7 @@ public class ChannelizerViewer extends JFrame return tunerChannels; } - public class PrimarySpectrumPanel extends JPanel implements Listener, ISourceEventProcessor + public class PrimarySpectrumPanel extends JPanel implements Listener, ISourceEventProcessor { private DFTProcessor mDFTProcessor = new DFTProcessor(SampleType.COMPLEX); private ComplexDecibelConverter mComplexDecibelConverter = new ComplexDecibelConverter(); @@ -324,9 +324,10 @@ public class ChannelizerViewer extends JFrame } @Override - public void receive(ComplexBuffer complexBuffer) + public void receive(ReusableComplexBuffer reusableComplexBuffer) { - mDFTProcessor.receive(complexBuffer); + mDFTProcessor.receive(reusableComplexBuffer.incrementUserCount()); + reusableComplexBuffer.decrementUserCount(); } @Override @@ -336,7 +337,7 @@ public class ChannelizerViewer extends JFrame } } - public class ChannelPanel extends JPanel implements Listener, ISourceEventProcessor + public class ChannelPanel extends JPanel implements Listener, ISourceEventProcessor { private TunerChannelSource mSource; private DFTProcessor mDFTProcessor = new DFTProcessor(SampleType.COMPLEX); @@ -373,17 +374,18 @@ public class ChannelizerViewer extends JFrame if(mSource != null) { - mSource.setListener(new Listener() + mSource.setListener(new Listener() { @Override - public void receive(ComplexBuffer complexBuffer) + public void receive(ReusableComplexBuffer complexBuffer) { if(mLoggingEnabled) { mLog.debug("Samples:" + Arrays.toString(complexBuffer.getSamples())); } - mDFTProcessor.receive(complexBuffer); + mDFTProcessor.receive(complexBuffer.incrementUserCount()); + complexBuffer.decrementUserCount(); } }); @@ -406,10 +408,10 @@ public class ChannelizerViewer extends JFrame } @Override - public void receive(ComplexBuffer complexBuffer) + public void receive(ReusableComplexBuffer reusableComplexBuffer) { - mLog.debug("Got a buffer for the channel dft processor"); - mDFTProcessor.receive(complexBuffer); + mDFTProcessor.receive(reusableComplexBuffer.incrementUserCount()); + reusableComplexBuffer.decrementUserCount(); } @Override @@ -419,7 +421,7 @@ public class ChannelizerViewer extends JFrame } } - public class DiscreteChannelPanel extends JPanel implements Listener, ISourceEventProcessor + public class DiscreteChannelPanel extends JPanel implements Listener, ISourceEventProcessor { private TunerChannelSource mSource; private DFTProcessor mDFTProcessor = new DFTProcessor(SampleType.COMPLEX); @@ -454,17 +456,18 @@ public class ChannelizerViewer extends JFrame if(mSource != null) { - mSource.setListener(new Listener() + mSource.setListener(new Listener() { @Override - public void receive(ComplexBuffer complexBuffer) + public void receive(ReusableComplexBuffer complexBuffer) { if(mLoggingEnabled) { mLog.debug("Samples:" + Arrays.toString(complexBuffer.getSamples())); } - mDFTProcessor.receive(complexBuffer); + mDFTProcessor.receive(complexBuffer.incrementUserCount()); + complexBuffer.decrementUserCount(); } }); @@ -487,10 +490,10 @@ public class ChannelizerViewer extends JFrame } @Override - public void receive(ComplexBuffer complexBuffer) + public void receive(ReusableComplexBuffer reusableComplexBuffer) { - mLog.debug("Got a buffer for the channel dft processor"); - mDFTProcessor.receive(complexBuffer); + mDFTProcessor.receive(reusableComplexBuffer.incrementUserCount()); + reusableComplexBuffer.decrementUserCount(); } @Override @@ -524,16 +527,13 @@ public class ChannelizerViewer extends JFrame { TestTuner tuner = new TestTuner(); - //Turn on buffer reuse since we're simply profiling data flow/throughput - tuner.getTunerController().setReuseBuffers(true); - List tunerChannels = getTunerChannels(tuner); List sources = new ArrayList<>(); int maxSourceCount = 30; int sourceCount = 0; - for(TunerChannel tunerChannel: tunerChannels) + for(TunerChannel tunerChannel : tunerChannels) { if(sourceCount < maxSourceCount) { @@ -553,21 +553,24 @@ public class ChannelizerViewer extends JFrame mLog.debug("Starting [" + sources.size() + "] tuner channel sources"); - for(TunerChannelSource tunerChannelSource: sources) + for(TunerChannelSource tunerChannelSource : sources) { - tunerChannelSource.setListener(new Listener() + tunerChannelSource.setListener(new Listener() { @Override - public void receive(ComplexBuffer complexBuffer) + public void receive(ReusableComplexBuffer reusableComplexBuffer) { - //Do nothing + reusableComplexBuffer.decrementUserCount(); } }); tunerChannelSource.start(); } - while(true); + while(true) + { + ; + } } } } \ No newline at end of file diff --git a/src/main/java/io/github/dsheirer/dsp/filter/channelizer/ComplexPolyphaseChannelizerM2.java b/src/main/java/io/github/dsheirer/dsp/filter/channelizer/ComplexPolyphaseChannelizerM2.java index 662bbdbc..ddb01ae7 100644 --- a/src/main/java/io/github/dsheirer/dsp/filter/channelizer/ComplexPolyphaseChannelizerM2.java +++ b/src/main/java/io/github/dsheirer/dsp/filter/channelizer/ComplexPolyphaseChannelizerM2.java @@ -21,7 +21,7 @@ package io.github.dsheirer.dsp.filter.channelizer; import io.github.dsheirer.dsp.filter.FilterFactory; import io.github.dsheirer.dsp.filter.Window.WindowType; import io.github.dsheirer.dsp.filter.design.FilterDesignException; -import io.github.dsheirer.sample.complex.ComplexBuffer; +import io.github.dsheirer.sample.complex.reusable.ReusableComplexBuffer; import org.jtransforms.fft.FloatFFT_1D; import org.slf4j.Logger; import org.slf4j.LoggerFactory; @@ -84,7 +84,7 @@ public class ComplexPolyphaseChannelizerM2 extends AbstractComplexPolyphaseChann */ public ComplexPolyphaseChannelizerM2(double sampleRate, int tapsPerFilter) throws FilterDesignException { - super(sampleRate, (int)(sampleRate / DEFAULT_MINIMUM_CHANNEL_BANDWIDTH)); + super(sampleRate, getChannelCount(sampleRate)); float[] filterTaps = FilterFactory.getSincM2Channelizer(getChannelSampleRate(), getChannelCount(), tapsPerFilter, WindowType.BLACKMAN_HARRIS_7, true); @@ -95,6 +95,25 @@ public class ComplexPolyphaseChannelizerM2 extends AbstractComplexPolyphaseChann init(filterTaps); } + /** + * Calculates the multiple of two number of channels that can be channelized from the specified sample rate so that + * each channel has a minimum bandwidth of the default channel bandwidth (12.5 kHz). + * @param sampleRate to channelize + * @return number of multiple of two channels that can be channelized. + */ + private static int getChannelCount(double sampleRate) + { + int channels = (int)(sampleRate / DEFAULT_MINIMUM_CHANNEL_BANDWIDTH); + + if(channels % 2 != 0) + { + channels--; + } + + mLog.debug("Sample Rate [" + sampleRate + "] will produce [" + channels + "] channels at [" + (sampleRate / (double)channels) + "] each"); + return channels; + } + /** * Updates this channelizer to use the new sample rate. This method creates a new filter suitable for the * sample rate and reinitializes all internal data structures to prepare for processing the new sample rate. @@ -123,15 +142,12 @@ public class ComplexPolyphaseChannelizerM2 extends AbstractComplexPolyphaseChann * Receives the complex sample buffer and processes the results through the channelizer. */ @Override - public void receive(ComplexBuffer complexBuffer) + public void receive(ReusableComplexBuffer reusableComplexBuffer) { //Use the buffer's reference timestamp to update our timestamp manager (for timestamping output buffers) - if(complexBuffer.hasTimestamp()) - { - mTimestampManager.setReferenceTimestamp(complexBuffer.getTimestamp()); - } + mTimestampManager.setReferenceTimestamp(reusableComplexBuffer.getTimestamp()); - float[] samples = complexBuffer.getSamples(); + float[] samples = reusableComplexBuffer.getSamples(); int samplesPointer = 0; @@ -153,6 +169,9 @@ public class ComplexPolyphaseChannelizerM2 extends AbstractComplexPolyphaseChann process(); } } + + //Decrement the user count to let the originator know we're done with their buffer + reusableComplexBuffer.decrementUserCount(); } /** diff --git a/src/main/java/io/github/dsheirer/dsp/filter/channelizer/PolyphaseChannelManager.java b/src/main/java/io/github/dsheirer/dsp/filter/channelizer/PolyphaseChannelManager.java index ccc13d1d..028cfb26 100644 --- a/src/main/java/io/github/dsheirer/dsp/filter/channelizer/PolyphaseChannelManager.java +++ b/src/main/java/io/github/dsheirer/dsp/filter/channelizer/PolyphaseChannelManager.java @@ -23,8 +23,8 @@ import io.github.dsheirer.dsp.filter.channelizer.output.TwoChannelOutputProcesso import io.github.dsheirer.dsp.filter.design.FilterDesignException; import io.github.dsheirer.sample.Broadcaster; import io.github.dsheirer.sample.Listener; -import io.github.dsheirer.sample.complex.ComplexBuffer; -import io.github.dsheirer.sample.complex.IComplexBufferProvider; +import io.github.dsheirer.sample.complex.reusable.IReusableComplexBufferProvider; +import io.github.dsheirer.sample.complex.reusable.ReusableComplexBuffer; import io.github.dsheirer.source.ISourceEventProcessor; import io.github.dsheirer.source.SourceEvent; import io.github.dsheirer.source.SourceException; @@ -52,7 +52,7 @@ public class PolyphaseChannelManager implements ISourceEventProcessor private static final int POLYPHASE_FILTER_TAPS_PER_CHANNEL = 17; private Broadcaster mSourceEventBroadcaster = new Broadcaster<>(); - private IComplexBufferProvider mComplexBufferProvider; + private IReusableComplexBufferProvider mReusableBufferProvider; private List mChannelSources = new CopyOnWriteArrayList<>(); private ChannelCalculator mChannelCalculator; private ComplexPolyphaseChannelizerM2 mPolyphaseChannelizer; @@ -72,18 +72,18 @@ public class PolyphaseChannelManager implements ISourceEventProcessor * Note: add this channel manager as a source event listener to the complex buffer provider to ensure this manager * adapts to changes in source frequency and sample rate. * - * @param complexBufferProvider that supports register/deregister of this channel manager + * @param reusableBufferProvider that supports register/deregister of this channel manager * @param frequency of the provided complex buffer samples * @param sampleRate of the provided complex buffer samples */ - public PolyphaseChannelManager(IComplexBufferProvider complexBufferProvider, long frequency, double sampleRate) + public PolyphaseChannelManager(IReusableComplexBufferProvider reusableBufferProvider, long frequency, double sampleRate) { - if(complexBufferProvider == null) + if(reusableBufferProvider == null) { throw new IllegalArgumentException("Complex buffer provider argument cannot be null"); } - mComplexBufferProvider = complexBufferProvider; + mReusableBufferProvider = reusableBufferProvider; int channelCount = (int)(sampleRate / MINIMUM_CHANNEL_BANDWIDTH); @@ -229,7 +229,7 @@ public class PolyphaseChannelManager implements ISourceEventProcessor if(mPolyphaseChannelizer.getRegisteredChannelCount() == 1) { - mComplexBufferProvider.addComplexBufferListener(mBufferProcessor); + mReusableBufferProvider.addBufferListener(mBufferProcessor); mBufferProcessor.start(); } } @@ -250,7 +250,7 @@ public class PolyphaseChannelManager implements ISourceEventProcessor if(mPolyphaseChannelizer.getRegisteredChannelCount() == 0) { - mComplexBufferProvider.removeComplexBufferListener(mBufferProcessor); + mReusableBufferProvider.removeBufferListener(mBufferProcessor); mBufferProcessor.stop(); } } @@ -538,7 +538,7 @@ public class PolyphaseChannelManager implements ISourceEventProcessor * that they can be processed on the buffer processor calling thread, avoiding unnecessary locks on the channelizer * and/or the channel sources and output processors. */ - public class BufferSourceEventMonitor implements Listener + public class BufferSourceEventMonitor implements Listener { private Queue mQueuedSourceEvents = new ConcurrentLinkedQueue<>(); @@ -553,7 +553,7 @@ public class PolyphaseChannelManager implements ISourceEventProcessor } @Override - public void receive(ComplexBuffer complexBuffer) + public void receive(ReusableComplexBuffer reusableComplexBuffer) { try { @@ -578,13 +578,15 @@ public class PolyphaseChannelManager implements ISourceEventProcessor if(mPolyphaseChannelizer != null) { - mPolyphaseChannelizer.receive(complexBuffer); + mPolyphaseChannelizer.receive(reusableComplexBuffer.incrementUserCount()); } } catch(Throwable throwable) { mLog.error("Error", throwable); } + + reusableComplexBuffer.decrementUserCount(); } } } \ No newline at end of file diff --git a/src/main/java/io/github/dsheirer/dsp/filter/channelizer/PolyphaseChannelSource.java b/src/main/java/io/github/dsheirer/dsp/filter/channelizer/PolyphaseChannelSource.java index 3d2bcab4..0e62e6c7 100644 --- a/src/main/java/io/github/dsheirer/dsp/filter/channelizer/PolyphaseChannelSource.java +++ b/src/main/java/io/github/dsheirer/dsp/filter/channelizer/PolyphaseChannelSource.java @@ -17,8 +17,8 @@ package io.github.dsheirer.dsp.filter.channelizer; import io.github.dsheirer.dsp.filter.channelizer.output.IPolyphaseChannelOutputProcessor; import io.github.dsheirer.sample.Listener; -import io.github.dsheirer.sample.complex.ComplexBuffer; -import io.github.dsheirer.sample.complex.TimestampedBufferAssembler; +import io.github.dsheirer.sample.complex.reusable.ReusableBufferAssembler; +import io.github.dsheirer.sample.complex.reusable.ReusableComplexBuffer; import io.github.dsheirer.source.SourceEvent; import io.github.dsheirer.source.tuner.channel.TunerChannel; import io.github.dsheirer.source.tuner.channel.TunerChannelSource; @@ -29,7 +29,7 @@ public class PolyphaseChannelSource extends TunerChannelSource { private final static Logger mLog = LoggerFactory.getLogger(PolyphaseChannelSource.class); - private TimestampedBufferAssembler mTimestampedBufferAssembler; + private ReusableBufferAssembler mReusableBufferAssembler; private IPolyphaseChannelOutputProcessor mPolyphaseChannelOutputProcessor; private double mChannelSampleRate; private long mCenterFrequency; @@ -51,25 +51,25 @@ public class PolyphaseChannelSource extends TunerChannelSource super(producerSourceEventListener, tunerChannel); mPolyphaseChannelOutputProcessor = outputProcessor; mChannelSampleRate = channelSampleRate; - mTimestampedBufferAssembler = new TimestampedBufferAssembler(2500, mChannelSampleRate); + mReusableBufferAssembler = new ReusableBufferAssembler(2500, mChannelSampleRate); } /** * Registers the listener to receive complex sample buffers from this channel source */ @Override - public void setListener(final Listener listener) + public void setListener(final Listener listener) { - mTimestampedBufferAssembler.setListener(listener); + mReusableBufferAssembler.setListener(listener); } /** * Removes the listener from receiving complex sample buffers from this channel source */ @Override - public void removeListener(Listener listener) + public void removeListener(Listener listener) { - mTimestampedBufferAssembler.setListener(null); + mReusableBufferAssembler.setListener(null); } /** @@ -108,7 +108,7 @@ public class PolyphaseChannelSource extends TunerChannelSource //Fully process the residual channel results buffer of the previous channel output processor if(existingProcessor != null) { - existingProcessor.processChannelResults(mTimestampedBufferAssembler); + existingProcessor.processChannelResults(mReusableBufferAssembler); } //Finally, setup the frequency offset for the output processor and reset the frequency correction value @@ -129,7 +129,7 @@ public class PolyphaseChannelSource extends TunerChannelSource */ public void receiveChannelResults(PolyphaseChannelResultsBuffer channelResultsBuffer) { - mTimestampedBufferAssembler.updateTimestamp(channelResultsBuffer.getTimestamp()); + mReusableBufferAssembler.updateTimestamp(channelResultsBuffer.getTimestamp()); mPolyphaseChannelOutputProcessor.receiveChannelResults(channelResultsBuffer); } @@ -200,7 +200,7 @@ public class PolyphaseChannelSource extends TunerChannelSource //Lock on the output processor so that it can't be changed in the middle of processing synchronized(mPolyphaseChannelOutputProcessor) { - mPolyphaseChannelOutputProcessor.processChannelResults(mTimestampedBufferAssembler); + mPolyphaseChannelOutputProcessor.processChannelResults(mReusableBufferAssembler); } } diff --git a/src/main/java/io/github/dsheirer/dsp/filter/channelizer/ScheduledBufferProcessor.java b/src/main/java/io/github/dsheirer/dsp/filter/channelizer/ScheduledBufferProcessor.java index 57c3b9fc..7c3ca3d7 100644 --- a/src/main/java/io/github/dsheirer/dsp/filter/channelizer/ScheduledBufferProcessor.java +++ b/src/main/java/io/github/dsheirer/dsp/filter/channelizer/ScheduledBufferProcessor.java @@ -59,7 +59,29 @@ public class ScheduledBufferProcessor implements Listener */ public ScheduledBufferProcessor(int maximumSize, int resetThreshold, long distributionInterval, int maxBuffersPerInterval) { - mQueue = new OverflowableTransferQueue<>(maximumSize, resetThreshold); + this(new OverflowableTransferQueue<>(maximumSize, resetThreshold), distributionInterval, maxBuffersPerInterval); + } + + /** + * Scheduled Buffer Processor combines an internal overflowable buffer with a scheduled runnable processing task + * for periodically distributing internally queued elements to the registered listener. This processor provides + * a convenient way to create a thread-safe buffer for receiving elements from one thread/runnable and then + * distributing those elements to a registered listener where distribution occurs on a separate scheduled thread + * pool runnable thread. This allows the calling input thread to quickly return without incurring any subsequent + * processing workload. + * + * The internal queue is an overflowable queue implementation that allows a listener to be registered to receive + * notifications of overflow and reset state. Queue sizing parameters are specified in the constructor. + * + * @param queue implmentation of an overflowable transfer queue + * @param distributionInterval in milliseconds specifying how often the internal scheduled thread pool buffer + * distributor should run + * @param maxBuffersPerInterval maximum number of buffers to pull from the internal queue during each distribution + * interval + */ + public ScheduledBufferProcessor(OverflowableTransferQueue queue, long distributionInterval, int maxBuffersPerInterval) + { + mQueue = queue; mDistributionInterval = distributionInterval; mMaxBuffersPerInterval = maxBuffersPerInterval; } @@ -121,10 +143,18 @@ public class ScheduledBufferProcessor implements Listener mScheduledFuture = null; } - mQueue.clear(); + clearQueue(); } } + /** + * Clears any buffers from the queue + */ + protected void clearQueue() + { + mQueue.clear(); + } + /** * Indicates if this processor is currently running */ diff --git a/src/main/java/io/github/dsheirer/dsp/filter/channelizer/ScheduledReusableBufferProcessor.java b/src/main/java/io/github/dsheirer/dsp/filter/channelizer/ScheduledReusableBufferProcessor.java new file mode 100644 index 00000000..24eb7843 --- /dev/null +++ b/src/main/java/io/github/dsheirer/dsp/filter/channelizer/ScheduledReusableBufferProcessor.java @@ -0,0 +1,45 @@ +/******************************************************************************* + * 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 + * + ******************************************************************************/ +package io.github.dsheirer.dsp.filter.channelizer; + +import io.github.dsheirer.sample.complex.reusable.OverflowableReusableBufferTransferQueue; +import io.github.dsheirer.sample.complex.reusable.ReusableComplexBuffer; + +public class ScheduledReusableBufferProcessor extends ScheduledBufferProcessor +{ + /** + * Scheduled Reusable Buffer Processor combines an internal overflowable buffer with a scheduled runnable processing + * task for periodically distributing internally queued elements to the registered listener. This processor + * provides a convenient way to create a thread-safe buffer for receiving elements from one thread/runnable and then + * distributing those elements to a registered listener where distribution occurs on a separate scheduled thread + * pool runnable thread. This allows the calling input thread to quickly return without incurring any subsequent + * processing workload. + * + * The internal queue is an overflowable queue implementation that allows a listener to be registered to receive + * notifications of overflow and reset state. Queue sizing parameters are specified in the constructor. + * + * @param maximumSize of the internal queue (overflow happens when this is exceeded) + * @param resetThreshold of the internal queue (overflow reset happens once queue size falls below this threshold + * @param distributionInterval in milliseconds specifying how often the internal scheduled thread pool buffer + * distributor should run + * @param maxBuffersPerInterval maximum number of buffers to pull from the internal queue during each distribution + */ + public ScheduledReusableBufferProcessor(int maximumSize, int resetThreshold, long distributionInterval, int maxBuffersPerInterval) + { + super(new OverflowableReusableBufferTransferQueue(maximumSize, resetThreshold), + distributionInterval, maxBuffersPerInterval); + } +} diff --git a/src/main/java/io/github/dsheirer/dsp/filter/channelizer/SynthesizerViewer.java b/src/main/java/io/github/dsheirer/dsp/filter/channelizer/SynthesizerViewer.java index 94de48c7..75d48dea 100644 --- a/src/main/java/io/github/dsheirer/dsp/filter/channelizer/SynthesizerViewer.java +++ b/src/main/java/io/github/dsheirer/dsp/filter/channelizer/SynthesizerViewer.java @@ -23,7 +23,8 @@ 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.sample.complex.reusable.ReusableBufferQueue; +import io.github.dsheirer.sample.complex.reusable.ReusableComplexBuffer; import io.github.dsheirer.sample.real.IOverflowListener; import io.github.dsheirer.settings.SettingsManager; import io.github.dsheirer.source.SourceEvent; @@ -140,7 +141,7 @@ public class SynthesizerViewer extends JFrame return mChannel2Panel; } - public class PrimarySpectrumPanel extends JPanel implements Listener + public class PrimarySpectrumPanel extends JPanel implements Listener { private DFTProcessor mDFTProcessor = new DFTProcessor(SampleType.COMPLEX); private ComplexDecibelConverter mComplexDecibelConverter = new ComplexDecibelConverter(); @@ -174,13 +175,14 @@ public class SynthesizerViewer extends JFrame } @Override - public void receive(ComplexBuffer complexBuffer) + public void receive(ReusableComplexBuffer reusableComplexBuffer) { - mDFTProcessor.receive(complexBuffer); + mDFTProcessor.receive(reusableComplexBuffer.incrementUserCount()); + reusableComplexBuffer.decrementUserCount(); } } - public class ChannelPanel extends JPanel implements Listener + public class ChannelPanel extends JPanel implements Listener { private DFTProcessor mDFTProcessor = new DFTProcessor(SampleType.COMPLEX); private ComplexDecibelConverter mComplexDecibelConverter = new ComplexDecibelConverter(); @@ -208,9 +210,10 @@ public class SynthesizerViewer extends JFrame } @Override - public void receive(ComplexBuffer complexBuffer) + public void receive(ReusableComplexBuffer reusableComplexBuffer) { - mDFTProcessor.receive(complexBuffer); + mDFTProcessor.receive(reusableComplexBuffer.incrementUserCount()); + reusableComplexBuffer.decrementUserCount(); } } @@ -274,6 +277,7 @@ public class SynthesizerViewer extends JFrame private TwoChannelSynthesizerM2 mSynthesizer; private FS4DownConverter mFS4DownConverter = new FS4DownConverter(); private int mSamplesPerCycle = CHANNEL_SAMPLE_RATE / DATA_GENERATOR_FRAME_RATE; + private ReusableBufferQueue mReusableBufferQueue = new ReusableBufferQueue(); public DataGenerationManager() { @@ -292,15 +296,19 @@ public class SynthesizerViewer extends JFrame @Override public void run() { - float[] channel1 = getChannel1ControlPanel().getOscillator().generateComplex(mSamplesPerCycle); - float[] channel2 = getChannel2ControlPanel().getOscillator().generateComplex(mSamplesPerCycle); + ReusableComplexBuffer channel1Buffer = mReusableBufferQueue.getBuffer(mSamplesPerCycle); + getChannel1ControlPanel().getOscillator().generateComplex(channel1Buffer); - float[] synthesized = mSynthesizer.process(channel1, channel2); + ReusableComplexBuffer channel2Buffer = mReusableBufferQueue.getBuffer(mSamplesPerCycle); + getChannel2ControlPanel().getOscillator().generateComplex(channel2Buffer); + + float[] synthesized = mSynthesizer.process(channel1Buffer.getSamples(), channel2Buffer.getSamples()); mFS4DownConverter.mixComplex(synthesized); + ReusableComplexBuffer synthesizedBuffer = mReusableBufferQueue.getBuffer(synthesized.length); - getChannel1Panel().receive(new ComplexBuffer(channel1)); - getChannel2Panel().receive(new ComplexBuffer(channel2)); - getSpectrumPanel().receive(new ComplexBuffer(synthesized)); + getChannel1Panel().receive(channel1Buffer.incrementUserCount()); + getChannel2Panel().receive(channel2Buffer.incrementUserCount()); + getSpectrumPanel().receive(synthesizedBuffer.incrementUserCount()); } } diff --git a/src/main/java/io/github/dsheirer/dsp/filter/channelizer/output/ChannelOutputProcessor.java b/src/main/java/io/github/dsheirer/dsp/filter/channelizer/output/ChannelOutputProcessor.java index c75205a4..5f9f2954 100644 --- a/src/main/java/io/github/dsheirer/dsp/filter/channelizer/output/ChannelOutputProcessor.java +++ b/src/main/java/io/github/dsheirer/dsp/filter/channelizer/output/ChannelOutputProcessor.java @@ -22,7 +22,7 @@ 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; +import io.github.dsheirer.sample.complex.reusable.ReusableBufferAssembler; import io.github.dsheirer.sample.real.IOverflowListener; import io.github.dsheirer.source.Source; import org.slf4j.Logger; @@ -89,10 +89,10 @@ public abstract class ChannelOutputProcessor implements IPolyphaseChannelOutputP /** * Processes all enqueued polyphase channelizer results until the internal queue is empty - * @param timestampedBufferAssembler to receive the processed channel results + * @param reusableBufferAssembler to receive the processed channel results */ @Override - public void processChannelResults(TimestampedBufferAssembler timestampedBufferAssembler) + public void processChannelResults(ReusableBufferAssembler reusableBufferAssembler) { try { @@ -100,7 +100,7 @@ public abstract class ChannelOutputProcessor implements IPolyphaseChannelOutputP if(toProcess > 0) { - process(mChannelResultsToProcess, timestampedBufferAssembler); + process(mChannelResultsToProcess, reusableBufferAssembler); } } catch(Throwable throwable) @@ -114,10 +114,10 @@ public abstract class ChannelOutputProcessor implements IPolyphaseChannelOutputP /** * Sub-class implementation to process one polyphase channelizer result array. * @param channelResults to process - * @param timestampedBufferAssembler to receive the processed channelizer results + * @param reusableBufferAssembler to receive the processed channelizer results */ public abstract void process(List channelResults, - TimestampedBufferAssembler timestampedBufferAssembler); + ReusableBufferAssembler reusableBufferAssembler); /** diff --git a/src/main/java/io/github/dsheirer/dsp/filter/channelizer/output/IPolyphaseChannelOutputProcessor.java b/src/main/java/io/github/dsheirer/dsp/filter/channelizer/output/IPolyphaseChannelOutputProcessor.java index 916ae095..2dc2216a 100644 --- a/src/main/java/io/github/dsheirer/dsp/filter/channelizer/output/IPolyphaseChannelOutputProcessor.java +++ b/src/main/java/io/github/dsheirer/dsp/filter/channelizer/output/IPolyphaseChannelOutputProcessor.java @@ -19,7 +19,7 @@ package io.github.dsheirer.dsp.filter.channelizer.output; import io.github.dsheirer.dsp.filter.channelizer.PolyphaseChannelResultsBuffer; -import io.github.dsheirer.sample.complex.TimestampedBufferAssembler; +import io.github.dsheirer.sample.complex.reusable.ReusableBufferAssembler; import io.github.dsheirer.source.Source; import java.util.List; @@ -35,9 +35,9 @@ public interface IPolyphaseChannelOutputProcessor /** * Process the channel output channel results queue and deliver the output to the listener * - * @param timestampedBufferAssembler to receive the processed channel results + * @param reusableBufferAssembler to receive the processed channel results */ - void processChannelResults(TimestampedBufferAssembler timestampedBufferAssembler); + void processChannelResults(ReusableBufferAssembler reusableBufferAssembler); /** * Sets the desired frequency offset from center. The samples will be mixed with an oscillator set to this offset diff --git a/src/main/java/io/github/dsheirer/dsp/filter/channelizer/output/OneChannelOutputProcessor.java b/src/main/java/io/github/dsheirer/dsp/filter/channelizer/output/OneChannelOutputProcessor.java index e6f52d3f..b17970f9 100644 --- a/src/main/java/io/github/dsheirer/dsp/filter/channelizer/output/OneChannelOutputProcessor.java +++ b/src/main/java/io/github/dsheirer/dsp/filter/channelizer/output/OneChannelOutputProcessor.java @@ -19,7 +19,7 @@ package io.github.dsheirer.dsp.filter.channelizer.output; import io.github.dsheirer.dsp.filter.channelizer.PolyphaseChannelResultsBuffer; -import io.github.dsheirer.sample.complex.TimestampedBufferAssembler; +import io.github.dsheirer.sample.complex.reusable.ReusableBufferAssembler; import org.slf4j.Logger; import org.slf4j.LoggerFactory; @@ -73,11 +73,11 @@ public class OneChannelOutputProcessor extends ChannelOutputProcessor * extracted frequency-corrected channel I/Q sample set to the complex sample listener. * * @param channelResults to process containing a list of channel array of I/Q sample pairs (I0,Q0,I1,Q1...In,Qn) - * @param timestampedBufferAssembler to receive the extracted, frequency-translated channel results + * @param reusableBufferAssembler to receive the extracted, frequency-translated channel results */ @Override public void process(List channelResults, - TimestampedBufferAssembler timestampedBufferAssembler) + ReusableBufferAssembler reusableBufferAssembler) { for(PolyphaseChannelResultsBuffer channelResultsBuffer: channelResults) { @@ -90,7 +90,7 @@ public class OneChannelOutputProcessor extends ChannelOutputProcessor getFrequencyCorrectionMixer().mixComplex(samples); } - timestampedBufferAssembler.receive(samples); + reusableBufferAssembler.receive(samples); } catch(IllegalArgumentException iae) { diff --git a/src/main/java/io/github/dsheirer/dsp/filter/channelizer/output/TwoChannelOutputProcessor.java b/src/main/java/io/github/dsheirer/dsp/filter/channelizer/output/TwoChannelOutputProcessor.java index 7b30e2ed..dcc637a5 100644 --- a/src/main/java/io/github/dsheirer/dsp/filter/channelizer/output/TwoChannelOutputProcessor.java +++ b/src/main/java/io/github/dsheirer/dsp/filter/channelizer/output/TwoChannelOutputProcessor.java @@ -18,7 +18,7 @@ package io.github.dsheirer.dsp.filter.channelizer.output; import io.github.dsheirer.dsp.filter.channelizer.PolyphaseChannelResultsBuffer; import io.github.dsheirer.dsp.filter.channelizer.TwoChannelSynthesizerM2; import io.github.dsheirer.dsp.mixer.FS4DownConverter; -import io.github.dsheirer.sample.complex.TimestampedBufferAssembler; +import io.github.dsheirer.sample.complex.reusable.ReusableBufferAssembler; import org.slf4j.Logger; import org.slf4j.LoggerFactory; @@ -92,11 +92,11 @@ public class TwoChannelOutputProcessor extends ChannelOutputProcessor * extracted frequency-corrected channel I/Q sample set to the complex sample listener. * * @param channelResultsBuffers to process containing an array of channel I/Q sample pairs (I0,Q0,I1,Q1...In,Qn) - * @param timestampedBufferAssembler to receive the extracted, frequency-translated channel results + * @param reusableBufferAssembler to receive the extracted, frequency-translated channel results */ @Override public void process(List channelResultsBuffers, - TimestampedBufferAssembler timestampedBufferAssembler) + ReusableBufferAssembler reusableBufferAssembler) { for(PolyphaseChannelResultsBuffer buffer: channelResultsBuffers) { @@ -112,7 +112,7 @@ public class TwoChannelOutputProcessor extends ChannelOutputProcessor //Apply offset and frequency correction to center the signal of interest within the synthesized channel getFrequencyCorrectionMixer().mixComplex(synthesized); - timestampedBufferAssembler.receive(synthesized); + reusableBufferAssembler.receive(synthesized); } } } diff --git a/src/main/java/io/github/dsheirer/dsp/mixer/AbstractOscillator.java b/src/main/java/io/github/dsheirer/dsp/mixer/AbstractOscillator.java index b0432b2e..5be64115 100644 --- a/src/main/java/io/github/dsheirer/dsp/mixer/AbstractOscillator.java +++ b/src/main/java/io/github/dsheirer/dsp/mixer/AbstractOscillator.java @@ -16,9 +16,17 @@ package io.github.dsheirer.dsp.mixer; import io.github.dsheirer.sample.complex.Complex; +import io.github.dsheirer.sample.complex.reusable.ReusableComplexBuffer; +import org.slf4j.Logger; +import org.slf4j.LoggerFactory; + +import java.nio.FloatBuffer; public abstract class AbstractOscillator implements IOscillator { + private final static Logger mLog = LoggerFactory.getLogger(AbstractOscillator.class); + + private FloatBuffer mSampleBuffer; private boolean mEnabled; private double mFrequency; private double mSampleRate; @@ -144,4 +152,30 @@ public abstract class AbstractOscillator implements IOscillator return samples; } + + /** + * Generates enough complex samples to fill the reusable complex buffer + * @param reusableComplexBuffer to fill with complex samples + */ + @Override + public void generateComplex(ReusableComplexBuffer reusableComplexBuffer) + { + int sampleCount = reusableComplexBuffer.getSampleLength(); + + if(mSampleBuffer == null || mSampleBuffer.capacity() != sampleCount * 2) + { + mSampleBuffer = FloatBuffer.allocate(sampleCount * 2); + } + + mSampleBuffer.rewind(); + + for(int x = 0; x < sampleCount; x++) + { + mSampleBuffer.put(inphase()); + mSampleBuffer.put(quadrature()); + rotate(); + } + + reusableComplexBuffer.reloadFrom(mSampleBuffer, System.currentTimeMillis()); + } } diff --git a/src/main/java/io/github/dsheirer/dsp/mixer/IOscillator.java b/src/main/java/io/github/dsheirer/dsp/mixer/IOscillator.java index b9e23c08..1f843dda 100644 --- a/src/main/java/io/github/dsheirer/dsp/mixer/IOscillator.java +++ b/src/main/java/io/github/dsheirer/dsp/mixer/IOscillator.java @@ -15,6 +15,8 @@ ******************************************************************************/ package io.github.dsheirer.dsp.mixer; +import io.github.dsheirer.sample.complex.reusable.ReusableComplexBuffer; + /** * Oscillator interface. */ @@ -86,6 +88,15 @@ public interface IOscillator */ float[] generateComplex(int sampleCount); + + /** + * Generates complex samples and fills the reusable complex buffer using the current system time in millis for + * the update timestamp. + * + * @param reusableComplexBuffer to fill with complex samples + */ + void generateComplex(ReusableComplexBuffer reusableComplexBuffer); + /** * Mixes (heterodynes) the complex sample array using the current settings of this oscillator. * @param complexSamples to mix to a new frequency diff --git a/src/main/java/io/github/dsheirer/module/Module.java b/src/main/java/io/github/dsheirer/module/Module.java index afeb566a..9137643b 100644 --- a/src/main/java/io/github/dsheirer/module/Module.java +++ b/src/main/java/io/github/dsheirer/module/Module.java @@ -32,12 +32,11 @@ public abstract class Module * ICallEventProvider provides call events * IChannelEventListener consumes channel events * IChannelEventProvider provides channel events (reset, selection, start, etc) - * IComplexBufferListener consumes complex samples and normally - * produces demodulated real sample buffers * IFrequencyCorrectionController provides tuned frequency error corrections * IMessageProvider produces messages * IRealBufferListener consumes demodulated real sample buffers * IRealBufferListener produces demodulated real sample buffers + * IReusableComplexBufferListener consumes complex samples and normally produces demodulated real sample buffers * ISquelchStateListener consumes squelch states * ISquelchStateProvider provides squelch states */ diff --git a/src/main/java/io/github/dsheirer/module/ProcessingChain.java b/src/main/java/io/github/dsheirer/module/ProcessingChain.java index 7dddae2a..060190ff 100644 --- a/src/main/java/io/github/dsheirer/module/ProcessingChain.java +++ b/src/main/java/io/github/dsheirer/module/ProcessingChain.java @@ -49,8 +49,8 @@ import io.github.dsheirer.record.wave.ComplexBufferWaveRecorder; import io.github.dsheirer.record.wave.RealBufferWaveRecorder; import io.github.dsheirer.sample.Broadcaster; import io.github.dsheirer.sample.Listener; -import io.github.dsheirer.sample.complex.ComplexBuffer; -import io.github.dsheirer.sample.complex.IComplexBufferListener; +import io.github.dsheirer.sample.complex.reusable.IReusableComplexBufferListener; +import io.github.dsheirer.sample.complex.reusable.ReusableBufferBroadcaster; import io.github.dsheirer.sample.real.IFilteredRealBufferListener; import io.github.dsheirer.sample.real.IFilteredRealBufferProvider; import io.github.dsheirer.sample.real.IUnFilteredRealBufferListener; @@ -98,7 +98,7 @@ public class ProcessingChain implements IChannelEventListener private Broadcaster mAudioPacketBroadcaster = new Broadcaster<>(); private Broadcaster mCallEventBroadcaster = new Broadcaster<>(); private Broadcaster mChannelEventBroadcaster = new Broadcaster<>(); - private Broadcaster mComplexBufferBroadcaster = new Broadcaster<>(); + private ReusableBufferBroadcaster mComplexBufferBroadcaster = new ReusableBufferBroadcaster(); private Broadcaster mDecoderStateEventBroadcaster = new Broadcaster<>(); private Broadcaster mHeartbeatBroadcaster = new Broadcaster<>(); private Broadcaster mSourceEventBroadcaster = new Broadcaster<>(); @@ -307,24 +307,14 @@ public class ProcessingChain implements IChannelEventListener mChannelEventBroadcaster.addListener(((IChannelEventListener) module).getChannelEventListener()); } - if(module instanceof IComplexBufferListener) - { - mComplexBufferBroadcaster.addListener(((IComplexBufferListener) module).getComplexBufferListener()); - } - if(module instanceof IDecoderStateEventListener) { mDecoderStateEventBroadcaster.addListener(((IDecoderStateEventListener) module).getDecoderStateListener()); } - if(module instanceof ISourceEventListener) + if(module instanceof IFilteredRealBufferListener) { - Listener listener = ((ISourceEventListener)module).getSourceEventListener(); - - if(listener != null) - { - mSourceEventBroadcaster.addListener(listener); - } + mFilteredRealBufferBroadcaster.addListener(((IFilteredRealBufferListener) module).getFilteredRealBufferListener()); } if(module instanceof IHeartbeatListener) @@ -337,9 +327,19 @@ public class ProcessingChain implements IChannelEventListener mMessageBroadcaster.addListener(((IMessageListener) module).getMessageListener()); } - if(module instanceof IFilteredRealBufferListener) + if(module instanceof IReusableComplexBufferListener) { - mFilteredRealBufferBroadcaster.addListener(((IFilteredRealBufferListener) module).getFilteredRealBufferListener()); + mComplexBufferBroadcaster.addListener(((IReusableComplexBufferListener) module).getReusableComplexBufferListener()); + } + + if(module instanceof ISourceEventListener) + { + Listener listener = ((ISourceEventListener)module).getSourceEventListener(); + + if(listener != null) + { + mSourceEventBroadcaster.addListener(listener); + } } if(module instanceof ISquelchStateListener) @@ -379,19 +379,14 @@ public class ProcessingChain implements IChannelEventListener mChannelEventBroadcaster.removeListener(((IChannelEventListener) module).getChannelEventListener()); } - if(module instanceof IComplexBufferListener) - { - mComplexBufferBroadcaster.removeListener(((IComplexBufferListener) module).getComplexBufferListener()); - } - if(module instanceof IDecoderStateEventListener) { mDecoderStateEventBroadcaster.removeListener(((IDecoderStateEventListener) module).getDecoderStateListener()); } - if(module instanceof ISourceEventListener) + if(module instanceof IFilteredRealBufferListener) { - mSourceEventBroadcaster.removeListener(((ISourceEventListener) module).getSourceEventListener()); + mFilteredRealBufferBroadcaster.removeListener(((IFilteredRealBufferListener) module).getFilteredRealBufferListener()); } if(module instanceof IHeartbeatListener) @@ -399,16 +394,21 @@ public class ProcessingChain implements IChannelEventListener mHeartbeatBroadcaster.removeListener(((IHeartbeatListener)module).getHeartbeatListener()); } + if(module instanceof IReusableComplexBufferListener) + { + mComplexBufferBroadcaster.removeListener(((IReusableComplexBufferListener) module).getReusableComplexBufferListener()); + } + + if(module instanceof ISourceEventListener) + { + mSourceEventBroadcaster.removeListener(((ISourceEventListener) module).getSourceEventListener()); + } + if(module instanceof IMessageListener) { mMessageBroadcaster.removeListener(((IMessageListener) module).getMessageListener()); } - if(module instanceof IFilteredRealBufferListener) - { - mFilteredRealBufferBroadcaster.removeListener(((IFilteredRealBufferListener) module).getFilteredRealBufferListener()); - } - if(module instanceof ISquelchStateListener) { mSquelchStateBroadcaster.removeListener(((ISquelchStateListener) module).getSquelchStateListener()); diff --git a/src/main/java/io/github/dsheirer/module/decode/p25/P25_LSMDecoder.java b/src/main/java/io/github/dsheirer/module/decode/p25/P25_LSMDecoder.java index 9ec1ec00..518eb07f 100644 --- a/src/main/java/io/github/dsheirer/module/decode/p25/P25_LSMDecoder.java +++ b/src/main/java/io/github/dsheirer/module/decode/p25/P25_LSMDecoder.java @@ -1,19 +1,17 @@ /******************************************************************************* - * SDR Trunk - * Copyright (C) 2014,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 + * 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 + * ******************************************************************************/ package io.github.dsheirer.module.decode.p25; @@ -33,7 +31,8 @@ import io.github.dsheirer.instrument.tap.stream.QPSKTap; import io.github.dsheirer.sample.Listener; import io.github.dsheirer.sample.complex.ComplexBuffer; import io.github.dsheirer.sample.complex.ComplexBufferToStreamConverter; -import io.github.dsheirer.sample.complex.IComplexBufferListener; +import io.github.dsheirer.sample.complex.reusable.IReusableComplexBufferListener; +import io.github.dsheirer.sample.complex.reusable.ReusableComplexBuffer; import io.github.dsheirer.source.SourceEvent; import org.slf4j.Logger; import org.slf4j.LoggerFactory; @@ -41,192 +40,207 @@ import org.slf4j.LoggerFactory; import java.util.ArrayList; import java.util.List; -public class P25_LSMDecoder extends P25Decoder implements IComplexBufferListener +public class P25_LSMDecoder extends P25Decoder implements IReusableComplexBufferListener { - private final static Logger mLog = LoggerFactory.getLogger( P25_LSMDecoder.class ); - + private final static Logger mLog = LoggerFactory.getLogger(P25_LSMDecoder.class); + /* Instrumentation Taps */ - private static final String INSTRUMENT_BASEBAND_FILTER_OUTPUT = "Tap Point: Baseband Filter Output"; - private static final String INSTRUMENT_AGC_OUTPUT = "Tap Point: AGC Output"; - private static final String INSTRUMENT_LSM_DEMODULATOR_OUTPUT = "Tap Point: LSM Demodulator Output"; - private static final String INSTRUMENT_QPSK_SLICER_OUTPUT = "Tap Point: QPSK Slicer Output"; + private static final String INSTRUMENT_BASEBAND_FILTER_OUTPUT = "Tap Point: Baseband Filter Output"; + private static final String INSTRUMENT_AGC_OUTPUT = "Tap Point: AGC Output"; + private static final String INSTRUMENT_LSM_DEMODULATOR_OUTPUT = "Tap Point: LSM Demodulator Output"; + private static final String INSTRUMENT_QPSK_SLICER_OUTPUT = "Tap Point: QPSK Slicer Output"; - private List mAvailableTaps; - - private ComplexFIRFilter_CB_CB mBasebandFilter; - private ComplexBufferToStreamConverter mStreamConverter = new ComplexBufferToStreamConverter(); - private ComplexFeedForwardGainControl mAGC = - new ComplexFeedForwardGainControl( 32 ); - private LSMDemodulator mLSMDemodulator = new LSMDemodulator(); - private QPSKPolarSlicer mQPSKSlicer = new QPSKPolarSlicer(); - private P25MessageFramer mMessageFramer; - - public P25_LSMDecoder( AliasList aliasList ) - { - super( aliasList ); - - mBasebandFilter = new ComplexFIRFilter_CB_CB( FilterFactory.getLowPass( - 48000, 7250, 8000, 60, WindowType.HANN, true ), 1.0f ); - - mBasebandFilter.setListener( mStreamConverter ); - - mStreamConverter.setListener( mAGC ); + private List mAvailableTaps; - mAGC.setListener( mLSMDemodulator ); - - mLSMDemodulator.setSymbolListener( mQPSKSlicer ); - - mMessageFramer = new P25MessageFramer( aliasList, mLSMDemodulator ); - mQPSKSlicer.addListener( mMessageFramer ); - - mMessageFramer.setListener( getMessageProcessor() ); - } - - public void dispose() - { - super.dispose(); - - mBasebandFilter.dispose(); - mBasebandFilter = null; - - mStreamConverter.dispose(); - mStreamConverter = null; - - mAGC.dispose(); - mAGC = null; + private ComplexFIRFilter_CB_CB mBasebandFilter; + private ComplexBufferToStreamConverter mStreamConverter = new ComplexBufferToStreamConverter(); + private ComplexFeedForwardGainControl mAGC = + new ComplexFeedForwardGainControl(32); + private LSMDemodulator mLSMDemodulator = new LSMDemodulator(); + private QPSKPolarSlicer mQPSKSlicer = new QPSKPolarSlicer(); + private P25MessageFramer mMessageFramer; + private ReusableBufferListener mReusableBufferListener = new ReusableBufferListener(); - mLSMDemodulator.dispose(); - mLSMDemodulator = null; - - mQPSKSlicer.dispose(); - mQPSKSlicer = null; - - mMessageFramer.dispose(); - mMessageFramer = null; - } + public P25_LSMDecoder(AliasList aliasList) + { + super(aliasList); + + mBasebandFilter = new ComplexFIRFilter_CB_CB(FilterFactory.getLowPass( + 48000, 7250, 8000, 60, WindowType.HANN, true), 1.0f); + + mBasebandFilter.setListener(mStreamConverter); + + mStreamConverter.setListener(mAGC); + + mAGC.setListener(mLSMDemodulator); + + mLSMDemodulator.setSymbolListener(mQPSKSlicer); + + mMessageFramer = new P25MessageFramer(aliasList, mLSMDemodulator); + mQPSKSlicer.addListener(mMessageFramer); + + mMessageFramer.setListener(getMessageProcessor()); + } + + public void dispose() + { + super.dispose(); + + mBasebandFilter.dispose(); + mBasebandFilter = null; + + mStreamConverter.dispose(); + mStreamConverter = null; + + mAGC.dispose(); + mAGC = null; + + mLSMDemodulator.dispose(); + mLSMDemodulator = null; + + mQPSKSlicer.dispose(); + mQPSKSlicer = null; + + mMessageFramer.dispose(); + mMessageFramer = null; + } @Override - public Listener getComplexBufferListener() - { - return mBasebandFilter; - } + public Listener getReusableComplexBufferListener() + { + return mReusableBufferListener; + } - public Modulation getModulation() - { - return Modulation.CQPSK; - } - - /** - * Provides a list of instrumentation taps for monitoring internal processing - */ - @Override + public Modulation getModulation() + { + return Modulation.CQPSK; + } + + /** + * Provides a list of instrumentation taps for monitoring internal processing + */ + @Override public List getTapGroups() { - if( mAvailableTaps == null ) - { - mAvailableTaps = new ArrayList<>(); + if(mAvailableTaps == null) + { + mAvailableTaps = new ArrayList<>(); - TapGroup group = new TapGroup( "P25 LSM Decoder" ); - - group.add( new ComplexTap( INSTRUMENT_BASEBAND_FILTER_OUTPUT, 0, 1.0f ) ); - group.add( new ComplexTap( INSTRUMENT_AGC_OUTPUT, 0, 1.0f ) ); - group.add( new QPSKTap( INSTRUMENT_LSM_DEMODULATOR_OUTPUT, 0, 1.0f ) ); - group.add( new DibitTap( INSTRUMENT_QPSK_SLICER_OUTPUT, 0, 0.1f ) ); + TapGroup group = new TapGroup("P25 LSM Decoder"); - mAvailableTaps.add( group ); - - if( mLSMDemodulator != null ) - { - mAvailableTaps.addAll( mLSMDemodulator.getTapGroups() ); - } - } - - return mAvailableTaps; + group.add(new ComplexTap(INSTRUMENT_BASEBAND_FILTER_OUTPUT, 0, 1.0f)); + group.add(new ComplexTap(INSTRUMENT_AGC_OUTPUT, 0, 1.0f)); + group.add(new QPSKTap(INSTRUMENT_LSM_DEMODULATOR_OUTPUT, 0, 1.0f)); + group.add(new DibitTap(INSTRUMENT_QPSK_SLICER_OUTPUT, 0, 0.1f)); + + mAvailableTaps.add(group); + + if(mLSMDemodulator != null) + { + mAvailableTaps.addAll(mLSMDemodulator.getTapGroups()); + } + } + + return mAvailableTaps; } - /** - * Adds the instrumentation tap - */ - @Override - public void registerTap( Tap tap ) + /** + * Adds the instrumentation tap + */ + @Override + public void registerTap(Tap tap) { - if( mLSMDemodulator != null ) - { - mLSMDemodulator.registerTap( tap ); - } - - switch( tap.getName() ) - { - case INSTRUMENT_BASEBAND_FILTER_OUTPUT: - ComplexSampleTap baseband = (ComplexSampleTap)tap; - mStreamConverter.setListener( baseband ); - baseband.setListener( mAGC ); - break; - case INSTRUMENT_AGC_OUTPUT: - ComplexSampleTap agcSymbol = (ComplexSampleTap)tap; - mAGC.setListener( agcSymbol ); - agcSymbol.setListener( mLSMDemodulator ); - break; - case INSTRUMENT_LSM_DEMODULATOR_OUTPUT: - QPSKTap qpsk = (QPSKTap)tap; - mLSMDemodulator.setSymbolListener( qpsk ); - qpsk.setListener( mQPSKSlicer ); - break; - case INSTRUMENT_QPSK_SLICER_OUTPUT: - mQPSKSlicer.addListener( (DibitTap)tap ); - break; - default: - throw new IllegalArgumentException( "Unrecognized tap: " + - tap.getName() ); - } + if(mLSMDemodulator != null) + { + mLSMDemodulator.registerTap(tap); + } + + switch(tap.getName()) + { + case INSTRUMENT_BASEBAND_FILTER_OUTPUT: + ComplexSampleTap baseband = (ComplexSampleTap)tap; + mStreamConverter.setListener(baseband); + baseband.setListener(mAGC); + break; + case INSTRUMENT_AGC_OUTPUT: + ComplexSampleTap agcSymbol = (ComplexSampleTap)tap; + mAGC.setListener(agcSymbol); + agcSymbol.setListener(mLSMDemodulator); + break; + case INSTRUMENT_LSM_DEMODULATOR_OUTPUT: + QPSKTap qpsk = (QPSKTap)tap; + mLSMDemodulator.setSymbolListener(qpsk); + qpsk.setListener(mQPSKSlicer); + break; + case INSTRUMENT_QPSK_SLICER_OUTPUT: + mQPSKSlicer.addListener((DibitTap)tap); + break; + default: + throw new IllegalArgumentException("Unrecognized tap: " + + tap.getName()); + } } - /** - * Removes the instrumentation tap - */ - @Override - public void unregisterTap( Tap tap ) + /** + * Removes the instrumentation tap + */ + @Override + public void unregisterTap(Tap tap) { - if( mLSMDemodulator != null ) - { - mLSMDemodulator.unregisterTap( tap ); - } - - switch( tap.getName() ) - { - case INSTRUMENT_BASEBAND_FILTER_OUTPUT: - mStreamConverter.setListener( mAGC ); - break; - case INSTRUMENT_AGC_OUTPUT: - mAGC.setListener( mLSMDemodulator ); - break; - case INSTRUMENT_LSM_DEMODULATOR_OUTPUT: - mLSMDemodulator.setSymbolListener( mQPSKSlicer ); - break; - case INSTRUMENT_QPSK_SLICER_OUTPUT: - mQPSKSlicer.removeListener( (DibitTap)tap ); - break; - default: - throw new IllegalArgumentException( "Unrecognized tap: " + - tap.getName() ); - } + if(mLSMDemodulator != null) + { + mLSMDemodulator.unregisterTap(tap); + } + + switch(tap.getName()) + { + case INSTRUMENT_BASEBAND_FILTER_OUTPUT: + mStreamConverter.setListener(mAGC); + break; + case INSTRUMENT_AGC_OUTPUT: + mAGC.setListener(mLSMDemodulator); + break; + case INSTRUMENT_LSM_DEMODULATOR_OUTPUT: + mLSMDemodulator.setSymbolListener(mQPSKSlicer); + break; + case INSTRUMENT_QPSK_SLICER_OUTPUT: + mQPSKSlicer.removeListener((DibitTap)tap); + break; + default: + throw new IllegalArgumentException("Unrecognized tap: " + + tap.getName()); + } } - @Override - public Listener getSourceEventListener() - { - return null; - } + @Override + public Listener getSourceEventListener() + { + return null; + } - @Override - public void setSourceEventListener(Listener listener) - { + @Override + public void setSourceEventListener(Listener listener) + { - } + } - @Override - public void removeSourceEventListener() - { + @Override + public void removeSourceEventListener() + { - } + } + + public class ReusableBufferListener implements Listener + { + @Override + public void receive(ReusableComplexBuffer reusableComplexBuffer) + { + float[] samples = reusableComplexBuffer.getCopyOfSamples(); + + //TODO: redesign the filter chain so that we can simply pass a float array ... + + mBasebandFilter.receive(new ComplexBuffer(samples)); + reusableComplexBuffer.decrementUserCount(); + } + } } diff --git a/src/main/java/io/github/dsheirer/module/demodulate/am/AMDemodulatorModule.java b/src/main/java/io/github/dsheirer/module/demodulate/am/AMDemodulatorModule.java index 0cbfdf7a..8677b89a 100644 --- a/src/main/java/io/github/dsheirer/module/demodulate/am/AMDemodulatorModule.java +++ b/src/main/java/io/github/dsheirer/module/demodulate/am/AMDemodulatorModule.java @@ -1,19 +1,17 @@ /******************************************************************************* - * SDR Trunk - * Copyright (C) 2014,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 + * 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 + * ******************************************************************************/ package io.github.dsheirer.module.demodulate.am; @@ -26,12 +24,12 @@ import io.github.dsheirer.dsp.gain.AutomaticGainControl_RB; import io.github.dsheirer.module.Module; import io.github.dsheirer.sample.Listener; import io.github.dsheirer.sample.complex.ComplexBuffer; -import io.github.dsheirer.sample.complex.IComplexBufferListener; +import io.github.dsheirer.sample.complex.reusable.IReusableComplexBufferListener; +import io.github.dsheirer.sample.complex.reusable.ReusableComplexBuffer; import io.github.dsheirer.sample.real.IFilteredRealBufferProvider; import io.github.dsheirer.sample.real.RealBuffer; -public class AMDemodulatorModule extends Module - implements IComplexBufferListener, IFilteredRealBufferProvider +public class AMDemodulatorModule extends Module implements IReusableComplexBufferListener, IFilteredRealBufferProvider { private static final int SAMPLE_RATE = 48000; @@ -39,6 +37,7 @@ public class AMDemodulatorModule extends Module private AMDemodulator_CB mDemodulator; private RealFIRFilter_RB_RB mLowPassFilter; private AutomaticGainControl_RB mAGC; + private ReusableBufferListener mReusableBufferListener = new ReusableBufferListener(); /** * AM Demodulator Module @@ -60,9 +59,9 @@ public class AMDemodulatorModule extends Module } @Override - public Listener getComplexBufferListener() + public Listener getReusableComplexBufferListener() { - return mIQFilter; + return mReusableBufferListener; } @Override @@ -107,4 +106,19 @@ public class AMDemodulatorModule extends Module public void stop() { } + + + public class ReusableBufferListener implements Listener + { + @Override + public void receive(ReusableComplexBuffer reusableComplexBuffer) + { + float[] samples = reusableComplexBuffer.getCopyOfSamples(); + + //TODO: redesign the filter chain so that we can simply pass a float array ... + + mIQFilter.receive(new ComplexBuffer(samples)); + reusableComplexBuffer.decrementUserCount(); + } + } } diff --git a/src/main/java/io/github/dsheirer/module/demodulate/fm/FMDemodulatorModule.java b/src/main/java/io/github/dsheirer/module/demodulate/fm/FMDemodulatorModule.java index 7a06160a..72380d34 100644 --- a/src/main/java/io/github/dsheirer/module/demodulate/fm/FMDemodulatorModule.java +++ b/src/main/java/io/github/dsheirer/module/demodulate/fm/FMDemodulatorModule.java @@ -24,13 +24,14 @@ import io.github.dsheirer.dsp.fm.FMDemodulator_CB; import io.github.dsheirer.module.Module; import io.github.dsheirer.sample.Listener; import io.github.dsheirer.sample.complex.ComplexBuffer; -import io.github.dsheirer.sample.complex.IComplexBufferListener; +import io.github.dsheirer.sample.complex.reusable.IReusableComplexBufferListener; +import io.github.dsheirer.sample.complex.reusable.ReusableComplexBuffer; import io.github.dsheirer.sample.real.IUnFilteredRealBufferProvider; import io.github.dsheirer.sample.real.RealBuffer; import org.slf4j.Logger; import org.slf4j.LoggerFactory; -public class FMDemodulatorModule extends Module implements IComplexBufferListener, IUnFilteredRealBufferProvider +public class FMDemodulatorModule extends Module implements IReusableComplexBufferListener, IUnFilteredRealBufferProvider { private final static Logger mLog = LoggerFactory.getLogger(FMDemodulatorModule.class); @@ -38,6 +39,7 @@ public class FMDemodulatorModule extends Module implements IComplexBufferListene private ComplexFIRFilter_CB_CB mIQFilter; private FMDemodulator_CB mDemodulator; + private ReusableBufferListener mReusableBufferListener = new ReusableBufferListener(); /** * FM Demodulator with I/Q filter. Demodulated output is unfiltered and @@ -68,9 +70,9 @@ public class FMDemodulatorModule extends Module implements IComplexBufferListene } @Override - public Listener getComplexBufferListener() + public Listener getReusableComplexBufferListener() { - return mIQFilter; + return mReusableBufferListener; } @Override @@ -110,4 +112,18 @@ public class FMDemodulatorModule extends Module implements IComplexBufferListene public void stop() { } + + public class ReusableBufferListener implements Listener + { + @Override + public void receive(ReusableComplexBuffer reusableComplexBuffer) + { + float[] samples = reusableComplexBuffer.getCopyOfSamples(); + + //TODO: redesign the filter chain so that we can simply pass a float array ... + + mIQFilter.receive(new ComplexBuffer(samples)); + reusableComplexBuffer.decrementUserCount(); + } + } } diff --git a/src/main/java/io/github/dsheirer/record/wave/ComplexBufferWaveRecorder.java b/src/main/java/io/github/dsheirer/record/wave/ComplexBufferWaveRecorder.java index a2e203c6..12b519a3 100644 --- a/src/main/java/io/github/dsheirer/record/wave/ComplexBufferWaveRecorder.java +++ b/src/main/java/io/github/dsheirer/record/wave/ComplexBufferWaveRecorder.java @@ -1,28 +1,26 @@ /******************************************************************************* - * SDR Trunk - * Copyright (C) 2014,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 + * 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 + * ******************************************************************************/ package io.github.dsheirer.record.wave; import io.github.dsheirer.module.Module; -import io.github.dsheirer.sample.Buffer; import io.github.dsheirer.sample.ConversionUtils; import io.github.dsheirer.sample.Listener; -import io.github.dsheirer.sample.complex.ComplexBuffer; -import io.github.dsheirer.sample.complex.IComplexBufferListener; +import io.github.dsheirer.sample.complex.reusable.IReusableBufferDisposedListener; +import io.github.dsheirer.sample.complex.reusable.IReusableComplexBufferListener; +import io.github.dsheirer.sample.complex.reusable.ReusableComplexBuffer; import io.github.dsheirer.util.ThreadPool; import io.github.dsheirer.util.TimeStamp; import org.slf4j.Logger; @@ -40,32 +38,26 @@ import java.util.concurrent.atomic.AtomicBoolean; /** * WAVE audio recorder module for recording complex (I&Q) samples to a wave file */ -public class ComplexBufferWaveRecorder extends Module - implements IComplexBufferListener, Listener +public class ComplexBufferWaveRecorder extends Module implements IReusableComplexBufferListener, Listener { - private final static Logger mLog = LoggerFactory.getLogger( ComplexBufferWaveRecorder.class ); + private final static Logger mLog = LoggerFactory.getLogger(ComplexBufferWaveRecorder.class); private WaveWriter mWriter; private String mFilePrefix; private Path mFile; - private AudioFormat mAudioFormat; - - private BufferProcessor mBufferProcessor; - private ScheduledFuture mProcessorHandle; - private LinkedBlockingQueue mBuffers = new LinkedBlockingQueue<>( 500 ); - - private AtomicBoolean mRunning = new AtomicBoolean(); - - public ComplexBufferWaveRecorder( int sampleRate, String filePrefix ) - { - mAudioFormat = new AudioFormat( sampleRate, //SampleRate - 16, //Sample Size - 2, //Channels - true, //Signed - false ); //Little Endian + private AudioFormat mAudioFormat; - mFilePrefix = filePrefix; - } + private BufferProcessor mBufferProcessor; + private ScheduledFuture mProcessorHandle; + private LinkedBlockingQueue mBuffers = new LinkedBlockingQueue<>(500); + + private AtomicBoolean mRunning = new AtomicBoolean(); + + public ComplexBufferWaveRecorder(int sampleRate, String filePrefix) + { + mAudioFormat = new AudioFormat(sampleRate, 16, 2, true, false); + mFilePrefix = filePrefix; + } public Path getFile() { @@ -81,137 +73,148 @@ public class ComplexBufferWaveRecorder extends Module mBufferProcessor = new BufferProcessor(); } - try - { - StringBuilder sb = new StringBuilder(); - sb.append( mFilePrefix ); - sb.append( "_" ); - sb.append( TimeStamp.getTimeStamp( "_" ) ); - sb.append( ".wav" ); - - mFile = Paths.get( sb.toString() ); - - mWriter = new WaveWriter( mAudioFormat, mFile ); - - /* Schedule the processor to run every 500 milliseconds */ - mProcessorHandle = ThreadPool.SCHEDULED.scheduleAtFixedRate( - mBufferProcessor, 0, 500, TimeUnit.MILLISECONDS ); - } - catch( IOException io ) - { - mLog.error( "Error starting complex baseband recorder", io ); - } - } - } - - public void stop() - { - if( mRunning.compareAndSet( true, false ) ) - { - receive( new PoisonPill() ); - } - } - - @Override - public void receive( ComplexBuffer buffer ) - { - if( mRunning.get() ) - { - boolean success = mBuffers.offer( buffer ); - - if( !success ) - { - mLog.error( "recorder buffer overflow - purging [" + - mFile.toFile().getAbsolutePath() + "]" ); - - mBuffers.clear(); - } - } - } - - @Override - public Listener getComplexBufferListener() - { - return this; - } - - @Override - public void dispose() - { - stop(); - } - - @Override - public void reset() - { - } - - public class BufferProcessor implements Runnable - { - public void run() - { - try + try { - Buffer buffer = mBuffers.poll(); - - while( buffer != null ) - { - if( buffer instanceof PoisonPill ) - { - buffer = null; - - mBuffers.clear(); - - if( mWriter != null ) - { - try - { - mWriter.close(); - mWriter = null; - } - catch( IOException io ) - { - mLog.error( "Error stopping complex wave recorder [" + - getFile() + "]", io ); - } - } - - mFile = null; + StringBuilder sb = new StringBuilder(); + sb.append(mFilePrefix); + sb.append("_"); + sb.append(TimeStamp.getTimeStamp("_")); + sb.append(".wav"); - if( mProcessorHandle != null ) - { - mProcessorHandle.cancel( true ); - } - - mProcessorHandle = null; - } - else - { - mWriter.writeData( ConversionUtils.convertToSigned16BitSamples( buffer ) ); - buffer = mBuffers.poll(); - } - } + mFile = Paths.get(sb.toString()); + + mWriter = new WaveWriter(mAudioFormat, mFile); + + /* Schedule the processor to run every 500 milliseconds */ + mProcessorHandle = ThreadPool.SCHEDULED.scheduleAtFixedRate( + mBufferProcessor, 0, 500, TimeUnit.MILLISECONDS); } - catch ( IOException ioe ) - { - /* Stop this module if/when we get an IO exception */ - mBuffers.clear(); - stop(); - - mLog.error( "IOException while trying to write to the wave " - + "writer", ioe ); - } - } + catch(IOException io) + { + mLog.error("Error starting complex baseband recorder", io); + } + } } - /** - * This is used as a sentinel value to signal the buffer processor to end - */ - public class PoisonPill extends ComplexBuffer - { - public PoisonPill() - { - super( new float[ 1 ] ); - } - } + public void stop() + { + if(mRunning.compareAndSet(true, false)) + { + receive(new PoisonPill()); + } + } + + @Override + public void receive(ReusableComplexBuffer buffer) + { + if(mRunning.get()) + { + boolean success = mBuffers.offer(buffer); + + if(!success) + { + mLog.error("recorder buffer overflow - purging [" + + mFile.toFile().getAbsolutePath() + "]"); + + mBuffers.clear(); + } + } + } + + @Override + public Listener getReusableComplexBufferListener() + { + return this; + } + + @Override + public void dispose() + { + stop(); + } + + @Override + public void reset() + { + } + + public class BufferProcessor implements Runnable + { + public void run() + { + try + { + ReusableComplexBuffer reusableComplexBuffer = mBuffers.poll(); + + while(reusableComplexBuffer != null) + { + if(reusableComplexBuffer instanceof PoisonPill) + { + //Clear the buffer queue + reusableComplexBuffer = mBuffers.poll(); + + while(reusableComplexBuffer != null) + { + reusableComplexBuffer.decrementUserCount(); + reusableComplexBuffer = mBuffers.poll(); + } + + if(mWriter != null) + { + try + { + mWriter.close(); + mWriter = null; + } + catch(IOException io) + { + mLog.error("Error stopping complex wave recorder [" + getFile() + "]", io); + } + } + + mFile = null; + + if(mProcessorHandle != null) + { + mProcessorHandle.cancel(true); + } + + mProcessorHandle = null; + } + else + { + mWriter.writeData(ConversionUtils.convertToSigned16BitSamples(reusableComplexBuffer)); + reusableComplexBuffer.decrementUserCount(); + reusableComplexBuffer = mBuffers.poll(); + } + } + } + catch(IOException ioe) + { + /* Stop this module if/when we get an IO exception */ + mBuffers.clear(); + stop(); + + mLog.error("IOException while trying to write to the wave writer", ioe); + } + } + } + + /** + * This is used as a sentinel value to signal the buffer processor to end + */ + public class PoisonPill extends ReusableComplexBuffer + { + public PoisonPill() + { + super(new IReusableBufferDisposedListener() + { + @Override + public void disposed(ReusableComplexBuffer reusableComplexBuffer) + { + //no-op + } + }, new float[2], System.currentTimeMillis()); + } + } } diff --git a/src/main/java/io/github/dsheirer/sample/Broadcaster.java b/src/main/java/io/github/dsheirer/sample/Broadcaster.java index 52df7104..c705bf1d 100644 --- a/src/main/java/io/github/dsheirer/sample/Broadcaster.java +++ b/src/main/java/io/github/dsheirer/sample/Broadcaster.java @@ -25,7 +25,7 @@ import java.util.concurrent.CopyOnWriteArrayList; */ public class Broadcaster implements Listener { - private List> mListeners = new CopyOnWriteArrayList<>(); + protected List> mListeners = new CopyOnWriteArrayList<>(); /** * Implements the Listener interface to receive an element and broadcast that element to all registered diff --git a/src/main/java/io/github/dsheirer/sample/Buffer.java b/src/main/java/io/github/dsheirer/sample/Buffer.java index bb1f6aa4..bb059cb9 100644 --- a/src/main/java/io/github/dsheirer/sample/Buffer.java +++ b/src/main/java/io/github/dsheirer/sample/Buffer.java @@ -17,7 +17,7 @@ package io.github.dsheirer.sample; public class Buffer { - private float[] mSamples; + protected float[] mSamples; /** * Buffer containing float sample data diff --git a/src/main/java/io/github/dsheirer/sample/OverflowableTransferQueue.java b/src/main/java/io/github/dsheirer/sample/OverflowableTransferQueue.java index 6eb49d6d..c2b5a6d9 100644 --- a/src/main/java/io/github/dsheirer/sample/OverflowableTransferQueue.java +++ b/src/main/java/io/github/dsheirer/sample/OverflowableTransferQueue.java @@ -36,9 +36,9 @@ public class OverflowableTransferQueue private IOverflowListener mOverflowListener; private Source mSourceOverflowListener; - private LinkedTransferQueue mQueue = new LinkedTransferQueue(); - private AtomicInteger mCounter = new AtomicInteger(); - private AtomicBoolean mOverflow = new AtomicBoolean(); + protected LinkedTransferQueue mQueue = new LinkedTransferQueue(); + protected AtomicInteger mCounter = new AtomicInteger(); + protected AtomicBoolean mOverflow = new AtomicBoolean(); private int mMaximumSize; private int mResetThreshold; @@ -73,6 +73,21 @@ public class OverflowableTransferQueue setOverflow(true); } } + else + { + overflow(e); + } + } + + /** + * Invoked when the buffer is in an overflow state. The element argument is thrown away. Override this method + * in subclasses to perform any necessary cleanup action(s). + * + * @param e element that is being thrown away due to an overflow condition + */ + protected void overflow(E e) + { + //No-op. Override in subclass to perform any cleanup actions during overflow } /** diff --git a/src/main/java/io/github/dsheirer/sample/adapter/ComplexShortAdapter.java b/src/main/java/io/github/dsheirer/sample/adapter/ComplexShortAdapter.java new file mode 100644 index 00000000..705a5448 --- /dev/null +++ b/src/main/java/io/github/dsheirer/sample/adapter/ComplexShortAdapter.java @@ -0,0 +1,58 @@ +/******************************************************************************* + * 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 + * + ******************************************************************************/ +package io.github.dsheirer.sample.adapter; + +import io.github.dsheirer.sample.complex.reusable.ReusableComplexBuffer; + +import java.nio.ByteBuffer; +import java.nio.ByteOrder; +import java.nio.FloatBuffer; + +/** + * Converts 16-bit little endian byte data into an array of complex floats + */ +public class ComplexShortAdapter implements IComplexSampleAdapter +{ + private ShortToFloatMap mMap = new ShortToFloatMap(); + private FloatBuffer mFloatBuffer; + private ByteOrder mByteOrder = ByteOrder.LITTLE_ENDIAN; + + @Override + public void convertAndLoad(byte[] samples, ReusableComplexBuffer reusableComplexBuffer) + { + int sampleCount = samples.length / 2; + + if(mFloatBuffer == null || mFloatBuffer.capacity() != sampleCount) + { + mFloatBuffer = FloatBuffer.allocate(sampleCount); + } + + mFloatBuffer.rewind(); + + /* Wrap byte array in a byte buffer so we can process them as shorts */ + ByteBuffer buffer = ByteBuffer.wrap(samples); + + /* Set endian to correct byte ordering */ + buffer.order(mByteOrder); + + while(buffer.hasRemaining()) + { + mFloatBuffer.put(mMap.get(buffer.getShort())); + } + + reusableComplexBuffer.reloadFrom(mFloatBuffer, System.currentTimeMillis()); + } +} diff --git a/src/main/java/io/github/dsheirer/sample/adapter/IComplexSampleAdapter.java b/src/main/java/io/github/dsheirer/sample/adapter/IComplexSampleAdapter.java new file mode 100644 index 00000000..471ee032 --- /dev/null +++ b/src/main/java/io/github/dsheirer/sample/adapter/IComplexSampleAdapter.java @@ -0,0 +1,28 @@ +/******************************************************************************* + * 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 + * + ******************************************************************************/ +package io.github.dsheirer.sample.adapter; + +import io.github.dsheirer.sample.complex.reusable.ReusableComplexBuffer; + +public interface IComplexSampleAdapter +{ + /** + * Converts the sample byte array into complex float samples, loading the converted float samples into the buffer. + * @param samples + * @param reusableComplexBuffer + */ + void convertAndLoad(byte[] samples, ReusableComplexBuffer reusableComplexBuffer); +} diff --git a/src/main/java/io/github/dsheirer/sample/complex/ComplexBuffer.java b/src/main/java/io/github/dsheirer/sample/complex/ComplexBuffer.java index 884a6f20..edc3d4ce 100644 --- a/src/main/java/io/github/dsheirer/sample/complex/ComplexBuffer.java +++ b/src/main/java/io/github/dsheirer/sample/complex/ComplexBuffer.java @@ -19,9 +19,6 @@ import io.github.dsheirer.sample.Buffer; public class ComplexBuffer extends Buffer { - //Default timestamp - we use a static variable here so that there is only ever 1 instance in memory - private static final long DEFAULT_TIMESTAMP = 0; - /** * Wrapper around float array containing interleaved I/Q samples */ @@ -29,24 +26,4 @@ public class ComplexBuffer extends Buffer { super(samples); } - - - /** - * Reference timestamp for the first sample in this buffer. This functionality is intended to be implemented by a - * subclass implementation. The default value is 0. - * - * @return reference timestamp or a value of 0 if hasTimestamp() indicates false. - */ - public long getTimestamp() - { - return DEFAULT_TIMESTAMP; - } - - /** - * Indicates if this buffer has a timestamp that references the first sample in the buffer. - */ - public boolean hasTimestamp() - { - return false; - } } diff --git a/src/main/java/io/github/dsheirer/sample/complex/reusable/OverflowableReusableBufferTransferQueue.java b/src/main/java/io/github/dsheirer/sample/complex/reusable/OverflowableReusableBufferTransferQueue.java new file mode 100644 index 00000000..05486981 --- /dev/null +++ b/src/main/java/io/github/dsheirer/sample/complex/reusable/OverflowableReusableBufferTransferQueue.java @@ -0,0 +1,69 @@ +/******************************************************************************* + * 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 + * + ******************************************************************************/ +package io.github.dsheirer.sample.complex.reusable; + +import io.github.dsheirer.sample.OverflowableTransferQueue; + +public class OverflowableReusableBufferTransferQueue extends OverflowableTransferQueue +{ + /** + * Concurrent transfer queue that couples a higher-throughput linked transfer queue with an atomic integer for + * monitoring queue size. When the queue size exceeds maximum size (overflow), all inbound elements are ignored + * until the queue size is reduced to or below the reset threshold. + * + * This implementation includes special handling for reusable complex buffers. + * + * @param maximumSize of the queue. Overflow state will occur once queue size exceeds this value. + * @param resetThreshold for resetting overflow state to normal, once queue size is at or below this value. + */ + public OverflowableReusableBufferTransferQueue(int maximumSize, int resetThreshold) + { + super(maximumSize, resetThreshold); + } + + /** + * Overrides the overflow method to decrement the user count on any buffers that are being discarded when the queue + * is in an overflow state. + * + * @param reusableComplexBuffer that will be discarded + */ + @Override + protected void overflow(ReusableComplexBuffer reusableComplexBuffer) + { + reusableComplexBuffer.decrementUserCount(); + } + + /** + * Overrides the buffer clear method to decrement the user count on each buffer that is being cleared from the queue. + */ + @Override + public void clear() + { + synchronized(mQueue) + { + ReusableComplexBuffer buffer = mQueue.poll(); + + while(buffer != null) + { + buffer.decrementUserCount(); + buffer = mQueue.poll(); + } + + mCounter.set(0); + mOverflow.set(false); + } + } +} diff --git a/src/main/java/io/github/dsheirer/source/ComplexSource.java b/src/main/java/io/github/dsheirer/source/ComplexSource.java index e23e3305..a06be2dc 100644 --- a/src/main/java/io/github/dsheirer/source/ComplexSource.java +++ b/src/main/java/io/github/dsheirer/source/ComplexSource.java @@ -1,10 +1,25 @@ +/******************************************************************************* + * 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 + * + ******************************************************************************/ package io.github.dsheirer.source; import io.github.dsheirer.sample.Provider; import io.github.dsheirer.sample.SampleType; -import io.github.dsheirer.sample.complex.ComplexBuffer; +import io.github.dsheirer.sample.complex.reusable.ReusableComplexBuffer; -public abstract class ComplexSource extends Source implements Provider +public abstract class ComplexSource extends Source implements Provider { @Override public SampleType getSampleType() diff --git a/src/main/java/io/github/dsheirer/source/mixer/ComplexMixer.java b/src/main/java/io/github/dsheirer/source/mixer/ComplexMixer.java index 735afe52..fc117def 100644 --- a/src/main/java/io/github/dsheirer/source/mixer/ComplexMixer.java +++ b/src/main/java/io/github/dsheirer/source/mixer/ComplexMixer.java @@ -1,25 +1,24 @@ /******************************************************************************* - * SDR Trunk - * Copyright (C) 2014 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 + * 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 + * ******************************************************************************/ package io.github.dsheirer.source.mixer; import io.github.dsheirer.sample.Listener; -import io.github.dsheirer.sample.adapter.ISampleAdapter; -import io.github.dsheirer.sample.complex.ComplexBuffer; +import io.github.dsheirer.sample.adapter.IComplexSampleAdapter; +import io.github.dsheirer.sample.complex.reusable.ReusableBufferQueue; +import io.github.dsheirer.sample.complex.reusable.ReusableComplexBuffer; import io.github.dsheirer.source.SourceException; import org.slf4j.Logger; import org.slf4j.LoggerFactory; @@ -32,87 +31,72 @@ import java.util.concurrent.atomic.AtomicBoolean; public class ComplexMixer { - private final static Logger mLog = - LoggerFactory.getLogger( ComplexMixer.class ); + private final static Logger mLog = LoggerFactory.getLogger(ComplexMixer.class); - private long mFrequency = 0; - private int mBufferSize = 16384; - - private BufferReader mBufferReader = new BufferReader(); - private TargetDataLine mTargetDataLine; - private AudioFormat mAudioFormat; - private String mName; - private int mBytesPerFrame = 0; - private ISampleAdapter mSampleAdapter; - private Listener mListener; - - /** - * Complex Mixer - constructs a reader on the mixer/sound card target - * data line using the specified audio format (sample size, sample rate ) - * and broadcasts complex (I/Q) sample buffers to all registered listeners. - * Reads sample buffers sized to 10% of the sample rate specified in audio - * format. - * - * @param targetDataLine - mixer or sound card to be used - * - * @param format - audio format - * - * @param name - token name to use for this mixer - * - * @param sampleAdapter - adapter to convert byte array data read from the - * mixer into ComplexBuffer. The adapter can optionally invert the channel - * data if the left/right stereo channels are inverted. - */ - public ComplexMixer( TargetDataLine targetDataLine, - AudioFormat format, - String name, - ISampleAdapter sampleAdapter, - Listener listener ) + private long mFrequency = 0; + + private BufferReader mBufferReader = new BufferReader(); + private TargetDataLine mTargetDataLine; + private AudioFormat mAudioFormat; + private String mName; + private int mBytesPerFrame = 0; + private IComplexSampleAdapter mSampleAdapter; + private Listener mListener; + private ReusableBufferQueue mReusableBufferQueue = new ReusableBufferQueue(); + + /** + * Complex Mixer - constructs a reader on the mixer/sound card target data line using the specified audio + * format (sample size, sample rate ) and broadcasts complex (I/Q) sample buffers to all registered listeners. + * Reads sample buffers sized to 10% of the sample rate specified in audio format. + * + * @param targetDataLine - mixer or sound card to be used + * @param format - audio format + * @param name - token name to use for this mixer + * @param complexSampleAdapter - adapter to convert byte array data read from the mixer into ComplexBuffer. + * @param listener to receive generated complex buffers + */ + public ComplexMixer(TargetDataLine targetDataLine, AudioFormat format, String name, + IComplexSampleAdapter complexSampleAdapter, Listener listener) { - mTargetDataLine = targetDataLine; - mName = name; + mTargetDataLine = targetDataLine; + mName = name; mAudioFormat = format; - mSampleAdapter = sampleAdapter; + mSampleAdapter = complexSampleAdapter; mListener = listener; } - - public TargetDataLine getTargetDataLine() - { - return mTargetDataLine; - } - + public void start() { - if( !mBufferReader.isRunning() ) - { - Thread thread = new Thread( mBufferReader ); - thread.setDaemon( true ); - thread.setName( mName + " Complex Sample Reader" ); - thread.start(); - } + if(!mBufferReader.isRunning()) + { + Thread thread = new Thread(mBufferReader); + thread.setDaemon(true); + thread.setName(mName + " Complex Sample Reader"); + thread.start(); + } } public void stop() { - mBufferReader.stop(); + mBufferReader.stop(); + } + + @Override + public String toString() + { + return mName; } - @Override - public String toString() - { - return mName; - } - public int getSampleRate() { - if( mTargetDataLine != null ) - { - return (int)mTargetDataLine.getFormat().getSampleRate(); - } - else - { - return 0; - } + if(mTargetDataLine != null) + { + return (int)mTargetDataLine.getFormat().getSampleRate(); + } + else + { + return 0; + } } /** @@ -120,7 +104,7 @@ public class ComplexMixer */ public long getFrequency() throws SourceException { - return mFrequency; + return mFrequency; } /** @@ -128,125 +112,119 @@ public class ComplexMixer * be useful if you are streaming an external audio source in through the * sound card and you want to specify a frequency for that source */ - public void setFrequency( long frequency ) + public void setFrequency(long frequency) { - mFrequency = frequency; + mFrequency = frequency; } - /** - * Reader thread. Performs blocking read against the mixer target data - * line, converts the samples to an array of floats using the specified - * adapter. Dispatches float arrays to all registered listeners. - */ - public class BufferReader implements Runnable - { - private AtomicBoolean mRunning = new AtomicBoolean(); - - @Override + /** + * Reader thread. Performs blocking read against the mixer target data + * line, converts the samples to an array of floats using the specified + * adapter. Dispatches float arrays to all registered listeners. + */ + public class BufferReader implements Runnable + { + private AtomicBoolean mRunning = new AtomicBoolean(); + + @Override public void run() { - if( mRunning.compareAndSet( false, true ) ) - { - if( mTargetDataLine == null ) - { - mRunning.set( false ); + if(mRunning.compareAndSet(false, true)) + { + if(mTargetDataLine == null) + { + mRunning.set(false); + mLog.error("ComplexMixerSource - mixer target data line is null"); + } + else + { + mBytesPerFrame = mAudioFormat.getSampleSizeInBits() / 8; - mLog.error( "ComplexMixerSource - mixer target data line" - + " is null" ); - } - else - { - mBytesPerFrame = mAudioFormat.getSampleSizeInBits() / 8; - - if( mBytesPerFrame == AudioSystem.NOT_SPECIFIED ) - { - mBytesPerFrame = 2; - } - - /* Set buffer size to 1/10 second of samples */ - mBufferSize = (int)( mAudioFormat.getSampleRate() - * 0.05 ) * mBytesPerFrame; - - /* We'll reuse the same buffer for each read */ - byte[] buffer = new byte[ mBufferSize ]; - - try + if(mBytesPerFrame == AudioSystem.NOT_SPECIFIED) { - mTargetDataLine.open( mAudioFormat ); - - mTargetDataLine.start(); + mBytesPerFrame = 2; } - catch ( LineUnavailableException e ) + + //Set buffer size to 1/10 second of samples + int sampleCount = (int)(mAudioFormat.getSampleRate() * 0.10) * 2; + int bufferSize = sampleCount * mBytesPerFrame; + + //We'll reuse the same buffer for each read + byte[] buffer = new byte[bufferSize]; + + try { - mLog.error( "ComplexMixerSource - mixer target data line" - + "not available to read data from", e ); - - mRunning.set( false ); + mTargetDataLine.open(mAudioFormat); + mTargetDataLine.start(); + } + catch(LineUnavailableException e) + { + mLog.error("ComplexMixerSource - mixer target data line not available to read data from", e); + mRunning.set(false); } - - while( mRunning.get() ) - { - try - { - /* Blocking read - waits until the buffer fills */ - mTargetDataLine.read( buffer, 0, buffer.length ); - /* Convert samples to float array */ - float[] samples = - mSampleAdapter.convert( buffer ); - - /* Dispatch samples to registered listeners */ - if( mListener != null ) - { - mListener.receive( new ComplexBuffer( samples ) ); - } - } - catch ( Exception e ) + while(mRunning.get()) + { + try { - mLog.error( "error while reading" - + "from the mixer target data line", e ); - - mRunning.set( false ); + //Blocking read - waits until the buffer fills + mTargetDataLine.read(buffer, 0, buffer.length); + + /* Dispatch samples to registered listeners */ + if(mListener != null) + { + ReusableComplexBuffer reusableComplexBuffer = mReusableBufferQueue.getBuffer(sampleCount); + + //Convert samples and load into reusable buffer + mSampleAdapter.convertAndLoad(buffer, reusableComplexBuffer); + + mListener.receive(reusableComplexBuffer); + } } - } - } - - /* Close the data line if it is still open */ - if( mTargetDataLine != null && mTargetDataLine.isOpen() ) - { - mTargetDataLine.close(); - } - } + catch(Exception e) + { + mLog.error("error while reading from the mixer target data line", e); + mRunning.set(false); + } + } + } + + /* Close the data line if it is still open */ + if(mTargetDataLine != null && mTargetDataLine.isOpen()) + { + mTargetDataLine.close(); + } + } } - /** - * Stops the reader thread - */ - public void stop() - { - if( mRunning.compareAndSet( true, false ) ) - { - mTargetDataLine.stop(); - mTargetDataLine.close(); - } - } + /** + * Stops the reader thread + */ + public void stop() + { + if(mRunning.compareAndSet(true, false)) + { + mTargetDataLine.stop(); + mTargetDataLine.close(); + } + } - /** - * Indicates if the reader thread is running - */ - public boolean isRunning() - { - return mRunning.get(); - } - } + /** + * Indicates if the reader thread is running + */ + public boolean isRunning() + { + return mRunning.get(); + } + } public void dispose() { - if( mBufferReader != null ) - { - mBufferReader.stop(); - } - - mListener = null; + if(mBufferReader != null) + { + mBufferReader.stop(); + } + + mListener = null; } } diff --git a/src/main/java/io/github/dsheirer/source/recording/Recording.java b/src/main/java/io/github/dsheirer/source/recording/Recording.java index 1fb2495f..77be419b 100644 --- a/src/main/java/io/github/dsheirer/source/recording/Recording.java +++ b/src/main/java/io/github/dsheirer/source/recording/Recording.java @@ -19,7 +19,6 @@ import io.github.dsheirer.settings.SettingsManager; import io.github.dsheirer.source.ISourceEventProcessor; import io.github.dsheirer.source.SourceEvent; import io.github.dsheirer.source.SourceException; -import io.github.dsheirer.source.tuner.ITunerChannelProvider; import io.github.dsheirer.source.tuner.channel.TunerChannel; import io.github.dsheirer.source.tuner.channel.TunerChannelSource; @@ -27,16 +26,14 @@ import java.io.File; import java.util.ArrayList; import java.util.List; -public class Recording implements Comparable, ISourceEventProcessor, ITunerChannelProvider +public class Recording implements Comparable, ISourceEventProcessor { private RecordingConfiguration mConfiguration; private List mTunerChannels = new ArrayList(); - @SuppressWarnings( "unused" ) private long mCenterFrequency; - @SuppressWarnings( "unused" ) private SettingsManager mSettingsManager; public Recording( SettingsManager settingsManager, @@ -75,19 +72,12 @@ public class Recording implements Comparable, ISourceEventProcessor, return mTunerChannels.size() > 0; } - @Override public TunerChannelSource getChannel( TunerChannel channel ) { // TODO Auto-generated method stub return null; } - @Override - public void releaseChannel( TunerChannelSource source ) - { - // TODO Auto-generated method stub - } - public RecordingConfiguration getRecordingConfiguration() { return mConfiguration; diff --git a/src/main/java/io/github/dsheirer/source/tuner/ITunerChannelProvider.java b/src/main/java/io/github/dsheirer/source/tuner/ITunerChannelProvider.java deleted file mode 100644 index 550931c9..00000000 --- a/src/main/java/io/github/dsheirer/source/tuner/ITunerChannelProvider.java +++ /dev/null @@ -1,42 +0,0 @@ -/******************************************************************************* - * SDR Trunk - * Copyright (C) 2014-2016 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 io.github.dsheirer.source.tuner; - -import io.github.dsheirer.source.SourceException; -import io.github.dsheirer.source.tuner.channel.TunerChannel; -import io.github.dsheirer.source.tuner.channel.TunerChannelSource; - -import java.util.concurrent.RejectedExecutionException; - -public interface ITunerChannelProvider -{ - /** - * Returns a tuner frequency channel source, tuned to the correct frequency - * - * @return - source for 48k sample rate - */ - public abstract TunerChannelSource getChannel(TunerChannel channel ) - throws RejectedExecutionException, SourceException; - - /** - * Releases the tuned channel resources - */ - public abstract void releaseChannel( TunerChannelSource source ); - - -} diff --git a/src/main/java/io/github/dsheirer/source/tuner/TunerController.java b/src/main/java/io/github/dsheirer/source/tuner/TunerController.java index 5a7b4abf..60394ee6 100644 --- a/src/main/java/io/github/dsheirer/source/tuner/TunerController.java +++ b/src/main/java/io/github/dsheirer/source/tuner/TunerController.java @@ -17,10 +17,10 @@ ******************************************************************************/ package io.github.dsheirer.source.tuner; -import io.github.dsheirer.sample.Broadcaster; import io.github.dsheirer.sample.Listener; -import io.github.dsheirer.sample.complex.ComplexBuffer; -import io.github.dsheirer.sample.complex.IComplexBufferProvider; +import io.github.dsheirer.sample.complex.reusable.IReusableComplexBufferProvider; +import io.github.dsheirer.sample.complex.reusable.ReusableBufferBroadcaster; +import io.github.dsheirer.sample.complex.reusable.ReusableComplexBuffer; import io.github.dsheirer.source.ISourceEventListener; import io.github.dsheirer.source.ISourceEventProcessor; import io.github.dsheirer.source.SourceEvent; @@ -33,10 +33,10 @@ import org.slf4j.Logger; import org.slf4j.LoggerFactory; public abstract class TunerController implements Tunable, ISourceEventProcessor, ISourceEventListener, - IComplexBufferProvider, Listener + IReusableComplexBufferProvider, Listener { private final static Logger mLog = LoggerFactory.getLogger(TunerController.class); - protected Broadcaster mSampleBroadcaster = new Broadcaster<>(); + protected ReusableBufferBroadcaster mSampleBroadcaster = new ReusableBufferBroadcaster(); protected FrequencyController mFrequencyController; private int mMiddleUnusableHalfBandwidth; private double mUsableBandwidthPercentage; @@ -89,13 +89,13 @@ public abstract class TunerController implements Tunable, ISourceEventProcessor, case REQUEST_START_SAMPLE_STREAM: if(sourceEvent.getSource() instanceof Listener) { - addComplexBufferListener((Listener)sourceEvent.getSource()); + addBufferListener((Listener)sourceEvent.getSource()); } break; case REQUEST_STOP_SAMPLE_STREAM: if(sourceEvent.getSource() instanceof Listener) { - removeComplexBufferListener((Listener)sourceEvent.getSource()); + removeBufferListener((Listener)sourceEvent.getSource()); } break; default: @@ -230,7 +230,7 @@ public abstract class TunerController implements Tunable, ISourceEventProcessor, * Adds the listener to receive complex buffer samples */ @Override - public void addComplexBufferListener(Listener listener) + public void addBufferListener(Listener listener) { mSampleBroadcaster.addListener(listener); } @@ -239,7 +239,7 @@ public abstract class TunerController implements Tunable, ISourceEventProcessor, * Removes the listener from receiving complex buffer samples */ @Override - public void removeComplexBufferListener(Listener listener) + public void removeBufferListener(Listener listener) { mSampleBroadcaster.removeListener(listener); } @@ -248,7 +248,7 @@ public abstract class TunerController implements Tunable, ISourceEventProcessor, * Indicates if there are any complex buffer listeners registered on this controller */ @Override - public boolean hasComplexBufferListeners() + public boolean hasBufferListeners() { return mSampleBroadcaster.hasListeners(); } @@ -256,16 +256,16 @@ public abstract class TunerController implements Tunable, ISourceEventProcessor, /** * Broadcasts the buffer to any registered listeners */ - protected void broadcast(ComplexBuffer complexBuffer) + protected void broadcast(ReusableComplexBuffer reusableComplexBuffer) { - mSampleBroadcaster.broadcast(complexBuffer); + mSampleBroadcaster.broadcast(reusableComplexBuffer.incrementUserCount()); } /** * Implements the Listener interface to receive and distribute complex buffers from subclass implementations */ @Override - public void receive(ComplexBuffer complexBuffer) + public void receive(ReusableComplexBuffer complexBuffer) { broadcast(complexBuffer); } diff --git a/src/main/java/io/github/dsheirer/source/tuner/TunerManager.java b/src/main/java/io/github/dsheirer/source/tuner/TunerManager.java index 74ee26b0..91aae7ba 100644 --- a/src/main/java/io/github/dsheirer/source/tuner/TunerManager.java +++ b/src/main/java/io/github/dsheirer/source/tuner/TunerManager.java @@ -31,6 +31,7 @@ import io.github.dsheirer.source.tuner.rtl.RTL2832Tuner; import io.github.dsheirer.source.tuner.rtl.RTL2832TunerController; import io.github.dsheirer.source.tuner.rtl.e4k.E4KTunerController; import io.github.dsheirer.source.tuner.rtl.r820t.R820TTunerController; +import io.github.dsheirer.source.tuner.usb.USBMasterProcessor; import org.slf4j.Logger; import org.slf4j.LoggerFactory; import org.usb4java.Device; @@ -53,11 +54,11 @@ public class TunerManager * buffers can register with this processor and the processor will auto-start and auto-stop while USB transfer * processors are registered. */ - public static final LibUSBTransferProcessor LIBUSB_TRANSFER_PROCESSOR; + public static final USBMasterProcessor LIBUSB_TRANSFER_PROCESSOR; static { - LIBUSB_TRANSFER_PROCESSOR = new LibUSBTransferProcessor(); + LIBUSB_TRANSFER_PROCESSOR = new USBMasterProcessor(); } public TunerManager(MixerManager mixerManager, TunerModel tunerModel) diff --git a/src/main/java/io/github/dsheirer/source/tuner/airspy/AirspySampleAdapter.java b/src/main/java/io/github/dsheirer/source/tuner/airspy/AirspySampleConverter.java similarity index 54% rename from src/main/java/io/github/dsheirer/source/tuner/airspy/AirspySampleAdapter.java rename to src/main/java/io/github/dsheirer/source/tuner/airspy/AirspySampleConverter.java index fa0fa459..799d4e84 100644 --- a/src/main/java/io/github/dsheirer/source/tuner/airspy/AirspySampleAdapter.java +++ b/src/main/java/io/github/dsheirer/source/tuner/airspy/AirspySampleConverter.java @@ -20,25 +20,46 @@ package io.github.dsheirer.source.tuner.airspy; import io.github.dsheirer.dsp.filter.dc.DCRemovalFilter_RB; import io.github.dsheirer.dsp.filter.hilbert.HilbertTransform; -import io.github.dsheirer.sample.adapter.ISampleAdapter; +import io.github.dsheirer.source.tuner.usb.converter.NativeBufferConverter; -public class AirspySampleAdapter implements ISampleAdapter +import java.nio.ByteBuffer; +import java.nio.ByteOrder; +import java.nio.FloatBuffer; + +public class AirspySampleConverter extends NativeBufferConverter { private static final float SCALE_SIGNED_12_BIT_TO_FLOAT = 1.0f / 2048.0f; private DCRemovalFilter_RB mDCFilter = new DCRemovalFilter_RB(0.01f); private HilbertTransform mHilbertTransform = new HilbertTransform(); private boolean mSamplePacking = false; - + private FloatBuffer mFloatBuffer; + private float[] mConvertedSamples; /** * Adapter to translate byte buffers received from the airspy tuner into * float buffers for processing. */ - public AirspySampleAdapter() + public AirspySampleConverter() { } + @Override + protected FloatBuffer convertSamples(ByteBuffer buffer, int length) + { + float[] samples = convert(buffer); + + if(mFloatBuffer == null || mFloatBuffer.capacity() != samples.length) + { + mFloatBuffer = FloatBuffer.allocate(samples.length); + } + + mFloatBuffer.rewind(); + mFloatBuffer.put(samples); + + return mFloatBuffer; + } + /** * Sample packing places two 12-bit samples into 3 bytes when enabled or * places two 12-bit samples into 4 bytes when disabled. @@ -50,98 +71,85 @@ public class AirspySampleAdapter implements ISampleAdapter mSamplePacking = enabled; } - @Override - public float[] convert(byte[] samples) + private float[] convert(ByteBuffer samples) { - float[] realSamples; - if(mSamplePacking) { - realSamples = convertPacked(samples); + convertPacked(samples.order(ByteOrder.LITTLE_ENDIAN)); } else { - realSamples = convertUnpacked(samples); + convertUnpacked(samples); } - mDCFilter.filter(realSamples); + mDCFilter.filter(mConvertedSamples); - return mHilbertTransform.filter(realSamples); + return mHilbertTransform.filter(mConvertedSamples); } /** * Converts the byte array containing unsigned 12-bit short values into * signed float values in the range -1 to 1; * - * @param data - byte array of unsigned 16-bit values - * @return converted float values + * @param buffer - native byte buffer containing unsigned 16-bit values */ - private float[] convertUnpacked(byte[] data) + private void convertUnpacked(ByteBuffer buffer) { - float[] samples = new float[data.length / 2]; + buffer.rewind(); + + if(mConvertedSamples == null || mConvertedSamples.length != buffer.capacity() / 2) + { + mConvertedSamples = new float[buffer.capacity() / 2]; + } int pointer = 0; - for(int x = 0; x < data.length; x += 2) - { - samples[pointer++] = scale((data[x] & 0xFF) | - (data[x + 1] << 8)); - } - return samples; + while(buffer.remaining() >= 2) + { + byte lsb = buffer.get(); + byte msb = buffer.get(); + mConvertedSamples[pointer++] = scale((lsb & 0xFF) | (msb << 8)); + } } /** * Converts every 3 bytes containing a pair of 12-bit unsigned values into * a pair of float values in the range -1 to 1; * - * @param data1 - byte array of unsigned 12-bit values - * @return converted float values + * @param buffer - native byte buffer containing packet 12-bit unsigned samples */ - private float[] convertPacked(byte[] data1) + private void convertPacked(ByteBuffer buffer) { - byte[] data = new byte[data1.length]; + buffer.rewind(); - //Convert big-endian to little-endian - for(int x = 0; x < data1.length; x += 4) + int sampleCount = buffer.capacity() / 3 * 2; + + if(mConvertedSamples == null || mConvertedSamples.length != sampleCount) { - data[x] = data1[x + 3]; - data[x + 1] = data1[x + 2]; - data[x + 2] = data1[x + 1]; - data[x + 3] = data1[x]; + mConvertedSamples = new float[sampleCount]; } - int count = (int) ((float) data.length / 1.5f); - - /* Ensure we have an even number of samples */ - if(count % 2 == 1) - { - count--; - } - - int bytes = (int) ((float) count * 1.5f); - - float[] samples = new float[count]; - int pointer = 0; + byte b1; + byte b2; + byte b3; int first; int second; - for(int x = 0; x < bytes; x += 3) + while(buffer.remaining() >= 3) { - first = ((data[x] << 4) & 0xFF0) | - ((data[x + 1] >> 4) & 0xF); + b1 = buffer.get(); + b2 = buffer.get(); + b3 = buffer.get(); - samples[pointer++] = scale(first); + first = ((b1 << 4) & 0xFF0) | ((b2 >> 4) & 0xF); + mConvertedSamples[pointer++] = scale(first); - second = ((data[x + 1] << 8) & 0xF00) | - (data[x + 2] & 0xFF); - - samples[pointer++] = scale(second); + second = ((b2 << 8) & 0xF00) | (b3 & 0xFF); + mConvertedSamples[pointer++] = scale(second); } - - return samples; } /** diff --git a/src/main/java/io/github/dsheirer/source/tuner/airspy/AirspyTunerController.java b/src/main/java/io/github/dsheirer/source/tuner/airspy/AirspyTunerController.java index 8ea4ad65..a709bad9 100644 --- a/src/main/java/io/github/dsheirer/source/tuner/airspy/AirspyTunerController.java +++ b/src/main/java/io/github/dsheirer/source/tuner/airspy/AirspyTunerController.java @@ -22,11 +22,12 @@ import java.util.List; /** * SDR Trunk - * Copyright (C) 2015-2017 Dennis Sheirer + * Copyright (C) 2015-2018 Dennis Sheirer * + * ----------------------------------------------------------------------------- * Ported from libairspy at: * https://github.com/airspy/host/tree/master/libairspy - * ----------------------------------------------------------------------------- + * * Copyright (c) 2013, Michael Ossmann * Copyright (c) 2012, Jared Boone * Copyright (c) 2014, Youssef Touil @@ -94,7 +95,7 @@ public class AirspyTunerController extends USBTunerController private Device mDevice; private DeviceHandle mDeviceHandle; - private AirspySampleAdapter mSampleAdapter = new AirspySampleAdapter(); + private AirspySampleConverter mSampleAdapter = new AirspySampleConverter(); private AirspyDeviceInformation mDeviceInfo; private List mSampleRates = new ArrayList<>(); private int mSampleRate = 0; @@ -122,8 +123,7 @@ public class AirspyTunerController extends USBTunerController + "airspy rules file in \\etc\\udev\\rules.d ??"); } - throw new SourceException("Couldn't open airspy device - " + - LibUsb.strError(result)); + throw new SourceException("Couldn't open airspy device - " + LibUsb.strError(result)); } try diff --git a/src/main/java/io/github/dsheirer/source/tuner/channel/CICTunerChannelSource.java b/src/main/java/io/github/dsheirer/source/tuner/channel/CICTunerChannelSource.java index ddd74ab4..efddbc58 100644 --- a/src/main/java/io/github/dsheirer/source/tuner/channel/CICTunerChannelSource.java +++ b/src/main/java/io/github/dsheirer/source/tuner/channel/CICTunerChannelSource.java @@ -25,7 +25,6 @@ import io.github.dsheirer.sample.Listener; import io.github.dsheirer.sample.OverflowableTransferQueue; import io.github.dsheirer.sample.complex.Complex; import io.github.dsheirer.sample.complex.ComplexBuffer; -import io.github.dsheirer.source.SourceEvent; import io.github.dsheirer.source.tuner.Tuner; import org.slf4j.Logger; import org.slf4j.LoggerFactory; @@ -33,7 +32,7 @@ import org.slf4j.LoggerFactory; import java.util.ArrayList; import java.util.List; -public class CICTunerChannelSource extends TunerChannelSource +public class CICTunerChannelSource { private final static Logger mLog = LoggerFactory.getLogger(CICTunerChannelSource.class); @@ -69,14 +68,12 @@ public class CICTunerChannelSource extends TunerChannelSource */ public CICTunerChannelSource(Tuner tuner, TunerChannel tunerChannel) { - super(tuner.getTunerController().getSourceEventListener(), tunerChannel); - mBuffer = new OverflowableTransferQueue<>(BUFFER_MAX_CAPACITY, BUFFER_OVERFLOW_RESET_THRESHOLD); - mBuffer.setSourceOverflowListener(this); +// mBuffer.setSourceOverflowListener(this); //Setup the frequency translator to the current source frequency mTunerFrequency = tuner.getTunerController().getFrequency(); - long frequencyOffset = mTunerFrequency - getTunerChannel().getFrequency(); + long frequencyOffset = mTunerFrequency - tunerChannel.getFrequency(); mFrequencyCorrectionMixer = new Oscillator(frequencyOffset, tuner.getTunerController().getSampleRate()); //Setup the decimation filter chain @@ -87,7 +84,6 @@ public class CICTunerChannelSource extends TunerChannelSource * Sets the center frequency for the incoming sample buffers * @param frequency in hertz */ - @Override protected void setFrequency(long frequency) { mTunerFrequency = frequency; @@ -102,7 +98,6 @@ public class CICTunerChannelSource extends TunerChannelSource * Current frequency correction being applied to the channel sample stream * @return correction in hertz */ - @Override public long getFrequencyCorrection() { return mChannelFrequencyCorrection; @@ -118,7 +113,7 @@ public class CICTunerChannelSource extends TunerChannelSource updateMixerFrequencyOffset(); - broadcastConsumerSourceEvent(SourceEvent.frequencyCorrectionChange(mChannelFrequencyCorrection)); +// broadcastConsumerSourceEvent(SourceEvent.frequencyCorrectionChange(mChannelFrequencyCorrection)); } /** @@ -130,7 +125,6 @@ public class CICTunerChannelSource extends TunerChannelSource mBuffer.offer(buffer); } - @Override public void setListener(Listener listener) { /* Save a pointer to the listener so that if we have to change the @@ -140,7 +134,6 @@ public class CICTunerChannelSource extends TunerChannelSource mDecimationFilter.setListener(listener); } - @Override public void removeListener(Listener listener) { mDecimationFilter.removeListener(); @@ -165,7 +158,7 @@ public class CICTunerChannelSource extends TunerChannelSource mTunerSampleRate = sampleRate; - broadcastConsumerSourceEvent(SourceEvent.channelSampleRateChange(getSampleRate())); +// broadcastConsumerSourceEvent(SourceEvent.channelSampleRateChange(getSampleRate())); } } @@ -175,8 +168,8 @@ public class CICTunerChannelSource extends TunerChannelSource */ private void updateMixerFrequencyOffset() { - long offset = mTunerFrequency - getTunerChannel().getFrequency() - mChannelFrequencyCorrection; - mFrequencyCorrectionMixer.setFrequency(offset); +// long offset = mTunerFrequency - getTunerChannel().getFrequency() - mChannelFrequencyCorrection; +// mFrequencyCorrectionMixer.setFrequency(offset); } public double getSampleRate() @@ -184,7 +177,6 @@ public class CICTunerChannelSource extends TunerChannelSource return CHANNEL_RATE; } - @Override protected void processSamples() { mBuffer.drainTo(mSampleBuffers, 20); diff --git a/src/main/java/io/github/dsheirer/source/tuner/channel/TunerChannelSource.java b/src/main/java/io/github/dsheirer/source/tuner/channel/TunerChannelSource.java index 7137b611..396d223c 100644 --- a/src/main/java/io/github/dsheirer/source/tuner/channel/TunerChannelSource.java +++ b/src/main/java/io/github/dsheirer/source/tuner/channel/TunerChannelSource.java @@ -19,7 +19,7 @@ package io.github.dsheirer.source.tuner.channel; import io.github.dsheirer.sample.Listener; -import io.github.dsheirer.sample.complex.ComplexBuffer; +import io.github.dsheirer.sample.complex.reusable.ReusableComplexBuffer; import io.github.dsheirer.source.ComplexSource; import io.github.dsheirer.source.ISourceEventProcessor; import io.github.dsheirer.source.SourceEvent; @@ -89,7 +89,7 @@ public abstract class TunerChannelSource extends ComplexSource implements ISourc * Sets the listener to receive the complex buffer sample output from this channel * @param complexBufferListener to receive complex buffers */ - public abstract void setListener(Listener complexBufferListener); + public abstract void setListener(Listener complexBufferListener); /** * Commands sub-class to process queued samples and distribute them to the consumer. This method will be invoked diff --git a/src/main/java/io/github/dsheirer/source/tuner/fcd/FCDTunerController.java b/src/main/java/io/github/dsheirer/source/tuner/fcd/FCDTunerController.java index db6cf1bd..9e77661e 100644 --- a/src/main/java/io/github/dsheirer/source/tuner/fcd/FCDTunerController.java +++ b/src/main/java/io/github/dsheirer/source/tuner/fcd/FCDTunerController.java @@ -19,8 +19,8 @@ package io.github.dsheirer.source.tuner.fcd; import io.github.dsheirer.sample.Listener; -import io.github.dsheirer.sample.adapter.ShortAdapter; -import io.github.dsheirer.sample.complex.ComplexBuffer; +import io.github.dsheirer.sample.adapter.ComplexShortAdapter; +import io.github.dsheirer.sample.complex.reusable.ReusableComplexBuffer; import io.github.dsheirer.source.SourceException; import io.github.dsheirer.source.mixer.ComplexMixer; import io.github.dsheirer.source.tuner.MixerTunerDataLine; @@ -87,19 +87,19 @@ public abstract class FCDTunerController extends TunerController mLog.error("Error setting sample rate to [" + audioFormat.getSampleRate() + "]", se); } - mComplexMixer = new ComplexMixer( mixerTDL.getTargetDataLine(), audioFormat, tunerName, new ShortAdapter(), - mSampleBroadcaster); + mComplexMixer = new ComplexMixer( mixerTDL.getTargetDataLine(), audioFormat, tunerName, + new ComplexShortAdapter(), mSampleBroadcaster); } /** * Overrides the super class functionality to auto-start the complex mixer and provide samples to listeners */ @Override - public void addComplexBufferListener(Listener listener) + public void addBufferListener(Listener listener) { - boolean hasExistingListeners = hasComplexBufferListeners(); + boolean hasExistingListeners = hasBufferListeners(); - super.addComplexBufferListener(listener); + super.addBufferListener(listener); if(!hasExistingListeners) { @@ -112,11 +112,11 @@ public abstract class FCDTunerController extends TunerController * more registered listeners */ @Override - public void removeComplexBufferListener(Listener listener) + public void removeBufferListener(Listener listener) { - super.removeComplexBufferListener(listener); + super.removeBufferListener(listener); - if(!hasComplexBufferListeners()) + if(!hasBufferListeners()) { mComplexMixer.stop(); } diff --git a/src/main/java/io/github/dsheirer/source/tuner/hackrf/HackRFTunerController.java b/src/main/java/io/github/dsheirer/source/tuner/hackrf/HackRFTunerController.java index 01773f25..e9082ec7 100644 --- a/src/main/java/io/github/dsheirer/source/tuner/hackrf/HackRFTunerController.java +++ b/src/main/java/io/github/dsheirer/source/tuner/hackrf/HackRFTunerController.java @@ -53,11 +53,12 @@ ******************************************************************************/ package io.github.dsheirer.source.tuner.hackrf; -import io.github.dsheirer.sample.adapter.ByteSampleAdapter; import io.github.dsheirer.source.SourceException; import io.github.dsheirer.source.tuner.configuration.TunerConfiguration; import io.github.dsheirer.source.tuner.usb.USBTransferProcessor; import io.github.dsheirer.source.tuner.usb.USBTunerController; +import io.github.dsheirer.source.tuner.usb.converter.ByteSampleConverter; +import io.github.dsheirer.source.tuner.usb.converter.NativeBufferConverter; import org.apache.commons.io.EndianUtils; import org.slf4j.Logger; import org.slf4j.LoggerFactory; @@ -90,7 +91,7 @@ public class HackRFTunerController extends USBTunerController public static final double USABLE_BANDWIDTH_PERCENT = 0.95; public static final int DC_SPIKE_AVOID_BUFFER = 5000; - private ByteSampleAdapter mSampleAdapter = new ByteSampleAdapter(); + private NativeBufferConverter mNativeBufferConverter = new ByteSampleConverter(); private USBTransferProcessor mUSBTransferProcessor; private HackRFSampleRate mSampleRate = HackRFSampleRate.RATE2_016MHZ; @@ -153,7 +154,7 @@ public class HackRFTunerController extends USBTunerController name = "HackRF - Unidentified Serial"; } - mUSBTransferProcessor = new USBTransferProcessor(name, mDeviceHandle, mSampleAdapter, USB_TRANSFER_BUFFER_SIZE); + mUSBTransferProcessor = new USBTransferProcessor(name, mDeviceHandle, mNativeBufferConverter, USB_TRANSFER_BUFFER_SIZE); } @Override diff --git a/src/main/java/io/github/dsheirer/source/tuner/rtl/RTL2832TunerController.java b/src/main/java/io/github/dsheirer/source/tuner/rtl/RTL2832TunerController.java index c71218a2..a6c8c138 100644 --- a/src/main/java/io/github/dsheirer/source/tuner/rtl/RTL2832TunerController.java +++ b/src/main/java/io/github/dsheirer/source/tuner/rtl/RTL2832TunerController.java @@ -1,6 +1,6 @@ /******************************************************************************* * SDR Trunk - * Copyright (C) 2014-2017 Dennis Sheirer + * Copyright (C) 2014-2018 Dennis Sheirer * * Java version based on librtlsdr * Copyright (C) 2012-2013 by Steve Markgraf @@ -22,14 +22,14 @@ package io.github.dsheirer.source.tuner.rtl; import io.github.dsheirer.sample.Listener; -import io.github.dsheirer.sample.adapter.ByteSampleAdapter; -import io.github.dsheirer.sample.adapter.ISampleAdapter; -import io.github.dsheirer.sample.complex.ComplexBuffer; +import io.github.dsheirer.sample.complex.reusable.ReusableComplexBuffer; import io.github.dsheirer.source.SourceException; import io.github.dsheirer.source.tuner.TunerManager; import io.github.dsheirer.source.tuner.TunerType; import io.github.dsheirer.source.tuner.usb.USBTransferProcessor; import io.github.dsheirer.source.tuner.usb.USBTunerController; +import io.github.dsheirer.source.tuner.usb.converter.ByteSampleConverter; +import io.github.dsheirer.source.tuner.usb.converter.NativeBufferConverter; import org.slf4j.Logger; import org.slf4j.LoggerFactory; import org.usb4java.Device; @@ -77,7 +77,7 @@ public abstract class RTL2832TunerController extends USBTunerController private SampleRate mSampleRate = DEFAULT_SAMPLE_RATE; - protected ByteSampleAdapter mSampleAdapter = new ByteSampleAdapter(); + protected NativeBufferConverter mNativeBufferConverter = new ByteSampleConverter(); protected int mOscillatorFrequency = 28800000; //28.8 MHz protected USBTransferProcessor mUSBTransferProcessor; protected Descriptor mDescriptor; @@ -91,11 +91,7 @@ public abstract class RTL2832TunerController extends USBTunerController long maxTunableFrequency, int centerUnusableBandwidth, double usableBandwidthPercentage) throws SourceException { - super(minTunableFrequency, - maxTunableFrequency, - centerUnusableBandwidth, - usableBandwidthPercentage); - + super(minTunableFrequency, maxTunableFrequency, centerUnusableBandwidth, usableBandwidthPercentage); mDevice = device; mDeviceDescriptor = deviceDescriptor; } @@ -158,7 +154,7 @@ public abstract class RTL2832TunerController extends USBTunerController String deviceName = getTunerType().getLabel() + " " + getUniqueID(); - mUSBTransferProcessor = new RTL2832USBTransferProcessor(deviceName, mDeviceHandle, mSampleAdapter, + mUSBTransferProcessor = new RTL2832USBTransferProcessor(deviceName, mDeviceHandle, mNativeBufferConverter, USB_TRANSFER_BUFFER_SIZE); } @@ -1498,11 +1494,11 @@ public abstract class RTL2832TunerController extends USBTunerController * Adds the IQ buffer listener and automatically starts buffer transfer processing, if not already started. */ @Override - public void addComplexBufferListener(Listener listener) + public void addBufferListener(Listener listener) { - boolean hasExistingListeners = hasComplexBufferListeners(); + boolean hasExistingListeners = hasBufferListeners(); - super.addComplexBufferListener(listener); + super.addBufferListener(listener); if(!hasExistingListeners) { @@ -1514,11 +1510,11 @@ public abstract class RTL2832TunerController extends USBTunerController * Removes the IQ buffer listener and stops buffer transfer processing if there are no more listeners. */ @Override - public void removeComplexBufferListener(Listener listener) + public void removeBufferListener(Listener listener) { - super.removeComplexBufferListener(listener); + super.removeBufferListener(listener); - if(!hasComplexBufferListeners()) + if(!hasBufferListeners()) { mUSBTransferProcessor.removeListener(); } @@ -1536,12 +1532,12 @@ public abstract class RTL2832TunerController extends USBTunerController * * @param deviceName to use when logging information or errors * @param deviceHandle to the USB bulk transfer device - * @param sampleAdapter specific to the tuner's byte buffer format for converting to floating point I/Q samples + * @param nativeBufferConverter specific to the tuner's byte buffer format for converting to floating point I/Q samples * @param bufferSize in bytes. Should be a multiple of two: 65536, 131072 or 262144. */ - public RTL2832USBTransferProcessor(String deviceName, DeviceHandle deviceHandle, ISampleAdapter sampleAdapter, int bufferSize) + public RTL2832USBTransferProcessor(String deviceName, DeviceHandle deviceHandle, NativeBufferConverter nativeBufferConverter, int bufferSize) { - super(deviceName, deviceHandle, sampleAdapter, bufferSize); + super(deviceName, deviceHandle, nativeBufferConverter, bufferSize); } @Override diff --git a/src/main/java/io/github/dsheirer/source/tuner/rtl/e4k/E4KTunerController.java b/src/main/java/io/github/dsheirer/source/tuner/rtl/e4k/E4KTunerController.java index 79a6e00a..dcc0c24b 100644 --- a/src/main/java/io/github/dsheirer/source/tuner/rtl/e4k/E4KTunerController.java +++ b/src/main/java/io/github/dsheirer/source/tuner/rtl/e4k/E4KTunerController.java @@ -252,7 +252,7 @@ public class E4KTunerController extends RTL2832TunerController String deviceName = getTunerType().getLabel() + " " + getUniqueID(); - mUSBTransferProcessor = new RTL2832USBTransferProcessor(deviceName, mDeviceHandle, mSampleAdapter, + mUSBTransferProcessor = new RTL2832USBTransferProcessor(deviceName, mDeviceHandle, mNativeBufferConverter, USB_TRANSFER_BUFFER_SIZE); } diff --git a/src/main/java/io/github/dsheirer/source/tuner/test/SampleGenerator.java b/src/main/java/io/github/dsheirer/source/tuner/test/SampleGenerator.java index b4a09a40..123fde8f 100644 --- a/src/main/java/io/github/dsheirer/source/tuner/test/SampleGenerator.java +++ b/src/main/java/io/github/dsheirer/source/tuner/test/SampleGenerator.java @@ -17,10 +17,10 @@ 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; -import io.github.dsheirer.sample.complex.ComplexBuffer; -import io.github.dsheirer.sample.complex.TimestampedComplexBuffer; +import io.github.dsheirer.sample.complex.reusable.ReusableBufferBroadcaster; +import io.github.dsheirer.sample.complex.reusable.ReusableBufferQueue; +import io.github.dsheirer.sample.complex.reusable.ReusableComplexBuffer; import io.github.dsheirer.util.ThreadPool; import org.slf4j.Logger; import org.slf4j.LoggerFactory; @@ -32,7 +32,7 @@ public class SampleGenerator { private final static Logger mLog = LoggerFactory.getLogger(SampleGenerator.class); - private Broadcaster mComplexBufferBroadcaster = new Broadcaster<>(); + private ReusableBufferBroadcaster mComplexBufferBroadcaster = new ReusableBufferBroadcaster(); private IOscillator mOscillator; private int mSweepUpdateInterval; private long mInterval; @@ -79,21 +79,6 @@ public class SampleGenerator this(sampleRate, frequency, interval, 0); } - /** - * Instructs the sample generator to reuse the same sample buffer each time instead of generating new samples for - * each and every buffer. This is useful when you simply want to generate data to test or profile data flows and - * are not concerned about the content of the buffers. - * - * The generator will create a new buffer of data each time that it detects that the sample size changes or when - * the tone frequency changes. - * - * @param reuseBuffers set to true to turn on buffer reuse - */ - public void setReuseBuffers(boolean reuseBuffers) - { - mReuseBuffers = reuseBuffers; - } - /** * Updates the number of complex samples generated per interval */ @@ -139,7 +124,7 @@ public class SampleGenerator * * @param listener to receive complex sample buffers */ - public void addListener(Listener listener) + public void addListener(Listener listener) { mComplexBufferBroadcaster.addListener(listener); @@ -152,7 +137,7 @@ public class SampleGenerator /** * Removes the listener and stops the sample generator if there are no more listeners. */ - public void removeListener(Listener listener) + public void removeListener(Listener listener) { mComplexBufferBroadcaster.removeListener(listener); @@ -200,31 +185,18 @@ public class SampleGenerator public class Generator implements Runnable { private int mTriggerInterval = 0; - private long mOscillatorFrequency = 1; - private ComplexBuffer mReusableBuffer; + private ReusableBufferQueue mReusableBufferQueue = new ReusableBufferQueue("** Sample Generator **"); @Override public void run() { if(mComplexBufferBroadcaster.hasListeners()) { - if(mReuseBuffers) - { - if(mReusableBuffer == null || - mReusableBuffer.getSamples().length != mSamplesPerInterval || - mOscillatorFrequency != mOscillator.getFrequency()) - { - mOscillatorFrequency = (long)mOscillator.getFrequency(); - mReusableBuffer = new TimestampedComplexBuffer(mOscillator.generateComplex(mSamplesPerInterval)); - } + ReusableComplexBuffer reusableComplexBuffer = mReusableBufferQueue.getBuffer(mSamplesPerInterval); - mComplexBufferBroadcaster.receive(mReusableBuffer); - } - else - { - float[] samples = mOscillator.generateComplex(mSamplesPerInterval); - mComplexBufferBroadcaster.receive(new TimestampedComplexBuffer(samples)); - } + mOscillator.generateComplex(reusableComplexBuffer); + + mComplexBufferBroadcaster.broadcast(reusableComplexBuffer.incrementUserCount()); if(mSweepUpdateInterval != 0) { diff --git a/src/main/java/io/github/dsheirer/source/tuner/test/TestTuner.java b/src/main/java/io/github/dsheirer/source/tuner/test/TestTuner.java index 4a81a60b..5449280e 100644 --- a/src/main/java/io/github/dsheirer/source/tuner/test/TestTuner.java +++ b/src/main/java/io/github/dsheirer/source/tuner/test/TestTuner.java @@ -41,9 +41,6 @@ public class TestTuner extends Tuner /** * Returns the tuner controller cast as a test tuner controller. - * - * Note: this is a temporary method until the tuner controller interface can be updated with the start(listener) - * and stop() methods. */ public TestTunerController getTunerController() { diff --git a/src/main/java/io/github/dsheirer/source/tuner/test/TestTunerController.java b/src/main/java/io/github/dsheirer/source/tuner/test/TestTunerController.java index c9dac7ce..576ca349 100644 --- a/src/main/java/io/github/dsheirer/source/tuner/test/TestTunerController.java +++ b/src/main/java/io/github/dsheirer/source/tuner/test/TestTunerController.java @@ -19,7 +19,7 @@ package io.github.dsheirer.source.tuner.test; import io.github.dsheirer.sample.Listener; -import io.github.dsheirer.sample.complex.ComplexBuffer; +import io.github.dsheirer.sample.complex.reusable.ReusableComplexBuffer; import io.github.dsheirer.source.SourceException; import io.github.dsheirer.source.tuner.TunerController; import io.github.dsheirer.source.tuner.configuration.TunerConfiguration; @@ -34,7 +34,7 @@ public class TestTunerController extends TunerController public static final long MAXIMUM_FREQUENCY = 1000000000l; public static final int SAMPLE_RATE = 800000; public static final int DC_NOISE_BANDWIDTH = 0; - public static final double USABLE_BANDWIDTH = 1.00; + public static final double USABLE_BANDWIDTH_PERCENTAGE = 1.00; public static final int SPECTRAL_FRAME_RATE = 20; public static final long SAMPLE_GENERATION_INTERVAL = 1000 / SPECTRAL_FRAME_RATE; @@ -47,7 +47,7 @@ public class TestTunerController extends TunerController */ public TestTunerController() { - super(MINIMUM_FREQUENCY, MAXIMUM_FREQUENCY, DC_NOISE_BANDWIDTH, USABLE_BANDWIDTH); + super(MINIMUM_FREQUENCY, MAXIMUM_FREQUENCY, DC_NOISE_BANDWIDTH, USABLE_BANDWIDTH_PERCENTAGE); int sweepRate = 0; //Hz per interval long initialToneFrequency = SAMPLE_RATE / 2 + 100; @@ -65,25 +65,14 @@ public class TestTunerController extends TunerController } } - /** - * Sets the sample generator to reuse buffers instead of generating new buffers each time. Enable this setting - * when you simply want to test/profile buffer data flows for consumer processes. - * - * @param reuseBuffers set to true to turn on buffer reuse - */ - public void setReuseBuffers(boolean reuseBuffers) - { - mSampleGenerator.setReuseBuffers(reuseBuffers); - } - @Override - public void addComplexBufferListener(Listener listener) + public void addBufferListener(Listener listener) { mSampleGenerator.addListener(listener); } @Override - public void removeComplexBufferListener(Listener listener) + public void removeBufferListener(Listener listener) { mSampleGenerator.removeListener(listener); } diff --git a/src/main/java/io/github/dsheirer/source/tuner/LibUSBTransferProcessor.java b/src/main/java/io/github/dsheirer/source/tuner/usb/USBMasterProcessor.java similarity index 68% rename from src/main/java/io/github/dsheirer/source/tuner/LibUSBTransferProcessor.java rename to src/main/java/io/github/dsheirer/source/tuner/usb/USBMasterProcessor.java index 09c10372..d13e4a9b 100644 --- a/src/main/java/io/github/dsheirer/source/tuner/LibUSBTransferProcessor.java +++ b/src/main/java/io/github/dsheirer/source/tuner/usb/USBMasterProcessor.java @@ -1,24 +1,20 @@ /******************************************************************************* - * 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 + * You should have received a copy of the GNU General Public License along with this program. + * If not, see * ******************************************************************************/ -package io.github.dsheirer.source.tuner; +package io.github.dsheirer.source.tuner.usb; -import io.github.dsheirer.source.tuner.usb.USBTransferProcessor; import io.github.dsheirer.util.ThreadPool; import org.slf4j.Logger; import org.slf4j.LoggerFactory; @@ -32,11 +28,12 @@ import java.util.concurrent.TimeUnit; import java.util.concurrent.atomic.AtomicBoolean; /** - * Runnable for executing LibUsb processing of bulk transfer buffers + * Scheduled runnable for the primary LibUsb processing thread. Each USB device's transfer processor + * registers with this processor to support auto-start/stop of this event processing thread. */ -public class LibUSBTransferProcessor +public class USBMasterProcessor { - private final static Logger mLog = LoggerFactory.getLogger(LibUSBTransferProcessor.class); + private final static Logger mLog = LoggerFactory.getLogger(USBMasterProcessor.class); private static final long USB_TIMEOUT_MS = 2000l; //milliseconds @@ -112,14 +109,21 @@ public class LibUSBTransferProcessor @Override public void run() { - int result = LibUsb.handleEventsTimeoutCompleted(null, USB_TIMEOUT_MS, mStatusBuffer.asIntBuffer()); - - if(result != LibUsb.SUCCESS) + try { - mLog.error("Error processing timeout events for LibUSB - error code:" + result); - } + int result = LibUsb.handleEventsTimeoutCompleted(null, USB_TIMEOUT_MS, mStatusBuffer.asIntBuffer()); - mStatusBuffer.rewind(); + if(result != LibUsb.SUCCESS) + { + mLog.error("Error processing timeout events for LibUSB - error code:" + result); + } + + mStatusBuffer.rewind(); + } + catch(Throwable throwable) + { + mLog.error("Error while processing LibUSB timeout events", throwable); + } } } } diff --git a/src/main/java/io/github/dsheirer/source/tuner/usb/USBTransferProcessor.java b/src/main/java/io/github/dsheirer/source/tuner/usb/USBTransferProcessor.java index b1bf58a0..129f228e 100644 --- a/src/main/java/io/github/dsheirer/source/tuner/usb/USBTransferProcessor.java +++ b/src/main/java/io/github/dsheirer/source/tuner/usb/USBTransferProcessor.java @@ -1,29 +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 + * You should have received a copy of the GNU General Public License along with this program. + * If not, see * ******************************************************************************/ package io.github.dsheirer.source.tuner.usb; import io.github.dsheirer.sample.Listener; -import io.github.dsheirer.sample.OverflowableTransferQueue; -import io.github.dsheirer.sample.adapter.ISampleAdapter; -import io.github.dsheirer.sample.complex.ComplexBuffer; -import io.github.dsheirer.sample.real.IOverflowListener; +import io.github.dsheirer.sample.complex.reusable.ReusableComplexBuffer; import io.github.dsheirer.source.tuner.TunerManager; +import io.github.dsheirer.source.tuner.usb.converter.NativeBufferConverter; import io.github.dsheirer.util.ThreadPool; import org.slf4j.Logger; import org.slf4j.LoggerFactory; @@ -34,8 +29,6 @@ import org.usb4java.Transfer; import org.usb4java.TransferCallback; import java.nio.ByteBuffer; -import java.util.ArrayList; -import java.util.List; import java.util.concurrent.LinkedTransferQueue; import java.util.concurrent.ScheduledFuture; import java.util.concurrent.TimeUnit; @@ -49,72 +42,48 @@ public class USBTransferProcessor implements TransferCallback private static final long USB_TIMEOUT_MS = 2000l; //milliseconds //Number of native byte buffers to allocate for transferring data from the USB device - private static final int TRANSFER_BUFFER_POOL_SIZE = 16; + private static final int TRANSFER_BUFFER_POOL_SIZE = 20; + private static final int MAX_TRANSFERS_IN_PROGRESS_SIZE = 10; + private int mTransfersToSubmit = 0; - //Maximum number of filled buffers for the blocking queue - private static final int FILLED_BUFFER_MAX_CAPACITY = 500; + private LinkedTransferQueue mAvailableTransfers = new LinkedTransferQueue<>(); + private LinkedTransferQueue mInProgressTransfers = new LinkedTransferQueue<>(); + private LinkedTransferQueue mCompletedTransfers = new LinkedTransferQueue<>(); - //Threshold for resetting buffer overflow condition - private static final int FILLED_BUFFER_OVERFLOW_RESET_THRESHOLD = 100; - - private String mDeviceName; - - //Handle to the USB bulk transfer device - private DeviceHandle mDeviceHandle; - - //Tuner format-specific byte to IQ float sample converter - private ISampleAdapter mSampleAdapter; + //Tuner format-specific byte buffer to IQ float sample converter + private NativeBufferConverter mNativeBufferConverter; //Byte array transfer buffers size in bytes private int mBufferSize; - private OverflowableTransferQueue mFilledBuffers; - private LinkedTransferQueue mAvailableTransfers = new LinkedTransferQueue<>(); - private LinkedTransferQueue mTransfersInProgress = new LinkedTransferQueue<>(); - private Listener mComplexBufferListener; + private Listener mComplexBufferListener; + //Handle to the USB bulk transfer device + private DeviceHandle mUsbBulkTransferDeviceHandle; private AtomicBoolean mRunning = new AtomicBoolean(); - private ByteBuffer mLibUsbHandlerStatus = ByteBuffer.allocateDirect(4); - - private BufferDispatcher mBufferDispatcher = new BufferDispatcher(); + private CompletedTransferProcessor mCompletedTransferProcessor = new CompletedTransferProcessor(); private ScheduledFuture mBufferDispatcherFuture; + private ScheduledFuture mRestartFuture; + private String mDeviceName; + /** * Manages stream of USB transfer buffers and converts buffers to complex buffer samples for distribution to * any registered listeners. * * @param deviceName to use when logging information or errors - * @param deviceHandle to the USB bulk transfer device - * @param sampleAdapter specific to the tuner's byte buffer format for converting to floating point I/Q samples + * @param usbBulkTransferDeviceHandle to the USB bulk transfer device + * @param nativeBufferConverter specific to the tuner's byte buffer format for converting to floating point I/Q samples * @param bufferSize in bytes. Should be a multiple of two: 65536, 131072 or 262144. */ - public USBTransferProcessor(String deviceName, - DeviceHandle deviceHandle, - ISampleAdapter sampleAdapter, - int bufferSize) + public USBTransferProcessor(String deviceName, DeviceHandle usbBulkTransferDeviceHandle, + NativeBufferConverter nativeBufferConverter, int bufferSize) { mDeviceName = deviceName; - mDeviceHandle = deviceHandle; - mSampleAdapter = sampleAdapter; + mUsbBulkTransferDeviceHandle = usbBulkTransferDeviceHandle; + mNativeBufferConverter = nativeBufferConverter; mBufferSize = bufferSize; - - mFilledBuffers = new OverflowableTransferQueue<>(FILLED_BUFFER_MAX_CAPACITY, FILLED_BUFFER_OVERFLOW_RESET_THRESHOLD); - mFilledBuffers.setOverflowListener(new IOverflowListener() - { - @Override - public void sourceOverflow(boolean overflow) - { - if(overflow) - { - mLog.debug(mDeviceName + " - buffer overflow - temporary pause until processing catches up"); - } - else - { - mLog.debug(mDeviceName + " - buffer overflow cleared - resuming normal processing"); - } - } - }); } /** @@ -125,10 +94,104 @@ public class USBTransferProcessor implements TransferCallback if(mRunning.compareAndSet(false, true)) { prepareDeviceStart(); - prepareTransfers(); + mTransfersToSubmit = MAX_TRANSFERS_IN_PROGRESS_SIZE; + submitTransfers(); - while(!mAvailableTransfers.isEmpty()) + //Start transferred buffer dispatcher + mBufferDispatcherFuture = ThreadPool.SCHEDULED.scheduleAtFixedRate(mCompletedTransferProcessor, + 0, 6, TimeUnit.MILLISECONDS); + + //Register with LibUSB processor so that it auto-starts LibUSB processing + TunerManager.LIBUSB_TRANSFER_PROCESSOR.registerTransferProcessor(this); + } + } + + /** + * Stop USB transfer buffer processing. Subsequent calls to this method after stopped will be ignored. + */ + private void stop() + { + if(mRunning.compareAndSet(true, false)) + { + mBufferDispatcherFuture.cancel(true); + + //Cancel all buffers that are currently in progress + for(Transfer transfer : mInProgressTransfers) + { + LibUsb.cancelTransfer(transfer); + } + + //Clear all completed buffers + Transfer completedTransfer = mCompletedTransfers.poll(); + + while(completedTransfer != null) + { + completedTransfer.buffer().rewind(); + mAvailableTransfers.add(completedTransfer); + completedTransfer = mCompletedTransfers.poll(); + } + + //Unregister from LibUSB processor so that it auto-stops LibUSB processing + TunerManager.LIBUSB_TRANSFER_PROCESSOR.unregisterTransferProcessor(this); + + //Directly invoke the timeout handler to ensure that our cancelled transfer buffers are flushed. + int result = LibUsb.handleEventsTimeoutCompleted(null, USB_TIMEOUT_MS, + mLibUsbHandlerStatus.asIntBuffer()); + + if(result != LibUsb.SUCCESS) + { + mLog.error(mDeviceName + " - error while cancelling transfer buffers during shutdown/pause - error code:" + result); + } + + mLibUsbHandlerStatus.rewind(); + + executeDeviceStop(); + } + } + + /** + * Restarts the device after there is an error. Initially stops the device and then schedules a start() to + * occur in 10 milliseconds. + */ + private void restart() + { + stop(); + + //Attempt to clear any halt condition + LibUsb.clearHalt(mUsbBulkTransferDeviceHandle, USB_BULK_TRANSFER_ENDPOINT); + + mLog.warn("USB tuner [" + mDeviceName + "] stopped due to a buffer transfer error. Restarting in 10 milliseconds"); + + if(mRestartFuture == null) + { + Runnable runnable = new Runnable() + { + @Override + public void run() + { + start(); + mRestartFuture = null; + } + }; + + mRestartFuture = ThreadPool.SCHEDULED.schedule(runnable, 10, TimeUnit.MILLISECONDS); + } + } + + /** + * (Re)Submits transfer buffers to the USB device. Since we're offloading byte buffer to complex sample conversion + * to another thread, there may be times when sufficient transfer buffers are not available to keep a level number + * of transfer buffers in progress. Therefore, we track the transfer buffers to submit deficit number and attempt + * to play catch-up each time this method is invoked. + */ + private void submitTransfers() + { + if(mRunning.get()) + { + int submitted = 0; + + for(int x = 0; x < mTransfersToSubmit; x++) { Transfer transfer = mAvailableTransfers.poll(); @@ -138,11 +201,14 @@ public class USBTransferProcessor implements TransferCallback if(result == LibUsb.SUCCESS) { - mTransfersInProgress.add(transfer); + mInProgressTransfers.add(transfer); + submitted++; } else if(result == LibUsb.ERROR_PIPE) { - int resetResult = LibUsb.clearHalt(mDeviceHandle, USB_BULK_TRANSFER_ENDPOINT); + mLog.warn("USB pipe error - attempting to clear halt on USB device [" + mDeviceName + "]"); + + int resetResult = LibUsb.clearHalt(mUsbBulkTransferDeviceHandle, USB_BULK_TRANSFER_ENDPOINT); if(resetResult == LibUsb.SUCCESS) { @@ -150,7 +216,8 @@ public class USBTransferProcessor implements TransferCallback if(resubmitResult == LibUsb.SUCCESS) { - mTransfersInProgress.add(transfer); + mInProgressTransfers.add(transfer); + submitted++; } else { @@ -170,48 +237,7 @@ public class USBTransferProcessor implements TransferCallback } } - //Start transferred buffer dispatcher - mBufferDispatcherFuture = ThreadPool.SCHEDULED.scheduleAtFixedRate(mBufferDispatcher, - 0, 11, TimeUnit.MILLISECONDS); - - //Register with LibUSB processor so that it auto-starts LibUSB processing - TunerManager.LIBUSB_TRANSFER_PROCESSOR.registerTransferProcessor(this); - } - } - - /** - * Stop USB transfer buffer processing. Subsequent calls to this method after stopped will be ignored. - */ - private void stop() - { - if(mRunning.compareAndSet(true, false)) - { - mBufferDispatcherFuture.cancel(true); - - mFilledBuffers.clear(); - - //Cancel the lib usb process timer - for(Transfer transfer : mTransfersInProgress) - { - LibUsb.cancelTransfer(transfer); - } - - - //Unregister from LibUSB processor so that it auto-stops LibUSB processing - TunerManager.LIBUSB_TRANSFER_PROCESSOR.unregisterTransferProcessor(this); - - //Directly invoke the timeout handler to ensure that our cancelled transfer buffers are flushed. - int result = LibUsb.handleEventsTimeoutCompleted(null, USB_TIMEOUT_MS, - mLibUsbHandlerStatus.asIntBuffer()); - - if(result != LibUsb.SUCCESS) - { - mLog.error(mDeviceName + " - error while cancelling transfer buffers during shutdown/pause - error code:" + result); - } - - mLibUsbHandlerStatus.rewind(); - - executeDeviceStop(); + mTransfersToSubmit -= submitted; } } @@ -234,7 +260,7 @@ public class USBTransferProcessor implements TransferCallback /** * Sets the listener and auto-starts the buffer processor */ - public void setListener(Listener listener) + public void setListener(Listener listener) { mComplexBufferListener = listener; start(); @@ -254,7 +280,7 @@ public class USBTransferProcessor implements TransferCallback /** * Prepares (allocates) a set of transfer buffers for use in transferring data from the USB device via the bulk - * interface. Since we're using direct allocation (native), buffers are retained and reused across multiple + * interface. Since we're using direct memory allocation (native), buffers are retained and reused across multiple * start/stop cycles. */ private void prepareTransfers() throws LibUsbException @@ -270,7 +296,7 @@ public class USBTransferProcessor implements TransferCallback final ByteBuffer buffer = ByteBuffer.allocateDirect(mBufferSize); - LibUsb.fillBulkTransfer(transfer, mDeviceHandle, USB_BULK_TRANSFER_ENDPOINT, buffer, this, + LibUsb.fillBulkTransfer(transfer, mUsbBulkTransferDeviceHandle, USB_BULK_TRANSFER_ENDPOINT, buffer, this, "Buffer", USB_TIMEOUT_MS); mAvailableTransfers.add(transfer); @@ -278,12 +304,15 @@ public class USBTransferProcessor implements TransferCallback } /** - * Process a filled transfer buffer received back from the USB device + * Process a filled transfer buffer received back from the USB device. Note: this method is invoked on the USB + * bus processing thread, so we try to keep processing to a minimum and place transfers in the completed + * transfer queue so that the scheduled processor thread handles any conversion and additional downstream + * processing workload. */ @Override public void processTransfer(Transfer transfer) { - mTransfersInProgress.remove(transfer); + mInProgressTransfers.remove(transfer); switch(transfer.status()) { @@ -292,78 +321,32 @@ public class USBTransferProcessor implements TransferCallback case LibUsb.TRANSFER_TIMED_OUT: if(transfer.actualLength() > 0) { - ByteBuffer buffer = transfer.buffer(); - - byte[] data = new byte[transfer.actualLength()]; - buffer.get(data); - buffer.rewind(); - - if(mRunning.get()) - { - mFilledBuffers.offer(data); - } - } - break; - case LibUsb.TRANSFER_CANCELLED: - break; - default: - //Unexpected transfer error - mLog.error(mDeviceName + " - transfer error [" + getTransferStatus(transfer.status()) + - "] transferred actual: " + transfer.actualLength()); - } - - - if(mRunning.get()) - { - int result = LibUsb.submitTransfer(transfer); - - if(result == LibUsb.SUCCESS) - { - mTransfersInProgress.add(transfer); - } - else if(result == LibUsb.ERROR_PIPE) - { - int resetResult = LibUsb.clearHalt(mDeviceHandle, USB_BULK_TRANSFER_ENDPOINT); - - if(resetResult == LibUsb.SUCCESS) - { - int resubmitResult = LibUsb.submitTransfer(transfer); - - if(resubmitResult == LibUsb.SUCCESS) - { - mTransfersInProgress.add(transfer); - } - else - { - mLog.error(mDeviceName + " - error resubmitting transfer after endpoint clear halt"); - } + mCompletedTransfers.add(transfer); } else { - mLog.error(mDeviceName + " - unable to clear device endpoint halt"); + transfer.buffer().rewind(); + mAvailableTransfers.add(transfer); } - } - else - { + break; + case LibUsb.TRANSFER_CANCELLED: + transfer.buffer().rewind(); mAvailableTransfers.add(transfer); - mLog.error(mDeviceName + " - error submitting transfer [" + LibUsb.errorName(result) + "]"); + break; + default: + //Unexpected transfer error - need to reset the bulk transfer interface + mLog.error(mDeviceName + " - transfer error [" + getTransferStatus(transfer.status()) + + "] transferred actual: " + transfer.actualLength()); + transfer.buffer().rewind(); + mAvailableTransfers.add(transfer); + restart(); + return; + } - if(mTransfersInProgress.isEmpty()) - { - mLog.warn(mDeviceName + " - all transfer buffer processing is stopped"); - //TODO: no transfers are in progress ... need to alert the user and the registered complex - //TODO: buffer listeners so that they can respond accordingly - } - } - } - else - { - //We're stopping - park the transfer buffers - if(!mAvailableTransfers.contains(transfer)) - { - mAvailableTransfers.add(transfer); - } - } + mTransfersToSubmit++; + + //Dispatch a new transfer + submitTransfers(); } /** @@ -393,39 +376,39 @@ public class USBTransferProcessor implements TransferCallback } /** - * Fetches byte[] chunks from the raw sample buffer. Converts each byte - * array and broadcasts the array to all registered listeners + * Processes completed USB buffer transfers, converts the transferred bytes into complex samples and dispatches the + * sample buffer to the listener. */ - public class BufferDispatcher implements Runnable + public class CompletedTransferProcessor implements Runnable { - private List mBuffersToDispatch = new ArrayList<>(); - @Override public void run() { - mFilledBuffers.drainTo(mBuffersToDispatch, 50); - - while(!mBuffersToDispatch.isEmpty()) + try { - try + Transfer transfer = mCompletedTransfers.poll(); + + while(transfer != null) { - for(byte[] buffer : mBuffersToDispatch) + if(mRunning.get() && mComplexBufferListener != null) { - float[] complexSamples = mSampleAdapter.convert(buffer); + ByteBuffer nativeBuffer = transfer.buffer(); - if(mComplexBufferListener != null) - { - mComplexBufferListener.receive(new ComplexBuffer(complexSamples)); - } + ReusableComplexBuffer sampleBuffer = mNativeBufferConverter.convert(nativeBuffer, transfer.actualLength()); + + mComplexBufferListener.receive(sampleBuffer); } - } - catch(Exception e) - { - mLog.error(mDeviceName + " - error while dispatching complex IQ buffer samples", e); - } - mBuffersToDispatch.clear(); - mFilledBuffers.drainTo(mBuffersToDispatch, 50); + transfer.buffer().rewind(); + mAvailableTransfers.add(transfer); + + //Fetch the next transfer to process + transfer = mCompletedTransfers.poll(); + } + } + catch(Throwable throwable) + { + mLog.error("Error while processing USB transfer buffers", throwable); } } } diff --git a/src/main/java/io/github/dsheirer/source/tuner/usb/USBTunerController.java b/src/main/java/io/github/dsheirer/source/tuner/usb/USBTunerController.java index 19d60729..db114e7d 100644 --- a/src/main/java/io/github/dsheirer/source/tuner/usb/USBTunerController.java +++ b/src/main/java/io/github/dsheirer/source/tuner/usb/USBTunerController.java @@ -19,7 +19,7 @@ package io.github.dsheirer.source.tuner.usb; import io.github.dsheirer.sample.Listener; -import io.github.dsheirer.sample.complex.ComplexBuffer; +import io.github.dsheirer.sample.complex.reusable.ReusableComplexBuffer; import io.github.dsheirer.source.tuner.TunerController; public abstract class USBTunerController extends TunerController @@ -44,11 +44,11 @@ public abstract class USBTunerController extends TunerController * Adds the IQ buffer listener and automatically starts buffer transfer processing, if not already started. */ @Override - public void addComplexBufferListener(Listener listener) + public void addBufferListener(Listener listener) { - boolean hasExistingListeners = hasComplexBufferListeners(); + boolean hasExistingListeners = hasBufferListeners(); - super.addComplexBufferListener(listener); + super.addBufferListener(listener); if(!hasExistingListeners) { @@ -60,11 +60,11 @@ public abstract class USBTunerController extends TunerController * Removes the IQ buffer listener and stops buffer transfer processing if there are no more listeners. */ @Override - public void removeComplexBufferListener(Listener listener) + public void removeBufferListener(Listener listener) { - super.removeComplexBufferListener(listener); + super.removeBufferListener(listener); - if(!hasComplexBufferListeners()) + if(!hasBufferListeners()) { getUSBTransferProcessor().removeListener(); } diff --git a/src/main/java/io/github/dsheirer/source/tuner/usb/converter/ByteSampleConverter.java b/src/main/java/io/github/dsheirer/source/tuner/usb/converter/ByteSampleConverter.java new file mode 100644 index 00000000..8a20164f --- /dev/null +++ b/src/main/java/io/github/dsheirer/source/tuner/usb/converter/ByteSampleConverter.java @@ -0,0 +1,80 @@ +/******************************************************************************* + * 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 + * + ******************************************************************************/ +package io.github.dsheirer.source.tuner.usb.converter; + +import java.nio.ByteBuffer; +import java.nio.FloatBuffer; + +public class ByteSampleConverter extends NativeBufferConverter +{ + private final static float[] LOOKUP_VALUES; + + //Creates a static lookup table that converts the 8-bit valued range from 0 - 255 into scaled float values + //of -1.0 to 0 to 1.0 + static + { + LOOKUP_VALUES = new float[256]; + + for(int x = 0; x < 256; x++) + { + LOOKUP_VALUES[x] = (float)(x - 127) / 128.0f; + } + } + + private FloatBuffer mFloatBuffer; + + /** + * Converts native byte buffers containing 8-bit complex samples into complex float samples loaded into a tracked, + * reusable complex sample buffer. Internally tracks the reusable buffer until all downstream consumers have finished + * processing the buffer contents and then reuses the buffer (and memory) for subsequent samples. + */ + public ByteSampleConverter() + { + } + + /** + * Converts the 8-bit complex samples contained in the native buffer into floats that are loaded into a float + * buffer and subsequently transferred to a reusable complex buffer by the parent class. + * + * @param nativeBuffer containing 8-bit complex samples + * @param length of bytes to read from the native buffer + * @return float buffer loaded with converted samples + */ + @Override + protected FloatBuffer convertSamples(ByteBuffer nativeBuffer, int length) + { + nativeBuffer.rewind(); + + if(mFloatBuffer == null || mFloatBuffer.capacity() != nativeBuffer.capacity()) + { + mFloatBuffer = FloatBuffer.allocate(nativeBuffer.capacity()); + } + + mFloatBuffer.rewind(); + + int count = 0; + + while(nativeBuffer.hasRemaining() && count < length) + { + byte sample = nativeBuffer.get(); + count++; + + mFloatBuffer.put(LOOKUP_VALUES[(sample & 0xFF)]); + } + + return mFloatBuffer; + } +} diff --git a/src/main/java/io/github/dsheirer/source/tuner/usb/converter/NativeBufferConverter.java b/src/main/java/io/github/dsheirer/source/tuner/usb/converter/NativeBufferConverter.java new file mode 100644 index 00000000..9d9dfcbf --- /dev/null +++ b/src/main/java/io/github/dsheirer/source/tuner/usb/converter/NativeBufferConverter.java @@ -0,0 +1,64 @@ +/******************************************************************************* + * 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 + * + ******************************************************************************/ +package io.github.dsheirer.source.tuner.usb.converter; + +import io.github.dsheirer.sample.complex.reusable.ReusableBufferQueue; +import io.github.dsheirer.sample.complex.reusable.ReusableComplexBuffer; + +import java.nio.ByteBuffer; +import java.nio.FloatBuffer; + +public abstract class NativeBufferConverter +{ + private ReusableBufferQueue mReusableBufferQueue = new ReusableBufferQueue(); + + /** + * Converts native byte buffers into complex float samples and produces reusable complex sample buffers. Tracks + * each reusable buffer until the downstream consumer(s) are finished with the buffer and then reuses the buffer. + */ + public NativeBufferConverter() + { + } + + /** + * Converts the native byte buffer to complex float samples and returns the samples in a reusable buffer. Internally + * tracks each reusable buffer until all consumers indicate they are finished processing the buffer and then reuses + * the buffer. + * + * @param byteBuffer containing native memory byte samples + * @return native buffer samples converted to complex floats loaded into a reusable buffer + */ + public ReusableComplexBuffer convert(ByteBuffer byteBuffer, int length) + { + FloatBuffer floatBuffer = convertSamples(byteBuffer, length); + + ReusableComplexBuffer reusableComplexBuffer = mReusableBufferQueue.getBuffer(floatBuffer.capacity()); + + //The reusable buffer will rewind the float buffer to the beginning + reusableComplexBuffer.reloadFrom(floatBuffer, System.currentTimeMillis()); + + return reusableComplexBuffer; + } + + /** + * Converts the native byte buffer bytes into complex float samples. + * + * @param buffer containing native byte buffer samples + * @return a buffer with complex float samples. This float buffer will not be modified, therefore sub-class + * implementations can reuse the float buffer. + */ + protected abstract FloatBuffer convertSamples(ByteBuffer buffer, int length); +} diff --git a/src/main/java/io/github/dsheirer/source/wave/ComplexWaveSource.java b/src/main/java/io/github/dsheirer/source/wave/ComplexWaveSource.java index e219d85b..4ddb7704 100644 --- a/src/main/java/io/github/dsheirer/source/wave/ComplexWaveSource.java +++ b/src/main/java/io/github/dsheirer/source/wave/ComplexWaveSource.java @@ -1,26 +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 + * You should have received a copy of the GNU General Public License along with this program. + * If not, see * ******************************************************************************/ package io.github.dsheirer.source.wave; import io.github.dsheirer.sample.ConversionUtils; import io.github.dsheirer.sample.Listener; -import io.github.dsheirer.sample.complex.ComplexBuffer; +import io.github.dsheirer.sample.complex.reusable.ReusableBufferQueue; +import io.github.dsheirer.sample.complex.reusable.ReusableComplexBuffer; import io.github.dsheirer.source.ComplexSource; import io.github.dsheirer.source.IControllableFileSource; import io.github.dsheirer.source.IFrameLocationListener; @@ -44,9 +42,10 @@ public class ComplexWaveSource extends ComplexSource implements IControllableFil private int mBytesPerFrame; private int mFrameCounter = 0; private long mFrequency = 0; - private Listener mListener; + private Listener mListener; private AudioInputStream mInputStream; private File mFile; + private ReusableBufferQueue mReusableBufferQueue = new ReusableBufferQueue(); public ComplexWaveSource(File file) throws IOException { @@ -220,10 +219,10 @@ public class ComplexWaveSource extends ComplexSource implements IControllableFil buffer = Arrays.copyOf(buffer, samplesRead); } - float[] samples = ConversionUtils - .convertFromSigned16BitSamples(buffer); + float[] samples = ConversionUtils.convertFromSigned16BitSamples(buffer); - mListener.receive(new ComplexBuffer(samples)); + ReusableComplexBuffer reusableBuffer = mReusableBufferQueue.getBuffer(samples.length); + mListener.receive(reusableBuffer.incrementUserCount()); } } } @@ -233,7 +232,7 @@ public class ComplexWaveSource extends ComplexSource implements IControllableFil * the wave file */ @Override - public void setListener(Listener listener) + public void setListener(Listener listener) { mListener = listener; } @@ -241,7 +240,7 @@ public class ComplexWaveSource extends ComplexSource implements IControllableFil /** * Unregisters the listener from receiving sample buffers */ - public void removeListener(Listener listener) + public void removeListener(Listener listener) { mListener = null; } diff --git a/src/main/java/io/github/dsheirer/spectrum/ChannelSpectrumPanel.java b/src/main/java/io/github/dsheirer/spectrum/ChannelSpectrumPanel.java index 08a4e355..c646aba7 100644 --- a/src/main/java/io/github/dsheirer/spectrum/ChannelSpectrumPanel.java +++ b/src/main/java/io/github/dsheirer/spectrum/ChannelSpectrumPanel.java @@ -26,7 +26,8 @@ import io.github.dsheirer.dsp.filter.smoothing.SmoothingFilter.SmoothingType; import io.github.dsheirer.module.ProcessingChain; import io.github.dsheirer.sample.Listener; import io.github.dsheirer.sample.SampleType; -import io.github.dsheirer.sample.complex.ComplexBuffer; +import io.github.dsheirer.sample.complex.reusable.ReusableBufferQueue; +import io.github.dsheirer.sample.complex.reusable.ReusableComplexBuffer; import io.github.dsheirer.sample.real.RealBuffer; import io.github.dsheirer.settings.ColorSetting.ColorSettingName; import io.github.dsheirer.settings.ColorSettingMenuItem; @@ -68,6 +69,7 @@ public class ChannelSpectrumPanel extends JPanel implements ChannelEventListener private JLayeredPane mLayeredPane; private SpectrumPanel mSpectrumPanel; private ChannelOverlayPanel mOverlayPanel; + private ReusableBufferQueue mReusableBufferQueue = new ReusableBufferQueue(); private Channel mCurrentChannel; @@ -274,7 +276,9 @@ public class ChannelSpectrumPanel extends JPanel implements ChannelEventListener //Hack: we're placing real samples in a complex buffer that the DFT //processor is expecting. - mDFTProcessor.receive(new ComplexBuffer(decimated.getSamples())); + ReusableComplexBuffer reusableComplexBuffer = mReusableBufferQueue.getBuffer(decimated.getSamples().length); + reusableComplexBuffer.reloadFrom(decimated.getSamples(), System.currentTimeMillis()); + mDFTProcessor.receive(reusableComplexBuffer.incrementUserCount()); } /** diff --git a/src/main/java/io/github/dsheirer/spectrum/DFTProcessor.java b/src/main/java/io/github/dsheirer/spectrum/DFTProcessor.java index fa0cb3f3..172a278f 100644 --- a/src/main/java/io/github/dsheirer/spectrum/DFTProcessor.java +++ b/src/main/java/io/github/dsheirer/spectrum/DFTProcessor.java @@ -20,7 +20,7 @@ import io.github.dsheirer.dsp.filter.Window.WindowType; import io.github.dsheirer.properties.SystemProperties; import io.github.dsheirer.sample.Listener; import io.github.dsheirer.sample.SampleType; -import io.github.dsheirer.sample.complex.ComplexBuffer; +import io.github.dsheirer.sample.complex.reusable.ReusableComplexBuffer; import io.github.dsheirer.sample.real.IOverflowListener; import io.github.dsheirer.source.ISourceEventProcessor; import io.github.dsheirer.source.SourceEvent; @@ -41,7 +41,7 @@ import java.util.concurrent.atomic.AtomicBoolean; * Processes both complex samples or float samples and dispatches a float array of DFT results, using configurable fft * size and output dispatch timelines. */ -public class DFTProcessor implements Listener, ISourceEventProcessor, IDFTWidthChangeProcessor +public class DFTProcessor implements Listener, ISourceEventProcessor, IDFTWidthChangeProcessor { private static final Logger mLog = LoggerFactory.getLogger(DFTProcessor.class); private static final int BUFFER_QUEUE_MAX_SIZE = 12; @@ -192,13 +192,18 @@ public class DFTProcessor implements Listener, ISourceEventProces * Places the sample into a transfer queue for future processing. */ @Override - public void receive(ComplexBuffer sampleBuffer) + public void receive(ReusableComplexBuffer sampleBuffer) { mOverflowableBufferStream.offer(sampleBuffer); } private void calculate() { + //We always send the previous calculated samples - this should improve the screen rendering since the frame + //rate will always occur on an even rhythm. Any delays caused by processing will be absorbed and not impact + //the screen rendering. + dispatch(mPreviousSamples); + try { if(mFrameFlushCount > 0) @@ -220,14 +225,12 @@ public class DFTProcessor implements Listener, ISourceEventProces mFFT.complexForward(samples); } - dispatch(samples); - mPreviousSamples = samples; } catch(IOException ioe) { //No new data, dispatch the previous samples again - dispatch(mPreviousSamples); + //no-op } } diff --git a/src/main/java/io/github/dsheirer/spectrum/OverflowableBufferStream.java b/src/main/java/io/github/dsheirer/spectrum/OverflowableBufferStream.java index 0faec35c..1525d8e4 100644 --- a/src/main/java/io/github/dsheirer/spectrum/OverflowableBufferStream.java +++ b/src/main/java/io/github/dsheirer/spectrum/OverflowableBufferStream.java @@ -16,8 +16,8 @@ package io.github.dsheirer.spectrum; -import io.github.dsheirer.sample.Buffer; import io.github.dsheirer.sample.OverflowableTransferQueue; +import io.github.dsheirer.sample.complex.reusable.ReusableComplexBuffer; import org.slf4j.Logger; import org.slf4j.LoggerFactory; @@ -25,20 +25,20 @@ import java.io.IOException; import java.nio.FloatBuffer; import java.util.LinkedList; -public class OverflowableBufferStream extends OverflowableTransferQueue +public class OverflowableBufferStream extends OverflowableTransferQueue { private static final Logger mLog = LoggerFactory.getLogger(OverflowableBufferStream.class); private int mFlushCount = 0; - private Buffer mCurrentBuffer; + private ReusableComplexBuffer mCurrentBuffer; private int mCurrentBufferPointer = 0; private FloatBuffer mFloatBuffer; - private LinkedList mBufferList = new LinkedList<>(); + private LinkedList mBufferList = new LinkedList<>(); private int mBufferFetchLimit; /** - * Stream for receiving Buffers from a producer thread and providing access to a consumer thread to read the - * sample data. Extends the overflowable transfer queue to define maximum and reset thresholds to ensure the + * Stream for receiving reusable buffers from a producer thread and providing access to a consumer thread to read + * the sample data. Extends the overflowable transfer queue to define maximum and reset thresholds to ensure the * queue doesn't grow too large. * * This stream is designed for consistent sample size requests each time the get() samples method is invoked, such @@ -55,7 +55,7 @@ public class OverflowableBufferStream extends OverflowableTransferQueue { super(maxSize, resetThreshold); mFloatBuffer = FloatBuffer.allocate(arraySize); - mBufferFetchLimit = maxSize = resetThreshold; + mBufferFetchLimit = maxSize - resetThreshold; } /** @@ -181,7 +181,13 @@ public class OverflowableBufferStream extends OverflowableTransferQueue */ private void getNextBuffer() throws IOException { - mCurrentBuffer = null; + if(mCurrentBuffer != null) + { + //Decrement the user count to let the originator know we're done with their buffer + mCurrentBuffer.decrementUserCount(); + mCurrentBuffer = null; + } + mCurrentBufferPointer = 0; if(mBufferList.isEmpty()) @@ -196,7 +202,28 @@ public class OverflowableBufferStream extends OverflowableTransferQueue if(mCurrentBuffer == null) { - throw new IOException("Buffer queue is (currently) empty"); + throw new IOException("Reusable complex buffer queue is (currently) empty"); + } + } + + /** + * Clears all elements from the queue and resets the internal counter to 0 + */ + @Override + public void clear() + { + synchronized(mQueue) + { + ReusableComplexBuffer buffer = mQueue.poll(); + + while(buffer != null) + { + buffer.decrementUserCount(); + buffer = mQueue.poll(); + } + + mCounter.set(0); + mOverflow.set(false); } } } diff --git a/src/main/java/io/github/dsheirer/spectrum/SpectralDisplayPanel.java b/src/main/java/io/github/dsheirer/spectrum/SpectralDisplayPanel.java index a5032815..1b07c2f2 100644 --- a/src/main/java/io/github/dsheirer/spectrum/SpectralDisplayPanel.java +++ b/src/main/java/io/github/dsheirer/spectrum/SpectralDisplayPanel.java @@ -26,7 +26,7 @@ import io.github.dsheirer.module.decode.DecoderType; import io.github.dsheirer.properties.SystemProperties; import io.github.dsheirer.sample.Listener; import io.github.dsheirer.sample.SampleType; -import io.github.dsheirer.sample.complex.ComplexBuffer; +import io.github.dsheirer.sample.complex.reusable.ReusableComplexBuffer; import io.github.dsheirer.settings.ColorSetting.ColorSettingName; import io.github.dsheirer.settings.ColorSettingMenuItem; import io.github.dsheirer.settings.SettingsManager; @@ -75,7 +75,8 @@ import java.text.DecimalFormat; import java.util.ArrayList; import java.util.Hashtable; -public class SpectralDisplayPanel extends JPanel implements Listener, ISourceEventProcessor, IDFTWidthChangeProcessor +public class SpectralDisplayPanel extends JPanel implements Listener, ISourceEventProcessor, + IDFTWidthChangeProcessor { private static final long serialVersionUID = 1L; @@ -412,9 +413,10 @@ public class SpectralDisplayPanel extends JPanel implements Listener Date: Sun, 4 Feb 2018 04:59:27 -0500 Subject: [PATCH 53/87] [WIP] Testing completed for channel manager allocating tuner channels when the tuner controller indicates that there is a center DC spike blocked region or when the tuner has clipped outer spectral regions resulting in less than 100% usable bandwidth. --- .../filter/channelizer/ChannelizerViewer.java | 38 +++-- .../complex/IComplexBufferListener.java | 8 -- .../complex/IComplexBufferProvider.java | 39 ------ .../complex/TimestampedBufferAssembler.java | 132 ------------------ .../complex/TimestampedComplexBuffer.java | 63 --------- .../manager/TunerChannelSourceManager.java | 4 +- .../tuner/test/TestTunerController.java | 6 +- 7 files changed, 29 insertions(+), 261 deletions(-) delete mode 100644 src/main/java/io/github/dsheirer/sample/complex/IComplexBufferListener.java delete mode 100644 src/main/java/io/github/dsheirer/sample/complex/IComplexBufferProvider.java delete mode 100644 src/main/java/io/github/dsheirer/sample/complex/TimestampedBufferAssembler.java delete mode 100644 src/main/java/io/github/dsheirer/sample/complex/TimestampedComplexBuffer.java diff --git a/src/main/java/io/github/dsheirer/dsp/filter/channelizer/ChannelizerViewer.java b/src/main/java/io/github/dsheirer/dsp/filter/channelizer/ChannelizerViewer.java index 283a83c2..820ea2c0 100644 --- a/src/main/java/io/github/dsheirer/dsp/filter/channelizer/ChannelizerViewer.java +++ b/src/main/java/io/github/dsheirer/dsp/filter/channelizer/ChannelizerViewer.java @@ -82,7 +82,8 @@ public class ChannelizerViewer extends JFrame { mTestTuner = new TestTuner(); - mChannelCount = (int)(mTestTuner.getTunerController().getUsableBandwidth() / CHANNEL_BANDWIDTH); +// mChannelCount = (int)(mTestTuner.getTunerController().getUsableBandwidth() / CHANNEL_BANDWIDTH); + mChannelCount = (int)(mTestTuner.getTunerController().getBandwidth() / CHANNEL_BANDWIDTH); mChannelsPerRow = channelsPerRow; init(); @@ -221,7 +222,7 @@ public class ChannelizerViewer extends JFrame { long frequency = baseFrequency + (x * CHANNEL_BANDWIDTH); - mLog.debug("Channel " + x + " Frequency: " + frequency); + mLog.debug("Channel " + x + "/" + mChannelCount + " Frequency: " + frequency); ChannelPanel channelPanel = new ChannelPanel(mSettingsManager, CHANNEL_BANDWIDTH * 2, frequency, CHANNEL_BANDWIDTH, (x == channelToLog)); channelPanel.setDFTSize(mChannelPanelDFTSize); @@ -352,18 +353,6 @@ public class ChannelizerViewer extends JFrame mSpectrumPanel = new SpectrumPanel(settingsManager); mSpectrumPanel.setSampleSize(16); add(mSpectrumPanel, "span"); - add(new JLabel("Center:" + frequency)); - - mLoggingButton = new JToggleButton("Logging"); - mLoggingButton.addActionListener(new ActionListener() - { - @Override - public void actionPerformed(ActionEvent e) - { - mLoggingEnabled = mLoggingButton.isSelected(); - } - }); -// add(mLoggingButton); mDFTProcessor.addConverter(mComplexDecibelConverter); mDFTProcessor.process(SourceEvent.sampleRateChange(sampleRate)); @@ -395,6 +384,27 @@ public class ChannelizerViewer extends JFrame { mLog.error("Couldn't get a source from the tuner for frequency: " + frequency); } + + if(mSource != null) + { + add(new JLabel("Center:" + frequency)); + } + else + { + add(new JLabel("NO SRC:" + frequency)); + } + + mLoggingButton = new JToggleButton("Logging"); + mLoggingButton.addActionListener(new ActionListener() + { + @Override + public void actionPerformed(ActionEvent e) + { + mLoggingEnabled = mLoggingButton.isSelected(); + } + }); +// add(mLoggingButton); + } public TunerChannelSource getSource() diff --git a/src/main/java/io/github/dsheirer/sample/complex/IComplexBufferListener.java b/src/main/java/io/github/dsheirer/sample/complex/IComplexBufferListener.java deleted file mode 100644 index 7de93053..00000000 --- a/src/main/java/io/github/dsheirer/sample/complex/IComplexBufferListener.java +++ /dev/null @@ -1,8 +0,0 @@ -package io.github.dsheirer.sample.complex; - -import io.github.dsheirer.sample.Listener; - -public interface IComplexBufferListener -{ - public Listener getComplexBufferListener(); -} diff --git a/src/main/java/io/github/dsheirer/sample/complex/IComplexBufferProvider.java b/src/main/java/io/github/dsheirer/sample/complex/IComplexBufferProvider.java deleted file mode 100644 index af6f93a3..00000000 --- a/src/main/java/io/github/dsheirer/sample/complex/IComplexBufferProvider.java +++ /dev/null @@ -1,39 +0,0 @@ -/******************************************************************************* - * 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 io.github.dsheirer.sample.complex; - -import io.github.dsheirer.sample.Listener; - -public interface IComplexBufferProvider -{ - /** - * Adds the listener to receive complex buffer samples - */ - void addComplexBufferListener(Listener listener); - - /** - * Removes the listener from receiving complex buffer samples - */ - void removeComplexBufferListener(Listener listener); - - /** - * Indicates if there are any complex buffer listeners registered - */ - boolean hasComplexBufferListeners(); -} diff --git a/src/main/java/io/github/dsheirer/sample/complex/TimestampedBufferAssembler.java b/src/main/java/io/github/dsheirer/sample/complex/TimestampedBufferAssembler.java deleted file mode 100644 index fbc7cfed..00000000 --- a/src/main/java/io/github/dsheirer/sample/complex/TimestampedBufferAssembler.java +++ /dev/null @@ -1,132 +0,0 @@ -/******************************************************************************* - * 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 - * - ******************************************************************************/ -package io.github.dsheirer.sample.complex; - -import io.github.dsheirer.dsp.filter.channelizer.SampleTimestampManager; -import io.github.dsheirer.sample.Listener; -import org.apache.commons.lang3.Validate; - -import java.nio.FloatBuffer; - -/** - * Assembles complex float samples into a ComplexBuffer containing an array of floats. Monitors incoming sample count - * to accurately assign a relative timestamp to each assembled buffer. - */ -public class TimestampedBufferAssembler -{ - private SampleTimestampManager mTimestampManager; - private FloatBuffer mBuffer; - private long mCurrentBufferTimestamp; - private Listener mListener; - - public TimestampedBufferAssembler(int bufferSize, double sampleRate) - { - Validate.isTrue(bufferSize % 2 == 0); - mBuffer = FloatBuffer.allocate(bufferSize); - mTimestampManager = new SampleTimestampManager(sampleRate); - } - - /** - * Updates the reference timestamp to use for all subsequent samples that are assembled. This timestamp should - * identify the relative timestamp of the first/next sample that is received by this assembler. - * @param timestamp to use relative to the incoming sample arrays. - */ - public void updateTimestamp(long timestamp) - { - mTimestampManager.setReferenceTimestamp(timestamp); - - if(mBuffer.position() == 0) - { - mCurrentBufferTimestamp = timestamp; - } - } - - public void dispose() - { - mListener = null; - mBuffer = null; - } - - /** - * Adds the samples to this assembler. As each buffer is assembled, it will be dispatched to the registered - * listener. - * - * @param samples to assemble - */ - public void receive(float[] samples) - { - if(mBuffer.remaining() >= samples.length) - { - mBuffer.put(samples); - mTimestampManager.increment(samples.length / 2); - - if(!mBuffer.hasRemaining()) - { - flush(); - } - } - else - { - int offset = 0; - - while(offset < samples.length) - { - int toCopy = mBuffer.remaining(); - - if((samples.length - offset) < toCopy) - { - toCopy = samples.length - offset; - } - - mBuffer.put(samples, offset, toCopy); - - offset += toCopy; - mTimestampManager.increment(toCopy / 2); - - if(!mBuffer.hasRemaining()) - { - flush(); - } - } - } - } - - /** - * Flushes the current buffer contents to the registered listener - */ - public void flush() - { - if(mListener != null && mBuffer.position() > 0) - { - float[] samples = new float[mBuffer.position()]; - mBuffer.rewind(); - mBuffer.get(samples); - - mListener.receive(new TimestampedComplexBuffer(samples, mCurrentBufferTimestamp)); - } - - mBuffer.rewind(); - mCurrentBufferTimestamp = mTimestampManager.getCurrentTimestamp(); - } - - /** - * Sets the listener to receive the output from this buffer assembler. - */ - public void setListener(Listener listener) - { - mListener = listener; - } -} diff --git a/src/main/java/io/github/dsheirer/sample/complex/TimestampedComplexBuffer.java b/src/main/java/io/github/dsheirer/sample/complex/TimestampedComplexBuffer.java deleted file mode 100644 index 9e453ec7..00000000 --- a/src/main/java/io/github/dsheirer/sample/complex/TimestampedComplexBuffer.java +++ /dev/null @@ -1,63 +0,0 @@ -/******************************************************************************* - * 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 - * - ******************************************************************************/ -package io.github.dsheirer.sample.complex; - -public class TimestampedComplexBuffer extends ComplexBuffer -{ - private long mTimestamp; - - /** - * Constructs a timestamped complex buffer using the specified time in milliseconds. - * - * @param samples of data - */ - public TimestampedComplexBuffer(float[] samples, long timestamp) - { - super(samples); - - mTimestamp = timestamp; - } - - /** - * Constructs a timestamped complex buffer using the current system time in milliseconds. - * - * @param samples of data - */ - public TimestampedComplexBuffer(float[] samples) - { - this(samples, System.currentTimeMillis()); - } - - /** - * Reference timestamp for this complex buffer. Timestamp is relative to the first sample in the buffer. - * @return timestamp as milliseconds since epoch - */ - @Override - public long getTimestamp() - { - return mTimestamp; - } - - /** - * Indicates that this complex buffer has a reference timestamp - * @return true - */ - @Override - public boolean hasTimestamp() - { - return true; - } -} diff --git a/src/main/java/io/github/dsheirer/source/tuner/manager/TunerChannelSourceManager.java b/src/main/java/io/github/dsheirer/source/tuner/manager/TunerChannelSourceManager.java index 510aba97..7e46be1b 100644 --- a/src/main/java/io/github/dsheirer/source/tuner/manager/TunerChannelSourceManager.java +++ b/src/main/java/io/github/dsheirer/source/tuner/manager/TunerChannelSourceManager.java @@ -412,8 +412,8 @@ public class TunerChannelSourceManager extends ChannelSourceManager catch(IllegalArgumentException iae) { //Center frequency calculation failed - mLog.error("Couldn't calculate new tuner center frequency for new tuner channel [" + - tunerChannel + "] unable to allocate new tuner channel", iae); + mLog.debug("Couldn't calculate new tuner center frequency for new tuner channel [" + + tunerChannel + "] unable to allocate new tuner channel"); } } } diff --git a/src/main/java/io/github/dsheirer/source/tuner/test/TestTunerController.java b/src/main/java/io/github/dsheirer/source/tuner/test/TestTunerController.java index 576ca349..ec1bf0d6 100644 --- a/src/main/java/io/github/dsheirer/source/tuner/test/TestTunerController.java +++ b/src/main/java/io/github/dsheirer/source/tuner/test/TestTunerController.java @@ -32,9 +32,9 @@ 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 = 800000; - public static final int DC_NOISE_BANDWIDTH = 0; - public static final double USABLE_BANDWIDTH_PERCENTAGE = 1.00; + public static final int SAMPLE_RATE = 100000; + public static final int DC_NOISE_BANDWIDTH = 5000; + public static final double USABLE_BANDWIDTH_PERCENTAGE = .90; public static final int SPECTRAL_FRAME_RATE = 20; public static final long SAMPLE_GENERATION_INTERVAL = 1000 / SPECTRAL_FRAME_RATE; From 96d9d6f750ef264109317fe7b80bf1c0def6b835 Mon Sep 17 00:00:00 2001 From: Denny Date: Sun, 4 Feb 2018 06:30:25 -0500 Subject: [PATCH 54/87] [WIP] Adds locking mechanism to tuner controllers so that the polyphase channel manager can block users from changing frequency and/or sample rate when polyphase channels are allocated. --- .../channelizer/PolyphaseChannelManager.java | 2 +- .../gui/control/JFrequencyControl.java | 35 +- .../github/dsheirer/source/SourceEvent.java | 18 + .../source/tuner/TunerController.java | 31 + .../dsheirer/source/tuner/TunerEditor.java | 503 +++--- .../tuner/airspy/AirspyTunerEditor.java | 1471 +++++++++-------- .../TunerConfigurationEditor.java | 49 +- .../tuner/fcd/proV1/FCD1TunerEditor.java | 962 +++++------ .../tuner/fcd/proplusV2/FCD2TunerEditor.java | 489 +++--- .../tuner/frequency/FrequencyController.java | 453 ++--- .../tuner/hackrf/HackRFTunerEditor.java | 346 ++-- .../manager/TunerChannelSourceManager.java | 3 + .../source/tuner/rtl/e4k/E4KTunerEditor.java | 898 +++++----- .../tuner/rtl/r820t/R820TTunerEditor.java | 1042 ++++++------ 14 files changed, 3271 insertions(+), 3031 deletions(-) diff --git a/src/main/java/io/github/dsheirer/dsp/filter/channelizer/PolyphaseChannelManager.java b/src/main/java/io/github/dsheirer/dsp/filter/channelizer/PolyphaseChannelManager.java index 028cfb26..5fd66a2e 100644 --- a/src/main/java/io/github/dsheirer/dsp/filter/channelizer/PolyphaseChannelManager.java +++ b/src/main/java/io/github/dsheirer/dsp/filter/channelizer/PolyphaseChannelManager.java @@ -147,6 +147,7 @@ public class PolyphaseChannelManager implements ISourceEventProcessor mChannelCalculator.getChannelSampleRate()); mChannelSources.add(channelSource); + mSourceEventBroadcaster.broadcast(SourceEvent.channelCountChange(getTunerChannelCount())); } } catch(IllegalArgumentException iae) @@ -225,7 +226,6 @@ public class PolyphaseChannelManager implements ISourceEventProcessor synchronized(mBufferProcessor) { mPolyphaseChannelizer.addChannel(channelSource); - mSourceEventBroadcaster.broadcast(SourceEvent.channelCountChange(getTunerChannelCount())); if(mPolyphaseChannelizer.getRegisteredChannelCount() == 1) { diff --git a/src/main/java/io/github/dsheirer/gui/control/JFrequencyControl.java b/src/main/java/io/github/dsheirer/gui/control/JFrequencyControl.java index 4dfef7a2..bd6c4162 100644 --- a/src/main/java/io/github/dsheirer/gui/control/JFrequencyControl.java +++ b/src/main/java/io/github/dsheirer/gui/control/JFrequencyControl.java @@ -20,7 +20,6 @@ package io.github.dsheirer.gui.control; import io.github.dsheirer.source.ISourceEventProcessor; import io.github.dsheirer.source.InvalidFrequencyException; import io.github.dsheirer.source.SourceEvent; -import io.github.dsheirer.source.SourceEvent.Event; import io.github.dsheirer.source.SourceException; import net.miginfocom.swing.MigLayout; import org.slf4j.Logger; @@ -32,6 +31,7 @@ import javax.swing.JPanel; import javax.swing.JTextField; import java.awt.Color; import java.awt.Cursor; +import java.awt.EventQueue; import java.awt.Font; import java.awt.Image; import java.awt.Point; @@ -146,9 +146,38 @@ public class JFrequencyControl extends JPanel implements ISourceEventProcessor @Override public void process(SourceEvent event) { - if(event.getEvent() == Event.NOTIFICATION_FREQUENCY_CHANGE) + switch(event.getEvent()) { - setFrequency(event.getValue().longValue(), false); + case NOTIFICATION_FREQUENCY_CHANGE: + EventQueue.invokeLater(new Runnable() + { + @Override + public void run() + { + setFrequency(event.getValue().longValue(), false); + } + }); + break; + case NOTIFICATION_FREQUENCY_AND_SAMPLE_RATE_LOCKED: + EventQueue.invokeLater(new Runnable() + { + @Override + public void run() + { + setEnabled(false); + } + }); + break; + case NOTIFICATION_FREQUENCY_AND_SAMPLE_RATE_UNLOCKED: + EventQueue.invokeLater(new Runnable() + { + @Override + public void run() + { + setEnabled(true); + } + }); + break; } } diff --git a/src/main/java/io/github/dsheirer/source/SourceEvent.java b/src/main/java/io/github/dsheirer/source/SourceEvent.java index a5c23087..c8b6996e 100644 --- a/src/main/java/io/github/dsheirer/source/SourceEvent.java +++ b/src/main/java/io/github/dsheirer/source/SourceEvent.java @@ -27,6 +27,8 @@ public class SourceEvent NOTIFICATION_CHANNEL_COUNT_CHANGE, NOTIFICATION_CHANNEL_FREQUENCY_CORRECTION_CHANGE, NOTIFICATION_CHANNEL_SAMPLE_RATE_CHANGE, + NOTIFICATION_FREQUENCY_AND_SAMPLE_RATE_LOCKED, + NOTIFICATION_FREQUENCY_AND_SAMPLE_RATE_UNLOCKED, NOTIFICATION_FREQUENCY_CHANGE, NOTIFICATION_FREQUENCY_CORRECTION_CHANGE, NOTIFICATION_SAMPLE_RATE_CHANGE, @@ -119,6 +121,22 @@ public class SourceEvent return mSource != null; } + /** + * Creates a new locked state change event + */ + public static SourceEvent lockedState() + { + return new SourceEvent(Event.NOTIFICATION_FREQUENCY_AND_SAMPLE_RATE_LOCKED, 1); + } + + /** + * Creates a new unlocked state change event + */ + public static SourceEvent unlockedState() + { + return new SourceEvent(Event.NOTIFICATION_FREQUENCY_AND_SAMPLE_RATE_UNLOCKED, 0); + } + /** * Creates a new frequency change event * diff --git a/src/main/java/io/github/dsheirer/source/tuner/TunerController.java b/src/main/java/io/github/dsheirer/source/tuner/TunerController.java index 60394ee6..ebd1c516 100644 --- a/src/main/java/io/github/dsheirer/source/tuner/TunerController.java +++ b/src/main/java/io/github/dsheirer/source/tuner/TunerController.java @@ -104,6 +104,37 @@ public abstract class TunerController implements Tunable, ISourceEventProcessor, } } + /** + * Indicates if the frequency and sample rate controls are locked by another process. User interface controls + * should monitor source events and check the locked state via this method to correctly render the enabled state + * of the frequency and sample rate controls. + * + * @return true if the tuner controller is locked. + */ + public boolean isLocked() + { + return mFrequencyController.isLocked(); + } + + /** + * Sets the frequency and sample rate locked state to the locked argument value. This should only be changed + * by a downstream consumer of samples to prevent users or other processes from modifying the center frequency + * and/or sample rate of the tuner while processing samples. + * + * @param locked true to indicate the tuner controller is in a locked state, otherwise false. + */ + public void setLocked(boolean locked) + { + try + { + mFrequencyController.setLocked(locked); + } + catch(SourceException se) + { + mLog.error("Couldn't set frequency controller locked state to: " + locked); + } + } + public int getBandwidth() { return mFrequencyController.getBandwidth(); diff --git a/src/main/java/io/github/dsheirer/source/tuner/TunerEditor.java b/src/main/java/io/github/dsheirer/source/tuner/TunerEditor.java index e9c6a3f3..abb5f418 100644 --- a/src/main/java/io/github/dsheirer/source/tuner/TunerEditor.java +++ b/src/main/java/io/github/dsheirer/source/tuner/TunerEditor.java @@ -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 @@ -44,306 +44,305 @@ import java.awt.event.ActionListener; public class TunerEditor extends Editor { - private static final long serialVersionUID = 1L; + private static final long serialVersionUID = 1L; - private final static Logger mLog = LoggerFactory.getLogger( TunerEditor.class ); + private final static Logger mLog = LoggerFactory.getLogger(TunerEditor.class); - private TunerConfigurationModel mTunerConfigurationModel; - private JTable mTunerConfigurationTable; - private TableRowSorter mRowSorter; - private JFrequencyControl mFrequencyControl = new JFrequencyControl(); - private JLabel mSelectedTunerType = new JLabel( "No Tuner Selected" ); - private JButton mNewConfigurationButton = new JButton( "New" ); - private JButton mDeleteConfigurationButton = new JButton( "Delete" ); - private JButton mAssignConfigurationButton = new JButton( "Assign" ); - private JScrollPane mEditorScroller; - private Editor mEditor = new EmptyEditor<>( "a tuner" ); - private JideSplitPane mEditorSplitPane = new JideSplitPane(); + private TunerConfigurationModel mTunerConfigurationModel; + private JTable mTunerConfigurationTable; + private TableRowSorter mRowSorter; + private JFrequencyControl mFrequencyControl = new JFrequencyControl(); + private JLabel mSelectedTunerType = new JLabel("No Tuner Selected"); + private JButton mNewConfigurationButton = new JButton("New"); + private JButton mDeleteConfigurationButton = new JButton("Delete"); + private JButton mAssignConfigurationButton = new JButton("Assign"); + private JScrollPane mEditorScroller; + private Editor mEditor = new EmptyEditor<>("a tuner"); + private JideSplitPane mEditorSplitPane = new JideSplitPane(); - public TunerEditor( TunerConfigurationModel tunerConfigurationModel ) - { - mTunerConfigurationModel = tunerConfigurationModel; - init(); - } + public TunerEditor(TunerConfigurationModel tunerConfigurationModel) + { + mTunerConfigurationModel = tunerConfigurationModel; + init(); + } - public void init() - { - setLayout( new MigLayout( "insets 0 0 0 0", "[grow,fill]", "[grow,fill]" ) ); + public void init() + { + setLayout(new MigLayout("insets 0 0 0 0", "[grow,fill]", "[grow,fill]")); - JPanel listPanel = new JPanel(); - listPanel.setLayout( new MigLayout( "fill,wrap 3", "[grow,fill]", "[][][][grow,fill][]" ) ); + JPanel listPanel = new JPanel(); + listPanel.setLayout(new MigLayout("fill,wrap 3", "[grow,fill]", "[][][][grow,fill][]")); - listPanel.add( mSelectedTunerType, "span" ); + listPanel.add(mSelectedTunerType, "span"); - mFrequencyControl.setEnabled( false ); - listPanel.add( mFrequencyControl, "span" ); + mFrequencyControl.setEnabled(false); + listPanel.add(mFrequencyControl, "span"); - mRowSorter = new TableRowSorter<>( mTunerConfigurationModel ); - mTunerConfigurationTable = new JTable( mTunerConfigurationModel ); - mTunerConfigurationTable.setRowSorter( mRowSorter ); - mTunerConfigurationTable.setEnabled( false ); - mTunerConfigurationTable.getSelectionModel().addListSelectionListener( new ListSelectionListener() - { - @Override - public void valueChanged( ListSelectionEvent event ) - { - if( !event.getValueIsAdjusting() ) - { - int row = mTunerConfigurationTable.getSelectedRow(); + mRowSorter = new TableRowSorter<>(mTunerConfigurationModel); + mTunerConfigurationTable = new JTable(mTunerConfigurationModel); + mTunerConfigurationTable.setRowSorter(mRowSorter); + mTunerConfigurationTable.setEnabled(false); + mTunerConfigurationTable.getSelectionModel().addListSelectionListener(new ListSelectionListener() + { + @Override + public void valueChanged(ListSelectionEvent event) + { + if(!event.getValueIsAdjusting()) + { + int row = mTunerConfigurationTable.getSelectedRow(); - if( row >= 0 ) - { - int modelRow = mTunerConfigurationTable.convertRowIndexToModel( row ); + if(row >= 0) + { + int modelRow = mTunerConfigurationTable.convertRowIndexToModel(row); - if( modelRow >= 0 ) - { - setTunerConfiguration( mTunerConfigurationModel.getTunerConfiguration( modelRow ) ); - } - } - } - } - } ); + if(modelRow >= 0) + { + setTunerConfiguration(mTunerConfigurationModel.getTunerConfiguration(modelRow)); + } + } + } + } + }); - JScrollPane tableScroller = new JScrollPane( mTunerConfigurationTable ); - tableScroller.setHorizontalScrollBarPolicy( - ScrollPaneConstants.HORIZONTAL_SCROLLBAR_NEVER ); - listPanel.add( new JLabel( "Tuner Configurations" ), "span" ); - listPanel.add( tableScroller, "span" ); + JScrollPane tableScroller = new JScrollPane(mTunerConfigurationTable); + tableScroller.setHorizontalScrollBarPolicy( + ScrollPaneConstants.HORIZONTAL_SCROLLBAR_NEVER); + listPanel.add(new JLabel("Tuner Configurations"), "span"); + listPanel.add(tableScroller, "span"); - mNewConfigurationButton.setEnabled( false ); - mNewConfigurationButton.setToolTipText( "Create a new configuration for the currently selected tuner" ); - mNewConfigurationButton.addActionListener( new ActionListener() - { - @Override - public void actionPerformed( ActionEvent e ) - { - Tuner tuner = getItem(); + mNewConfigurationButton.setEnabled(false); + mNewConfigurationButton.setToolTipText("Create a new configuration for the currently selected tuner"); + mNewConfigurationButton.addActionListener(new ActionListener() + { + @Override + public void actionPerformed(ActionEvent e) + { + Tuner tuner = getItem(); - String name = mTunerConfigurationModel - .getDistinctName( tuner.getTunerType(), tuner.getUniqueID() ); + String name = mTunerConfigurationModel + .getDistinctName(tuner.getTunerType(), tuner.getUniqueID()); - TunerConfiguration config = TunerConfigurationFactory - .getTunerConfiguration( tuner.getTunerType(), - tuner.getUniqueID(), name ); + TunerConfiguration config = TunerConfigurationFactory + .getTunerConfiguration(tuner.getTunerType(), + tuner.getUniqueID(), name); - mTunerConfigurationModel.addTunerConfiguration( config ); - } - } ); - listPanel.add( mNewConfigurationButton ); + mTunerConfigurationModel.addTunerConfiguration(config); + } + }); + listPanel.add(mNewConfigurationButton); - mDeleteConfigurationButton.setEnabled( false ); - mDeleteConfigurationButton.setToolTipText( "Deletes the currently selected tuner" ); - mDeleteConfigurationButton.addActionListener( new ActionListener() - { - @Override - public void actionPerformed( ActionEvent e ) - { - int choice = JOptionPane.showConfirmDialog( TunerEditor.this, - "Do you want to delete this tuner configuration?", - "Delete Tuner Configuration?", JOptionPane.YES_NO_OPTION ); + mDeleteConfigurationButton.setEnabled(false); + mDeleteConfigurationButton.setToolTipText("Deletes the currently selected tuner"); + mDeleteConfigurationButton.addActionListener(new ActionListener() + { + @Override + public void actionPerformed(ActionEvent e) + { + int choice = JOptionPane.showConfirmDialog(TunerEditor.this, + "Do you want to delete this tuner configuration?", + "Delete Tuner Configuration?", JOptionPane.YES_NO_OPTION); - if( choice == JOptionPane.YES_OPTION ) - { - TunerConfiguration selected = getSelectedTunerConfiguration(); + if(choice == JOptionPane.YES_OPTION) + { + TunerConfiguration selected = getSelectedTunerConfiguration(); - mTunerConfigurationModel.removeTunerConfiguration( selected ); - mTunerConfigurationTable.setRowSelectionInterval( 0, 0 ); - } - } - } ); - listPanel.add( mDeleteConfigurationButton ); + mTunerConfigurationModel.removeTunerConfiguration(selected); + mTunerConfigurationTable.setRowSelectionInterval(0, 0); + } + } + }); + listPanel.add(mDeleteConfigurationButton); - mAssignConfigurationButton.setEnabled( false ); - mAssignConfigurationButton.setToolTipText( "Assigns to the currently selected tuner configuration to the selected tuner" ); - mAssignConfigurationButton.addActionListener( new ActionListener() - { - @Override - public void actionPerformed( ActionEvent e ) - { - TunerConfiguration selected = getSelectedTunerConfiguration(); + mAssignConfigurationButton.setEnabled(false); + mAssignConfigurationButton.setToolTipText("Assigns to the currently selected tuner configuration to the selected tuner"); + mAssignConfigurationButton.addActionListener(new ActionListener() + { + @Override + public void actionPerformed(ActionEvent e) + { + TunerConfiguration selected = getSelectedTunerConfiguration(); - mTunerConfigurationModel.assignTunerConfiguration( selected ); - setTunerConfiguration( getSelectedTunerConfiguration() ); - } - } ); - listPanel.add( mAssignConfigurationButton ); + mTunerConfigurationModel.assignTunerConfiguration(selected); + setTunerConfiguration(getSelectedTunerConfiguration()); + } + }); + listPanel.add(mAssignConfigurationButton); - //Set preferred size on both scrollers to same width, so they split the space - listPanel.setPreferredSize( new Dimension( 100, 80 ) ); - JScrollPane listScroller = new JScrollPane( listPanel ); - listScroller.setPreferredSize( new Dimension( 100, 80 ) ); + //Set preferred size on both scrollers to same width, so they split the space + listPanel.setPreferredSize(new Dimension(100, 80)); + JScrollPane listScroller = new JScrollPane(listPanel); + listScroller.setPreferredSize(new Dimension(100, 80)); - mEditorScroller = new JScrollPane( mEditor ); - mEditorScroller.setPreferredSize( new Dimension( 100, 80 ) ); + mEditorScroller = new JScrollPane(mEditor); + mEditorScroller.setPreferredSize(new Dimension(100, 80)); - mEditorSplitPane = new JideSplitPane( JideSplitPane.HORIZONTAL_SPLIT ); - mEditorSplitPane.add( listScroller ); - mEditorSplitPane.add( mEditorScroller ); + mEditorSplitPane = new JideSplitPane(JideSplitPane.HORIZONTAL_SPLIT); + mEditorSplitPane.add(listScroller); + mEditorSplitPane.add(mEditorScroller); - add( mEditorSplitPane ); - } + add(mEditorSplitPane); + } - private TunerConfiguration getSelectedTunerConfiguration() - { - int tableRow = mTunerConfigurationTable.getSelectedRow(); + private TunerConfiguration getSelectedTunerConfiguration() + { + int tableRow = mTunerConfigurationTable.getSelectedRow(); - if( tableRow >= 0 ) - { - int modelRow = mTunerConfigurationTable.convertRowIndexToModel( tableRow ); + if(tableRow >= 0) + { + int modelRow = mTunerConfigurationTable.convertRowIndexToModel(tableRow); - if( modelRow >= 0 ) - { - return mTunerConfigurationModel.getTunerConfiguration( modelRow ); - } - } + if(modelRow >= 0) + { + return mTunerConfigurationModel.getTunerConfiguration(modelRow); + } + } - return null; - } + return null; + } - @Override - public void save() - { - //Unused - } + @Override + public void save() + { + //Unused + } - @Override - public void setItem( Tuner tuner ) - { - //Cleanup previously selected tuner - if( hasItem() ) - { - Tuner previous = getItem(); - previous.getTunerController().removeListener( mFrequencyControl ); - mFrequencyControl.removeListener( previous.getTunerController() ); - } + @Override + public void setItem(Tuner tuner) + { + //Cleanup previously selected tuner + if(hasItem()) + { + Tuner previous = getItem(); + previous.getTunerController().removeListener(mFrequencyControl); + mFrequencyControl.removeListener(previous.getTunerController()); + } - super.setItem( tuner ); + super.setItem(tuner); - if( hasItem() ) - { - mSelectedTunerType.setText( tuner.getName() ); - mFrequencyControl.setEnabled( true ); - mTunerConfigurationTable.setEnabled( true ); - mNewConfigurationButton.setEnabled( true ); + if(hasItem()) + { + mSelectedTunerType.setText(tuner.getName()); + mTunerConfigurationTable.setEnabled(true); + mNewConfigurationButton.setEnabled(true); - //Change to an editor for this config's tuner type - mEditor = TunerConfigurationFactory.getEditor( getItem(), mTunerConfigurationModel ); + //Change to an editor for this config's tuner type + mEditor = TunerConfigurationFactory.getEditor(getItem(), mTunerConfigurationModel); - mRowSorter.setRowFilter( new ConfigurationRowFilter( tuner.getTunerType(), - tuner.getUniqueID() ) ); + mRowSorter.setRowFilter(new ConfigurationRowFilter(tuner.getTunerType(), + tuner.getUniqueID())); - TunerConfiguration assigned = mTunerConfigurationModel - .getTunerConfiguration( tuner.getTunerType(), tuner.getUniqueID() ); + TunerConfiguration assigned = mTunerConfigurationModel + .getTunerConfiguration(tuner.getTunerType(), tuner.getUniqueID()); - int modelIndex = mTunerConfigurationModel.getRowIndex( assigned ); + int modelIndex = mTunerConfigurationModel.getRowIndex(assigned); - if( modelIndex >= 0 ) - { - int viewIndex = mTunerConfigurationTable - .convertRowIndexToView( modelIndex ); + if(modelIndex >= 0) + { + int viewIndex = mTunerConfigurationTable + .convertRowIndexToView(modelIndex); - if( viewIndex >= 0 ) - { - mTunerConfigurationTable - .setRowSelectionInterval( viewIndex, viewIndex ); - } - } + if(viewIndex >= 0) + { + mTunerConfigurationTable + .setRowSelectionInterval(viewIndex, viewIndex); + } + } - //Link frequency control to the tuner - mFrequencyControl.addListener( tuner.getTunerController() ); + //Link frequency control to the tuner and set enabled state + mFrequencyControl.addListener(tuner.getTunerController()); + mFrequencyControl.setEnabled(!tuner.getTunerController().isLocked()); - //Link tuner to frequency control - tuner.getTunerController().addListener( mFrequencyControl ); + //Link tuner to frequency control + tuner.getTunerController().addListener(mFrequencyControl); - //Set the displayed frequency without adjusting the tuner's frequency - mFrequencyControl.setFrequency( - tuner.getTunerController().getFrequency(), false ); - } - else - { - mSelectedTunerType.setText( "No Tuner Selected" ); - mFrequencyControl.setEnabled( false ); - mTunerConfigurationTable.setEnabled( false ); - mNewConfigurationButton.setEnabled( false ); - mRowSorter.setRowFilter( null ); - mEditor = new EmptyEditor(); - } + //Set the displayed frequency without adjusting the tuner's frequency + mFrequencyControl.setFrequency(tuner.getTunerController().getFrequency(), false); + } + else + { + mSelectedTunerType.setText("No Tuner Selected"); + mFrequencyControl.setEnabled(false); + mTunerConfigurationTable.setEnabled(false); + mNewConfigurationButton.setEnabled(false); + mRowSorter.setRowFilter(null); + mEditor = new EmptyEditor(); + } - //Swap out the editor - int split = mEditorSplitPane.getDividerLocation( 0 ); - mEditorSplitPane.remove( mEditorScroller ); - mEditorScroller = new JScrollPane( mEditor ); - mEditorSplitPane.add( mEditorScroller ); - mEditorSplitPane.setDividerLocation( 0, split ); - } + //Swap out the editor + int split = mEditorSplitPane.getDividerLocation(0); + mEditorSplitPane.remove(mEditorScroller); + mEditorScroller = new JScrollPane(mEditor); + mEditorSplitPane.add(mEditorScroller); + mEditorSplitPane.setDividerLocation(0, split); + } - private void setTunerConfiguration( TunerConfiguration config ) - { - if( config != null ) - { - if( !config.isAssigned() ) - { - if( !mAssignConfigurationButton.isEnabled() ) - { - mAssignConfigurationButton.setEnabled( true ); - } - if( !mDeleteConfigurationButton.isEnabled() ) - { - mDeleteConfigurationButton.setEnabled( true ); - } - } - else - { - if( mAssignConfigurationButton.isEnabled() ) - { - mAssignConfigurationButton.setEnabled( false ); - } + private void setTunerConfiguration(TunerConfiguration config) + { + if(config != null) + { + if(!config.isAssigned()) + { + if(!mAssignConfigurationButton.isEnabled()) + { + mAssignConfigurationButton.setEnabled(true); + } + if(!mDeleteConfigurationButton.isEnabled()) + { + mDeleteConfigurationButton.setEnabled(true); + } + } + else + { + if(mAssignConfigurationButton.isEnabled()) + { + mAssignConfigurationButton.setEnabled(false); + } - if( mDeleteConfigurationButton.isEnabled() ) - { - mDeleteConfigurationButton.setEnabled( false ); - } - } - } - else - { - if( mDeleteConfigurationButton.isEnabled() ) - { - mDeleteConfigurationButton.setEnabled( false ); - } + if(mDeleteConfigurationButton.isEnabled()) + { + mDeleteConfigurationButton.setEnabled(false); + } + } + } + else + { + if(mDeleteConfigurationButton.isEnabled()) + { + mDeleteConfigurationButton.setEnabled(false); + } - if( mAssignConfigurationButton.isEnabled() ) - { - mAssignConfigurationButton.setEnabled( false ); - } - } + if(mAssignConfigurationButton.isEnabled()) + { + mAssignConfigurationButton.setEnabled(false); + } + } - mEditor.setItem( config ); - } + mEditor.setItem(config); + } - public class ConfigurationRowFilter extends RowFilter - { - private TunerType mTunerType; - private String mUniqueID; + public class ConfigurationRowFilter extends RowFilter + { + private TunerType mTunerType; + private String mUniqueID; - public ConfigurationRowFilter( TunerType type, String uniqueID ) - { - mTunerType = type; - mUniqueID = uniqueID; - } + public ConfigurationRowFilter(TunerType type, String uniqueID) + { + mTunerType = type; + mUniqueID = uniqueID; + } - @Override - public boolean include( javax.swing.RowFilter.Entry entry ) - { - TunerConfiguration config = entry.getModel() - .getTunerConfiguration( entry.getIdentifier() ); + @Override + public boolean include(javax.swing.RowFilter.Entry entry) + { + TunerConfiguration config = entry.getModel() + .getTunerConfiguration(entry.getIdentifier()); - return config != null && - config.getTunerType() == mTunerType && - config.getUniqueID() != null && - config.getUniqueID().contentEquals( mUniqueID ); - } - } + return config != null && + config.getTunerType() == mTunerType && + config.getUniqueID() != null && + config.getUniqueID().contentEquals(mUniqueID); + } + } } \ No newline at end of file diff --git a/src/main/java/io/github/dsheirer/source/tuner/airspy/AirspyTunerEditor.java b/src/main/java/io/github/dsheirer/source/tuner/airspy/AirspyTunerEditor.java index 2d47f9c2..e8ef26c1 100644 --- a/src/main/java/io/github/dsheirer/source/tuner/airspy/AirspyTunerEditor.java +++ b/src/main/java/io/github/dsheirer/source/tuner/airspy/AirspyTunerEditor.java @@ -1,19 +1,17 @@ /******************************************************************************* - * SDR Trunk - * Copyright (C) 2015-2016 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 + * 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 + * ******************************************************************************/ package io.github.dsheirer.source.tuner.airspy; @@ -45,6 +43,7 @@ import javax.swing.SwingConstants; import javax.swing.event.ChangeEvent; import javax.swing.event.ChangeListener; import javax.usb.UsbException; +import java.awt.EventQueue; import java.awt.event.ActionEvent; import java.awt.event.ActionListener; import java.awt.event.FocusEvent; @@ -54,713 +53,737 @@ import java.util.List; public class AirspyTunerEditor extends TunerConfigurationEditor { - private static final long serialVersionUID = 1L; - - private final static Logger mLog = - LoggerFactory.getLogger( AirspyTunerEditor.class ); - - private JTextField mConfigurationName; - private JButton mTunerInfo; - - private JComboBox mSampleRateCombo; - private JSpinner mFrequencyCorrection; - - private JComboBox mGainModeCombo; - - private JSlider mMasterGain; - private JLabel mMasterGainValueLabel; - - private JSlider mIFGain; - private JLabel mIFGainValueLabel; - - private JSlider mLNAGain; - private JLabel mLNAGainValueLabel; - - private JSlider mMixerGain; - private JLabel mMixerGainValueLabel; - - private JCheckBox mLNAAGC; - private JCheckBox mMixerAGC; - - private AirspyTunerController mController; - private boolean mLoading; - - public AirspyTunerEditor( TunerConfigurationModel tunerConfigurationModel, - AirspyTuner tuner ) - { - super( tunerConfigurationModel ); - mController = tuner.getController(); - - init(); - } - - private AirspyTunerConfiguration getConfiguration() - { - if( hasItem() ) - { - return (AirspyTunerConfiguration)getItem(); - } - - return null; - } - - private void init() - { - setLayout( new MigLayout( "fill,wrap 4", "[right][grow,fill][right][grow,fill]", - "[][][][][][grow]" ) ); - - add( new JLabel( "Airspy Tuner Configuration" ), "span,align center" ); - - mConfigurationName = new JTextField(); - mConfigurationName.setEnabled( false ); - mConfigurationName.addFocusListener( new FocusListener() - { - @Override - public void focusLost( FocusEvent e ) - { - save(); - } - @Override - public void focusGained( FocusEvent e ) {} - } ); - - add( new JLabel( "Name:" ) ); - add( mConfigurationName, "span 2" ); - - mTunerInfo = new JButton( "Tuner Info" ); - mTunerInfo.setEnabled( false ); - mTunerInfo.addActionListener( new ActionListener() - { - @Override - public void actionPerformed( ActionEvent e ) - { - JOptionPane.showMessageDialog( AirspyTunerEditor.this, getTunerInfo(), - "Tuner Info", JOptionPane.INFORMATION_MESSAGE ); - } - } ); - add( mTunerInfo ); - - /** - * Sample Rate - */ - add( new JLabel( "Sample Rate:" ) ); - - List rates = mController.getSampleRates(); - - mSampleRateCombo = new JComboBox( - new DefaultComboBoxModel( rates.toArray( - new AirspySampleRate[ rates.size() ] ) ) ); - mSampleRateCombo.setEnabled( false ); - mSampleRateCombo.addActionListener( new ActionListener() - { - @Override - public void actionPerformed( ActionEvent e ) - { - AirspySampleRate rate = (AirspySampleRate)mSampleRateCombo.getSelectedItem(); - - try - { - mController.setSampleRate( rate ); - save(); - } - catch ( LibUsbException | UsbException | SourceException e1 ) - { - JOptionPane.showMessageDialog( AirspyTunerEditor.this, - "Couldn't set sample rate to " + rate.getLabel() ); - - mLog.error( "Error setting airspy sample rate", e1 ); - } - } - } ); - - add( mSampleRateCombo ); - - /** - * Frequency Correction - */ - SpinnerModel model = - new SpinnerNumberModel( 0.0, //initial value - -1000.0, //min - 1000.0, //max - 0.1 ); //step - - mFrequencyCorrection = new JSpinner( model ); - mFrequencyCorrection.setEnabled( false ); - - JSpinner.NumberEditor editor = - (JSpinner.NumberEditor)mFrequencyCorrection.getEditor(); - - DecimalFormat format = editor.getFormat(); - format.setMinimumFractionDigits( 1 ); - editor.getTextField().setHorizontalAlignment( SwingConstants.CENTER ); - - mFrequencyCorrection.addChangeListener( new ChangeListener() - { - @Override - public void stateChanged( ChangeEvent e ) - { - final double value = ((SpinnerNumberModel)mFrequencyCorrection - .getModel()).getNumber().doubleValue(); - - try - { - mController.setFrequencyCorrection( value ); - } - catch ( SourceException e1 ) - { - mLog.error( "Error setting frequency correction value", e1 ); - } - - save(); - } - } ); - - add( new JLabel( "PPM:" ) ); - add( mFrequencyCorrection ); - - add( new JSeparator(), "span,growx,push" ); - - /** - * Gain Mode - */ - add( new JLabel( "Gain Mode:" ) ); - mGainModeCombo = new JComboBox( GainMode.values() ); - mGainModeCombo.setEnabled( false ); - mGainModeCombo.addActionListener( new ActionListener() - { - @Override - public void actionPerformed( ActionEvent e ) - { - GainMode mode = (GainMode)mGainModeCombo.getSelectedItem(); - - updateGainComponents( mode ); - - if( hasItem() ) - { - AirspyTunerConfiguration airspy = getConfiguration(); - - Gain gain = airspy.getGain(); - - if( mode == GainMode.CUSTOM ) - { - mIFGain.setValue( airspy.getIFGain() ); - mMixerGain.setValue( airspy.getMixerGain() ); - mMixerAGC.setSelected( airspy.isMixerAGC() ); - mLNAGain.setValue( airspy.getLNAGain() ); - mLNAAGC.setSelected( airspy.isLNAAGC() ); - } - else - { - mMixerAGC.setSelected( false ); - mLNAAGC.setSelected( false ); - mMasterGain.setValue( gain.getValue() ); - mIFGain.setValue( gain.getIF() ); - mMixerGain.setValue( gain.getMixer() ); - mLNAGain.setValue( gain.getLNA() ); - } - } - - save(); - } - } ); - - add( mGainModeCombo, "wrap" ); - - /** - * Gain - */ - add( new JLabel( "Master:" ) ); - - mMasterGain = new JSlider( JSlider.HORIZONTAL, - AirspyTunerController.GAIN_MIN, - AirspyTunerController.GAIN_MAX, - AirspyTunerController.GAIN_MIN ); - mMasterGain.setMajorTickSpacing( 1 ); - mMasterGain.setPaintTicks( true ); - - mMasterGain.addChangeListener( new ChangeListener() - { - @Override - public void stateChanged( ChangeEvent event ) - { - GainMode mode = (GainMode)mGainModeCombo.getSelectedItem(); - int value = mMasterGain.getValue(); - Gain gain = Gain.getGain( mode, value ); - - try - { - mController.setGain( gain ); - save(); - mMasterGainValueLabel.setText( String.valueOf( gain.getValue() ) ); - } - catch ( Exception e ) - { - mLog.error( "Couldn't set airspy gain to:" + gain.name(), e ); - JOptionPane.showMessageDialog( mMasterGain, "Couldn't set gain value to " + - gain.getValue() ); - } - } - } ); - - add( mMasterGain, "span 2" ); - - mMasterGainValueLabel = new JLabel( "0" ); - add( mMasterGainValueLabel ); - - add( new JLabel( "IF:" ) ); - - mIFGain = new JSlider( JSlider.HORIZONTAL, - AirspyTunerController.IF_GAIN_MIN, - AirspyTunerController.IF_GAIN_MAX, - AirspyTunerController.IF_GAIN_MIN ); - - mIFGain.setMajorTickSpacing( 1 ); - mIFGain.setPaintTicks( true ); - - mIFGain.addChangeListener( new ChangeListener() - { - @Override - public void stateChanged( ChangeEvent event ) - { - int gain = mIFGain.getValue(); - - try - { - mController.setIFGain( gain ); - save(); - mIFGainValueLabel.setText( String.valueOf( gain ) ); - } - catch ( Exception e ) - { - mLog.error( "Couldn't set airspy IF gain to:" + gain, e ); - - JOptionPane.showMessageDialog( mIFGain, "Couldn't set IF gain value to " + gain ); - } - } - } ); - - add( mIFGain, "span 2" ); - - mIFGainValueLabel = new JLabel( "0" ); - add( mIFGainValueLabel ); - - /** - * Mixer Gain - */ - add( new JLabel( "Mixer:" ) ); - - mMixerGain = new JSlider( JSlider.HORIZONTAL, - AirspyTunerController.MIXER_GAIN_MIN, - AirspyTunerController.MIXER_GAIN_MAX, - AirspyTunerController.MIXER_GAIN_MIN ); - - mMixerGain.setMajorTickSpacing( 1 ); - mMixerGain.setPaintTicks( true ); - - mMixerGain.addChangeListener( new ChangeListener() - { - @Override - public void stateChanged( ChangeEvent event ) - { - int gain = mMixerGain.getValue(); - - try - { - mController.setMixerGain( gain ); - save(); - mMixerGainValueLabel.setText( String.valueOf( gain ) ); - } - catch ( Exception e ) - { - mLog.error( "Couldn't set airspy Mixer gain to:" + gain, e ); - JOptionPane.showMessageDialog( mIFGain, "Couldn't set Mixer gain value to " + gain ); - } - } - } ); - - add( mMixerGain, "span 2" ); - - mMixerGainValueLabel = new JLabel( "0" ); - add( mMixerGainValueLabel ); - - add( new JLabel( "LNA:" ) ); - - mLNAGain = new JSlider( JSlider.HORIZONTAL, - AirspyTunerController.LNA_GAIN_MIN, - AirspyTunerController.LNA_GAIN_MAX, - AirspyTunerController.LNA_GAIN_MIN ); - - mLNAGain.setMajorTickSpacing( 1 ); - mLNAGain.setPaintTicks( true ); - - mLNAGain.addChangeListener( new ChangeListener() - { - @Override - public void stateChanged( ChangeEvent event ) - { - int gain = mLNAGain.getValue(); - - try - { - mController.setLNAGain( gain ); - save(); - mLNAGainValueLabel.setText( String.valueOf( gain ) ); - } - catch ( Exception e ) - { - mLog.error( "Couldn't set airspy LNA gain to:" + gain, e ); - JOptionPane.showMessageDialog( mIFGain, "Couldn't set LNA gain value to " + gain ); - } - } - } ); - - add( mLNAGain, "span 2" ); - - mLNAGainValueLabel = new JLabel( "0" ); - add( mLNAGainValueLabel ); - - mMixerAGC = new JCheckBox( "Mixer AGC" ); - mMixerAGC.addActionListener( new ActionListener() - { - @Override - public void actionPerformed( ActionEvent e ) - { - try - { - mController.setMixerAGC( mMixerAGC.isSelected() ); - mMixerGain.setEnabled( !mMixerAGC.isSelected() ); - - save(); - } - catch ( Exception e1 ) - { - mLog.error( "Error setting Mixer AGC Enabled" ); - } - } - } ); - - add( mMixerAGC, "span 2,center" ); - - mLNAAGC = new JCheckBox( "LNA AGC" ); - mLNAAGC.addActionListener( new ActionListener() - { - @Override - public void actionPerformed( ActionEvent e ) - { - try - { - mController.setLNAAGC( mLNAAGC.isSelected() ); - mLNAGain.setEnabled( !mLNAAGC.isSelected() ); - - save(); - } - catch ( Exception e1 ) - { - mLog.error( "Error setting LNA AGC Enabled" ); - } - - } - } ); - - add( mLNAAGC, "span 2,center" ); - } - - /** - * Updates the enabled state of each of the gain controls according to the - * specified gain mode. The master gain control is enabled for linearity - * and sensitivity and the individual gain controls are disabled, and - * vice-versa for custom mode. - */ - private void updateGainComponents( GainMode mode ) - { - switch( mode ) - { - case LINEARITY: - if( !mMasterGain.isEnabled() ) - { - mMasterGain.setEnabled( true ); - mMasterGainValueLabel.setEnabled( true ); - } - if( mIFGain.isEnabled() ) - { - mIFGain.setEnabled( false ); - mIFGainValueLabel.setEnabled( false ); - } - if( mLNAGain.isEnabled() ) - { - mLNAAGC.setEnabled( false ); - mLNAGainValueLabel.setEnabled( false ); - mLNAGain.setEnabled( false ); - } - if( mMixerGain.isEnabled() ) - { - mMixerGainValueLabel.setEnabled( false ); - mMixerAGC.setEnabled( false ); - mMixerGain.setEnabled( false ); - } - break; - case SENSITIVITY: - if( !mMasterGain.isEnabled() ) - { - mMasterGain.setEnabled( true ); - mMasterGainValueLabel.setEnabled( true ); - } - if( mIFGain.isEnabled() ) - { - mIFGain.setEnabled( false ); - mIFGainValueLabel.setEnabled( false ); - } - if( mLNAGain.isEnabled() ) - { - mLNAAGC.setEnabled( false ); - mLNAGainValueLabel.setEnabled( false ); - mLNAGain.setEnabled( false ); - } - if( mMixerGain.isEnabled() ) - { - mMixerGainValueLabel.setEnabled( false ); - mMixerAGC.setEnabled( false ); - mMixerGain.setEnabled( false ); - } - break; - case CUSTOM: - if( mMasterGain.isEnabled() ) - { - mMasterGain.setEnabled( false ); - mMasterGainValueLabel.setEnabled( false ); - } - if( !mIFGain.isEnabled() ) - { - mIFGain.setEnabled( true ); - mIFGainValueLabel.setEnabled( true ); - } - if( !mLNAGain.isEnabled() ) - { - mLNAAGC.setEnabled( true ); - mLNAGainValueLabel.setEnabled( true ); - mLNAGain.setEnabled( true ); - } - if( !mMixerGain.isEnabled() ) - { - mMixerGainValueLabel.setEnabled( true ); - mMixerAGC.setEnabled( true ); - mMixerGain.setEnabled( true ); - } - default: - break; - } - } - - @Override - public void save() - { - if( hasItem() && !mLoading ) - { - AirspyTunerConfiguration config = getConfiguration(); - config.setName( mConfigurationName.getText() ); - config.setSampleRate( ((AirspySampleRate)mSampleRateCombo.getSelectedItem()).getRate() ); - - double value = ((SpinnerNumberModel)mFrequencyCorrection.getModel()).getNumber().doubleValue(); - config.setFrequencyCorrection( value ); - - Gain gain = Gain.getGain( (GainMode)mGainModeCombo.getSelectedItem(), mMasterGain.getValue() ); - - config.setGain( gain ); - config.setIFGain( mIFGain.getValue() ); - config.setMixerGain( mMixerGain.getValue() ); - config.setLNAGain( mLNAGain.getValue() ); - config.setMixerAGC( mMixerAGC.isSelected() ); - config.setLNAAGC( mLNAAGC.isSelected() ); - - getTunerConfigurationModel().broadcast( new TunerConfigurationEvent( config, TunerConfigurationEvent.Event.CHANGE ) ); - } - } - - private AirspySampleRate fromValue( int value ) - { - List rates = mController.getSampleRates(); - - for( AirspySampleRate rate: rates ) - { - if( rate.getRate() == value ) - { - return rate; - } - } - - if( rates.size() > 0 ) - { - return rates.get( 0 ); - } - - return null; - } - - /** - * Sets all controls to the argument enabled state - */ - private void setControlsEnabled( boolean enabled ) - { - if( mConfigurationName.isEnabled() != enabled ) - { - mConfigurationName.setEnabled( enabled ); - } - - if( mTunerInfo.isEnabled() != enabled ) - { - mTunerInfo.setEnabled( enabled ); - } - - if( mSampleRateCombo.isEnabled() != enabled ) - { - mSampleRateCombo.setEnabled( enabled ); - } - - if( mFrequencyCorrection.isEnabled() != enabled ) - { - mFrequencyCorrection.setEnabled( enabled ); - } - - if( mGainModeCombo.isEnabled() != enabled ) - { - mGainModeCombo.setEnabled( enabled ); - } - - if( mMasterGain.isEnabled() != enabled ) - { - mMasterGain.setEnabled( enabled ); - } - - if( mIFGain.isEnabled() != enabled ) - { - mIFGain.setEnabled( enabled ); - } - - if( mMixerGain.isEnabled() != enabled ) - { - mMixerGain.setEnabled( enabled ); - } - - if( mMixerAGC.isEnabled() != enabled ) - { - mMixerAGC.setEnabled( enabled ); - } - - if( mLNAGain.isEnabled() != enabled ) - { - mLNAGain.setEnabled( enabled ); - } - - if( mLNAAGC.isEnabled() != enabled ) - { - mLNAAGC.setEnabled( enabled ); - } - } - - /** - * Applies the tuner configuration to the editor. If the configuration is assigned to the - * currently selected tuner, then the settings are applied. - */ - @Override - public void setItem( TunerConfiguration config ) - { - super.setItem( config ); - - mLoading = true; - - if( hasItem() && getItem().isAssigned() ) - { - AirspyTunerConfiguration airspy = getConfiguration(); - - setControlsEnabled( true ); - - mConfigurationName.setText( airspy.getName() ); - - AirspySampleRate rate = fromValue( airspy.getSampleRate() ); - - if( rate != null ) - { - mSampleRateCombo.setSelectedItem( rate ); - } - - mFrequencyCorrection.setValue( airspy.getFrequencyCorrection() ); - - GainMode mode = Gain.getGainMode( airspy.getGain() ); - - //This will set the master, IF, Mixer, LNA and AGC controls - mGainModeCombo.setSelectedItem( mode ); - - if( mode == GainMode.CUSTOM ) - { - mMasterGain.setValue( airspy.getGain().getValue() ); - mIFGain.setValue( airspy.getIFGain() ); - - if( airspy.isMixerAGC() ) - { - mMixerAGC.setSelected( airspy.isMixerAGC() ); - mMixerGain.setEnabled( false ); - } - else - { - mMixerGain.setValue( airspy.getMixerGain() ); - } - - if( airspy.isLNAAGC() ) - { - mLNAAGC.setSelected( airspy.isLNAAGC() ); - mLNAGain.setEnabled( false ); - } - else - { - mLNAGain.setValue( airspy.getLNAGain() ); - } - } - else - { - mMixerAGC.setSelected( false ); - mLNAAGC.setSelected( false ); - - Gain gain = airspy.getGain(); - - mMasterGain.setValue( gain.getValue() ); - mIFGain.setValue( gain.getIF() ); - mMixerGain.setValue( gain.getMixer() ); - mLNAGain.setValue( gain.getLNA() ); - } - } - else - { - setControlsEnabled( false ); - mConfigurationName.setText( hasItem() ? getItem().getName() : "" ); - } - - mLoading = false; - } - - private String getTunerInfo() - { - StringBuilder sb = new StringBuilder(); - - AirspyDeviceInformation info = mController.getDeviceInfo(); - - sb.append( "

Airspy Tuner

" ); - sb.append( "Serial: " ); - sb.append( info.getSerialNumber() ); - sb.append( "
" ); - - sb.append( "Firmware: " ); - String[] firmware = info.getVersion().split( " " ); - sb.append( firmware.length > 1 ? firmware[ 0 ] : info.getVersion() ); - sb.append( "
" ); - - sb.append( "Part: " ); - sb.append( info.getPartNumber() ); - sb.append( "
" ); - - sb.append( "Board ID: " ); - sb.append( info.getBoardID().getLabel() ); - sb.append( "
" ); - - return sb.toString(); - } + private static final long serialVersionUID = 1L; + + private final static Logger mLog = + LoggerFactory.getLogger(AirspyTunerEditor.class); + + private JTextField mConfigurationName; + private JButton mTunerInfo; + + private JComboBox mSampleRateCombo; + private JSpinner mFrequencyCorrection; + + private JComboBox mGainModeCombo; + + private JSlider mMasterGain; + private JLabel mMasterGainValueLabel; + + private JSlider mIFGain; + private JLabel mIFGainValueLabel; + + private JSlider mLNAGain; + private JLabel mLNAGainValueLabel; + + private JSlider mMixerGain; + private JLabel mMixerGainValueLabel; + + private JCheckBox mLNAAGC; + private JCheckBox mMixerAGC; + + private AirspyTunerController mController; + private boolean mLoading; + + public AirspyTunerEditor(TunerConfigurationModel tunerConfigurationModel, + AirspyTuner tuner) + { + super(tunerConfigurationModel); + mController = tuner.getController(); + + init(); + } + + private AirspyTunerConfiguration getConfiguration() + { + if(hasItem()) + { + return (AirspyTunerConfiguration)getItem(); + } + + return null; + } + + /** + * Updates the enabled state of the sample rate combo control. + * + * @param enabled state for the sample rate control + */ + @Override + public void setSampleRateControl(boolean enabled) + { + EventQueue.invokeLater(new Runnable() + { + @Override + public void run() + { + mSampleRateCombo.setEnabled(enabled); + } + }); + } + + private void init() + { + setLayout(new MigLayout("fill,wrap 4", "[right][grow,fill][right][grow,fill]", + "[][][][][][grow]")); + + add(new JLabel("Airspy Tuner Configuration"), "span,align center"); + + mConfigurationName = new JTextField(); + mConfigurationName.setEnabled(false); + mConfigurationName.addFocusListener(new FocusListener() + { + @Override + public void focusLost(FocusEvent e) + { + save(); + } + + @Override + public void focusGained(FocusEvent e) + { + } + }); + + add(new JLabel("Name:")); + add(mConfigurationName, "span 2"); + + mTunerInfo = new JButton("Tuner Info"); + mTunerInfo.setEnabled(false); + mTunerInfo.addActionListener(new ActionListener() + { + @Override + public void actionPerformed(ActionEvent e) + { + JOptionPane.showMessageDialog(AirspyTunerEditor.this, getTunerInfo(), + "Tuner Info", JOptionPane.INFORMATION_MESSAGE); + } + }); + add(mTunerInfo); + + /** + * Sample Rate + */ + add(new JLabel("Sample Rate:")); + + List rates = mController.getSampleRates(); + + mSampleRateCombo = new JComboBox( + new DefaultComboBoxModel(rates.toArray( + new AirspySampleRate[rates.size()]))); + mSampleRateCombo.setEnabled(false); + mSampleRateCombo.addActionListener(new ActionListener() + { + @Override + public void actionPerformed(ActionEvent e) + { + AirspySampleRate rate = (AirspySampleRate)mSampleRateCombo.getSelectedItem(); + + try + { + mController.setSampleRate(rate); + save(); + } + catch(LibUsbException | UsbException | SourceException e1) + { + JOptionPane.showMessageDialog(AirspyTunerEditor.this, + "Couldn't set sample rate to " + rate.getLabel()); + + mLog.error("Error setting airspy sample rate", e1); + } + } + }); + + add(mSampleRateCombo); + + /** + * Frequency Correction + */ + SpinnerModel model = + new SpinnerNumberModel(0.0, //initial value + -1000.0, //min + 1000.0, //max + 0.1); //step + + mFrequencyCorrection = new JSpinner(model); + mFrequencyCorrection.setEnabled(false); + + JSpinner.NumberEditor editor = + (JSpinner.NumberEditor)mFrequencyCorrection.getEditor(); + + DecimalFormat format = editor.getFormat(); + format.setMinimumFractionDigits(1); + editor.getTextField().setHorizontalAlignment(SwingConstants.CENTER); + + mFrequencyCorrection.addChangeListener(new ChangeListener() + { + @Override + public void stateChanged(ChangeEvent e) + { + final double value = ((SpinnerNumberModel)mFrequencyCorrection + .getModel()).getNumber().doubleValue(); + + try + { + mController.setFrequencyCorrection(value); + } + catch(SourceException e1) + { + mLog.error("Error setting frequency correction value", e1); + } + + save(); + } + }); + + add(new JLabel("PPM:")); + add(mFrequencyCorrection); + + add(new JSeparator(), "span,growx,push"); + + /** + * Gain Mode + */ + add(new JLabel("Gain Mode:")); + mGainModeCombo = new JComboBox(GainMode.values()); + mGainModeCombo.setEnabled(false); + mGainModeCombo.addActionListener(new ActionListener() + { + @Override + public void actionPerformed(ActionEvent e) + { + GainMode mode = (GainMode)mGainModeCombo.getSelectedItem(); + + updateGainComponents(mode); + + if(hasItem()) + { + AirspyTunerConfiguration airspy = getConfiguration(); + + Gain gain = airspy.getGain(); + + if(mode == GainMode.CUSTOM) + { + mIFGain.setValue(airspy.getIFGain()); + mMixerGain.setValue(airspy.getMixerGain()); + mMixerAGC.setSelected(airspy.isMixerAGC()); + mLNAGain.setValue(airspy.getLNAGain()); + mLNAAGC.setSelected(airspy.isLNAAGC()); + } + else + { + mMixerAGC.setSelected(false); + mLNAAGC.setSelected(false); + mMasterGain.setValue(gain.getValue()); + mIFGain.setValue(gain.getIF()); + mMixerGain.setValue(gain.getMixer()); + mLNAGain.setValue(gain.getLNA()); + } + } + + save(); + } + }); + + add(mGainModeCombo, "wrap"); + + /** + * Gain + */ + add(new JLabel("Master:")); + + mMasterGain = new JSlider(JSlider.HORIZONTAL, + AirspyTunerController.GAIN_MIN, + AirspyTunerController.GAIN_MAX, + AirspyTunerController.GAIN_MIN); + mMasterGain.setMajorTickSpacing(1); + mMasterGain.setPaintTicks(true); + + mMasterGain.addChangeListener(new ChangeListener() + { + @Override + public void stateChanged(ChangeEvent event) + { + GainMode mode = (GainMode)mGainModeCombo.getSelectedItem(); + int value = mMasterGain.getValue(); + Gain gain = Gain.getGain(mode, value); + + try + { + mController.setGain(gain); + save(); + mMasterGainValueLabel.setText(String.valueOf(gain.getValue())); + } + catch(Exception e) + { + mLog.error("Couldn't set airspy gain to:" + gain.name(), e); + JOptionPane.showMessageDialog(mMasterGain, "Couldn't set gain value to " + + gain.getValue()); + } + } + }); + + add(mMasterGain, "span 2"); + + mMasterGainValueLabel = new JLabel("0"); + add(mMasterGainValueLabel); + + add(new JLabel("IF:")); + + mIFGain = new JSlider(JSlider.HORIZONTAL, + AirspyTunerController.IF_GAIN_MIN, + AirspyTunerController.IF_GAIN_MAX, + AirspyTunerController.IF_GAIN_MIN); + + mIFGain.setMajorTickSpacing(1); + mIFGain.setPaintTicks(true); + + mIFGain.addChangeListener(new ChangeListener() + { + @Override + public void stateChanged(ChangeEvent event) + { + int gain = mIFGain.getValue(); + + try + { + mController.setIFGain(gain); + save(); + mIFGainValueLabel.setText(String.valueOf(gain)); + } + catch(Exception e) + { + mLog.error("Couldn't set airspy IF gain to:" + gain, e); + + JOptionPane.showMessageDialog(mIFGain, "Couldn't set IF gain value to " + gain); + } + } + }); + + add(mIFGain, "span 2"); + + mIFGainValueLabel = new JLabel("0"); + add(mIFGainValueLabel); + + /** + * Mixer Gain + */ + add(new JLabel("Mixer:")); + + mMixerGain = new JSlider(JSlider.HORIZONTAL, + AirspyTunerController.MIXER_GAIN_MIN, + AirspyTunerController.MIXER_GAIN_MAX, + AirspyTunerController.MIXER_GAIN_MIN); + + mMixerGain.setMajorTickSpacing(1); + mMixerGain.setPaintTicks(true); + + mMixerGain.addChangeListener(new ChangeListener() + { + @Override + public void stateChanged(ChangeEvent event) + { + int gain = mMixerGain.getValue(); + + try + { + mController.setMixerGain(gain); + save(); + mMixerGainValueLabel.setText(String.valueOf(gain)); + } + catch(Exception e) + { + mLog.error("Couldn't set airspy Mixer gain to:" + gain, e); + JOptionPane.showMessageDialog(mIFGain, "Couldn't set Mixer gain value to " + gain); + } + } + }); + + add(mMixerGain, "span 2"); + + mMixerGainValueLabel = new JLabel("0"); + add(mMixerGainValueLabel); + + add(new JLabel("LNA:")); + + mLNAGain = new JSlider(JSlider.HORIZONTAL, + AirspyTunerController.LNA_GAIN_MIN, + AirspyTunerController.LNA_GAIN_MAX, + AirspyTunerController.LNA_GAIN_MIN); + + mLNAGain.setMajorTickSpacing(1); + mLNAGain.setPaintTicks(true); + + mLNAGain.addChangeListener(new ChangeListener() + { + @Override + public void stateChanged(ChangeEvent event) + { + int gain = mLNAGain.getValue(); + + try + { + mController.setLNAGain(gain); + save(); + mLNAGainValueLabel.setText(String.valueOf(gain)); + } + catch(Exception e) + { + mLog.error("Couldn't set airspy LNA gain to:" + gain, e); + JOptionPane.showMessageDialog(mIFGain, "Couldn't set LNA gain value to " + gain); + } + } + }); + + add(mLNAGain, "span 2"); + + mLNAGainValueLabel = new JLabel("0"); + add(mLNAGainValueLabel); + + mMixerAGC = new JCheckBox("Mixer AGC"); + mMixerAGC.addActionListener(new ActionListener() + { + @Override + public void actionPerformed(ActionEvent e) + { + try + { + mController.setMixerAGC(mMixerAGC.isSelected()); + mMixerGain.setEnabled(!mMixerAGC.isSelected()); + + save(); + } + catch(Exception e1) + { + mLog.error("Error setting Mixer AGC Enabled"); + } + } + }); + + add(mMixerAGC, "span 2,center"); + + mLNAAGC = new JCheckBox("LNA AGC"); + mLNAAGC.addActionListener(new ActionListener() + { + @Override + public void actionPerformed(ActionEvent e) + { + try + { + mController.setLNAAGC(mLNAAGC.isSelected()); + mLNAGain.setEnabled(!mLNAAGC.isSelected()); + + save(); + } + catch(Exception e1) + { + mLog.error("Error setting LNA AGC Enabled"); + } + + } + }); + + add(mLNAAGC, "span 2,center"); + } + + /** + * Updates the enabled state of each of the gain controls according to the + * specified gain mode. The master gain control is enabled for linearity + * and sensitivity and the individual gain controls are disabled, and + * vice-versa for custom mode. + */ + private void updateGainComponents(GainMode mode) + { + switch(mode) + { + case LINEARITY: + if(!mMasterGain.isEnabled()) + { + mMasterGain.setEnabled(true); + mMasterGainValueLabel.setEnabled(true); + } + if(mIFGain.isEnabled()) + { + mIFGain.setEnabled(false); + mIFGainValueLabel.setEnabled(false); + } + if(mLNAGain.isEnabled()) + { + mLNAAGC.setEnabled(false); + mLNAGainValueLabel.setEnabled(false); + mLNAGain.setEnabled(false); + } + if(mMixerGain.isEnabled()) + { + mMixerGainValueLabel.setEnabled(false); + mMixerAGC.setEnabled(false); + mMixerGain.setEnabled(false); + } + break; + case SENSITIVITY: + if(!mMasterGain.isEnabled()) + { + mMasterGain.setEnabled(true); + mMasterGainValueLabel.setEnabled(true); + } + if(mIFGain.isEnabled()) + { + mIFGain.setEnabled(false); + mIFGainValueLabel.setEnabled(false); + } + if(mLNAGain.isEnabled()) + { + mLNAAGC.setEnabled(false); + mLNAGainValueLabel.setEnabled(false); + mLNAGain.setEnabled(false); + } + if(mMixerGain.isEnabled()) + { + mMixerGainValueLabel.setEnabled(false); + mMixerAGC.setEnabled(false); + mMixerGain.setEnabled(false); + } + break; + case CUSTOM: + if(mMasterGain.isEnabled()) + { + mMasterGain.setEnabled(false); + mMasterGainValueLabel.setEnabled(false); + } + if(!mIFGain.isEnabled()) + { + mIFGain.setEnabled(true); + mIFGainValueLabel.setEnabled(true); + } + if(!mLNAGain.isEnabled()) + { + mLNAAGC.setEnabled(true); + mLNAGainValueLabel.setEnabled(true); + mLNAGain.setEnabled(true); + } + if(!mMixerGain.isEnabled()) + { + mMixerGainValueLabel.setEnabled(true); + mMixerAGC.setEnabled(true); + mMixerGain.setEnabled(true); + } + default: + break; + } + } + + @Override + public void save() + { + if(hasItem() && !mLoading) + { + AirspyTunerConfiguration config = getConfiguration(); + config.setName(mConfigurationName.getText()); + config.setSampleRate(((AirspySampleRate)mSampleRateCombo.getSelectedItem()).getRate()); + + double value = ((SpinnerNumberModel)mFrequencyCorrection.getModel()).getNumber().doubleValue(); + config.setFrequencyCorrection(value); + + Gain gain = Gain.getGain((GainMode)mGainModeCombo.getSelectedItem(), mMasterGain.getValue()); + + config.setGain(gain); + config.setIFGain(mIFGain.getValue()); + config.setMixerGain(mMixerGain.getValue()); + config.setLNAGain(mLNAGain.getValue()); + config.setMixerAGC(mMixerAGC.isSelected()); + config.setLNAAGC(mLNAAGC.isSelected()); + + getTunerConfigurationModel().broadcast(new TunerConfigurationEvent(config, TunerConfigurationEvent.Event.CHANGE)); + } + } + + private AirspySampleRate fromValue(int value) + { + List rates = mController.getSampleRates(); + + for(AirspySampleRate rate : rates) + { + if(rate.getRate() == value) + { + return rate; + } + } + + if(rates.size() > 0) + { + return rates.get(0); + } + + return null; + } + + /** + * Sets all controls to the argument enabled state + */ + private void setControlsEnabled(boolean enabled) + { + if(mConfigurationName.isEnabled() != enabled) + { + mConfigurationName.setEnabled(enabled); + } + + if(mTunerInfo.isEnabled() != enabled) + { + mTunerInfo.setEnabled(enabled); + } + + if(mSampleRateCombo.isEnabled() != enabled) + { + mSampleRateCombo.setEnabled(enabled); + } + + if(mFrequencyCorrection.isEnabled() != enabled) + { + mFrequencyCorrection.setEnabled(enabled); + } + + if(mGainModeCombo.isEnabled() != enabled) + { + mGainModeCombo.setEnabled(enabled); + } + + if(mMasterGain.isEnabled() != enabled) + { + mMasterGain.setEnabled(enabled); + } + + if(mIFGain.isEnabled() != enabled) + { + mIFGain.setEnabled(enabled); + } + + if(mMixerGain.isEnabled() != enabled) + { + mMixerGain.setEnabled(enabled); + } + + if(mMixerAGC.isEnabled() != enabled) + { + mMixerAGC.setEnabled(enabled); + } + + if(mLNAGain.isEnabled() != enabled) + { + mLNAGain.setEnabled(enabled); + } + + if(mLNAAGC.isEnabled() != enabled) + { + mLNAAGC.setEnabled(enabled); + } + } + + /** + * Applies the tuner configuration to the editor. If the configuration is assigned to the + * currently selected tuner, then the settings are applied. + */ + @Override + public void setItem(TunerConfiguration config) + { + super.setItem(config); + + mLoading = true; + + if(hasItem() && getItem().isAssigned()) + { + AirspyTunerConfiguration airspy = getConfiguration(); + + setControlsEnabled(true); + + mConfigurationName.setText(airspy.getName()); + + AirspySampleRate rate = fromValue(airspy.getSampleRate()); + + if(rate != null) + { + mSampleRateCombo.setSelectedItem(rate); + } + + //Update enabled state to reflect when frequency and sample rate controls are locked + mSampleRateCombo.setEnabled(!mController.isLocked()); + + mFrequencyCorrection.setValue(airspy.getFrequencyCorrection()); + + GainMode mode = Gain.getGainMode(airspy.getGain()); + + //This will set the master, IF, Mixer, LNA and AGC controls + mGainModeCombo.setSelectedItem(mode); + + if(mode == GainMode.CUSTOM) + { + mMasterGain.setValue(airspy.getGain().getValue()); + mIFGain.setValue(airspy.getIFGain()); + + if(airspy.isMixerAGC()) + { + mMixerAGC.setSelected(airspy.isMixerAGC()); + mMixerGain.setEnabled(false); + } + else + { + mMixerGain.setValue(airspy.getMixerGain()); + } + + if(airspy.isLNAAGC()) + { + mLNAAGC.setSelected(airspy.isLNAAGC()); + mLNAGain.setEnabled(false); + } + else + { + mLNAGain.setValue(airspy.getLNAGain()); + } + } + else + { + mMixerAGC.setSelected(false); + mLNAAGC.setSelected(false); + + Gain gain = airspy.getGain(); + + mMasterGain.setValue(gain.getValue()); + mIFGain.setValue(gain.getIF()); + mMixerGain.setValue(gain.getMixer()); + mLNAGain.setValue(gain.getLNA()); + } + } + else + { + setControlsEnabled(false); + mConfigurationName.setText(hasItem() ? getItem().getName() : ""); + } + + mLoading = false; + } + + private String getTunerInfo() + { + StringBuilder sb = new StringBuilder(); + + AirspyDeviceInformation info = mController.getDeviceInfo(); + + sb.append("

Airspy Tuner

"); + sb.append("Serial: "); + sb.append(info.getSerialNumber()); + sb.append("
"); + + sb.append("Firmware: "); + String[] firmware = info.getVersion().split(" "); + sb.append(firmware.length > 1 ? firmware[0] : info.getVersion()); + sb.append("
"); + + sb.append("Part: "); + sb.append(info.getPartNumber()); + sb.append("
"); + + sb.append("Board ID: "); + sb.append(info.getBoardID().getLabel()); + sb.append("
"); + + return sb.toString(); + } } \ No newline at end of file diff --git a/src/main/java/io/github/dsheirer/source/tuner/configuration/TunerConfigurationEditor.java b/src/main/java/io/github/dsheirer/source/tuner/configuration/TunerConfigurationEditor.java index 6c8af503..c328c3af 100644 --- a/src/main/java/io/github/dsheirer/source/tuner/configuration/TunerConfigurationEditor.java +++ b/src/main/java/io/github/dsheirer/source/tuner/configuration/TunerConfigurationEditor.java @@ -1,20 +1,43 @@ +/******************************************************************************* + * 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 + * + ******************************************************************************/ package io.github.dsheirer.source.tuner.configuration; import io.github.dsheirer.gui.editor.Editor; public abstract class TunerConfigurationEditor extends Editor { - private static final long serialVersionUID = 1L; - - private TunerConfigurationModel mTunerConfigurationModel; - - public TunerConfigurationEditor( TunerConfigurationModel model ) - { - mTunerConfigurationModel = model; - } - - public TunerConfigurationModel getTunerConfigurationModel() - { - return mTunerConfigurationModel; - } + private static final long serialVersionUID = 1L; + + private TunerConfigurationModel mTunerConfigurationModel; + + public TunerConfigurationEditor(TunerConfigurationModel model) + { + mTunerConfigurationModel = model; + } + + public TunerConfigurationModel getTunerConfigurationModel() + { + return mTunerConfigurationModel; + } + + /** + * Sets the state of the sample rate editor control. When enabled, users are allowed to change the sample rate + * of the tuner. When disabled, user controls should NOT allow sample rate changes. + * + * @param enabled state for the sample rate control + */ + public abstract void setSampleRateControl(boolean enabled); } diff --git a/src/main/java/io/github/dsheirer/source/tuner/fcd/proV1/FCD1TunerEditor.java b/src/main/java/io/github/dsheirer/source/tuner/fcd/proV1/FCD1TunerEditor.java index eb8ac835..9839526b 100644 --- a/src/main/java/io/github/dsheirer/source/tuner/fcd/proV1/FCD1TunerEditor.java +++ b/src/main/java/io/github/dsheirer/source/tuner/fcd/proV1/FCD1TunerEditor.java @@ -1,17 +1,17 @@ /******************************************************************************* * SDR Trunk * Copyright (C) 2014-2016 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 ******************************************************************************/ @@ -51,475 +51,489 @@ import java.text.DecimalFormat; public class FCD1TunerEditor extends TunerConfigurationEditor { - private static final long serialVersionUID = 1L; - private final static Logger mLog = LoggerFactory.getLogger( FCD1TunerEditor.class ); - - private JTextField mConfigurationName; - private JButton mTunerInfo; - private JSpinner mFrequencyCorrection; - private JComboBox mComboLNAGain; - private JComboBox mComboLNAEnhance; - private JComboBox mComboMixerGain; - private CorrectionSpinner mCorrectionDCI; - private CorrectionSpinner mCorrectionDCQ; - private CorrectionSpinner mCorrectionGain; - private CorrectionSpinner mCorrectionPhase; - private boolean mLoading; - - private FCD1TunerController mController; - - public FCD1TunerEditor( TunerConfigurationModel tunerConfigurationModel, FCDTuner tuner ) - { - super( tunerConfigurationModel ); - mController = (FCD1TunerController)tuner.getController(); - - init(); - } - - private FCD1TunerConfiguration getConfiguration() - { - if( hasItem() ) - { - return (FCD1TunerConfiguration)getItem(); - } - - return null; - } - - private void init() - { - setLayout( new MigLayout( "fill,wrap 4", "[right][grow,fill][right][grow,fill]", - "[][][][][][][][][][][][grow]" ) ); - - add( new JLabel( "FCD Pro Tuner Configuration" ), "span,align center" ); - - mConfigurationName = new JTextField(); - mConfigurationName.setEnabled( false ); - mConfigurationName.addFocusListener( new FocusListener() - { - @Override - public void focusLost( FocusEvent e ) - { - save(); - } - @Override - public void focusGained( FocusEvent e ) {} - } ); - - add( new JLabel( "Name:" ) ); - add( mConfigurationName, "span 2" ); - - mTunerInfo = new JButton( "Tuner Info" ); - mTunerInfo.setEnabled( false ); - mTunerInfo.addActionListener( new ActionListener() - { - @Override - public void actionPerformed( ActionEvent e ) - { - JOptionPane.showMessageDialog( FCD1TunerEditor.this, getTunerInfo(), - "Tuner Info", JOptionPane.INFORMATION_MESSAGE ); - } - } ); - add( mTunerInfo ); - - SpinnerModel model = new SpinnerNumberModel( 0.0, //initial value - -1000.0, //min - 1000.0, //max - 0.1 ); //step - - mFrequencyCorrection = new JSpinner( model ); - mFrequencyCorrection.setEnabled( false ); - JSpinner.NumberEditor editor = - (JSpinner.NumberEditor)mFrequencyCorrection.getEditor(); - - DecimalFormat format = editor.getFormat(); - format.setMinimumFractionDigits( 1 ); - editor.getTextField().setHorizontalAlignment( SwingConstants.CENTER ); - - mFrequencyCorrection.addChangeListener( new ChangeListener() - { - @Override - public void stateChanged( ChangeEvent e ) - { - double value = ((SpinnerNumberModel)mFrequencyCorrection - .getModel()).getNumber().doubleValue(); - - try - { - mController.setFrequencyCorrection( value ); - save(); - } - catch ( SourceException e1 ) - { - JOptionPane.showMessageDialog( FCD1TunerEditor.this, "FCD Pro Tuner " - + "Controller - couldn't apply frequency correction value: " + value + - e1.getLocalizedMessage() ); - - mLog.error( "FuncubeDonglePro Controller - couldn't apply " - + "frequency correction value: " + value, e1 ); - } - } - } ); - - add( new JLabel( "PPM:" ) ); - add( mFrequencyCorrection, "wrap" ); - - add( new JSeparator( JSeparator.HORIZONTAL ), "span,grow" ); - add( new JLabel( "Gain" ), "span,align center" ); - - mComboLNAGain = new JComboBox( LNAGain.values() ); - mComboLNAGain.setEnabled( false ); - mComboLNAGain.setToolTipText( "Adjust the low noise amplifier gain setting." ); - mComboLNAGain.addActionListener( new ActionListener() - { - @Override - public void actionPerformed( ActionEvent arg0 ) - { - LNAGain gain = (LNAGain)mComboLNAGain.getSelectedItem(); - - - try - { - mController.setLNAGain( gain ); - save(); - } - catch ( Exception e ) - { - JOptionPane.showMessageDialog( FCD1TunerEditor.this, "FCD Pro Tuner Controller" - + " - error setting LNA gain [" + gain.toString() + "]" ); - - mLog.error( "FuncubeDonglePro Controller - error setting gain [" + gain.toString() + "]", e ); - } - } - } ); - - add( new JLabel( "LNA:" ) ); - add( mComboLNAGain ); - - /** - * LNA Enhance - */ - mComboLNAEnhance = new JComboBox( LNAEnhance.values() ); - mComboLNAEnhance.setToolTipText( "Adjust the LNA enhance setting. Default value is OFF" ); - - mComboLNAEnhance.addActionListener( new ActionListener() - { - @Override - public void actionPerformed( ActionEvent arg0 ) - { - LNAEnhance enhance = (LNAEnhance)mComboLNAEnhance.getSelectedItem(); - - try - { - mController.setLNAEnhance( enhance ); - save(); - } - catch ( Exception e1 ) - { - JOptionPane.showMessageDialog( FCD1TunerEditor.this, "FCD Pro Tuner" - + " error setting LNA enhance gain [" + enhance.toString() + "]" ); - - mLog.error( "FCDPro - error setting LNA enhance [" + enhance.toString() + "]", e1 ); - } - } - } ); - - add( new JLabel( "Enhance:" ) ); - add( mComboLNAEnhance ); - - /** - * Mixer Gain - */ - mComboMixerGain = new JComboBox( MixerGain.values() ); - mComboMixerGain.setToolTipText( "Adjust mixer gain setting" ); - - - mComboMixerGain.addActionListener( new ActionListener() - { - @Override - public void actionPerformed( ActionEvent arg0 ) - { - MixerGain gain = (MixerGain)mComboMixerGain.getSelectedItem(); - - try - { - mController.setMixerGain( gain ); - save(); - } - catch ( Exception e1 ) - { - JOptionPane.showMessageDialog( FCD1TunerEditor.this, "FCDPro - error setting" - + " mixer gain [" + gain.toString() + "]" ); - - mLog.error( "FCDPro - error setting mixer gain [" + gain.toString() + "]", e1 ); - } - } - } ); - - add( new JLabel( "Mixer:" ) ); - add( mComboMixerGain, "wrap" ); - - add( new JSeparator( JSeparator.HORIZONTAL ), "span,grow" ); - add( new JLabel( "Correction" ), "span,align center" ); - - /** - * Inphase DC Correction - */ - mCorrectionDCI = new CorrectionSpinner( Correction.DC_INPHASE, 0.0, 0.00001, 5 ); - mCorrectionDCI.setEnabled( false ); - mCorrectionDCI.setToolTipText( "DC Bias Correction/Inphase " - + "Component: valid values are -1.0 to 1.0 (default: 0.0)" ); - add( new JLabel( "DC Inphase:" ) ); - add( mCorrectionDCI ); - - /** - * Quadrature DC Correction - */ - mCorrectionDCQ = new CorrectionSpinner( Correction.DC_QUADRATURE, 0.0, 0.00001, 5 ); - mCorrectionDCQ.setToolTipText( "DC Bias Correction/Quadrature " - + "Component: valid values are -1.0 to 1.0 (default: 0.0)" ); - add( new JLabel( "DC Quadrature:" ) ); - add( mCorrectionDCQ ); - - /** - * Gain Correction - */ - mCorrectionGain = new CorrectionSpinner( Correction.GAIN, 0.0, 0.00001, 5 ); - mCorrectionGain.setToolTipText( "Gain Correction: valid values are " - + "-1.0 to 1.0 (default: 0.0)" ); - add( new JLabel( "Gain:" ) ); - add( mCorrectionGain ); - - /** - * Phase Correction - */ - mCorrectionPhase = new CorrectionSpinner( Correction.PHASE, 0.0, 0.00001, 5 ); - mCorrectionPhase.setToolTipText( "Phase Correction: valid values are " - + "-1.0 to 1.0 (default: 0.0)" ); - add( new JLabel( "Phase:" ) ); - add( mCorrectionPhase ); - } - - /** - * Sets each of the tuner configuration controls to the enabled argument state - */ - private void setControlsEnabled( boolean enabled ) - { - if( mConfigurationName.isEnabled() != enabled ) - { - mConfigurationName.setEnabled( enabled ); - } - - if( mTunerInfo.isEnabled() != enabled ) - { - mTunerInfo.setEnabled( enabled ); - } - - if( mFrequencyCorrection.isEnabled() != enabled ) - { - mFrequencyCorrection.setEnabled( enabled ); - } - - if( mComboLNAEnhance.isEnabled() != enabled ) - { - mComboLNAEnhance.setEnabled( enabled ); - } - - if( mComboLNAGain.isEnabled() != enabled ) - { - mComboLNAGain.setEnabled( enabled ); - } - - if( mComboMixerGain.isEnabled() != enabled ) - { - mComboMixerGain.setEnabled( enabled ); - } - - if( mCorrectionDCI.isEnabled() != enabled ) - { - mCorrectionDCI.setEnabled( enabled ); - } - - if( mCorrectionDCQ.isEnabled() != enabled ) - { - mCorrectionDCQ.setEnabled( enabled ); - } - - if( mCorrectionGain.isEnabled() != enabled ) - { - mCorrectionGain.setEnabled( enabled ); - } - - if( mCorrectionPhase.isEnabled() != enabled ) - { - mCorrectionPhase.setEnabled( enabled ); - } - } - - private String getTunerInfo() - { - StringBuilder sb = new StringBuilder(); - - sb.append( "

Funcube Dongle Pro Tuner

" ); - - sb.append( "USB ID: " ); - sb.append( mController.getUSBID() ); - sb.append( "
" ); - - sb.append( "USB Address: " ); - sb.append( mController.getUSBAddress() ); - sb.append( "
" ); - - sb.append( "USB Speed: " ); - sb.append( mController.getUSBSpeed() ); - sb.append( "
" ); - - sb.append( "Cellular Band: " ); - sb.append( mController.getConfiguration().getBandBlocking() ); - sb.append( "
" ); - - sb.append( "Firmware: " ); - sb.append( mController.getConfiguration().getFirmware() ); - sb.append( "
" ); - - return sb.toString(); - } - - @Override - public void save() - { - if( hasItem() && !mLoading ) - { - FCD1TunerConfiguration config = getConfiguration(); - - config.setName( mConfigurationName.getText() ); - - double value = ((SpinnerNumberModel)mFrequencyCorrection - .getModel()).getNumber().doubleValue(); - config.setFrequencyCorrection( value ); - config.setLNAEnhance( (LNAEnhance)mComboLNAEnhance.getSelectedItem() ); - config.setLNAGain( (LNAGain)mComboLNAGain.getSelectedItem() ); - config.setMixerGain( (MixerGain)mComboMixerGain.getSelectedItem() ); - - double dci = ((SpinnerNumberModel)mCorrectionDCI.getModel()).getNumber().doubleValue(); - config.setInphaseDCCorrection( dci ); - - double dcq = ((SpinnerNumberModel)mCorrectionDCQ.getModel()).getNumber().doubleValue(); - config.setQuadratureDCCorrection( dcq ); - - double gain = ((SpinnerNumberModel)mCorrectionGain.getModel()).getNumber().doubleValue(); - config.setGainCorrection( gain ); - - double phase = ((SpinnerNumberModel)mCorrectionPhase.getModel()).getNumber().doubleValue(); - config.setPhaseCorrection( phase ); - - getTunerConfigurationModel().broadcast( new TunerConfigurationEvent( getConfiguration(), Event.CHANGE ) ); - } - } - - @Override - public void setItem( TunerConfiguration tunerConfiguration ) - { - super.setItem( tunerConfiguration ); - - //Toggle loading so that we don't fire a change event and schedule a settings file save - mLoading = true; - - if( hasItem() ) - { - FCD1TunerConfiguration config = getConfiguration(); - - if( tunerConfiguration.isAssigned() ) - { - setControlsEnabled( true ); - mConfigurationName.setText( config.getName() ); - mFrequencyCorrection.setValue( config.getFrequencyCorrection() ); - mComboLNAEnhance.setSelectedItem( config.getLNAEnhance() ); - mComboLNAGain.setSelectedItem( config.getLNAGain() ); - mComboMixerGain.setSelectedItem( config.getMixerGain() ); - mCorrectionDCI.setValue( config.getInphaseDCCorrection() ); - mCorrectionDCQ.setValue( config.getQuadratureDCCorrection() ); - mCorrectionGain.setValue( config.getGainCorrection() ); - mCorrectionPhase.setValue( config.getPhaseCorrection() ); - } - else - { - setControlsEnabled( false ); - mConfigurationName.setText( config.getName() ); - } - } - else - { - setControlsEnabled( false ); - mConfigurationName.setText( "" ); - } - - mLoading = false; - } - - public enum Correction { GAIN, PHASE, DC_INPHASE, DC_QUADRATURE }; - - public class CorrectionSpinner extends JSpinner - { - private static final long serialVersionUID = 1L; - private static final double MIN_VALUE = -1.0d; - private static final double MAX_VALUE = 1.0d; - - private Correction mCorrectionComponent; - - public CorrectionSpinner( Correction component, double initialValue, double step, int decimalPlaces ) - { - mCorrectionComponent = component; - - SpinnerModel model = new SpinnerNumberModel( initialValue, MIN_VALUE, MAX_VALUE, step ); - setModel( model ); - - JSpinner.NumberEditor editor = (JSpinner.NumberEditor)getEditor(); - - DecimalFormat format = editor.getFormat(); - format.setMinimumFractionDigits( decimalPlaces ); - - editor.getTextField().setHorizontalAlignment( SwingConstants.CENTER ); - - addChangeListener( new ChangeListener() - { - @Override - public void stateChanged( ChangeEvent e ) - { - double value = - ((SpinnerNumberModel)getModel()).getNumber().doubleValue(); - - try - { - switch( mCorrectionComponent ) - { - case DC_INPHASE: - mController.setDCCorrectionInPhase( value ); - break; - case DC_QUADRATURE: - mController.setDCCorrectionQuadrature( value ); - break; - case GAIN: - mController.setGainCorrection( value ); - break; - case PHASE: - mController.setPhaseCorrection( value ); - break; - } - - save(); - } - catch ( Exception e1 ) - { - JOptionPane.showMessageDialog( FCD1TunerEditor.this, "FCDPro - error " - + "applying " + mCorrectionComponent.toString() + " correction value [" - + value + "]" ); - - mLog.error( "FCDPro - error applying "+ mCorrectionComponent.toString() - + " correction value [" + value + "]", e1 ); - } - } - } ); - } - } + private static final long serialVersionUID = 1L; + private final static Logger mLog = LoggerFactory.getLogger(FCD1TunerEditor.class); + + private JTextField mConfigurationName; + private JButton mTunerInfo; + private JSpinner mFrequencyCorrection; + private JComboBox mComboLNAGain; + private JComboBox mComboLNAEnhance; + private JComboBox mComboMixerGain; + private CorrectionSpinner mCorrectionDCI; + private CorrectionSpinner mCorrectionDCQ; + private CorrectionSpinner mCorrectionGain; + private CorrectionSpinner mCorrectionPhase; + private boolean mLoading; + + private FCD1TunerController mController; + + public FCD1TunerEditor(TunerConfigurationModel tunerConfigurationModel, FCDTuner tuner) + { + super(tunerConfigurationModel); + mController = (FCD1TunerController)tuner.getController(); + + init(); + } + + private FCD1TunerConfiguration getConfiguration() + { + if(hasItem()) + { + return (FCD1TunerConfiguration)getItem(); + } + + return null; + } + + @Override + public void setSampleRateControl(boolean enabled) + { + //no-op + } + + private void init() + { + setLayout(new MigLayout("fill,wrap 4", "[right][grow,fill][right][grow,fill]", + "[][][][][][][][][][][][grow]")); + + add(new JLabel("FCD Pro Tuner Configuration"), "span,align center"); + + mConfigurationName = new JTextField(); + mConfigurationName.setEnabled(false); + mConfigurationName.addFocusListener(new FocusListener() + { + @Override + public void focusLost(FocusEvent e) + { + save(); + } + + @Override + public void focusGained(FocusEvent e) + { + } + }); + + add(new JLabel("Name:")); + add(mConfigurationName, "span 2"); + + mTunerInfo = new JButton("Tuner Info"); + mTunerInfo.setEnabled(false); + mTunerInfo.addActionListener(new ActionListener() + { + @Override + public void actionPerformed(ActionEvent e) + { + JOptionPane.showMessageDialog(FCD1TunerEditor.this, getTunerInfo(), + "Tuner Info", JOptionPane.INFORMATION_MESSAGE); + } + }); + add(mTunerInfo); + + SpinnerModel model = new SpinnerNumberModel(0.0, //initial value + -1000.0, //min + 1000.0, //max + 0.1); //step + + mFrequencyCorrection = new JSpinner(model); + mFrequencyCorrection.setEnabled(false); + JSpinner.NumberEditor editor = + (JSpinner.NumberEditor)mFrequencyCorrection.getEditor(); + + DecimalFormat format = editor.getFormat(); + format.setMinimumFractionDigits(1); + editor.getTextField().setHorizontalAlignment(SwingConstants.CENTER); + + mFrequencyCorrection.addChangeListener(new ChangeListener() + { + @Override + public void stateChanged(ChangeEvent e) + { + double value = ((SpinnerNumberModel)mFrequencyCorrection + .getModel()).getNumber().doubleValue(); + + try + { + mController.setFrequencyCorrection(value); + save(); + } + catch(SourceException e1) + { + JOptionPane.showMessageDialog(FCD1TunerEditor.this, "FCD Pro Tuner " + + "Controller - couldn't apply frequency correction value: " + value + + e1.getLocalizedMessage()); + + mLog.error("FuncubeDonglePro Controller - couldn't apply " + + "frequency correction value: " + value, e1); + } + } + }); + + add(new JLabel("PPM:")); + add(mFrequencyCorrection, "wrap"); + + add(new JSeparator(JSeparator.HORIZONTAL), "span,grow"); + add(new JLabel("Gain"), "span,align center"); + + mComboLNAGain = new JComboBox(LNAGain.values()); + mComboLNAGain.setEnabled(false); + mComboLNAGain.setToolTipText("Adjust the low noise amplifier gain setting."); + mComboLNAGain.addActionListener(new ActionListener() + { + @Override + public void actionPerformed(ActionEvent arg0) + { + LNAGain gain = (LNAGain)mComboLNAGain.getSelectedItem(); + + + try + { + mController.setLNAGain(gain); + save(); + } + catch(Exception e) + { + JOptionPane.showMessageDialog(FCD1TunerEditor.this, "FCD Pro Tuner Controller" + + " - error setting LNA gain [" + gain.toString() + "]"); + + mLog.error("FuncubeDonglePro Controller - error setting gain [" + gain.toString() + "]", e); + } + } + }); + + add(new JLabel("LNA:")); + add(mComboLNAGain); + + /** + * LNA Enhance + */ + mComboLNAEnhance = new JComboBox(LNAEnhance.values()); + mComboLNAEnhance.setToolTipText("Adjust the LNA enhance setting. Default value is OFF"); + + mComboLNAEnhance.addActionListener(new ActionListener() + { + @Override + public void actionPerformed(ActionEvent arg0) + { + LNAEnhance enhance = (LNAEnhance)mComboLNAEnhance.getSelectedItem(); + + try + { + mController.setLNAEnhance(enhance); + save(); + } + catch(Exception e1) + { + JOptionPane.showMessageDialog(FCD1TunerEditor.this, "FCD Pro Tuner" + + " error setting LNA enhance gain [" + enhance.toString() + "]"); + + mLog.error("FCDPro - error setting LNA enhance [" + enhance.toString() + "]", e1); + } + } + }); + + add(new JLabel("Enhance:")); + add(mComboLNAEnhance); + + /** + * Mixer Gain + */ + mComboMixerGain = new JComboBox(MixerGain.values()); + mComboMixerGain.setToolTipText("Adjust mixer gain setting"); + + + mComboMixerGain.addActionListener(new ActionListener() + { + @Override + public void actionPerformed(ActionEvent arg0) + { + MixerGain gain = (MixerGain)mComboMixerGain.getSelectedItem(); + + try + { + mController.setMixerGain(gain); + save(); + } + catch(Exception e1) + { + JOptionPane.showMessageDialog(FCD1TunerEditor.this, "FCDPro - error setting" + + " mixer gain [" + gain.toString() + "]"); + + mLog.error("FCDPro - error setting mixer gain [" + gain.toString() + "]", e1); + } + } + }); + + add(new JLabel("Mixer:")); + add(mComboMixerGain, "wrap"); + + add(new JSeparator(JSeparator.HORIZONTAL), "span,grow"); + add(new JLabel("Correction"), "span,align center"); + + /** + * Inphase DC Correction + */ + mCorrectionDCI = new CorrectionSpinner(Correction.DC_INPHASE, 0.0, 0.00001, 5); + mCorrectionDCI.setEnabled(false); + mCorrectionDCI.setToolTipText("DC Bias Correction/Inphase " + + "Component: valid values are -1.0 to 1.0 (default: 0.0)"); + add(new JLabel("DC Inphase:")); + add(mCorrectionDCI); + + /** + * Quadrature DC Correction + */ + mCorrectionDCQ = new CorrectionSpinner(Correction.DC_QUADRATURE, 0.0, 0.00001, 5); + mCorrectionDCQ.setToolTipText("DC Bias Correction/Quadrature " + + "Component: valid values are -1.0 to 1.0 (default: 0.0)"); + add(new JLabel("DC Quadrature:")); + add(mCorrectionDCQ); + + /** + * Gain Correction + */ + mCorrectionGain = new CorrectionSpinner(Correction.GAIN, 0.0, 0.00001, 5); + mCorrectionGain.setToolTipText("Gain Correction: valid values are " + + "-1.0 to 1.0 (default: 0.0)"); + add(new JLabel("Gain:")); + add(mCorrectionGain); + + /** + * Phase Correction + */ + mCorrectionPhase = new CorrectionSpinner(Correction.PHASE, 0.0, 0.00001, 5); + mCorrectionPhase.setToolTipText("Phase Correction: valid values are " + + "-1.0 to 1.0 (default: 0.0)"); + add(new JLabel("Phase:")); + add(mCorrectionPhase); + } + + /** + * Sets each of the tuner configuration controls to the enabled argument state + */ + private void setControlsEnabled(boolean enabled) + { + if(mConfigurationName.isEnabled() != enabled) + { + mConfigurationName.setEnabled(enabled); + } + + if(mTunerInfo.isEnabled() != enabled) + { + mTunerInfo.setEnabled(enabled); + } + + if(mFrequencyCorrection.isEnabled() != enabled) + { + mFrequencyCorrection.setEnabled(enabled); + } + + if(mComboLNAEnhance.isEnabled() != enabled) + { + mComboLNAEnhance.setEnabled(enabled); + } + + if(mComboLNAGain.isEnabled() != enabled) + { + mComboLNAGain.setEnabled(enabled); + } + + if(mComboMixerGain.isEnabled() != enabled) + { + mComboMixerGain.setEnabled(enabled); + } + + if(mCorrectionDCI.isEnabled() != enabled) + { + mCorrectionDCI.setEnabled(enabled); + } + + if(mCorrectionDCQ.isEnabled() != enabled) + { + mCorrectionDCQ.setEnabled(enabled); + } + + if(mCorrectionGain.isEnabled() != enabled) + { + mCorrectionGain.setEnabled(enabled); + } + + if(mCorrectionPhase.isEnabled() != enabled) + { + mCorrectionPhase.setEnabled(enabled); + } + } + + private String getTunerInfo() + { + StringBuilder sb = new StringBuilder(); + + sb.append("

Funcube Dongle Pro Tuner

"); + + sb.append("USB ID: "); + sb.append(mController.getUSBID()); + sb.append("
"); + + sb.append("USB Address: "); + sb.append(mController.getUSBAddress()); + sb.append("
"); + + sb.append("USB Speed: "); + sb.append(mController.getUSBSpeed()); + sb.append("
"); + + sb.append("Cellular Band: "); + sb.append(mController.getConfiguration().getBandBlocking()); + sb.append("
"); + + sb.append("Firmware: "); + sb.append(mController.getConfiguration().getFirmware()); + sb.append("
"); + + return sb.toString(); + } + + @Override + public void save() + { + if(hasItem() && !mLoading) + { + FCD1TunerConfiguration config = getConfiguration(); + + config.setName(mConfigurationName.getText()); + + double value = ((SpinnerNumberModel)mFrequencyCorrection + .getModel()).getNumber().doubleValue(); + config.setFrequencyCorrection(value); + config.setLNAEnhance((LNAEnhance)mComboLNAEnhance.getSelectedItem()); + config.setLNAGain((LNAGain)mComboLNAGain.getSelectedItem()); + config.setMixerGain((MixerGain)mComboMixerGain.getSelectedItem()); + + double dci = ((SpinnerNumberModel)mCorrectionDCI.getModel()).getNumber().doubleValue(); + config.setInphaseDCCorrection(dci); + + double dcq = ((SpinnerNumberModel)mCorrectionDCQ.getModel()).getNumber().doubleValue(); + config.setQuadratureDCCorrection(dcq); + + double gain = ((SpinnerNumberModel)mCorrectionGain.getModel()).getNumber().doubleValue(); + config.setGainCorrection(gain); + + double phase = ((SpinnerNumberModel)mCorrectionPhase.getModel()).getNumber().doubleValue(); + config.setPhaseCorrection(phase); + + getTunerConfigurationModel().broadcast(new TunerConfigurationEvent(getConfiguration(), Event.CHANGE)); + } + } + + @Override + public void setItem(TunerConfiguration tunerConfiguration) + { + super.setItem(tunerConfiguration); + + //Toggle loading so that we don't fire a change event and schedule a settings file save + mLoading = true; + + if(hasItem()) + { + FCD1TunerConfiguration config = getConfiguration(); + + if(tunerConfiguration.isAssigned()) + { + setControlsEnabled(true); + mConfigurationName.setText(config.getName()); + mFrequencyCorrection.setValue(config.getFrequencyCorrection()); + mComboLNAEnhance.setSelectedItem(config.getLNAEnhance()); + mComboLNAGain.setSelectedItem(config.getLNAGain()); + mComboMixerGain.setSelectedItem(config.getMixerGain()); + mCorrectionDCI.setValue(config.getInphaseDCCorrection()); + mCorrectionDCQ.setValue(config.getQuadratureDCCorrection()); + mCorrectionGain.setValue(config.getGainCorrection()); + mCorrectionPhase.setValue(config.getPhaseCorrection()); + } + else + { + setControlsEnabled(false); + mConfigurationName.setText(config.getName()); + } + } + else + { + setControlsEnabled(false); + mConfigurationName.setText(""); + } + + mLoading = false; + } + + public enum Correction + { + GAIN, PHASE, DC_INPHASE, DC_QUADRATURE + } + + ; + + public class CorrectionSpinner extends JSpinner + { + private static final long serialVersionUID = 1L; + private static final double MIN_VALUE = -1.0d; + private static final double MAX_VALUE = 1.0d; + + private Correction mCorrectionComponent; + + public CorrectionSpinner(Correction component, double initialValue, double step, int decimalPlaces) + { + mCorrectionComponent = component; + + SpinnerModel model = new SpinnerNumberModel(initialValue, MIN_VALUE, MAX_VALUE, step); + setModel(model); + + JSpinner.NumberEditor editor = (JSpinner.NumberEditor)getEditor(); + + DecimalFormat format = editor.getFormat(); + format.setMinimumFractionDigits(decimalPlaces); + + editor.getTextField().setHorizontalAlignment(SwingConstants.CENTER); + + addChangeListener(new ChangeListener() + { + @Override + public void stateChanged(ChangeEvent e) + { + double value = + ((SpinnerNumberModel)getModel()).getNumber().doubleValue(); + + try + { + switch(mCorrectionComponent) + { + case DC_INPHASE: + mController.setDCCorrectionInPhase(value); + break; + case DC_QUADRATURE: + mController.setDCCorrectionQuadrature(value); + break; + case GAIN: + mController.setGainCorrection(value); + break; + case PHASE: + mController.setPhaseCorrection(value); + break; + } + + save(); + } + catch(Exception e1) + { + JOptionPane.showMessageDialog(FCD1TunerEditor.this, "FCDPro - error " + + "applying " + mCorrectionComponent.toString() + " correction value [" + + value + "]"); + + mLog.error("FCDPro - error applying " + mCorrectionComponent.toString() + + " correction value [" + value + "]", e1); + } + } + }); + } + } } \ No newline at end of file diff --git a/src/main/java/io/github/dsheirer/source/tuner/fcd/proplusV2/FCD2TunerEditor.java b/src/main/java/io/github/dsheirer/source/tuner/fcd/proplusV2/FCD2TunerEditor.java index ce8d6fb7..a9d854e8 100644 --- a/src/main/java/io/github/dsheirer/source/tuner/fcd/proplusV2/FCD2TunerEditor.java +++ b/src/main/java/io/github/dsheirer/source/tuner/fcd/proplusV2/FCD2TunerEditor.java @@ -1,19 +1,17 @@ /******************************************************************************* - * SDR Trunk - * Copyright (C) 2014-2016 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 + * 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 + * ******************************************************************************/ package io.github.dsheirer.source.tuner.fcd.proplusV2; @@ -48,278 +46,287 @@ import java.text.DecimalFormat; public class FCD2TunerEditor extends TunerConfigurationEditor { - private final static Logger mLog = LoggerFactory.getLogger( FCD2TunerEditor.class ); - private static final long serialVersionUID = 1L; + private final static Logger mLog = LoggerFactory.getLogger(FCD2TunerEditor.class); + private static final long serialVersionUID = 1L; - private JTextField mConfigurationName; - private JButton mTunerInfo; - private JSpinner mFrequencyCorrection; - private JCheckBox mLNAGain; - private JCheckBox mMixerGain; - private boolean mLoading; + private JTextField mConfigurationName; + private JButton mTunerInfo; + private JSpinner mFrequencyCorrection; + private JCheckBox mLNAGain; + private JCheckBox mMixerGain; + private boolean mLoading; - private FCD2TunerController mController; + private FCD2TunerController mController; - public FCD2TunerEditor( TunerConfigurationModel tunerConfigurationModel, FCDTuner tuner ) - { - super( tunerConfigurationModel ); - mController = (FCD2TunerController)tuner.getController(); + public FCD2TunerEditor(TunerConfigurationModel tunerConfigurationModel, FCDTuner tuner) + { + super(tunerConfigurationModel); + mController = (FCD2TunerController)tuner.getController(); - init(); - } + init(); + } - private FCD2TunerConfiguration getConfiguration() - { - if( hasItem() ) - { - return (FCD2TunerConfiguration)getItem(); - } + private FCD2TunerConfiguration getConfiguration() + { + if(hasItem()) + { + return (FCD2TunerConfiguration)getItem(); + } - return null; - } + return null; + } - private void init() - { - setLayout( new MigLayout( "fill,wrap 4", "[right][grow,fill][right][grow,fill]", - "[][][][][][grow]" ) ); + @Override + public void setSampleRateControl(boolean enabled) + { + //no-op + } - add( new JLabel( "FCD Pro+ Tuner Configuration" ), "span,align center" ); + private void init() + { + setLayout(new MigLayout("fill,wrap 4", "[right][grow,fill][right][grow,fill]", + "[][][][][][grow]")); - mConfigurationName = new JTextField(); - mConfigurationName.setEnabled( false ); - mConfigurationName.addFocusListener( new FocusListener() - { - @Override - public void focusLost( FocusEvent e ) - { - save(); - } - @Override - public void focusGained( FocusEvent e ) {} - } ); + add(new JLabel("FCD Pro+ Tuner Configuration"), "span,align center"); - add( new JLabel( "Name:" ) ); - add( mConfigurationName, "span 2" ); + mConfigurationName = new JTextField(); + mConfigurationName.setEnabled(false); + mConfigurationName.addFocusListener(new FocusListener() + { + @Override + public void focusLost(FocusEvent e) + { + save(); + } - mTunerInfo = new JButton( "Tuner Info" ); - mTunerInfo.setEnabled( false ); - mTunerInfo.addActionListener( new ActionListener() - { - @Override - public void actionPerformed( ActionEvent e ) - { - JOptionPane.showMessageDialog( FCD2TunerEditor.this, getTunerInfo(), - "Tuner Info", JOptionPane.INFORMATION_MESSAGE ); - } - } ); - add( mTunerInfo ); + @Override + public void focusGained(FocusEvent e) + { + } + }); - SpinnerModel model = new SpinnerNumberModel( 0.0, //initial value - -1000.0, //min - 1000.0, //max - 0.1 ); //step + add(new JLabel("Name:")); + add(mConfigurationName, "span 2"); - mFrequencyCorrection = new JSpinner( model ); - mFrequencyCorrection.setEnabled( false ); - JSpinner.NumberEditor editor = - (JSpinner.NumberEditor)mFrequencyCorrection.getEditor(); + mTunerInfo = new JButton("Tuner Info"); + mTunerInfo.setEnabled(false); + mTunerInfo.addActionListener(new ActionListener() + { + @Override + public void actionPerformed(ActionEvent e) + { + JOptionPane.showMessageDialog(FCD2TunerEditor.this, getTunerInfo(), + "Tuner Info", JOptionPane.INFORMATION_MESSAGE); + } + }); + add(mTunerInfo); - DecimalFormat format = editor.getFormat(); - format.setMinimumFractionDigits( 1 ); - editor.getTextField().setHorizontalAlignment( SwingConstants.CENTER ); + SpinnerModel model = new SpinnerNumberModel(0.0, //initial value + -1000.0, //min + 1000.0, //max + 0.1); //step - mFrequencyCorrection.addChangeListener( new ChangeListener() - { - @Override - public void stateChanged( ChangeEvent e ) - { - double value = ((SpinnerNumberModel)mFrequencyCorrection - .getModel()).getNumber().doubleValue(); + mFrequencyCorrection = new JSpinner(model); + mFrequencyCorrection.setEnabled(false); + JSpinner.NumberEditor editor = + (JSpinner.NumberEditor)mFrequencyCorrection.getEditor(); - try - { - mController.setFrequencyCorrection( value ); - save(); - } - catch ( SourceException e1 ) - { - JOptionPane.showMessageDialog( FCD2TunerEditor.this, "FCD Pro Plus Tuner " - + "Controller - couldn't apply frequency correction value: " + value + - e1.getLocalizedMessage() ); + DecimalFormat format = editor.getFormat(); + format.setMinimumFractionDigits(1); + editor.getTextField().setHorizontalAlignment(SwingConstants.CENTER); - mLog.error( "FuncubeDongleProPlus Controller - couldn't apply " - + "frequency correction value: " + value, e1 ); - } - } - } ); + mFrequencyCorrection.addChangeListener(new ChangeListener() + { + @Override + public void stateChanged(ChangeEvent e) + { + double value = ((SpinnerNumberModel)mFrequencyCorrection + .getModel()).getNumber().doubleValue(); - add( new JLabel( "PPM:" ) ); - add( mFrequencyCorrection, "wrap" ); + try + { + mController.setFrequencyCorrection(value); + save(); + } + catch(SourceException e1) + { + JOptionPane.showMessageDialog(FCD2TunerEditor.this, "FCD Pro Plus Tuner " + + "Controller - couldn't apply frequency correction value: " + value + + e1.getLocalizedMessage()); - add( new JSeparator( JSeparator.HORIZONTAL ), "span,grow" ); + mLog.error("FuncubeDongleProPlus Controller - couldn't apply " + + "frequency correction value: " + value, e1); + } + } + }); - mLNAGain = new JCheckBox( "LNA Gain" ); - mLNAGain.setEnabled( false ); - mLNAGain.addActionListener( new ActionListener() - { - @Override - public void actionPerformed( ActionEvent event ) - { - try - { - mController.setLNAGain( mLNAGain.isSelected() ); - save(); - } - catch( SourceException e ) - { - mLog.error( "Couldn't set LNA gain for FCD2", e ); - } - } - } ); + add(new JLabel("PPM:")); + add(mFrequencyCorrection, "wrap"); - add( new JLabel( "" ) ); - add( mLNAGain ); + add(new JSeparator(JSeparator.HORIZONTAL), "span,grow"); - /** - * Mixer Gain - */ - mMixerGain = new JCheckBox( "Mixer Gain" ); + mLNAGain = new JCheckBox("LNA Gain"); + mLNAGain.setEnabled(false); + mLNAGain.addActionListener(new ActionListener() + { + @Override + public void actionPerformed(ActionEvent event) + { + try + { + mController.setLNAGain(mLNAGain.isSelected()); + save(); + } + catch(SourceException e) + { + mLog.error("Couldn't set LNA gain for FCD2", e); + } + } + }); - mMixerGain.addActionListener( new ActionListener() - { - @Override - public void actionPerformed( ActionEvent event ) - { - try - { - mController.setMixerGain( mMixerGain.isSelected() ); - save(); - } - catch( SourceException e ) - { - mLog.error( "Couldn't set mixer gain for FCD2", e ); - } - } - } ); + add(new JLabel("")); + add(mLNAGain); - add( new JLabel( "" ) ); - add( mMixerGain ); - } + /** + * Mixer Gain + */ + mMixerGain = new JCheckBox("Mixer Gain"); - /** - * Sets each of the tuner configuration controls to the enabled argument state - */ - private void setControlsEnabled( boolean enabled ) - { - if( mConfigurationName.isEnabled() != enabled ) - { - mConfigurationName.setEnabled( enabled ); - } + mMixerGain.addActionListener(new ActionListener() + { + @Override + public void actionPerformed(ActionEvent event) + { + try + { + mController.setMixerGain(mMixerGain.isSelected()); + save(); + } + catch(SourceException e) + { + mLog.error("Couldn't set mixer gain for FCD2", e); + } + } + }); - if( mTunerInfo.isEnabled() != enabled ) - { - mTunerInfo.setEnabled( enabled ); - } + add(new JLabel("")); + add(mMixerGain); + } - if( mFrequencyCorrection.isEnabled() != enabled ) - { - mFrequencyCorrection.setEnabled( enabled ); - } + /** + * Sets each of the tuner configuration controls to the enabled argument state + */ + private void setControlsEnabled(boolean enabled) + { + if(mConfigurationName.isEnabled() != enabled) + { + mConfigurationName.setEnabled(enabled); + } - if( mLNAGain.isEnabled() != enabled ) - { - mLNAGain.setEnabled( enabled ); - } + if(mTunerInfo.isEnabled() != enabled) + { + mTunerInfo.setEnabled(enabled); + } - if( mMixerGain.isEnabled() != enabled ) - { - mMixerGain.setEnabled( enabled ); - } - } + if(mFrequencyCorrection.isEnabled() != enabled) + { + mFrequencyCorrection.setEnabled(enabled); + } - private String getTunerInfo() - { - StringBuilder sb = new StringBuilder(); + if(mLNAGain.isEnabled() != enabled) + { + mLNAGain.setEnabled(enabled); + } - sb.append( "

Funcube Dongle Pro Plus Tuner

" ); + if(mMixerGain.isEnabled() != enabled) + { + mMixerGain.setEnabled(enabled); + } + } - sb.append( "USB ID: " ); - sb.append( mController.getUSBID() ); - sb.append( "
" ); + private String getTunerInfo() + { + StringBuilder sb = new StringBuilder(); - sb.append( "USB Address: " ); - sb.append( mController.getUSBAddress() ); - sb.append( "
" ); + sb.append("

Funcube Dongle Pro Plus Tuner

"); - sb.append( "USB Speed: " ); - sb.append( mController.getUSBSpeed() ); - sb.append( "
" ); + sb.append("USB ID: "); + sb.append(mController.getUSBID()); + sb.append("
"); - sb.append( "Cellular Band: " ); - sb.append( mController.getConfiguration().getBandBlocking() ); - sb.append( "
" ); + sb.append("USB Address: "); + sb.append(mController.getUSBAddress()); + sb.append("
"); - sb.append( "Firmware: " ); - sb.append( mController.getConfiguration().getFirmware() ); - sb.append( "
" ); + sb.append("USB Speed: "); + sb.append(mController.getUSBSpeed()); + sb.append("
"); - return sb.toString(); - } + sb.append("Cellular Band: "); + sb.append(mController.getConfiguration().getBandBlocking()); + sb.append("
"); - @Override - public void save() - { - if( hasItem() && !mLoading ) - { - FCD2TunerConfiguration config = getConfiguration(); + sb.append("Firmware: "); + sb.append(mController.getConfiguration().getFirmware()); + sb.append("
"); - config.setName( mConfigurationName.getText() ); + return sb.toString(); + } - double value = ((SpinnerNumberModel)mFrequencyCorrection - .getModel()).getNumber().doubleValue(); - config.setFrequencyCorrection( value ); + @Override + public void save() + { + if(hasItem() && !mLoading) + { + FCD2TunerConfiguration config = getConfiguration(); - config.setGainLNA( mLNAGain.isSelected() ); - config.setGainMixer( mMixerGain.isSelected() ); + config.setName(mConfigurationName.getText()); - getTunerConfigurationModel().broadcast( - new TunerConfigurationEvent( getConfiguration(), Event.CHANGE ) ); - } - } + double value = ((SpinnerNumberModel)mFrequencyCorrection + .getModel()).getNumber().doubleValue(); + config.setFrequencyCorrection(value); - @Override - public void setItem( TunerConfiguration tunerConfiguration ) - { - super.setItem( tunerConfiguration ); + config.setGainLNA(mLNAGain.isSelected()); + config.setGainMixer(mMixerGain.isSelected()); - //Toggle loading so that we don't fire a change event and schedule a settings file save - mLoading = true; + getTunerConfigurationModel().broadcast( + new TunerConfigurationEvent(getConfiguration(), Event.CHANGE)); + } + } - if( hasItem() ) - { - FCD2TunerConfiguration config = getConfiguration(); + @Override + public void setItem(TunerConfiguration tunerConfiguration) + { + super.setItem(tunerConfiguration); + + //Toggle loading so that we don't fire a change event and schedule a settings file save + mLoading = true; + + if(hasItem()) + { + FCD2TunerConfiguration config = getConfiguration(); - if( tunerConfiguration.isAssigned() ) - { - setControlsEnabled( true ); - mConfigurationName.setText( config.getName() ); - mFrequencyCorrection.setValue( config.getFrequencyCorrection() ); - mLNAGain.setSelected( config.getGainLNA() ); - mMixerGain.setSelected( config.getGainMixer() ); - } - else - { - setControlsEnabled( false ); - mConfigurationName.setText( config.getName() ); - } - } - else - { - setControlsEnabled( false ); - mConfigurationName.setText( "" ); - } + if(tunerConfiguration.isAssigned()) + { + setControlsEnabled(true); + mConfigurationName.setText(config.getName()); + mFrequencyCorrection.setValue(config.getFrequencyCorrection()); + mLNAGain.setSelected(config.getGainLNA()); + mMixerGain.setSelected(config.getGainMixer()); + } + else + { + setControlsEnabled(false); + mConfigurationName.setText(config.getName()); + } + } + else + { + setControlsEnabled(false); + mConfigurationName.setText(""); + } - mLoading = false; - } + mLoading = false; + } } \ No newline at end of file diff --git a/src/main/java/io/github/dsheirer/source/tuner/frequency/FrequencyController.java b/src/main/java/io/github/dsheirer/source/tuner/frequency/FrequencyController.java index 74d36f8f..aa8b98aa 100644 --- a/src/main/java/io/github/dsheirer/source/tuner/frequency/FrequencyController.java +++ b/src/main/java/io/github/dsheirer/source/tuner/frequency/FrequencyController.java @@ -1,7 +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 @@ -12,7 +11,7 @@ * * You should have received a copy of the GNU General Public License along with this program. * If not, see - * ********************************************************************************************************************* + * ******************************************************************************/ package io.github.dsheirer.source.tuner.frequency; @@ -28,248 +27,284 @@ import java.util.List; public class FrequencyController { - private final static Logger mLog = LoggerFactory.getLogger(FrequencyController.class); + private final static Logger mLog = LoggerFactory.getLogger(FrequencyController.class); - private Tunable mTunable; - private long mFrequency = 101100000; - private long mTunedFrequency = 101100000; - private long mMinimumFrequency; - private long mMaximumFrequency; - private double mFrequencyCorrection = 0.0d; - private double mSampleRate = 0.0d; + private Tunable mTunable; + private long mFrequency = 101100000; + private long mTunedFrequency = 101100000; + private long mMinimumFrequency; + private long mMaximumFrequency; + private double mFrequencyCorrection = 0.0d; + private double mSampleRate = 0.0d; + private boolean mLocked = false; - private List mProcessors = new ArrayList<>(); + private List mProcessors = new ArrayList<>(); - public FrequencyController(Tunable tunable, long minFrequency, long maxFrequency, double frequencyCorrection) - { - mTunable = tunable; - mMinimumFrequency = minFrequency; - mMaximumFrequency = maxFrequency; - mFrequencyCorrection = frequencyCorrection; - } + public FrequencyController(Tunable tunable, long minFrequency, long maxFrequency, double frequencyCorrection) + { + mTunable = tunable; + mMinimumFrequency = minFrequency; + mMaximumFrequency = maxFrequency; + mFrequencyCorrection = frequencyCorrection; + } - /** - * Get bandwidth in hertz - */ - public int getBandwidth() - { - return (int)mSampleRate; - } + /** + * Indicates if this frequency controller is locked by a remote process. A locked state indicates that neither + * the frequency nor the sample rate should be changed. The remote process will typically be a downstream + * consumer of the data produced by the device being controlled (ie polyphase channelizer) that does not want + * the sample rate or frequency to be changed while processing is ongoing. + * + * @return true if locked or false if not locked. + */ + public boolean isLocked() + { + return mLocked; + } - /** - * Set sample rate in hertz - */ - public void setSampleRate(int sampleRate) throws SourceException - { - mSampleRate = sampleRate; + /** + * Sets the lock state for this frequency controller. Set to true when a consumer process requires that the + * frequency and sample rate controls be locked so that only the singular consumer process makes changes. This + * is primarily to lock down user interface controls when the polyphase channelizer is producing channels. + * + * @param locked state + */ + public void setLocked(boolean locked) throws SourceException + { + mLocked = locked; - broadcastSampleRateChange(); - } + if(mLocked) + { + broadcast(SourceEvent.lockedState()); + } + else + { + broadcast(SourceEvent.unlockedState()); + } + } - /** - * Get sample rate in hertz - */ - public double getSampleRate() - { - return mSampleRate; - } + /** + * Get bandwidth in hertz + */ + public int getBandwidth() + { + return (int)mSampleRate; + } - /** - * Get frequency in hertz - */ - public long getFrequency() - { - return mFrequency; - } + /** + * Set sample rate in hertz + */ + public void setSampleRate(int sampleRate) throws SourceException + { + mSampleRate = sampleRate; - /** - * Set frequency - * - * @param frequency in hertz - * @throws SourceException if tunable doesn't support tuning a corrected - * version of the requested frequency - */ - public void setFrequency(long frequency) throws SourceException - { - setFrequency(frequency, true); - } + broadcastSampleRateChange(); + } - /** - * Indicates if the specified frequency can be tuned (ie is within min/max frequency) by this controller. - * @param frequency to evaluate - * @return true if the frequency falls within the tuning range of this controller - */ - public boolean canTune(long frequency) - { - return getMinimumFrequency() <= frequency && frequency <= getMaximumFrequency(); - } + /** + * Get sample rate in hertz + */ + public double getSampleRate() + { + return mSampleRate; + } - /** - * Set frequency with optional broadcast of frequency change event. This - * method supports changing the frequency correction value without - * broadcasting a frequency change event. - */ - private void setFrequency(long frequency, boolean broadcastChange) throws SourceException - { - long tunedFrequency = getTunedFrequency(frequency); + /** + * Get frequency in hertz + */ + public long getFrequency() + { + return mFrequency; + } - if(tunedFrequency < mMinimumFrequency) - { - throw new InvalidFrequencyException("Requested frequency not valid", frequency, mMinimumFrequency); - } + /** + * Set frequency + * + * @param frequency in hertz + * @throws SourceException if tunable doesn't support tuning a corrected + * version of the requested frequency + */ + public void setFrequency(long frequency) throws SourceException + { + setFrequency(frequency, true); + } - if(tunedFrequency > mMaximumFrequency) - { - throw new InvalidFrequencyException("Requested frequency not valid", frequency, mMaximumFrequency); - } + /** + * Indicates if the specified frequency can be tuned (ie is within min/max frequency) by this controller. + * + * @param frequency to evaluate + * @return true if the frequency falls within the tuning range of this controller + */ + public boolean canTune(long frequency) + { + return getMinimumFrequency() <= frequency && frequency <= getMaximumFrequency(); + } - mFrequency = frequency; + /** + * Set frequency with optional broadcast of frequency change event. This + * method supports changing the frequency correction value without + * broadcasting a frequency change event. + */ + private void setFrequency(long frequency, boolean broadcastChange) throws SourceException + { + long tunedFrequency = getTunedFrequency(frequency); - mTunedFrequency = tunedFrequency; + if(tunedFrequency < mMinimumFrequency) + { + throw new InvalidFrequencyException("Requested frequency not valid", frequency, mMinimumFrequency); + } - if(mTunable != null) - { - mTunable.setTunedFrequency(mTunedFrequency); - } + if(tunedFrequency > mMaximumFrequency) + { + throw new InvalidFrequencyException("Requested frequency not valid", frequency, mMaximumFrequency); + } - /* Broadcast to all listeners that the frequency has changed */ - if(broadcastChange) - { - broadcastFrequencyChange(); - } - } + mFrequency = frequency; - public long getTunedFrequency() - { - return mTunedFrequency; - } + mTunedFrequency = tunedFrequency; - public long getMinimumFrequency() - { - return mMinimumFrequency; - } + if(mTunable != null) + { + mTunable.setTunedFrequency(mTunedFrequency); + } - public long getMaximumFrequency() - { - return mMaximumFrequency; - } + /* Broadcast to all listeners that the frequency has changed */ + if(broadcastChange) + { + broadcastFrequencyChange(); + } + } - /** - * Calculate the tuned frequency by adding frequency correction to the - * corrected frequency. - * - * @param correctedFrequency - */ - private long getTunedFrequency(long correctedFrequency) - { - return (long)((double)correctedFrequency / - (1.0 + (mFrequencyCorrection / 1000000.0))); - } + public long getTunedFrequency() + { + return mTunedFrequency; + } - /** - * Calculate the corrected frequency by subtracting frequency correction - * from the tuned frequency. - * - * @param tunedFrequency - */ - @SuppressWarnings("unused") - private long getCorrectedFrequency(long tunedFrequency) - { - return (long)((double)tunedFrequency / - (1.0 - (mFrequencyCorrection / 1000000.0))); - } + public long getMinimumFrequency() + { + return mMinimumFrequency; + } - public double getFrequencyCorrection() - { - return mFrequencyCorrection; - } + public long getMaximumFrequency() + { + return mMaximumFrequency; + } - public void setFrequencyCorrection(double correction) throws SourceException - { - mFrequencyCorrection = correction; + /** + * Calculate the tuned frequency by adding frequency correction to the + * corrected frequency. + * + * @param correctedFrequency + */ + private long getTunedFrequency(long correctedFrequency) + { + return (long)((double)correctedFrequency / + (1.0 + (mFrequencyCorrection / 1000000.0))); + } - if(mFrequency > 0) - { - setFrequency(mFrequency, true); - } + /** + * Calculate the corrected frequency by subtracting frequency correction + * from the tuned frequency. + * + * @param tunedFrequency + */ + @SuppressWarnings("unused") + private long getCorrectedFrequency(long tunedFrequency) + { + return (long)((double)tunedFrequency / + (1.0 - (mFrequencyCorrection / 1000000.0))); + } - broadcastFrequencyCorrectionChange(); - } + public double getFrequencyCorrection() + { + return mFrequencyCorrection; + } - /** - * Adds listener to receive frequency change events - */ - public void addListener(ISourceEventProcessor processor) - { - if(!mProcessors.contains(processor)) - { - mProcessors.add(processor); - } - } + public void setFrequencyCorrection(double correction) throws SourceException + { + mFrequencyCorrection = correction; - /** - * Removes listener from receiving frequency change events - */ - public void removeFrequencyChangeProcessor(ISourceEventProcessor processor) - { - mProcessors.remove(processor); - } + if(mFrequency > 0) + { + setFrequency(mFrequency, true); + } + + broadcastFrequencyCorrectionChange(); + } + + /** + * Adds listener to receive frequency change events + */ + public void addListener(ISourceEventProcessor processor) + { + if(!mProcessors.contains(processor)) + { + mProcessors.add(processor); + } + } + + /** + * Removes listener from receiving frequency change events + */ + public void removeFrequencyChangeProcessor(ISourceEventProcessor processor) + { + mProcessors.remove(processor); + } - /** - * Broadcasts a change/update to the current (uncorrected) frequency or the - * bandwidth/sample rate value. - */ - protected void broadcastFrequencyChange() throws SourceException - { - broadcast(SourceEvent.frequencyChange(mFrequency)); - } + /** + * Broadcasts a change/update to the current (uncorrected) frequency or the + * bandwidth/sample rate value. + */ + protected void broadcastFrequencyChange() throws SourceException + { + broadcast(SourceEvent.frequencyChange(mFrequency)); + } - /** - * Broadcast a frequency error/correction value change - */ - protected void broadcastFrequencyCorrectionChange() throws SourceException - { - broadcast(SourceEvent.frequencyCorrectionChange((int)mFrequencyCorrection)); - } + /** + * Broadcast a frequency error/correction value change + */ + protected void broadcastFrequencyCorrectionChange() throws SourceException + { + broadcast(SourceEvent.frequencyCorrectionChange((int)mFrequencyCorrection)); + } - /** - * Broadcasts a sample rate change - */ - protected void broadcastSampleRateChange() throws SourceException - { - broadcast(SourceEvent.sampleRateChange(mSampleRate)); - } + /** + * Broadcasts a sample rate change + */ + protected void broadcastSampleRateChange() throws SourceException + { + broadcast(SourceEvent.sampleRateChange(mSampleRate)); + } - public void broadcast(SourceEvent event) throws SourceException - { - for(ISourceEventProcessor processor : mProcessors) - { - processor.process(event); - } - } + public void broadcast(SourceEvent event) throws SourceException + { + for(ISourceEventProcessor processor : mProcessors) + { + processor.process(event); + } + } - public interface Tunable - { - /** - * Gets the tuned frequency of the device - */ - public long getTunedFrequency() throws SourceException; + public interface Tunable + { + /** + * Gets the tuned frequency of the device + */ + public long getTunedFrequency() throws SourceException; - /** - * Sets the tuned frequency of the device - */ - public void setTunedFrequency(long frequency) throws SourceException; + /** + * Sets the tuned frequency of the device + */ + public void setTunedFrequency(long frequency) throws SourceException; - /** - * Gets the current bandwidth setting of the device - */ - public double getCurrentSampleRate() throws SourceException; + /** + * Gets the current bandwidth setting of the device + */ + public double getCurrentSampleRate() throws SourceException; - /** - * Indicates if this tunable can tune the frequency - */ - public boolean canTune(long frequency); - } + /** + * Indicates if this tunable can tune the frequency + */ + public boolean canTune(long frequency); + } } \ No newline at end of file diff --git a/src/main/java/io/github/dsheirer/source/tuner/hackrf/HackRFTunerEditor.java b/src/main/java/io/github/dsheirer/source/tuner/hackrf/HackRFTunerEditor.java index 9aea1db3..a2f78262 100644 --- a/src/main/java/io/github/dsheirer/source/tuner/hackrf/HackRFTunerEditor.java +++ b/src/main/java/io/github/dsheirer/source/tuner/hackrf/HackRFTunerEditor.java @@ -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 @@ -41,6 +41,7 @@ import javax.swing.SwingConstants; import javax.swing.event.ChangeEvent; import javax.swing.event.ChangeListener; import javax.usb.UsbException; +import java.awt.EventQueue; import java.awt.event.ActionEvent; import java.awt.event.ActionListener; import java.awt.event.FocusEvent; @@ -51,7 +52,7 @@ public class HackRFTunerEditor extends TunerConfigurationEditor { private static final long serialVersionUID = 1L; - private final static Logger mLog = LoggerFactory.getLogger( HackRFTunerEditor.class ); + private final static Logger mLog = LoggerFactory.getLogger(HackRFTunerEditor.class); private JTextField mConfigurationName; private JButton mTunerInfo; @@ -64,9 +65,9 @@ public class HackRFTunerEditor extends TunerConfigurationEditor private HackRFTunerController mController; - public HackRFTunerEditor( TunerConfigurationModel tunerConfigurationModel, HackRFTuner tuner ) + public HackRFTunerEditor(TunerConfigurationModel tunerConfigurationModel, HackRFTuner tuner) { - super( tunerConfigurationModel ); + super(tunerConfigurationModel); mController = tuner.getController(); @@ -75,7 +76,7 @@ public class HackRFTunerEditor extends TunerConfigurationEditor private HackRFTunerConfiguration getConfiguration() { - if( hasItem() ) + if(hasItem()) { return (HackRFTunerConfiguration)getItem(); } @@ -83,241 +84,257 @@ public class HackRFTunerEditor extends TunerConfigurationEditor return null; } - private void init() + @Override + public void setSampleRateControl(boolean enabled) { - setLayout( new MigLayout( "fill,wrap 4", "[right][grow,fill][right][grow,fill]", - "[][][][][][][grow]" ) ); - - add( new JLabel( "HackRF Tuner Configuration" ), "span,align center" ); - - mConfigurationName = new JTextField(); - mConfigurationName.setEnabled( false ); - mConfigurationName.addFocusListener( new FocusListener() + EventQueue.invokeLater(new Runnable() { @Override - public void focusLost( FocusEvent e ) + public void run() + { + mComboSampleRate.setEnabled(enabled); + } + }); + } + + private void init() + { + setLayout(new MigLayout("fill,wrap 4", "[right][grow,fill][right][grow,fill]", + "[][][][][][][grow]")); + + add(new JLabel("HackRF Tuner Configuration"), "span,align center"); + + mConfigurationName = new JTextField(); + mConfigurationName.setEnabled(false); + mConfigurationName.addFocusListener(new FocusListener() + { + @Override + public void focusLost(FocusEvent e) { save(); } - @Override - public void focusGained( FocusEvent e ) {} - } ); - add( new JLabel( "Name:" ) ); - add( mConfigurationName, "span 2" ); - - mTunerInfo = new JButton( "Tuner Info" ); - mTunerInfo.setEnabled( false ); - mTunerInfo.addActionListener( new ActionListener() - { @Override - public void actionPerformed( ActionEvent e ) + public void focusGained(FocusEvent e) { - JOptionPane.showMessageDialog( HackRFTunerEditor.this, getTunerInfo(), - "Tuner Info", JOptionPane.INFORMATION_MESSAGE ); } - } ); - add( mTunerInfo ); + }); - mComboSampleRate = new JComboBox<>( HackRFSampleRate.values() ); - mComboSampleRate.setEnabled( false ); - mComboSampleRate.addActionListener( new ActionListener() + add(new JLabel("Name:")); + add(mConfigurationName, "span 2"); + + mTunerInfo = new JButton("Tuner Info"); + mTunerInfo.setEnabled(false); + mTunerInfo.addActionListener(new ActionListener() { @Override - public void actionPerformed( ActionEvent e ) + public void actionPerformed(ActionEvent e) + { + JOptionPane.showMessageDialog(HackRFTunerEditor.this, getTunerInfo(), + "Tuner Info", JOptionPane.INFORMATION_MESSAGE); + } + }); + add(mTunerInfo); + + mComboSampleRate = new JComboBox<>(HackRFSampleRate.values()); + mComboSampleRate.setEnabled(false); + mComboSampleRate.addActionListener(new ActionListener() + { + @Override + public void actionPerformed(ActionEvent e) { HackRFSampleRate sampleRate = (HackRFSampleRate)mComboSampleRate.getSelectedItem(); try { - mController.setSampleRate( sampleRate ); + mController.setSampleRate(sampleRate); save(); } - catch ( SourceException|UsbException e2 ) + catch(SourceException | UsbException e2) { - JOptionPane.showMessageDialog( HackRFTunerEditor.this, "HackRF Tuner Controller" + JOptionPane.showMessageDialog(HackRFTunerEditor.this, "HackRF Tuner Controller" + " - couldn't apply the sample rate setting [" + sampleRate.getLabel() + - "] " + e2.getLocalizedMessage() ); + "] " + e2.getLocalizedMessage()); - mLog.error( "HackRF Tuner Controller - couldn't apply sample rate setting [" + - sampleRate.getLabel() + "]", e ); + mLog.error("HackRF Tuner Controller - couldn't apply sample rate setting [" + + sampleRate.getLabel() + "]", e); } } - } ); - add( new JLabel( "Sample Rate:" ) ); - add( mComboSampleRate ); + }); + add(new JLabel("Sample Rate:")); + add(mComboSampleRate); - SpinnerModel model = new SpinnerNumberModel( 0.0, //initial value + SpinnerModel model = new SpinnerNumberModel(0.0, //initial value -1000.0, //min 1000.0, //max - 0.1 ); //step + 0.1); //step - mFrequencyCorrection = new JSpinner( model ); - mFrequencyCorrection.setEnabled( false ); + mFrequencyCorrection = new JSpinner(model); + mFrequencyCorrection.setEnabled(false); JSpinner.NumberEditor editor = (JSpinner.NumberEditor)mFrequencyCorrection.getEditor(); DecimalFormat format = editor.getFormat(); - format.setMinimumFractionDigits( 1 ); - editor.getTextField().setHorizontalAlignment( SwingConstants.CENTER ); + format.setMinimumFractionDigits(1); + editor.getTextField().setHorizontalAlignment(SwingConstants.CENTER); - mFrequencyCorrection.addChangeListener( new ChangeListener() + mFrequencyCorrection.addChangeListener(new ChangeListener() { @Override - public void stateChanged( ChangeEvent e ) + public void stateChanged(ChangeEvent e) { final double value = ((SpinnerNumberModel)mFrequencyCorrection .getModel()).getNumber().doubleValue(); try { - mController.setFrequencyCorrection( value ); + mController.setFrequencyCorrection(value); save(); } - catch ( SourceException e1 ) + catch(SourceException e1) { - JOptionPane.showMessageDialog( HackRFTunerEditor.this, "HackRF Tuner Controller" + JOptionPane.showMessageDialog(HackRFTunerEditor.this, "HackRF Tuner Controller" + " - couldn't apply frequency correction value: " + value + - e1.getLocalizedMessage() ); + e1.getLocalizedMessage()); - mLog.error( "HackRF Tuner Controller - couldn't apply frequency correction " - + "value: " + value, e1 ); + mLog.error("HackRF Tuner Controller - couldn't apply frequency correction " + + "value: " + value, e1); } } - } ); + }); - add( new JLabel( "PPM:" ) ); - add( mFrequencyCorrection ); + add(new JLabel("PPM:")); + add(mFrequencyCorrection); - add( new JSeparator( JSeparator.HORIZONTAL ), "span,grow" ); - add( new JLabel( "Gain" ) ); - add( new JLabel( "" ), "span 2" ); //filler + add(new JSeparator(JSeparator.HORIZONTAL), "span,grow"); + add(new JLabel("Gain")); + add(new JLabel(""), "span 2"); //filler - mAmplifier = new JToggleButton( "Amplifier" ); - mAmplifier.setEnabled( false ); - mAmplifier.addActionListener( new ActionListener() + mAmplifier = new JToggleButton("Amplifier"); + mAmplifier.setEnabled(false); + mAmplifier.addActionListener(new ActionListener() { @Override - public void actionPerformed( ActionEvent arg0 ) + public void actionPerformed(ActionEvent arg0) { try { - mController.setAmplifierEnabled( mAmplifier.isSelected() ); + mController.setAmplifierEnabled(mAmplifier.isSelected()); save(); } - catch ( UsbException e ) + catch(UsbException e) { - mLog.error( "couldn't enable/disable amplifier", e ); + mLog.error("couldn't enable/disable amplifier", e); - JOptionPane.showMessageDialog( HackRFTunerEditor.this, "Couldn't change amplifier setting", - "Error changing amplifier setting", JOptionPane.ERROR_MESSAGE ); + JOptionPane.showMessageDialog(HackRFTunerEditor.this, "Couldn't change amplifier setting", + "Error changing amplifier setting", JOptionPane.ERROR_MESSAGE); } } - } ); - add( mAmplifier ); + }); + add(mAmplifier); - mComboLNAGain = new JComboBox( HackRFLNAGain.values() ); - mComboLNAGain.setEnabled( false ); - mComboLNAGain.addActionListener( new ActionListener() + mComboLNAGain = new JComboBox(HackRFLNAGain.values()); + mComboLNAGain.setEnabled(false); + mComboLNAGain.addActionListener(new ActionListener() { @Override - public void actionPerformed( ActionEvent arg0 ) + public void actionPerformed(ActionEvent arg0) { try { HackRFLNAGain lnaGain = (HackRFLNAGain)mComboLNAGain.getSelectedItem(); - if ( lnaGain == null ) + if(lnaGain == null) { lnaGain = HackRFLNAGain.GAIN_0; } - mController.setLNAGain( lnaGain ); + mController.setLNAGain(lnaGain); save(); } - catch ( UsbException e ) + catch(UsbException e) { - JOptionPane.showMessageDialog( HackRFTunerEditor.this, "HackRF Tuner Controller" - + " - couldn't apply the LNA gain setting - " + e.getLocalizedMessage() ); + JOptionPane.showMessageDialog(HackRFTunerEditor.this, "HackRF Tuner Controller" + + " - couldn't apply the LNA gain setting - " + e.getLocalizedMessage()); - mLog.error( "HackRF Tuner Controller - couldn't apply LNA gain setting - ", e ); + mLog.error("HackRF Tuner Controller - couldn't apply LNA gain setting - ", e); } } - } ); - mComboLNAGain.setToolTipText( "LNA Gain. Adjust to set the IF gain" ); - add( new JLabel( "LNA:" ) ); - add( mComboLNAGain ); + }); + mComboLNAGain.setToolTipText("LNA Gain. Adjust to set the IF gain"); + add(new JLabel("LNA:")); + add(mComboLNAGain); - mComboVGAGain = new JComboBox( HackRFVGAGain.values() ); - mComboVGAGain.addActionListener( new ActionListener() + mComboVGAGain = new JComboBox(HackRFVGAGain.values()); + mComboVGAGain.addActionListener(new ActionListener() { @Override - public void actionPerformed( ActionEvent arg0 ) + public void actionPerformed(ActionEvent arg0) { try { HackRFVGAGain vgaGain = (HackRFVGAGain)mComboVGAGain.getSelectedItem(); - if( vgaGain == null ) + if(vgaGain == null) { vgaGain = HackRFVGAGain.GAIN_4; } - mController.setVGAGain( vgaGain ); + mController.setVGAGain(vgaGain); save(); } - catch ( UsbException e ) + catch(UsbException e) { - JOptionPane.showMessageDialog( HackRFTunerEditor.this, "HackRF Tuner Controller" - + " - couldn't apply the VGA gain setting - " + e.getLocalizedMessage() ); + JOptionPane.showMessageDialog(HackRFTunerEditor.this, "HackRF Tuner Controller" + + " - couldn't apply the VGA gain setting - " + e.getLocalizedMessage()); - mLog.error( "HackRF Tuner Controller - couldn't apply VGA gain setting", e ); + mLog.error("HackRF Tuner Controller - couldn't apply VGA gain setting", e); } } - } ); - mComboVGAGain.setToolTipText( "VGA Gain. Adjust to set the baseband gain" ); - add( new JLabel( "VGA:" ) ); - add( mComboVGAGain ); + }); + mComboVGAGain.setToolTipText("VGA Gain. Adjust to set the baseband gain"); + add(new JLabel("VGA:")); + add(mComboVGAGain); } /** * Sets each of the tuner configuration controls to the enabled argument state */ - private void setControlsEnabled( boolean enabled ) + private void setControlsEnabled(boolean enabled) { - if( mConfigurationName.isEnabled() != enabled ) + if(mConfigurationName.isEnabled() != enabled) { - mConfigurationName.setEnabled( enabled ); + mConfigurationName.setEnabled(enabled); } - if( mTunerInfo.isEnabled() != enabled ) + if(mTunerInfo.isEnabled() != enabled) { - mTunerInfo.setEnabled( enabled ); + mTunerInfo.setEnabled(enabled); } - if( mFrequencyCorrection.isEnabled() != enabled ) + if(mFrequencyCorrection.isEnabled() != enabled) { - mFrequencyCorrection.setEnabled( enabled ); + mFrequencyCorrection.setEnabled(enabled); } - if( mComboSampleRate.isEnabled() != enabled ) + if(mComboSampleRate.isEnabled() != enabled) { - mComboSampleRate.setEnabled( enabled ); + mComboSampleRate.setEnabled(enabled); } - if( mAmplifier.isEnabled() != enabled ) + if(mAmplifier.isEnabled() != enabled) { - mAmplifier.setEnabled( enabled ); + mAmplifier.setEnabled(enabled); } - if( mComboLNAGain.isEnabled() != enabled ) + if(mComboLNAGain.isEnabled() != enabled) { - mComboLNAGain.setEnabled( enabled ); + mComboLNAGain.setEnabled(enabled); } - if( mComboVGAGain.isEnabled() != enabled ) + if(mComboVGAGain.isEnabled() != enabled) { - mComboVGAGain.setEnabled( enabled ); + mComboVGAGain.setEnabled(enabled); } } @@ -329,18 +346,18 @@ public class HackRFTunerEditor extends TunerConfigurationEditor { board = mController.getBoardID(); } - catch ( UsbException e ) + catch(UsbException e) { - mLog.error( "couldn't read HackRF board identifier", e ); + mLog.error("couldn't read HackRF board identifier", e); } StringBuilder sb = new StringBuilder(); - sb.append( "

HackRF Tuner

" ); + sb.append("

HackRF Tuner

"); - sb.append( "Board: " ); - sb.append( board.getLabel() ); - sb.append( "
" ); + sb.append("Board: "); + sb.append(board.getLabel()); + sb.append("
"); HackRFTunerController.Serial serial = null; @@ -348,25 +365,25 @@ public class HackRFTunerEditor extends TunerConfigurationEditor { serial = mController.getSerial(); } - catch( Exception e ) + catch(Exception e) { - mLog.error( "couldn't read HackRF serial number", e ); + mLog.error("couldn't read HackRF serial number", e); } - if( serial != null ) + if(serial != null) { - sb.append( "Serial: " ); - sb.append( serial.getSerialNumber() ); - sb.append( "
" ); + sb.append("Serial: "); + sb.append(serial.getSerialNumber()); + sb.append("
"); - sb.append( "Part: " ); - sb.append( serial.getPartID() ); - sb.append( "
" ); + sb.append("Part: "); + sb.append(serial.getPartID()); + sb.append("
"); } else { - sb.append( "Serial: Unknown
" ); - sb.append( "Part: Unknown
" ); + sb.append("Serial: Unknown
"); + sb.append("Part: Unknown
"); } String firmware = null; @@ -375,58 +392,61 @@ public class HackRFTunerEditor extends TunerConfigurationEditor { firmware = mController.getFirmwareVersion(); } - catch( Exception e ) + catch(Exception e) { - mLog.error( "couldn't read HackRF firmware version", e ); + mLog.error("couldn't read HackRF firmware version", e); } - if( firmware != null ) + if(firmware != null) { - sb.append( "Firmware: " ); - sb.append( firmware ); - sb.append( "
" ); + sb.append("Firmware: "); + sb.append(firmware); + sb.append("
"); } else { - sb.append( "Firmware: Unknown
" ); + sb.append("Firmware: Unknown
"); } return sb.toString(); } @Override - public void setItem( TunerConfiguration tunerConfiguration ) + public void setItem(TunerConfiguration tunerConfiguration) { - super.setItem( tunerConfiguration ); + super.setItem(tunerConfiguration); //Toggle loading so that we don't fire a change event and schedule a settings file save mLoading = true; - if( hasItem() ) + if(hasItem()) { HackRFTunerConfiguration config = getConfiguration(); - if( tunerConfiguration.isAssigned() ) + if(tunerConfiguration.isAssigned()) { - setControlsEnabled( true ); + setControlsEnabled(true); - mConfigurationName.setText( config.getName() ); - mComboSampleRate.setSelectedItem( config.getSampleRate() ); - mFrequencyCorrection.setValue( config.getFrequencyCorrection() ); - mAmplifier.setSelected( config.getAmplifierEnabled() ); - mComboLNAGain.setSelectedItem( config.getLNAGain() ); - mComboVGAGain.setSelectedItem( config.getVGAGain() ); + mConfigurationName.setText(config.getName()); + mComboSampleRate.setSelectedItem(config.getSampleRate()); + mFrequencyCorrection.setValue(config.getFrequencyCorrection()); + mAmplifier.setSelected(config.getAmplifierEnabled()); + mComboLNAGain.setSelectedItem(config.getLNAGain()); + mComboVGAGain.setSelectedItem(config.getVGAGain()); + + //Update enabled state to reflect when frequency and sample rate controls are locked + mComboSampleRate.setEnabled(!mController.isLocked()); } else { - setControlsEnabled( false ); - mConfigurationName.setText( config.getName() ); + setControlsEnabled(false); + mConfigurationName.setText(config.getName()); } } else { - setControlsEnabled( false ); - mConfigurationName.setText( "" ); + setControlsEnabled(false); + mConfigurationName.setText(""); } mLoading = false; @@ -435,22 +455,22 @@ public class HackRFTunerEditor extends TunerConfigurationEditor @Override public void save() { - if( hasItem() && !mLoading ) + if(hasItem() && !mLoading) { HackRFTunerConfiguration config = getConfiguration(); - config.setName( mConfigurationName.getText() ); + config.setName(mConfigurationName.getText()); double value = ((SpinnerNumberModel)mFrequencyCorrection .getModel()).getNumber().doubleValue(); - config.setFrequencyCorrection( value ); - config.setSampleRate( (HackRFSampleRate)mComboSampleRate.getSelectedItem() ); - config.setAmplifierEnabled( mAmplifier.isSelected() ); - config.setLNAGain( (HackRFLNAGain)mComboLNAGain.getSelectedItem() ); - config.setVGAGain( (HackRFVGAGain)mComboVGAGain.getSelectedItem() ); + config.setFrequencyCorrection(value); + config.setSampleRate((HackRFSampleRate)mComboSampleRate.getSelectedItem()); + config.setAmplifierEnabled(mAmplifier.isSelected()); + config.setLNAGain((HackRFLNAGain)mComboLNAGain.getSelectedItem()); + config.setVGAGain((HackRFVGAGain)mComboVGAGain.getSelectedItem()); getTunerConfigurationModel().broadcast( - new TunerConfigurationEvent( getConfiguration(), TunerConfigurationEvent.Event.CHANGE ) ); + new TunerConfigurationEvent(getConfiguration(), TunerConfigurationEvent.Event.CHANGE)); } } } \ No newline at end of file diff --git a/src/main/java/io/github/dsheirer/source/tuner/manager/TunerChannelSourceManager.java b/src/main/java/io/github/dsheirer/source/tuner/manager/TunerChannelSourceManager.java index 7e46be1b..64733a83 100644 --- a/src/main/java/io/github/dsheirer/source/tuner/manager/TunerChannelSourceManager.java +++ b/src/main/java/io/github/dsheirer/source/tuner/manager/TunerChannelSourceManager.java @@ -433,6 +433,9 @@ public class TunerChannelSourceManager extends ChannelSourceManager case NOTIFICATION_CHANNEL_COUNT_CHANGE: //Rebroadcast this event to any registered listeners (ie tuner and tuner controller) broadcast(sourceEvent); + //Lock the frequency and sample rate controls on the tuner controller so users can't change them + //when the polyphase manager has channels allocated + mTunerController.setLocked(getTunerChannelCount() > 0); break; default: mLog.info("Unrecognized source event: " + sourceEvent); diff --git a/src/main/java/io/github/dsheirer/source/tuner/rtl/e4k/E4KTunerEditor.java b/src/main/java/io/github/dsheirer/source/tuner/rtl/e4k/E4KTunerEditor.java index 394d785d..5bfa9382 100644 --- a/src/main/java/io/github/dsheirer/source/tuner/rtl/e4k/E4KTunerEditor.java +++ b/src/main/java/io/github/dsheirer/source/tuner/rtl/e4k/E4KTunerEditor.java @@ -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 @@ -45,6 +45,7 @@ import javax.swing.SwingConstants; import javax.swing.event.ChangeEvent; import javax.swing.event.ChangeListener; import javax.usb.UsbException; +import java.awt.EventQueue; import java.awt.event.ActionEvent; import java.awt.event.ActionListener; import java.awt.event.FocusEvent; @@ -53,442 +54,461 @@ import java.text.DecimalFormat; public class E4KTunerEditor extends TunerConfigurationEditor { - private final static Logger mLog = LoggerFactory.getLogger( E4KTunerEditor.class ); - - private static final long serialVersionUID = 1L; - - private JTextField mConfigurationName; - private JButton mTunerInfo; - private JComboBox mComboSampleRate; - private JSpinner mFrequencyCorrection; - private JComboBox mComboMasterGain; - private JComboBox mComboMixerGain; - private JComboBox mComboLNAGain; - private JComboBox mComboEnhanceGain; - - private E4KTunerController mController; - private boolean mLoading; - - public E4KTunerEditor( TunerConfigurationModel tunerConfigurationModel, RTL2832Tuner tuner ) - { - super( tunerConfigurationModel ); - mController = (E4KTunerController)tuner.getController(); - - init(); - } - - private E4KTunerConfiguration getConfiguration() - { - if( hasItem() ) - { - return (E4KTunerConfiguration)getItem(); - } - - return null; - } - - private void init() - { - setLayout( new MigLayout( "fill,wrap 4", "[right][grow,fill][right][grow,fill]", - "[][][][][][][][grow]" ) ); - - add( new JLabel( "E4000 Tuner Configuration" ), "span,align center" ); - - /** - * Tuner Configuration Name - */ - add( new JLabel( "Name:" ) ); - mConfigurationName = new JTextField(); - mConfigurationName.setEnabled( false ); - mConfigurationName.addFocusListener( new FocusListener() - { - @Override - public void focusLost( FocusEvent e ) - { - save(); - } - @Override - public void focusGained( FocusEvent e ) {} - } ); - - add( mConfigurationName, "span 2" ); - - mTunerInfo = new JButton( "Tuner Info" ); - mTunerInfo.setEnabled( false ); - mTunerInfo.addActionListener( new ActionListener() - { - @Override - public void actionPerformed( ActionEvent e ) - { - JOptionPane.showMessageDialog( E4KTunerEditor.this, getTunerInfo(), - "Tuner Info", JOptionPane.INFORMATION_MESSAGE ); - } - } ); - add( mTunerInfo ); - - mComboSampleRate = new JComboBox<>( SampleRate.values() ); - mComboSampleRate.setEnabled( false ); - mComboSampleRate.addActionListener( new ActionListener() - { - @Override - public void actionPerformed( ActionEvent e ) - { - SampleRate sampleRate = (SampleRate)mComboSampleRate.getSelectedItem(); - - try - { - mController.setSampleRate( sampleRate ); - save(); - } - catch ( SourceException | LibUsbException eSampleRate ) - { - JOptionPane.showMessageDialog( E4KTunerEditor.this, - "E4K Tuner Controller - couldn't apply the sample rate setting [" + - sampleRate.getLabel() + "] " + eSampleRate.getLocalizedMessage() ); - - mLog.error( "E4K Tuner Controller - couldn't apply sample rate setting [" + - sampleRate.getLabel() + "]", eSampleRate ); - } - } - } ); - add( new JLabel( "Sample Rate:" ) ); - add( mComboSampleRate ); - - SpinnerModel model = new SpinnerNumberModel( 0.0, //initial value - -1000.0, //min - 1000.0, //max - 0.1 ); //step - - mFrequencyCorrection = new JSpinner( model ); - mFrequencyCorrection.setEnabled( false ); - - JSpinner.NumberEditor editor = - (JSpinner.NumberEditor)mFrequencyCorrection.getEditor(); - - DecimalFormat format = editor.getFormat(); - format.setMinimumFractionDigits( 1 ); - editor.getTextField().setHorizontalAlignment( SwingConstants.CENTER ); - - mFrequencyCorrection.addChangeListener( new ChangeListener() - { - @Override - public void stateChanged( ChangeEvent e ) - { - final double value = ((SpinnerNumberModel)mFrequencyCorrection - .getModel()).getNumber().doubleValue(); - - try - { - mController.setFrequencyCorrection( value ); - save(); - } - catch ( SourceException e1 ) - { - JOptionPane.showMessageDialog( E4KTunerEditor.this, "E4K Tuner Controller - " - + "couldn't apply frequency correction value: " + value + e1.getLocalizedMessage() ); - - mLog.error( "E4K Tuner Controller - couldn't apply frequency correction value:" - + value, e1 ); - } - } - } ); - - add( new JLabel( "PPM:" ) ); - add( mFrequencyCorrection ); - - add( new JSeparator( JSeparator.HORIZONTAL ), "span,grow" ); - add( new JLabel( "Gain" ), "wrap" ); - - mComboMasterGain = new JComboBox( E4KGain.values() ); - mComboMasterGain.setEnabled( false ); - mComboMasterGain.addActionListener( new ActionListener() - { - @Override - public void actionPerformed( ActionEvent arg0 ) - { - try - { - E4KGain gain = (E4KGain)mComboMasterGain.getSelectedItem(); - - mController.setGain( (E4KGain)mComboMasterGain.getSelectedItem(), true ); - - if( gain == E4KGain.MANUAL ) - { - mComboMixerGain.setSelectedItem( mController.getMixerGain( true ) ); - mComboMixerGain.setEnabled( true ); - - mComboLNAGain.setSelectedItem( mController.getLNAGain( true ) ); - mComboLNAGain.setEnabled( true ); - - mComboEnhanceGain.setSelectedItem( mController.getEnhanceGain( true ) ); - mComboEnhanceGain.setEnabled( true ); - } - else - { - mComboMixerGain.setEnabled( false ); - mComboMixerGain.setSelectedItem( gain.getMixerGain() ); - - mComboLNAGain.setEnabled( false ); - mComboLNAGain.setSelectedItem( gain.getLNAGain() ); - - mComboEnhanceGain.setEnabled( false ); - mComboEnhanceGain.setSelectedItem( gain.getEnhanceGain() ); - } - - save(); - } - catch ( UsbException e ) - { - JOptionPane.showMessageDialog( E4KTunerEditor.this, "E4K Tuner Controller - " - + "couldn't apply the gain setting - " + e.getLocalizedMessage() ); - - mLog.error( "E4K Tuner Controller - couldn't apply gain setting", e ); - } - } - } ); - - mComboMasterGain.setToolTipText( "Select AUTOMATIC for auto " - + "gain, MANUAL to enable
independent control of " - + "Mixer, LNA and Enhance gain
" - + "settings, or one of the individual gain settings for
" - + "semi-manual gain control" ); - add( new JLabel( "Master:" ) ); - add( mComboMasterGain ); - - mComboMixerGain = new JComboBox( E4KMixerGain.values() ); - mComboMixerGain.addActionListener( new ActionListener() - { - @Override - public void actionPerformed( ActionEvent arg0 ) - { - try - { - E4KMixerGain mixerGain = (E4KMixerGain)mComboMixerGain.getSelectedItem(); - mController.setMixerGain( mixerGain, true ); - save(); - } - catch ( UsbException e ) - { - JOptionPane.showMessageDialog( E4KTunerEditor.this, "E4K Tuner Controller - " - + "couldn't apply the mixer gain setting - " + e.getLocalizedMessage() ); - - mLog.error( "E4K Tuner Controller - couldn't apply mixer gain setting", e ); - } - } - } ); - mComboMixerGain.setToolTipText( "Mixer Gain. Set master gain " - + "to MASTER to enable adjustment" ); - mComboMixerGain.setEnabled( false ); - add( new JLabel( "Mixer:" ) ); - add( mComboMixerGain ); - - mComboLNAGain = new JComboBox( E4KLNAGain.values() ); - mComboLNAGain.addActionListener( new ActionListener() - { - @Override - public void actionPerformed( ActionEvent arg0 ) - { - try - { - E4KLNAGain lnaGain = (E4KLNAGain)mComboLNAGain.getSelectedItem(); - mController.setLNAGain( lnaGain, true ); - save(); - } - catch ( UsbException e ) - { - JOptionPane.showMessageDialog( E4KTunerEditor.this, "E4K Tuner Controller - " - + "couldn't apply the LNA gain setting - " + e.getLocalizedMessage() ); - - mLog.error( "E4K Tuner Controller - couldn't apply LNA gain setting - ", e ); - } - } - } ); - mComboLNAGain.setToolTipText( "LNA Gain. Set master gain " - + "to MANUAL to enable adjustment" ); - mComboLNAGain.setEnabled( false ); - add( new JLabel( "LNA:" ) ); - add( mComboLNAGain ); - - mComboEnhanceGain = new JComboBox( E4KEnhanceGain.values() ); - mComboEnhanceGain.addActionListener( new ActionListener() - { - @Override - public void actionPerformed( ActionEvent arg0 ) - { - try - { - E4KEnhanceGain enhance = (E4KEnhanceGain)mComboEnhanceGain.getSelectedItem(); - mController.setEnhanceGain( enhance, true ); - save(); - } - catch ( UsbException e ) - { - JOptionPane.showMessageDialog( E4KTunerEditor.this, "E4K Tuner Controller - " - + "couldn't apply the enhance gain setting - " + e.getLocalizedMessage() ); - - mLog.error( "E4K Tuner Controller - couldn't apply enhance gain setting", e ); - } - } - } ); - mComboEnhanceGain.setToolTipText( "Enhance Gain. Set master gain " - + "to MANUAL to enable adjustment" ); - mComboEnhanceGain.setEnabled( false ); - add( new JLabel( "Enhance:" ) ); - add( mComboEnhanceGain ); - } - - /** - * Sets each of the tuner configuration controls to the enabled argument state - */ - private void setControlsEnabled( boolean enabled ) - { - if( mConfigurationName.isEnabled() != enabled ) - { - mConfigurationName.setEnabled( enabled ); - } - - if( mTunerInfo.isEnabled() != enabled ) - { - mTunerInfo.setEnabled( enabled ); - } - - if( mFrequencyCorrection.isEnabled() != enabled ) - { - mFrequencyCorrection.setEnabled( enabled ); - } - - if( mComboSampleRate.isEnabled() != enabled ) - { - mComboSampleRate.setEnabled( enabled ); - } - - if( mComboMasterGain.isEnabled() != enabled ) - { - mComboMasterGain.setEnabled( enabled ); - } - - if( mComboMixerGain.isEnabled() != enabled ) - { - mComboMixerGain.setEnabled( enabled ); - } - - if( mComboLNAGain.isEnabled() != enabled ) - { - mComboLNAGain.setEnabled( enabled ); - } - - if( mComboEnhanceGain.isEnabled() != enabled ) - { - mComboEnhanceGain.setEnabled( enabled ); - } - } - - private String getTunerInfo() - { - StringBuilder sb = new StringBuilder(); - - RTL2832TunerController.Descriptor descriptor = mController.getDescriptor(); - - sb.append( "

RTL-2832 with E4000 Tuner

" ); - - if( descriptor == null ) - { - sb.append( "No EEPROM Descriptor Available" ); - } - else - { - sb.append( "USB ID: " ); - sb.append( descriptor.getVendorID() ); - sb.append( ":" ); - sb.append( descriptor.getProductID() ); - sb.append( "
" ); - - sb.append( "Vendor: " ); - sb.append( descriptor.getVendorLabel() ); - sb.append( "
" ); - - sb.append( "Product: " ); - sb.append( descriptor.getProductLabel() ); - sb.append( "
" ); - - sb.append( "Serial: " ); - sb.append( descriptor.getSerial() ); - sb.append( "
" ); - - sb.append( "IR Enabled: " ); - sb.append( descriptor.irEnabled() ); - sb.append( "
" ); - - sb.append( "Remote Wake: " ); - sb.append( descriptor.remoteWakeupEnabled() ); - sb.append( "
" ); - } - - return sb.toString(); - } - - @Override - public void save() - { - if( hasItem() && !mLoading ) - { - E4KTunerConfiguration config = getConfiguration(); - - config.setName( mConfigurationName.getText() ); - - config.setSampleRate( (SampleRate)mComboSampleRate.getSelectedItem() ); - - double value = ((SpinnerNumberModel)mFrequencyCorrection - .getModel()).getNumber().doubleValue(); - config.setFrequencyCorrection( value ); - - config.setMasterGain( (E4KGain)mComboMasterGain.getSelectedItem() ); - config.setMixerGain( (E4KMixerGain)mComboMixerGain.getSelectedItem() ); - config.setLNAGain( (E4KLNAGain)mComboLNAGain.getSelectedItem() ); - config.setEnhanceGain( (E4KEnhanceGain)mComboEnhanceGain.getSelectedItem() ); - - getTunerConfigurationModel().broadcast( new TunerConfigurationEvent( - getConfiguration(), TunerConfigurationEvent.Event.CHANGE ) ); - } - - } - - @Override - public void setItem( TunerConfiguration tunerConfiguration ) - { - super.setItem( tunerConfiguration ); - - //Toggle loading so that we don't fire a change event and schedule a settings file save - mLoading = true; - - if( hasItem() ) - { - E4KTunerConfiguration config = getConfiguration(); - - if( tunerConfiguration.isAssigned() ) - { - setControlsEnabled( true ); - - mConfigurationName.setText( config.getName() ); - mFrequencyCorrection.setValue( config.getFrequencyCorrection() ); - mComboSampleRate.setSelectedItem( config.getSampleRate() ); - mComboMasterGain.setSelectedItem( config.getMasterGain() ); - mComboMixerGain.setSelectedItem( config.getMixerGain() ); - mComboLNAGain.setSelectedItem( config.getLNAGain() ); - mComboEnhanceGain.setSelectedItem( config.getEnhanceGain() ); - } - else - { - setControlsEnabled( false ); - mConfigurationName.setText( config.getName() ); - } - } - else - { - setControlsEnabled( false ); - mConfigurationName.setText( "" ); - } - - mLoading = false; - } + private final static Logger mLog = LoggerFactory.getLogger(E4KTunerEditor.class); + + private static final long serialVersionUID = 1L; + + private JTextField mConfigurationName; + private JButton mTunerInfo; + private JComboBox mComboSampleRate; + private JSpinner mFrequencyCorrection; + private JComboBox mComboMasterGain; + private JComboBox mComboMixerGain; + private JComboBox mComboLNAGain; + private JComboBox mComboEnhanceGain; + + private E4KTunerController mController; + private boolean mLoading; + + public E4KTunerEditor(TunerConfigurationModel tunerConfigurationModel, RTL2832Tuner tuner) + { + super(tunerConfigurationModel); + mController = (E4KTunerController)tuner.getController(); + + init(); + } + + private E4KTunerConfiguration getConfiguration() + { + if(hasItem()) + { + return (E4KTunerConfiguration)getItem(); + } + + return null; + } + + @Override + public void setSampleRateControl(boolean enabled) + { + EventQueue.invokeLater(new Runnable() + { + @Override + public void run() + { + mComboSampleRate.setEnabled(enabled); + } + }); + } + + private void init() + { + setLayout(new MigLayout("fill,wrap 4", "[right][grow,fill][right][grow,fill]", + "[][][][][][][][grow]")); + + add(new JLabel("E4000 Tuner Configuration"), "span,align center"); + + /** + * Tuner Configuration Name + */ + add(new JLabel("Name:")); + mConfigurationName = new JTextField(); + mConfigurationName.setEnabled(false); + mConfigurationName.addFocusListener(new FocusListener() + { + @Override + public void focusLost(FocusEvent e) + { + save(); + } + + @Override + public void focusGained(FocusEvent e) + { + } + }); + + add(mConfigurationName, "span 2"); + + mTunerInfo = new JButton("Tuner Info"); + mTunerInfo.setEnabled(false); + mTunerInfo.addActionListener(new ActionListener() + { + @Override + public void actionPerformed(ActionEvent e) + { + JOptionPane.showMessageDialog(E4KTunerEditor.this, getTunerInfo(), + "Tuner Info", JOptionPane.INFORMATION_MESSAGE); + } + }); + add(mTunerInfo); + + mComboSampleRate = new JComboBox<>(SampleRate.values()); + mComboSampleRate.setEnabled(false); + mComboSampleRate.addActionListener(new ActionListener() + { + @Override + public void actionPerformed(ActionEvent e) + { + SampleRate sampleRate = (SampleRate)mComboSampleRate.getSelectedItem(); + + try + { + mController.setSampleRate(sampleRate); + save(); + } + catch(SourceException | LibUsbException eSampleRate) + { + JOptionPane.showMessageDialog(E4KTunerEditor.this, + "E4K Tuner Controller - couldn't apply the sample rate setting [" + + sampleRate.getLabel() + "] " + eSampleRate.getLocalizedMessage()); + + mLog.error("E4K Tuner Controller - couldn't apply sample rate setting [" + + sampleRate.getLabel() + "]", eSampleRate); + } + } + }); + add(new JLabel("Sample Rate:")); + add(mComboSampleRate); + + SpinnerModel model = new SpinnerNumberModel(0.0, //initial value + -1000.0, //min + 1000.0, //max + 0.1); //step + + mFrequencyCorrection = new JSpinner(model); + mFrequencyCorrection.setEnabled(false); + + JSpinner.NumberEditor editor = + (JSpinner.NumberEditor)mFrequencyCorrection.getEditor(); + + DecimalFormat format = editor.getFormat(); + format.setMinimumFractionDigits(1); + editor.getTextField().setHorizontalAlignment(SwingConstants.CENTER); + + mFrequencyCorrection.addChangeListener(new ChangeListener() + { + @Override + public void stateChanged(ChangeEvent e) + { + final double value = ((SpinnerNumberModel)mFrequencyCorrection + .getModel()).getNumber().doubleValue(); + + try + { + mController.setFrequencyCorrection(value); + save(); + } + catch(SourceException e1) + { + JOptionPane.showMessageDialog(E4KTunerEditor.this, "E4K Tuner Controller - " + + "couldn't apply frequency correction value: " + value + e1.getLocalizedMessage()); + + mLog.error("E4K Tuner Controller - couldn't apply frequency correction value:" + + value, e1); + } + } + }); + + add(new JLabel("PPM:")); + add(mFrequencyCorrection); + + add(new JSeparator(JSeparator.HORIZONTAL), "span,grow"); + add(new JLabel("Gain"), "wrap"); + + mComboMasterGain = new JComboBox(E4KGain.values()); + mComboMasterGain.setEnabled(false); + mComboMasterGain.addActionListener(new ActionListener() + { + @Override + public void actionPerformed(ActionEvent arg0) + { + try + { + E4KGain gain = (E4KGain)mComboMasterGain.getSelectedItem(); + + mController.setGain((E4KGain)mComboMasterGain.getSelectedItem(), true); + + if(gain == E4KGain.MANUAL) + { + mComboMixerGain.setSelectedItem(mController.getMixerGain(true)); + mComboMixerGain.setEnabled(true); + + mComboLNAGain.setSelectedItem(mController.getLNAGain(true)); + mComboLNAGain.setEnabled(true); + + mComboEnhanceGain.setSelectedItem(mController.getEnhanceGain(true)); + mComboEnhanceGain.setEnabled(true); + } + else + { + mComboMixerGain.setEnabled(false); + mComboMixerGain.setSelectedItem(gain.getMixerGain()); + + mComboLNAGain.setEnabled(false); + mComboLNAGain.setSelectedItem(gain.getLNAGain()); + + mComboEnhanceGain.setEnabled(false); + mComboEnhanceGain.setSelectedItem(gain.getEnhanceGain()); + } + + save(); + } + catch(UsbException e) + { + JOptionPane.showMessageDialog(E4KTunerEditor.this, "E4K Tuner Controller - " + + "couldn't apply the gain setting - " + e.getLocalizedMessage()); + + mLog.error("E4K Tuner Controller - couldn't apply gain setting", e); + } + } + }); + + mComboMasterGain.setToolTipText("Select AUTOMATIC for auto " + + "gain, MANUAL to enable
independent control of " + + "Mixer, LNA and Enhance gain
" + + "settings, or one of the individual gain settings for
" + + "semi-manual gain control"); + add(new JLabel("Master:")); + add(mComboMasterGain); + + mComboMixerGain = new JComboBox(E4KMixerGain.values()); + mComboMixerGain.addActionListener(new ActionListener() + { + @Override + public void actionPerformed(ActionEvent arg0) + { + try + { + E4KMixerGain mixerGain = (E4KMixerGain)mComboMixerGain.getSelectedItem(); + mController.setMixerGain(mixerGain, true); + save(); + } + catch(UsbException e) + { + JOptionPane.showMessageDialog(E4KTunerEditor.this, "E4K Tuner Controller - " + + "couldn't apply the mixer gain setting - " + e.getLocalizedMessage()); + + mLog.error("E4K Tuner Controller - couldn't apply mixer gain setting", e); + } + } + }); + mComboMixerGain.setToolTipText("Mixer Gain. Set master gain " + + "to MASTER to enable adjustment"); + mComboMixerGain.setEnabled(false); + add(new JLabel("Mixer:")); + add(mComboMixerGain); + + mComboLNAGain = new JComboBox(E4KLNAGain.values()); + mComboLNAGain.addActionListener(new ActionListener() + { + @Override + public void actionPerformed(ActionEvent arg0) + { + try + { + E4KLNAGain lnaGain = (E4KLNAGain)mComboLNAGain.getSelectedItem(); + mController.setLNAGain(lnaGain, true); + save(); + } + catch(UsbException e) + { + JOptionPane.showMessageDialog(E4KTunerEditor.this, "E4K Tuner Controller - " + + "couldn't apply the LNA gain setting - " + e.getLocalizedMessage()); + + mLog.error("E4K Tuner Controller - couldn't apply LNA gain setting - ", e); + } + } + }); + mComboLNAGain.setToolTipText("LNA Gain. Set master gain " + + "to MANUAL to enable adjustment"); + mComboLNAGain.setEnabled(false); + add(new JLabel("LNA:")); + add(mComboLNAGain); + + mComboEnhanceGain = new JComboBox(E4KEnhanceGain.values()); + mComboEnhanceGain.addActionListener(new ActionListener() + { + @Override + public void actionPerformed(ActionEvent arg0) + { + try + { + E4KEnhanceGain enhance = (E4KEnhanceGain)mComboEnhanceGain.getSelectedItem(); + mController.setEnhanceGain(enhance, true); + save(); + } + catch(UsbException e) + { + JOptionPane.showMessageDialog(E4KTunerEditor.this, "E4K Tuner Controller - " + + "couldn't apply the enhance gain setting - " + e.getLocalizedMessage()); + + mLog.error("E4K Tuner Controller - couldn't apply enhance gain setting", e); + } + } + }); + mComboEnhanceGain.setToolTipText("Enhance Gain. Set master gain " + + "to MANUAL to enable adjustment"); + mComboEnhanceGain.setEnabled(false); + add(new JLabel("Enhance:")); + add(mComboEnhanceGain); + } + + /** + * Sets each of the tuner configuration controls to the enabled argument state + */ + private void setControlsEnabled(boolean enabled) + { + if(mConfigurationName.isEnabled() != enabled) + { + mConfigurationName.setEnabled(enabled); + } + + if(mTunerInfo.isEnabled() != enabled) + { + mTunerInfo.setEnabled(enabled); + } + + if(mFrequencyCorrection.isEnabled() != enabled) + { + mFrequencyCorrection.setEnabled(enabled); + } + + if(mComboSampleRate.isEnabled() != enabled) + { + mComboSampleRate.setEnabled(enabled); + } + + if(mComboMasterGain.isEnabled() != enabled) + { + mComboMasterGain.setEnabled(enabled); + } + + if(mComboMixerGain.isEnabled() != enabled) + { + mComboMixerGain.setEnabled(enabled); + } + + if(mComboLNAGain.isEnabled() != enabled) + { + mComboLNAGain.setEnabled(enabled); + } + + if(mComboEnhanceGain.isEnabled() != enabled) + { + mComboEnhanceGain.setEnabled(enabled); + } + } + + private String getTunerInfo() + { + StringBuilder sb = new StringBuilder(); + + RTL2832TunerController.Descriptor descriptor = mController.getDescriptor(); + + sb.append("

RTL-2832 with E4000 Tuner

"); + + if(descriptor == null) + { + sb.append("No EEPROM Descriptor Available"); + } + else + { + sb.append("USB ID: "); + sb.append(descriptor.getVendorID()); + sb.append(":"); + sb.append(descriptor.getProductID()); + sb.append("
"); + + sb.append("Vendor: "); + sb.append(descriptor.getVendorLabel()); + sb.append("
"); + + sb.append("Product: "); + sb.append(descriptor.getProductLabel()); + sb.append("
"); + + sb.append("Serial: "); + sb.append(descriptor.getSerial()); + sb.append("
"); + + sb.append("IR Enabled: "); + sb.append(descriptor.irEnabled()); + sb.append("
"); + + sb.append("Remote Wake: "); + sb.append(descriptor.remoteWakeupEnabled()); + sb.append("
"); + } + + return sb.toString(); + } + + @Override + public void save() + { + if(hasItem() && !mLoading) + { + E4KTunerConfiguration config = getConfiguration(); + + config.setName(mConfigurationName.getText()); + + config.setSampleRate((SampleRate)mComboSampleRate.getSelectedItem()); + + double value = ((SpinnerNumberModel)mFrequencyCorrection + .getModel()).getNumber().doubleValue(); + config.setFrequencyCorrection(value); + + config.setMasterGain((E4KGain)mComboMasterGain.getSelectedItem()); + config.setMixerGain((E4KMixerGain)mComboMixerGain.getSelectedItem()); + config.setLNAGain((E4KLNAGain)mComboLNAGain.getSelectedItem()); + config.setEnhanceGain((E4KEnhanceGain)mComboEnhanceGain.getSelectedItem()); + + getTunerConfigurationModel().broadcast(new TunerConfigurationEvent( + getConfiguration(), TunerConfigurationEvent.Event.CHANGE)); + } + + } + + @Override + public void setItem(TunerConfiguration tunerConfiguration) + { + super.setItem(tunerConfiguration); + + //Toggle loading so that we don't fire a change event and schedule a settings file save + mLoading = true; + + if(hasItem()) + { + E4KTunerConfiguration config = getConfiguration(); + + if(tunerConfiguration.isAssigned()) + { + setControlsEnabled(true); + + mConfigurationName.setText(config.getName()); + mFrequencyCorrection.setValue(config.getFrequencyCorrection()); + mComboSampleRate.setSelectedItem(config.getSampleRate()); + mComboMasterGain.setSelectedItem(config.getMasterGain()); + mComboMixerGain.setSelectedItem(config.getMixerGain()); + mComboLNAGain.setSelectedItem(config.getLNAGain()); + mComboEnhanceGain.setSelectedItem(config.getEnhanceGain()); + + //Update enabled state to reflect when frequency and sample rate controls are locked + mComboSampleRate.setEnabled(!mController.isLocked()); + } + else + { + setControlsEnabled(false); + mConfigurationName.setText(config.getName()); + } + } + else + { + setControlsEnabled(false); + mConfigurationName.setText(""); + } + + mLoading = false; + } } \ No newline at end of file diff --git a/src/main/java/io/github/dsheirer/source/tuner/rtl/r820t/R820TTunerEditor.java b/src/main/java/io/github/dsheirer/source/tuner/rtl/r820t/R820TTunerEditor.java index 43fa49d2..1105ffba 100644 --- a/src/main/java/io/github/dsheirer/source/tuner/rtl/r820t/R820TTunerEditor.java +++ b/src/main/java/io/github/dsheirer/source/tuner/rtl/r820t/R820TTunerEditor.java @@ -1,19 +1,17 @@ /******************************************************************************* - * SDR Trunk - * Copyright (C) 2014-2016 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 + * 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 + * ******************************************************************************/ package io.github.dsheirer.source.tuner.rtl.r820t; @@ -47,6 +45,7 @@ import javax.swing.SwingConstants; import javax.swing.event.ChangeEvent; import javax.swing.event.ChangeListener; import javax.usb.UsbException; +import java.awt.EventQueue; import java.awt.event.ActionEvent; import java.awt.event.ActionListener; import java.awt.event.FocusEvent; @@ -55,501 +54,520 @@ import java.text.DecimalFormat; public class R820TTunerEditor extends TunerConfigurationEditor { - private final static Logger mLog = LoggerFactory.getLogger( R820TTunerEditor.class ); - - private static final long serialVersionUID = 1L; - private static final R820TGain DEFAULT_GAIN = R820TGain.GAIN_279; - - private R820TTunerController mController; - - private JTextField mConfigurationName; - private JButton mTunerInfo; - private JComboBox mComboSampleRate; - private JSpinner mFrequencyCorrection; - private JComboBox mComboMasterGain; - private JComboBox mComboMixerGain; - private JComboBox mComboLNAGain; - private JComboBox mComboVGAGain; - private boolean mLoading; - - public R820TTunerEditor( TunerConfigurationModel tunerConfigurationModel, RTL2832Tuner tuner ) - { - super( tunerConfigurationModel ); - mController = (R820TTunerController)tuner.getController(); - - init(); - } - - private R820TTunerConfiguration getConfiguration() - { - if( hasItem() ) - { - return (R820TTunerConfiguration)getItem(); - } - - return null; - } - - private void init() - { - setLayout( new MigLayout( "fill,wrap 4", "[right][grow,fill][right][grow,fill]", - "[][][][][][][][grow]" ) ); - - add( new JLabel( "R820T Tuner Configuration" ), "span,align center" ); - - mConfigurationName = new JTextField(); - mConfigurationName.setEnabled( false ); - mConfigurationName.addFocusListener( new FocusListener() - { - @Override - public void focusLost( FocusEvent e ) - { - save(); - } - @Override - public void focusGained( FocusEvent e ) {} - } ); - - add( new JLabel( "Name:" ) ); - add( mConfigurationName, "span 2" ); - - mTunerInfo = new JButton( "Tuner Info" ); - mTunerInfo.setEnabled( false ); - mTunerInfo.addActionListener( new ActionListener() - { - @Override - public void actionPerformed( ActionEvent e ) - { - JOptionPane.showMessageDialog( R820TTunerEditor.this, getTunerInfo(), - "Tuner Info", JOptionPane.INFORMATION_MESSAGE ); - } - } ); - add( mTunerInfo ); - - mComboSampleRate = new JComboBox<>( SampleRate.values() ); - mComboSampleRate.setEnabled( false ); - mComboSampleRate.addActionListener( new ActionListener() - { - @Override - public void actionPerformed( ActionEvent e ) - { - SampleRate sampleRate = - (SampleRate)mComboSampleRate.getSelectedItem(); - try - { - - mController.setSampleRate( sampleRate ); - save(); - } - catch ( SourceException | LibUsbException eSampleRate ) - { - JOptionPane.showMessageDialog( - R820TTunerEditor.this, - "R820T Tuner Controller - couldn't apply the sample " - + "rate setting [" + sampleRate.getLabel() + "] " + - eSampleRate.getLocalizedMessage() ); - - mLog.error( "R820T Tuner Controller - couldn't apply sample " - + "rate setting [" + sampleRate.getLabel() + "]", - eSampleRate ); - } - } - } ); - add( new JLabel( "Sample Rate:" ) ); - add( mComboSampleRate ); - - /** - * Frequency Correction - */ - SpinnerModel model = - new SpinnerNumberModel( 0.0, //initial value - -1000.0, //min - 1000.0, //max - 0.1 ); //step - - mFrequencyCorrection = new JSpinner( model ); - mFrequencyCorrection.setEnabled( false ); - - JSpinner.NumberEditor editor = - (JSpinner.NumberEditor)mFrequencyCorrection.getEditor(); - - DecimalFormat format = editor.getFormat(); - format.setMinimumFractionDigits( 1 ); - editor.getTextField().setHorizontalAlignment( SwingConstants.CENTER ); - - mFrequencyCorrection.addChangeListener( new ChangeListener() - { - @Override - public void stateChanged( ChangeEvent e ) - { - final double value = ((SpinnerNumberModel)mFrequencyCorrection - .getModel()).getNumber().doubleValue(); - - try - { - mController.setFrequencyCorrection( value ); - } - catch ( SourceException e1 ) - { - mLog.error( "Error setting frequency correction value", e1 ); - } - - save(); - } - } ); - - add( new JLabel( "PPM:" ) ); - add( mFrequencyCorrection ); - - add( new JSeparator( JSeparator.HORIZONTAL ), "span,grow" ); - - /** - * Gain Controls - */ - add( new JLabel( "Gain" ), "wrap" ); - - /* Master Gain Control */ - mComboMasterGain = new JComboBox( R820TGain.values() ); - mComboMasterGain.setEnabled( false ); - mComboMasterGain.addActionListener( new ActionListener() - { - @Override - public void actionPerformed( ActionEvent arg0 ) - { - try - { - R820TGain gain = (R820TGain)mComboMasterGain.getSelectedItem(); - - mController.setGain( (R820TGain)mComboMasterGain.getSelectedItem(), true ); - - if( gain == R820TGain.MANUAL ) - { - mComboMixerGain.setSelectedItem( gain.getMixerGain() ); - mComboMixerGain.setEnabled( true ); - - mComboLNAGain.setSelectedItem( gain.getLNAGain() ); - mComboLNAGain.setEnabled( true ); - - mComboVGAGain.setSelectedItem( gain.getVGAGain() ); - mComboVGAGain.setEnabled( true ); - } - else - { - mComboMixerGain.setEnabled( false ); - mComboMixerGain.setSelectedItem( gain.getMixerGain() ); - - mComboLNAGain.setEnabled( false ); - mComboLNAGain.setSelectedItem( gain.getLNAGain() ); - - mComboVGAGain.setEnabled( false ); - mComboVGAGain.setSelectedItem( gain.getVGAGain() ); - } - - save(); - } - catch ( UsbException e ) - { - JOptionPane.showMessageDialog( - R820TTunerEditor.this, - "R820T Tuner Controller - couldn't apply the gain " - + "setting - " + e.getLocalizedMessage() ); - - mLog.error( "R820T Tuner Controller - couldn't apply " - + "gain setting - ", e ); - } - } - } ); - mComboMasterGain.setToolTipText( "Select AUTOMATIC for auto " - + "gain, MANUAL to enable
independent control of " - + "Mixer, LNA and Enhance gain
" - + "settings, or one of the individual gain settings for
" - + "semi-manual gain control" ); - add( new JLabel( "Master:" ) ); - add( mComboMasterGain ); - - /* Mixer Gain Control */ - mComboMixerGain = new JComboBox( R820TMixerGain.values() ); - mComboMixerGain.setEnabled( false ); - mComboMixerGain.addActionListener( new ActionListener() - { - @Override - public void actionPerformed( ActionEvent arg0 ) - { - try - { - R820TMixerGain mixerGain = - (R820TMixerGain)mComboMixerGain.getSelectedItem(); - - if( mixerGain == null ) - { - mixerGain = DEFAULT_GAIN.getMixerGain(); - } - - if( mComboMixerGain.isEnabled() ) - { - mController.setMixerGain( mixerGain, true ); - } - - save(); - } - catch ( UsbException e ) - { - JOptionPane.showMessageDialog( R820TTunerEditor.this, - "R820T Tuner Controller - couldn't apply the mixer " - + "gain setting - " + e.getLocalizedMessage() ); - - mLog.error( "R820T Tuner Controller - couldn't apply mixer " - + "gain setting - ", e ); - } - } - } ); - mComboMixerGain.setToolTipText( "Mixer Gain. Set master gain " - + "to MANUAL to enable adjustment" ); - - add( new JLabel( "Mixer:" ) ); - add( mComboMixerGain ); - - /* LNA Gain Control */ - mComboLNAGain = new JComboBox( R820TLNAGain.values() ); - mComboLNAGain.setEnabled( false ); - mComboLNAGain.addActionListener( new ActionListener() - { - @Override - public void actionPerformed( ActionEvent arg0 ) - { - try - { - R820TLNAGain lnaGain = - (R820TLNAGain)mComboLNAGain.getSelectedItem(); - - if ( lnaGain == null ) - { - lnaGain = DEFAULT_GAIN.getLNAGain(); - } - - if( mComboLNAGain.isEnabled() ) - { - mController.setLNAGain( lnaGain, true ); - } - - save(); - } - catch ( UsbException e ) - { - JOptionPane.showMessageDialog( R820TTunerEditor.this, - "R820T Tuner Controller - couldn't apply the LNA " - + "gain setting - " + e.getLocalizedMessage() ); - - mLog.error( "R820T Tuner Controller - couldn't apply LNA " - + "gain setting - ", e ); - } - } - } ); - mComboLNAGain.setToolTipText( "LNA Gain. Set master gain " - + "to MANUAL to enable adjustment" ); - - add( new JLabel( "LNA:" ) ); - add( mComboLNAGain ); - - /* VGA Gain Control */ - mComboVGAGain = new JComboBox( R820TVGAGain.values() ); - mComboVGAGain.setEnabled( false ); - mComboVGAGain.addActionListener( new ActionListener() - { - @Override - public void actionPerformed( ActionEvent arg0 ) - { - try - { - R820TVGAGain vgaGain = - (R820TVGAGain)mComboVGAGain.getSelectedItem(); - - if( vgaGain == null ) - { - vgaGain = DEFAULT_GAIN.getVGAGain(); - } - - if( mComboVGAGain.isEnabled() ) - { - mController.setVGAGain( vgaGain, true ); - } - - save(); - } - catch ( UsbException e ) - { - JOptionPane.showMessageDialog( R820TTunerEditor.this, - "R820T Tuner Controller - couldn't apply the VGA " - + "gain setting - " + e.getLocalizedMessage() ); - - mLog.error( "R820T Tuner Controller - couldn't apply VGA " - + "gain setting", e ); - } - } - } ); - mComboVGAGain.setToolTipText( "VGA Gain. Set master gain " - + "to MANUAL to enable adjustment" ); - add( new JLabel( "VGA:" ) ); - add( mComboVGAGain ); - } - - /** - * Sets each of the tuner configuration controls to the enabled argument state - */ - private void setControlsEnabled( boolean enabled ) - { - if( mConfigurationName.isEnabled() != enabled ) - { - mConfigurationName.setEnabled( enabled ); - } - - if( mTunerInfo.isEnabled() != enabled ) - { - mTunerInfo.setEnabled( enabled ); - } - - if( mFrequencyCorrection.isEnabled() != enabled ) - { - mFrequencyCorrection.setEnabled( enabled ); - } - - if( mComboSampleRate.isEnabled() != enabled ) - { - mComboSampleRate.setEnabled( enabled ); - } - - if( mComboMasterGain.isEnabled() != enabled ) - { - mComboMasterGain.setEnabled( enabled ); - } - - if( mComboMixerGain.isEnabled() != enabled ) - { - mComboMixerGain.setEnabled( enabled ); - } - - if( mComboLNAGain.isEnabled() != enabled ) - { - mComboLNAGain.setEnabled( enabled ); - } - - if( mComboVGAGain.isEnabled() != enabled ) - { - mComboVGAGain.setEnabled( enabled ); - } - } - - private String getTunerInfo() - { - StringBuilder sb = new StringBuilder(); - - RTL2832TunerController.Descriptor descriptor = mController.getDescriptor(); - - sb.append( "

RTL-2832 with R820T Tuner

" ); - - if( descriptor == null ) - { - sb.append( "No EEPROM Descriptor Available" ); - } - else - { - sb.append( "USB ID: " ); - sb.append( descriptor.getVendorID() ); - sb.append( ":" ); - sb.append( descriptor.getProductID() ); - sb.append( "
" ); - - sb.append( "Vendor: " ); - sb.append( descriptor.getVendorLabel() ); - sb.append( "
" ); - - sb.append( "Product: " ); - sb.append( descriptor.getProductLabel() ); - sb.append( "
" ); - - sb.append( "Serial: " ); - sb.append( descriptor.getSerial() ); - sb.append( "
" ); - - sb.append( "IR Enabled: " ); - sb.append( descriptor.irEnabled() ); - sb.append( "
" ); - - sb.append( "Remote Wake: " ); - sb.append( descriptor.remoteWakeupEnabled() ); - sb.append( "
" ); - } - - return sb.toString(); - } - - @Override - public void save() - { - if( hasItem() && !mLoading ) - { - R820TTunerConfiguration config = getConfiguration(); - - config.setName( mConfigurationName.getText() ); - - double value = ((SpinnerNumberModel)mFrequencyCorrection - .getModel()).getNumber().doubleValue(); - config.setFrequencyCorrection( value ); - - config.setSampleRate( (SampleRate)mComboSampleRate.getSelectedItem() ); - - R820TGain gain = (R820TGain)mComboMasterGain.getSelectedItem(); - config.setMasterGain( gain ); - - R820TMixerGain mixerGain = (R820TMixerGain)mComboMixerGain.getSelectedItem(); - config.setMixerGain( mixerGain ); - - R820TLNAGain lnaGain = (R820TLNAGain)mComboLNAGain.getSelectedItem(); - config.setLNAGain( lnaGain ); - - R820TVGAGain vgaGain = (R820TVGAGain)mComboVGAGain.getSelectedItem(); - config.setVGAGain( vgaGain ); - - getTunerConfigurationModel().broadcast( - new TunerConfigurationEvent( getConfiguration(), TunerConfigurationEvent.Event.CHANGE ) ); - } - } - - @Override - public void setItem( TunerConfiguration tunerConfiguration ) - { - super.setItem( tunerConfiguration ); - - //Toggle loading so that we don't fire a change event and schedule a settings file save - mLoading = true; - - if( hasItem() ) - { - R820TTunerConfiguration config = getConfiguration(); - - if( tunerConfiguration.isAssigned() ) - { - setControlsEnabled( true ); - mConfigurationName.setText( config.getName() ); - mFrequencyCorrection.setValue( config.getFrequencyCorrection() ); - mComboSampleRate.setSelectedItem( config.getSampleRate() ); - mComboMasterGain.setSelectedItem( config.getMasterGain() ); - mComboMixerGain.setSelectedItem( config.getMixerGain() ); - mComboLNAGain.setSelectedItem( config.getLNAGain() ); - mComboVGAGain.setSelectedItem( config.getVGAGain() ); - } - else - { - setControlsEnabled( false ); - mConfigurationName.setText( config.getName() ); - } - } - else - { - setControlsEnabled( false ); - mConfigurationName.setText( "" ); - } - - mLoading = false; - } + private final static Logger mLog = LoggerFactory.getLogger(R820TTunerEditor.class); + + private static final long serialVersionUID = 1L; + private static final R820TGain DEFAULT_GAIN = R820TGain.GAIN_279; + + private R820TTunerController mController; + + private JTextField mConfigurationName; + private JButton mTunerInfo; + private JComboBox mComboSampleRate; + private JSpinner mFrequencyCorrection; + private JComboBox mComboMasterGain; + private JComboBox mComboMixerGain; + private JComboBox mComboLNAGain; + private JComboBox mComboVGAGain; + private boolean mLoading; + + public R820TTunerEditor(TunerConfigurationModel tunerConfigurationModel, RTL2832Tuner tuner) + { + super(tunerConfigurationModel); + mController = (R820TTunerController)tuner.getController(); + + init(); + } + + private R820TTunerConfiguration getConfiguration() + { + if(hasItem()) + { + return (R820TTunerConfiguration)getItem(); + } + + return null; + } + + @Override + public void setSampleRateControl(boolean enabled) + { + EventQueue.invokeLater(new Runnable() + { + @Override + public void run() + { + mComboSampleRate.setEnabled(enabled); + } + }); + } + + private void init() + { + setLayout(new MigLayout("fill,wrap 4", "[right][grow,fill][right][grow,fill]", + "[][][][][][][][grow]")); + + add(new JLabel("R820T Tuner Configuration"), "span,align center"); + + mConfigurationName = new JTextField(); + mConfigurationName.setEnabled(false); + mConfigurationName.addFocusListener(new FocusListener() + { + @Override + public void focusLost(FocusEvent e) + { + save(); + } + + @Override + public void focusGained(FocusEvent e) + { + } + }); + + add(new JLabel("Name:")); + add(mConfigurationName, "span 2"); + + mTunerInfo = new JButton("Tuner Info"); + mTunerInfo.setEnabled(false); + mTunerInfo.addActionListener(new ActionListener() + { + @Override + public void actionPerformed(ActionEvent e) + { + JOptionPane.showMessageDialog(R820TTunerEditor.this, getTunerInfo(), + "Tuner Info", JOptionPane.INFORMATION_MESSAGE); + } + }); + add(mTunerInfo); + + mComboSampleRate = new JComboBox<>(SampleRate.values()); + mComboSampleRate.setEnabled(false); + mComboSampleRate.addActionListener(new ActionListener() + { + @Override + public void actionPerformed(ActionEvent e) + { + SampleRate sampleRate = + (SampleRate)mComboSampleRate.getSelectedItem(); + try + { + + mController.setSampleRate(sampleRate); + save(); + } + catch(SourceException | LibUsbException eSampleRate) + { + JOptionPane.showMessageDialog( + R820TTunerEditor.this, + "R820T Tuner Controller - couldn't apply the sample " + + "rate setting [" + sampleRate.getLabel() + "] " + + eSampleRate.getLocalizedMessage()); + + mLog.error("R820T Tuner Controller - couldn't apply sample " + + "rate setting [" + sampleRate.getLabel() + "]", + eSampleRate); + } + } + }); + add(new JLabel("Sample Rate:")); + add(mComboSampleRate); + + /** + * Frequency Correction + */ + SpinnerModel model = + new SpinnerNumberModel(0.0, //initial value + -1000.0, //min + 1000.0, //max + 0.1); //step + + mFrequencyCorrection = new JSpinner(model); + mFrequencyCorrection.setEnabled(false); + + JSpinner.NumberEditor editor = + (JSpinner.NumberEditor)mFrequencyCorrection.getEditor(); + + DecimalFormat format = editor.getFormat(); + format.setMinimumFractionDigits(1); + editor.getTextField().setHorizontalAlignment(SwingConstants.CENTER); + + mFrequencyCorrection.addChangeListener(new ChangeListener() + { + @Override + public void stateChanged(ChangeEvent e) + { + final double value = ((SpinnerNumberModel)mFrequencyCorrection + .getModel()).getNumber().doubleValue(); + + try + { + mController.setFrequencyCorrection(value); + } + catch(SourceException e1) + { + mLog.error("Error setting frequency correction value", e1); + } + + save(); + } + }); + + add(new JLabel("PPM:")); + add(mFrequencyCorrection); + + add(new JSeparator(JSeparator.HORIZONTAL), "span,grow"); + + /** + * Gain Controls + */ + add(new JLabel("Gain"), "wrap"); + + /* Master Gain Control */ + mComboMasterGain = new JComboBox(R820TGain.values()); + mComboMasterGain.setEnabled(false); + mComboMasterGain.addActionListener(new ActionListener() + { + @Override + public void actionPerformed(ActionEvent arg0) + { + try + { + R820TGain gain = (R820TGain)mComboMasterGain.getSelectedItem(); + + mController.setGain((R820TGain)mComboMasterGain.getSelectedItem(), true); + + if(gain == R820TGain.MANUAL) + { + mComboMixerGain.setSelectedItem(gain.getMixerGain()); + mComboMixerGain.setEnabled(true); + + mComboLNAGain.setSelectedItem(gain.getLNAGain()); + mComboLNAGain.setEnabled(true); + + mComboVGAGain.setSelectedItem(gain.getVGAGain()); + mComboVGAGain.setEnabled(true); + } + else + { + mComboMixerGain.setEnabled(false); + mComboMixerGain.setSelectedItem(gain.getMixerGain()); + + mComboLNAGain.setEnabled(false); + mComboLNAGain.setSelectedItem(gain.getLNAGain()); + + mComboVGAGain.setEnabled(false); + mComboVGAGain.setSelectedItem(gain.getVGAGain()); + } + + save(); + } + catch(UsbException e) + { + JOptionPane.showMessageDialog( + R820TTunerEditor.this, + "R820T Tuner Controller - couldn't apply the gain " + + "setting - " + e.getLocalizedMessage()); + + mLog.error("R820T Tuner Controller - couldn't apply " + + "gain setting - ", e); + } + } + }); + mComboMasterGain.setToolTipText("Select AUTOMATIC for auto " + + "gain, MANUAL to enable
independent control of " + + "Mixer, LNA and Enhance gain
" + + "settings, or one of the individual gain settings for
" + + "semi-manual gain control"); + add(new JLabel("Master:")); + add(mComboMasterGain); + + /* Mixer Gain Control */ + mComboMixerGain = new JComboBox(R820TMixerGain.values()); + mComboMixerGain.setEnabled(false); + mComboMixerGain.addActionListener(new ActionListener() + { + @Override + public void actionPerformed(ActionEvent arg0) + { + try + { + R820TMixerGain mixerGain = + (R820TMixerGain)mComboMixerGain.getSelectedItem(); + + if(mixerGain == null) + { + mixerGain = DEFAULT_GAIN.getMixerGain(); + } + + if(mComboMixerGain.isEnabled()) + { + mController.setMixerGain(mixerGain, true); + } + + save(); + } + catch(UsbException e) + { + JOptionPane.showMessageDialog(R820TTunerEditor.this, + "R820T Tuner Controller - couldn't apply the mixer " + + "gain setting - " + e.getLocalizedMessage()); + + mLog.error("R820T Tuner Controller - couldn't apply mixer " + + "gain setting - ", e); + } + } + }); + mComboMixerGain.setToolTipText("Mixer Gain. Set master gain " + + "to MANUAL to enable adjustment"); + + add(new JLabel("Mixer:")); + add(mComboMixerGain); + + /* LNA Gain Control */ + mComboLNAGain = new JComboBox(R820TLNAGain.values()); + mComboLNAGain.setEnabled(false); + mComboLNAGain.addActionListener(new ActionListener() + { + @Override + public void actionPerformed(ActionEvent arg0) + { + try + { + R820TLNAGain lnaGain = + (R820TLNAGain)mComboLNAGain.getSelectedItem(); + + if(lnaGain == null) + { + lnaGain = DEFAULT_GAIN.getLNAGain(); + } + + if(mComboLNAGain.isEnabled()) + { + mController.setLNAGain(lnaGain, true); + } + + save(); + } + catch(UsbException e) + { + JOptionPane.showMessageDialog(R820TTunerEditor.this, + "R820T Tuner Controller - couldn't apply the LNA " + + "gain setting - " + e.getLocalizedMessage()); + + mLog.error("R820T Tuner Controller - couldn't apply LNA " + + "gain setting - ", e); + } + } + }); + mComboLNAGain.setToolTipText("LNA Gain. Set master gain " + + "to MANUAL to enable adjustment"); + + add(new JLabel("LNA:")); + add(mComboLNAGain); + + /* VGA Gain Control */ + mComboVGAGain = new JComboBox(R820TVGAGain.values()); + mComboVGAGain.setEnabled(false); + mComboVGAGain.addActionListener(new ActionListener() + { + @Override + public void actionPerformed(ActionEvent arg0) + { + try + { + R820TVGAGain vgaGain = + (R820TVGAGain)mComboVGAGain.getSelectedItem(); + + if(vgaGain == null) + { + vgaGain = DEFAULT_GAIN.getVGAGain(); + } + + if(mComboVGAGain.isEnabled()) + { + mController.setVGAGain(vgaGain, true); + } + + save(); + } + catch(UsbException e) + { + JOptionPane.showMessageDialog(R820TTunerEditor.this, + "R820T Tuner Controller - couldn't apply the VGA " + + "gain setting - " + e.getLocalizedMessage()); + + mLog.error("R820T Tuner Controller - couldn't apply VGA " + + "gain setting", e); + } + } + }); + mComboVGAGain.setToolTipText("VGA Gain. Set master gain " + + "to MANUAL to enable adjustment"); + add(new JLabel("VGA:")); + add(mComboVGAGain); + } + + /** + * Sets each of the tuner configuration controls to the enabled argument state + */ + private void setControlsEnabled(boolean enabled) + { + if(mConfigurationName.isEnabled() != enabled) + { + mConfigurationName.setEnabled(enabled); + } + + if(mTunerInfo.isEnabled() != enabled) + { + mTunerInfo.setEnabled(enabled); + } + + if(mFrequencyCorrection.isEnabled() != enabled) + { + mFrequencyCorrection.setEnabled(enabled); + } + + if(mComboSampleRate.isEnabled() != enabled) + { + mComboSampleRate.setEnabled(enabled); + } + + if(mComboMasterGain.isEnabled() != enabled) + { + mComboMasterGain.setEnabled(enabled); + } + + if(mComboMixerGain.isEnabled() != enabled) + { + mComboMixerGain.setEnabled(enabled); + } + + if(mComboLNAGain.isEnabled() != enabled) + { + mComboLNAGain.setEnabled(enabled); + } + + if(mComboVGAGain.isEnabled() != enabled) + { + mComboVGAGain.setEnabled(enabled); + } + } + + private String getTunerInfo() + { + StringBuilder sb = new StringBuilder(); + + RTL2832TunerController.Descriptor descriptor = mController.getDescriptor(); + + sb.append("

RTL-2832 with R820T Tuner

"); + + if(descriptor == null) + { + sb.append("No EEPROM Descriptor Available"); + } + else + { + sb.append("USB ID: "); + sb.append(descriptor.getVendorID()); + sb.append(":"); + sb.append(descriptor.getProductID()); + sb.append("
"); + + sb.append("Vendor: "); + sb.append(descriptor.getVendorLabel()); + sb.append("
"); + + sb.append("Product: "); + sb.append(descriptor.getProductLabel()); + sb.append("
"); + + sb.append("Serial: "); + sb.append(descriptor.getSerial()); + sb.append("
"); + + sb.append("IR Enabled: "); + sb.append(descriptor.irEnabled()); + sb.append("
"); + + sb.append("Remote Wake: "); + sb.append(descriptor.remoteWakeupEnabled()); + sb.append("
"); + } + + return sb.toString(); + } + + @Override + public void save() + { + if(hasItem() && !mLoading) + { + R820TTunerConfiguration config = getConfiguration(); + + config.setName(mConfigurationName.getText()); + + double value = ((SpinnerNumberModel)mFrequencyCorrection + .getModel()).getNumber().doubleValue(); + config.setFrequencyCorrection(value); + + config.setSampleRate((SampleRate)mComboSampleRate.getSelectedItem()); + + R820TGain gain = (R820TGain)mComboMasterGain.getSelectedItem(); + config.setMasterGain(gain); + + R820TMixerGain mixerGain = (R820TMixerGain)mComboMixerGain.getSelectedItem(); + config.setMixerGain(mixerGain); + + R820TLNAGain lnaGain = (R820TLNAGain)mComboLNAGain.getSelectedItem(); + config.setLNAGain(lnaGain); + + R820TVGAGain vgaGain = (R820TVGAGain)mComboVGAGain.getSelectedItem(); + config.setVGAGain(vgaGain); + + getTunerConfigurationModel().broadcast( + new TunerConfigurationEvent(getConfiguration(), TunerConfigurationEvent.Event.CHANGE)); + } + } + + @Override + public void setItem(TunerConfiguration tunerConfiguration) + { + super.setItem(tunerConfiguration); + + //Toggle loading so that we don't fire a change event and schedule a settings file save + mLoading = true; + + if(hasItem()) + { + R820TTunerConfiguration config = getConfiguration(); + + if(tunerConfiguration.isAssigned()) + { + setControlsEnabled(true); + mConfigurationName.setText(config.getName()); + mFrequencyCorrection.setValue(config.getFrequencyCorrection()); + mComboSampleRate.setSelectedItem(config.getSampleRate()); + mComboMasterGain.setSelectedItem(config.getMasterGain()); + mComboMixerGain.setSelectedItem(config.getMixerGain()); + mComboLNAGain.setSelectedItem(config.getLNAGain()); + mComboVGAGain.setSelectedItem(config.getVGAGain()); + + //Update enabled state to reflect when frequency and sample rate controls are locked + mComboSampleRate.setEnabled(!mController.isLocked()); + } + else + { + setControlsEnabled(false); + mConfigurationName.setText(config.getName()); + } + } + else + { + setControlsEnabled(false); + mConfigurationName.setText(""); + } + + mLoading = false; + } } \ No newline at end of file From 28cb9d9407cbd8d7e6df2a590f49a1832c7f158f Mon Sep 17 00:00:00 2001 From: Denny Date: Fri, 9 Feb 2018 05:29:07 -0500 Subject: [PATCH 55/87] [WIP] Updating dsp modules to accept variable sample rate I/Q streams and creating a new audio resampler. --- .idea/dictionaries/denny.xml | 1 + .idea/libraries/imports.xml | 1 + .../dsheirer/dsp/filter/FilterFactory.java | 12 +- .../channelizer/PolyphaseChannelManager.java | 4 + .../dsp/filter/design/FilterViewer.java | 44 +++--- .../filter/fir/FIRFilterSpecification.java | 98 +++++++----- .../fir/complex/ComplexFIRFilter_CB_CB.java | 105 +++++++------ .../dsp/filter/resample/AudioResampler.java | 70 +++++++++ .../module/decode/DecoderFactory.java | 18 +-- .../module/decode/p25/P25_LSMDecoder.java | 2 +- .../demodulate/am/AMDemodulatorModule.java | 2 +- .../demodulate/fm/FMDemodulatorModule.java | 140 +++++++++++++----- .../tuner/channel/TunerChannelSource.java | 9 +- .../manager/TunerChannelSourceManager.java | 30 +++- .../tuner/test/TestTunerController.java | 4 +- 15 files changed, 369 insertions(+), 171 deletions(-) create mode 100644 src/main/java/io/github/dsheirer/dsp/filter/resample/AudioResampler.java diff --git a/.idea/dictionaries/denny.xml b/.idea/dictionaries/denny.xml index 30d94b58..a9674196 100644 --- a/.idea/dictionaries/denny.xml +++ b/.idea/dictionaries/denny.xml @@ -3,6 +3,7 @@ bahroo broadcastify + dinf esnid fleetsync heterdyned diff --git a/.idea/libraries/imports.xml b/.idea/libraries/imports.xml index e97bf8ec..b90eef82 100644 --- a/.idea/libraries/imports.xml +++ b/.idea/libraries/imports.xml @@ -6,6 +6,7 @@ +
diff --git a/src/main/java/io/github/dsheirer/dsp/filter/FilterFactory.java b/src/main/java/io/github/dsheirer/dsp/filter/FilterFactory.java index 440400f9..a0447635 100644 --- a/src/main/java/io/github/dsheirer/dsp/filter/FilterFactory.java +++ b/src/main/java/io/github/dsheirer/dsp/filter/FilterFactory.java @@ -51,7 +51,7 @@ public class FilterFactory * @param window - to apply against the coefficients * @return */ - public static float[] getSinc(int sampleRate, long frequency, int length, Window.WindowType window) + public static float[] getSinc(double sampleRate, long frequency, int length, Window.WindowType window) { int evenLength = length % 2 == 0 ? length : length + 1; @@ -137,7 +137,7 @@ public class FilterFactory * @param length * @return */ - public static float[] getUnityResponseArray(int sampleRate, long frequency, int length) + public static float[] getUnityResponseArray(double sampleRate, long frequency, int length) { float[] unityArray = new float[length * 2]; @@ -201,7 +201,7 @@ public class FilterFactory * @param windowType - window to apply against the generated coefficients * @return */ - public static float[] getLowPass(int sampleRate, long cutoff, int filterLength, Window.WindowType windowType) + public static float[] getLowPass(double sampleRate, long cutoff, int filterLength, Window.WindowType windowType) { return getSinc(sampleRate, cutoff, filterLength, windowType); } @@ -220,7 +220,7 @@ public class FilterFactory * - passFrequency < stopFrequency * - stopFrequency <= sampleRate/2 */ - public static float[] getLowPass(int sampleRate, int passFrequency, int stopFrequency, int attenuation, + public static float[] getLowPass(double sampleRate, int passFrequency, int stopFrequency, int attenuation, Window.WindowType windowType, boolean forceOddLength) { if(stopFrequency < passFrequency || stopFrequency > (sampleRate / 2)) @@ -323,9 +323,9 @@ public class FilterFactory * @param attenuation in dB * @return */ - public static int getTapCount(int sampleRate, long pass, long stop, int attenuation) + public static int getTapCount(double sampleRate, long pass, long stop, int attenuation) { - double frequency = ((double)stop - (double)pass) / (double)sampleRate; + double frequency = ((double)stop - (double)pass) / sampleRate; return (int)(Math.round((double)attenuation / (22.0d * frequency))); } diff --git a/src/main/java/io/github/dsheirer/dsp/filter/channelizer/PolyphaseChannelManager.java b/src/main/java/io/github/dsheirer/dsp/filter/channelizer/PolyphaseChannelManager.java index 5fd66a2e..ee5ec599 100644 --- a/src/main/java/io/github/dsheirer/dsp/filter/channelizer/PolyphaseChannelManager.java +++ b/src/main/java/io/github/dsheirer/dsp/filter/channelizer/PolyphaseChannelManager.java @@ -292,6 +292,10 @@ public class PolyphaseChannelManager implements ISourceEventProcessor //Defer channelizer configuration changes to be handled on the buffer processor thread mBufferSourceEventMonitor.receive(sourceEvent); break; + case NOTIFICATION_FREQUENCY_AND_SAMPLE_RATE_LOCKED: + case NOTIFICATION_FREQUENCY_AND_SAMPLE_RATE_UNLOCKED: + //no-op + break; default: mLog.info("Unrecognized source event: " + sourceEvent.toString()); break; diff --git a/src/main/java/io/github/dsheirer/dsp/filter/design/FilterViewer.java b/src/main/java/io/github/dsheirer/dsp/filter/design/FilterViewer.java index b7e26b4a..22354565 100644 --- a/src/main/java/io/github/dsheirer/dsp/filter/design/FilterViewer.java +++ b/src/main/java/io/github/dsheirer/dsp/filter/design/FilterViewer.java @@ -1,8 +1,7 @@ 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; @@ -41,14 +40,15 @@ public class FilterViewer extends Application private float[] getFilter() { FIRFilterSpecification specification = FIRFilterSpecification.lowPassBuilder() - .sampleRate(48000) + .sampleRate(25000) .gridDensity(16) - .passBandCutoff(2500) + .oddLength(true) + .passBandCutoff(6000) .passBandAmplitude(1.0) .passBandRipple(0.01) - .stopBandStart(4000) + .stopBandStart(6500) .stopBandAmplitude(0.0) - .stopBandRipple(0.008) + .stopBandRipple(0.028) //Approximately 60 dB attenuation .build(); // FIRFilterSpecification specification = FIRFilterSpecification.highPassBuilder() @@ -73,29 +73,29 @@ public class FilterViewer extends Application float[] taps = null; - try - { - taps = FilterFactory.getSincM2Channelizer(12500.0, 2, 19, Window.WindowType.BLACKMAN_HARRIS_7, true); - } - catch(FilterDesignException fde) - { - mLog.error("Error"); - } - // 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; } } diff --git a/src/main/java/io/github/dsheirer/dsp/filter/fir/FIRFilterSpecification.java b/src/main/java/io/github/dsheirer/dsp/filter/fir/FIRFilterSpecification.java index d26483dd..6fe1294e 100644 --- a/src/main/java/io/github/dsheirer/dsp/filter/fir/FIRFilterSpecification.java +++ b/src/main/java/io/github/dsheirer/dsp/filter/fir/FIRFilterSpecification.java @@ -295,8 +295,9 @@ public class FIRFilterSpecification */ public static class LowPassBuilder { - private int mSampleRate; + private double mSampleRate; private int mOrder; + private Boolean mOddLength; private int mGridDensity = 16; private int mPassBandEndFrequency; private int mStopBandStartFrequency; @@ -309,7 +310,7 @@ public class FIRFilterSpecification { } - public LowPassBuilder sampleRate(int sampleRateHz) + public LowPassBuilder sampleRate(double sampleRateHz) { mSampleRate = sampleRateHz; return this; @@ -325,6 +326,16 @@ public class FIRFilterSpecification return this; } + /** + * Indicates if the filters should be an odd-length (even order) filter. This will override any requested + * filter order if it is set, otherwise the filter length will be determined from the filter order. + */ + public LowPassBuilder oddLength(boolean oddLength) + { + mOddLength = oddLength; + return this; + } + public LowPassBuilder gridDensity(int density) { mGridDensity = density; @@ -371,13 +382,32 @@ public class FIRFilterSpecification { if(mOrder < 6) { - mOrder = estimateFilterOrder(mSampleRate, mPassBandEndFrequency, - mStopBandStartFrequency, mPassBandRipple, mStopBandRipple); + mOrder = estimateFilterOrder(mSampleRate, mPassBandEndFrequency, mStopBandStartFrequency, + mPassBandRipple, mStopBandRipple); } - FIRLinearPhaseFilterType type = (mOrder % 2 == 0 - ? FIRLinearPhaseFilterType.TYPE_1_ODD_LENGTH_EVEN_ORDER_SYMMETRICAL - : FIRLinearPhaseFilterType.TYPE_2_EVEN_LENGTH_ODD_ORDER_SYMMETRICAL); + FIRLinearPhaseFilterType type = null; + + if(mOddLength != null) + { + if(mOddLength) + { + type = FIRLinearPhaseFilterType.TYPE_1_ODD_LENGTH_EVEN_ORDER_SYMMETRICAL; + //Force order to even + mOrder += mOrder % 2; + } + else + { + type = FIRLinearPhaseFilterType.TYPE_2_EVEN_LENGTH_ODD_ORDER_SYMMETRICAL; + //Force order to odd + mOrder += (mOrder % 2 == 0 ? 1 : 0); + } + } + else + { + type = (mOrder % 2 == 0 ? FIRLinearPhaseFilterType.TYPE_1_ODD_LENGTH_EVEN_ORDER_SYMMETRICAL + : FIRLinearPhaseFilterType.TYPE_2_EVEN_LENGTH_ODD_ORDER_SYMMETRICAL); + } FIRFilterSpecification spec = new FIRFilterSpecification(type, mOrder, mGridDensity); @@ -386,7 +416,7 @@ public class FIRFilterSpecification spec.addFrequencyBand(passBand); FrequencyBand stopBand = new FrequencyBand(mSampleRate, mStopBandStartFrequency, - mSampleRate / 2, mStopBandAmplitude, mStopBandRipple); + (int)(mSampleRate / 2), mStopBandAmplitude, mStopBandRipple); spec.addFrequencyBand(stopBand); return spec; @@ -398,7 +428,7 @@ public class FIRFilterSpecification */ public static class HighPassBuilder { - private int mSampleRate; + private double mSampleRate; private int mOrder = -1; private int mGridDensity = 16; private int mPassBandStartFrequency; @@ -412,7 +442,7 @@ public class FIRFilterSpecification { } - public HighPassBuilder sampleRate(int sampleRateHz) + public HighPassBuilder sampleRate(double sampleRateHz) { mSampleRate = sampleRateHz; return this; @@ -474,8 +504,8 @@ public class FIRFilterSpecification { if(mOrder < 6) { - mOrder = estimateFilterOrder(mSampleRate, mStopBandEndFrequency, - mPassBandStartFrequency, mPassBandRipple, mStopBandRipple); + mOrder = estimateFilterOrder(mSampleRate, mStopBandEndFrequency, mPassBandStartFrequency, + mPassBandRipple, mStopBandRipple); } // High pass is a Type 1 filter -- ensure order is even @@ -493,7 +523,7 @@ public class FIRFilterSpecification spec.addFrequencyBand(stopBand); FrequencyBand passBand = new FrequencyBand(mSampleRate, mPassBandStartFrequency, - mSampleRate / 2, mPassBandAmplitude, mPassBandRipple); + (int)(mSampleRate / 2), mPassBandAmplitude, mPassBandRipple); spec.addFrequencyBand(passBand); return spec; @@ -507,7 +537,7 @@ public class FIRFilterSpecification { private int mOrder; private int mGridDensity = 16; - private int mSampleRate; + private double mSampleRate; private int mStopFrequency1; private int mPassFrequencyBegin; private int mPassFrequencyEnd; @@ -544,7 +574,7 @@ public class FIRFilterSpecification /** * Sets the filter sample rate in hertz */ - public BandPassBuilder sampleRate(int sampleRate) + public BandPassBuilder sampleRate(double sampleRate) { mSampleRate = sampleRate; return this; @@ -644,15 +674,13 @@ public class FIRFilterSpecification if(mPassFrequencyEnd >= mStopFrequency2) { throw new IllegalArgumentException("Pass band end frequency [" + mPassFrequencyEnd - + "] must be less than stop band 2 beginning frequency [" + mStopFrequency2 - + "]"); + + "] must be less than stop band 2 beginning frequency [" + mStopFrequency2 + "]"); } if(mStopFrequency2 >= (mSampleRate / 2)) { - throw new IllegalArgumentException("Stop band 2 beginning frequency [" - + mStopFrequency2 + "] must be less than half of the sample rate [" - + (mSampleRate / 2) + "]"); + throw new IllegalArgumentException("Stop band 2 beginning frequency [" + mStopFrequency2 + + "] must be less than half of the sample rate [" + (mSampleRate / 2) + "]"); } } @@ -693,7 +721,7 @@ public class FIRFilterSpecification public static class ChannelizerBuilder { private int mGridDensity = 16; - private int mSampleRate; + private double mSampleRate; private int mChannelCount; private int mChannelBandwidth; private int mTapsPerChannel; @@ -708,7 +736,7 @@ public class FIRFilterSpecification /** * Sets the filter sample rate in hertz */ - public ChannelizerBuilder sampleRate(int sampleRate) + public ChannelizerBuilder sampleRate(double sampleRate) { mSampleRate = sampleRate; return this; @@ -805,7 +833,7 @@ public class FIRFilterSpecification FIRFilterSpecification spec = new FIRFilterSpecification(type, order, mGridDensity); - double bandEdge = (double)mChannelBandwidth / (double)mSampleRate; + double bandEdge = (double)mChannelBandwidth / mSampleRate; double passEnd = bandEdge * (1.0 - mAlpha); double stopStart = bandEdge * (1.0 + mAlpha); @@ -877,15 +905,13 @@ public class FIRFilterSpecification * @param stopBandRipple stop band ripple in dB * @return estimated filter length */ - public static int estimateFilterOrder(int sampleRate, int frequency1, int frequency2, + public static int estimateFilterOrder(double sampleRate, int frequency1, int frequency2, double passBandRipple, double stopBandRipple) { - double df = Math.abs(frequency2 - frequency1) / (double)sampleRate; + double df = Math.abs(frequency2 - frequency1) / sampleRate; - double ddp = Math - .log10(stopBandRipple <= passBandRipple ? passBandRipple : stopBandRipple); - double dds = Math - .log10(stopBandRipple <= passBandRipple ? stopBandRipple : passBandRipple); + double ddp = Math.log10(stopBandRipple <= passBandRipple ? passBandRipple : stopBandRipple); + double dds = Math.log10(stopBandRipple <= passBandRipple ? stopBandRipple : passBandRipple); double a1 = 5.309e-3; double a2 = 7.114e-2; @@ -917,12 +943,12 @@ public class FIRFilterSpecification * @param passBandRippleDb * @param stopBandRippleDb */ - public static int estimateBandPassOrder(int sampleRate, int passBandStart, int passBandEnd, + public static int estimateBandPassOrder(double sampleRate, int passBandStart, int passBandEnd, double passBandRippleDb, double stopBandRippleDb) { - double df = (double)Math.abs(passBandEnd - passBandStart) / (double)sampleRate; - double ddp = (double)Math.log10(passBandRippleDb); - double dds = (double)Math.log10(stopBandRippleDb); + double df = (double)Math.abs(passBandEnd - passBandStart) / sampleRate; + double ddp = Math.log10(passBandRippleDb); + double dds = Math.log10(stopBandRippleDb); double a1 = 0.01201; double a2 = 0.09664; @@ -987,13 +1013,13 @@ public class FIRFilterSpecification * @param amplitude of the frequency band relative to unity (0.0 - 1.0) * @param ripple of the frequency band specified as db ripple */ - public FrequencyBand(int sampleRate, int start, int end, double amplitude, double ripple) + public FrequencyBand(double sampleRate, int start, int end, double amplitude, double ripple) { - this((double)start / (double)sampleRate, (double)end / (double)sampleRate, + this((double)start / sampleRate, (double)end / sampleRate, amplitude, ripple); } - public FrequencyBand(int sampleRate, int start, int end, double amplitude, double ripple, double weight) + public FrequencyBand(double sampleRate, int start, int end, double amplitude, double ripple, double weight) { this(sampleRate, start, end, amplitude, ripple); mWeight = weight; diff --git a/src/main/java/io/github/dsheirer/dsp/filter/fir/complex/ComplexFIRFilter_CB_CB.java b/src/main/java/io/github/dsheirer/dsp/filter/fir/complex/ComplexFIRFilter_CB_CB.java index 33cd4862..7e9ccc71 100644 --- a/src/main/java/io/github/dsheirer/dsp/filter/fir/complex/ComplexFIRFilter_CB_CB.java +++ b/src/main/java/io/github/dsheirer/dsp/filter/fir/complex/ComplexFIRFilter_CB_CB.java @@ -3,52 +3,65 @@ package io.github.dsheirer.dsp.filter.fir.complex; import io.github.dsheirer.sample.Listener; import io.github.dsheirer.sample.complex.ComplexBuffer; -public class ComplexFIRFilter_CB_CB extends ComplexFIRFilter - implements Listener +public class ComplexFIRFilter_CB_CB extends ComplexFIRFilter implements Listener { - private Listener mListener; - - /** - * Complex FIR Filter for processing complex buffers. Wraps two real FIR - * filters for processing each of the inphase and quadrature samples. - * - * @param coefficients - odd length symmetric filter taps - * @param gain - gain value to apply to each of the filtered samples - */ - public ComplexFIRFilter_CB_CB( float[] coefficients, float gain ) - { - super( coefficients, gain ); - } - - public void dispose() - { - mListener = null; - } + private Listener mListener; - @Override - public void receive( ComplexBuffer buffer ) - { - if( mListener != null ) - { - float[] samples = buffer.getSamples(); - - for( int x = 0; x < samples.length; x += 2 ) - { - samples[ x ] = filterInphase( samples[ x ] ); - samples[ x + 1 ] = filterQuadrature( samples[ x + 1 ] ); - } - - mListener.receive( buffer ); - } - } - - public void setListener( Listener listener ) - { - mListener = listener; - } - - public void removeListener() - { - mListener = null; - } + /** + * Complex FIR Filter for processing complex buffers. Wraps two real FIR + * filters for processing each of the inphase and quadrature samples. + * + * @param coefficients - odd length symmetric filter taps + * @param gain - gain value to apply to each of the filtered samples + */ + public ComplexFIRFilter_CB_CB(float[] coefficients, float gain) + { + super(coefficients, gain); + } + + public void dispose() + { + mListener = null; + } + + @Override + public void receive(ComplexBuffer buffer) + { + if(mListener != null) + { + float[] samples = buffer.getSamples(); + + for(int x = 0; x < samples.length; x += 2) + { + samples[x] = filterInphase(samples[x]); + samples[x + 1] = filterQuadrature(samples[x + 1]); + } + + mListener.receive(buffer); + } + } + + /** + * Filters the samples and send the results to the registered listener + */ + public void receive(float[] samples) + { + if(mListener != null) + { + float[] filtered = filter(samples); + + //TODO: this should be a reusable buffer here. + mListener.receive(new ComplexBuffer(filtered)); + } + } + + public void setListener(Listener listener) + { + mListener = listener; + } + + public void removeListener() + { + mListener = null; + } } diff --git a/src/main/java/io/github/dsheirer/dsp/filter/resample/AudioResampler.java b/src/main/java/io/github/dsheirer/dsp/filter/resample/AudioResampler.java new file mode 100644 index 00000000..c67401b9 --- /dev/null +++ b/src/main/java/io/github/dsheirer/dsp/filter/resample/AudioResampler.java @@ -0,0 +1,70 @@ +/******************************************************************************* + * 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 + * + ******************************************************************************/ +package io.github.dsheirer.dsp.filter.resample; + +import com.laszlosystems.libresample4j.Resampler; +import io.github.dsheirer.module.Module; +import io.github.dsheirer.sample.Listener; +import io.github.dsheirer.source.ISourceEventListener; +import io.github.dsheirer.source.SourceEvent; + +public class AudioResampler extends Module implements ISourceEventListener +{ + private Resampler mResampler; + + public AudioResampler(double inputRate, double outputRate) + { + double resampleRate = outputRate / inputRate; + + mResampler = new Resampler(true, resampleRate, resampleRate); + } + + public float[] resample(float[] samples) + { + return null; + } + + @Override + public void reset() + { + + } + + @Override + public void start() + { + + } + + @Override + public void stop() + { + + } + + @Override + public void dispose() + { + + } + + @Override + public Listener getSourceEventListener() + { + //TODO: we should get the channel sample rate and setup the resampler to output 8 kHz audio + return null; + } +} diff --git a/src/main/java/io/github/dsheirer/module/decode/DecoderFactory.java b/src/main/java/io/github/dsheirer/module/decode/DecoderFactory.java index 5092c317..dac8df21 100644 --- a/src/main/java/io/github/dsheirer/module/decode/DecoderFactory.java +++ b/src/main/java/io/github/dsheirer/module/decode/DecoderFactory.java @@ -101,6 +101,9 @@ public class DecoderFactory { private final static Logger mLog = LoggerFactory.getLogger(DecoderFactory.class); + private static final int NBFM_PASS_FREQUENCY = 6000; + private static final int NBFM_STOP_FREQUENCY = 6500; + //Low-pass filter with ~60 dB attenuation and 89 taps private static final FIRFilterSpecification MPT1327_FILTER_SPECIFICATION = FIRFilterSpecification.lowPassBuilder() .sampleRate( 48000 ).gridDensity( 16 ).passBandCutoff( 3200 ).passBandAmplitude( 1.0 ).passBandRipple( 0.02 ) @@ -174,9 +177,6 @@ public class DecoderFactory /* Baseband low-pass filter pass and stop frequencies */ DecodeConfiguration decodeConfig = channel.getDecodeConfiguration(); - int iqPass = decodeConfig.getDecoderType().getChannelBandwidth() / 2; - int iqStop = iqPass + 1250; - switch(decodeConfig.getDecoderType()) { case AM: @@ -189,7 +189,7 @@ public class DecoderFactory case NBFM: modules.add(new NBFMDecoder(decodeConfig)); modules.add(new AlwaysUnsquelchedDecoderState(DecoderType.NBFM, channel.getName())); - modules.add(new FMDemodulatorModule(iqPass, iqStop)); + modules.add(new FMDemodulatorModule(NBFM_PASS_FREQUENCY, NBFM_STOP_FREQUENCY)); modules.add(new DemodulatedAudioFilterModule(4000, 6000)); modules.add(new AudioModule(metadata)); break; @@ -197,14 +197,14 @@ public class DecoderFactory MessageDirection direction = ((DecodeConfigLTRStandard) decodeConfig).getMessageDirection(); modules.add(new LTRStandardDecoder(aliasList, direction)); modules.add(new LTRStandardDecoderState(aliasList)); - modules.add(new FMDemodulatorModule(iqPass, iqStop)); + modules.add(new FMDemodulatorModule(NBFM_PASS_FREQUENCY, NBFM_STOP_FREQUENCY)); modules.add(new DemodulatedAudioFilterModule(4000, 6000)); modules.add(new AudioModule(metadata)); break; case LTR_NET: modules.add(new LTRNetDecoder((DecodeConfigLTRNet) decodeConfig, aliasList)); modules.add(new LTRNetDecoderState(aliasList)); - modules.add(new FMDemodulatorModule(iqPass, iqStop)); + modules.add(new FMDemodulatorModule(NBFM_PASS_FREQUENCY, NBFM_STOP_FREQUENCY)); modules.add(new DemodulatedAudioFilterModule(4000, 6000)); modules.add(new AudioModule(metadata)); break; @@ -227,14 +227,14 @@ public class DecoderFactory (aliasList != null ? aliasList.getName() : null), mptConfig.getTrafficChannelPoolSize())); } - modules.add(new FMDemodulatorModule(iqPass, iqStop)); + modules.add(new FMDemodulatorModule(NBFM_PASS_FREQUENCY, NBFM_STOP_FREQUENCY)); modules.add(new DemodulatedAudioFilterModule(P25_C4FM_DEMOD_FILTER, 1.0f)); modules.add(new AudioModule(metadata)); break; case PASSPORT: modules.add(new PassportDecoder(decodeConfig, aliasList)); modules.add(new PassportDecoderState(aliasList)); - modules.add(new FMDemodulatorModule(iqPass, iqStop)); + modules.add(new FMDemodulatorModule(NBFM_PASS_FREQUENCY, NBFM_STOP_FREQUENCY)); modules.add(new DemodulatedAudioFilterModule(4000, 6000)); modules.add(new AudioModule(metadata)); break; @@ -247,7 +247,7 @@ public class DecoderFactory switch(modulation) { case C4FM: - modules.add(new FMDemodulatorModule(P25_C4FM_IQ_FILTER)); +// modules.add(new FMDemodulatorModule(P25_C4FM_IQ_FILTER)); modules.add(new DemodulatedAudioFilterModule(P25_C4FM_DEMOD_FILTER, 1.0f)); modules.add(new P25_C4FMDecoder(aliasList, decodeConfig.getAFCMaximumCorrection())); modules.add(new P25DecoderState(aliasList, channelType, Modulation.C4FM, diff --git a/src/main/java/io/github/dsheirer/module/decode/p25/P25_LSMDecoder.java b/src/main/java/io/github/dsheirer/module/decode/p25/P25_LSMDecoder.java index 518eb07f..f5bf2108 100644 --- a/src/main/java/io/github/dsheirer/module/decode/p25/P25_LSMDecoder.java +++ b/src/main/java/io/github/dsheirer/module/decode/p25/P25_LSMDecoder.java @@ -235,7 +235,7 @@ public class P25_LSMDecoder extends P25Decoder implements IReusableComplexBuffer @Override public void receive(ReusableComplexBuffer reusableComplexBuffer) { - float[] samples = reusableComplexBuffer.getCopyOfSamples(); + float[] samples = reusableComplexBuffer.getSamplesCopy(); //TODO: redesign the filter chain so that we can simply pass a float array ... diff --git a/src/main/java/io/github/dsheirer/module/demodulate/am/AMDemodulatorModule.java b/src/main/java/io/github/dsheirer/module/demodulate/am/AMDemodulatorModule.java index 8677b89a..e55b51cb 100644 --- a/src/main/java/io/github/dsheirer/module/demodulate/am/AMDemodulatorModule.java +++ b/src/main/java/io/github/dsheirer/module/demodulate/am/AMDemodulatorModule.java @@ -113,7 +113,7 @@ public class AMDemodulatorModule extends Module implements IReusableComplexBuffe @Override public void receive(ReusableComplexBuffer reusableComplexBuffer) { - float[] samples = reusableComplexBuffer.getCopyOfSamples(); + float[] samples = reusableComplexBuffer.getSamplesCopy(); //TODO: redesign the filter chain so that we can simply pass a float array ... diff --git a/src/main/java/io/github/dsheirer/module/demodulate/fm/FMDemodulatorModule.java b/src/main/java/io/github/dsheirer/module/demodulate/fm/FMDemodulatorModule.java index 72380d34..a976a3fa 100644 --- a/src/main/java/io/github/dsheirer/module/demodulate/fm/FMDemodulatorModule.java +++ b/src/main/java/io/github/dsheirer/module/demodulate/fm/FMDemodulatorModule.java @@ -1,85 +1,84 @@ /******************************************************************************* - * SDR Trunk - * Copyright (C) 2014-2016 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 * - * You should have received a copy of the GNU General Public License - * along with this program. If not, see ******************************************************************************/ package io.github.dsheirer.module.demodulate.fm; 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.FIRFilterSpecification; import io.github.dsheirer.dsp.filter.fir.complex.ComplexFIRFilter_CB_CB; import io.github.dsheirer.dsp.fm.FMDemodulator_CB; import io.github.dsheirer.module.Module; import io.github.dsheirer.sample.Listener; -import io.github.dsheirer.sample.complex.ComplexBuffer; import io.github.dsheirer.sample.complex.reusable.IReusableComplexBufferListener; import io.github.dsheirer.sample.complex.reusable.ReusableComplexBuffer; import io.github.dsheirer.sample.real.IUnFilteredRealBufferProvider; import io.github.dsheirer.sample.real.RealBuffer; +import io.github.dsheirer.source.ISourceEventListener; +import io.github.dsheirer.source.SourceEvent; import org.slf4j.Logger; import org.slf4j.LoggerFactory; -public class FMDemodulatorModule extends Module implements IReusableComplexBufferListener, IUnFilteredRealBufferProvider +public class FMDemodulatorModule extends Module implements ISourceEventListener, IReusableComplexBufferListener, + IUnFilteredRealBufferProvider { private final static Logger mLog = LoggerFactory.getLogger(FMDemodulatorModule.class); - private static final int SAMPLE_RATE = 48000; - private ComplexFIRFilter_CB_CB mIQFilter; private FMDemodulator_CB mDemodulator; - private ReusableBufferListener mReusableBufferListener = new ReusableBufferListener(); + private ReusableBufferProcessor mReusableBufferProcessor = new ReusableBufferProcessor(); + private SourceEventProcessor mSourceEventProcessor = new SourceEventProcessor(); + private int mPassFrequency; + private int mStopFrequency; /** - * FM Demodulator with I/Q filter. Demodulated output is unfiltered and - * may contain a DC component. + * FM Demodulator with I/Q filter. Demodulated output is unfiltered and may contain a DC component. * * @param pass - pass frequency for IQ filtering prior to demodulation. This - * frequency should be half of the signal bandwidth since the filter will + * frequency should be less than or equal to half of the signal bandwidth since the filter will * be applied against each of the inphase and quadrature signals and the * combined pass bandwidth will be twice this value. - * @param stop - stop frequency for IQ filtering prior to demodulation. */ public FMDemodulatorModule(int pass, int stop) { - assert (stop > pass); - - mIQFilter = new ComplexFIRFilter_CB_CB(FilterFactory.getLowPass( - SAMPLE_RATE, pass, stop, 60, Window.WindowType.HAMMING, true), 1.0f); - mDemodulator = new FMDemodulator_CB(1.0f); - mIQFilter.setListener(mDemodulator); - } - - public FMDemodulatorModule(float[] filter) - { - mIQFilter = new ComplexFIRFilter_CB_CB(filter, 1.0f); - mDemodulator = new FMDemodulator_CB(1.0f); - mIQFilter.setListener(mDemodulator); + mPassFrequency = pass; + mStopFrequency = stop; } @Override public Listener getReusableComplexBufferListener() { - return mReusableBufferListener; + return mReusableBufferProcessor; + } + + @Override + public Listener getSourceEventListener() + { + return mSourceEventProcessor; } @Override public void dispose() { - mIQFilter.dispose(); - mIQFilter = null; + if(mIQFilter != null) + { + mIQFilter.dispose(); + mIQFilter = null; + } mDemodulator.dispose(); mDemodulator = null; @@ -113,17 +112,76 @@ public class FMDemodulatorModule extends Module implements IReusableComplexBuffe { } - public class ReusableBufferListener implements Listener + /** + * Receives complex buffers and sends them through the filtering and demodulation process. Manages accounting for + * the reusable buffers. + */ + public class ReusableBufferProcessor implements Listener { @Override public void receive(ReusableComplexBuffer reusableComplexBuffer) { - float[] samples = reusableComplexBuffer.getCopyOfSamples(); + if(mIQFilter == null) + { + reusableComplexBuffer.decrementUserCount(); + throw new IllegalStateException("FM demodulator module must receive a sample rate change source " + + "event before it can process complex sample buffers"); + } - //TODO: redesign the filter chain so that we can simply pass a float array ... + float[] samples = reusableComplexBuffer.getSamplesCopy(); + + mIQFilter.receive(samples); - mIQFilter.receive(new ComplexBuffer(samples)); reusableComplexBuffer.decrementUserCount(); } } + + /** + * Monitors sample rate change source event(s) to setup the initial I/Q filter + */ + public class SourceEventProcessor implements Listener + { + @Override + public void receive(SourceEvent sourceEvent) + { + if(sourceEvent.getEvent() == SourceEvent.Event.NOTIFICATION_SAMPLE_RATE_CHANGE) + { + double sampleRate = sourceEvent.getValue().doubleValue(); + + float[] filterTaps = null; + + FIRFilterSpecification specification = FIRFilterSpecification.lowPassBuilder() + .sampleRate(sampleRate) + .gridDensity(16) + .oddLength(true) + .passBandCutoff(mPassFrequency) + .passBandAmplitude(1.0) + .passBandRipple(0.01) + .stopBandStart(mStopFrequency) + .stopBandAmplitude(0.0) + .stopBandRipple(0.028) //Approximately 60 dB attenuation + .build(); + + try + { + filterTaps = FilterFactory.getTaps(specification); + } + catch(FilterDesignException fde) + { + mLog.error("Couldn't design FM demodulator remez filter for sample rate [" + sampleRate + + "] pass frequency [" + mPassFrequency + "] and stop frequency [" + mStopFrequency + + "] - using sinc filter"); + } + + if(filterTaps == null) + { + filterTaps = FilterFactory.getLowPass(sampleRate, mPassFrequency, mStopFrequency, 60, + Window.WindowType.HAMMING, true); + } + + mIQFilter = new ComplexFIRFilter_CB_CB(filterTaps, 1.0f); + mIQFilter.setListener(mDemodulator); + } + } + } } diff --git a/src/main/java/io/github/dsheirer/source/tuner/channel/TunerChannelSource.java b/src/main/java/io/github/dsheirer/source/tuner/channel/TunerChannelSource.java index 396d223c..b195afac 100644 --- a/src/main/java/io/github/dsheirer/source/tuner/channel/TunerChannelSource.java +++ b/src/main/java/io/github/dsheirer/source/tuner/channel/TunerChannelSource.java @@ -180,15 +180,18 @@ public abstract class TunerChannelSource extends ComplexSource implements ISourc case NOTIFICATION_SAMPLE_RATE_CHANGE: setSampleRate(sourceEvent.getValue().doubleValue()); break; - + case NOTIFICATION_FREQUENCY_AND_SAMPLE_RATE_LOCKED: + case NOTIFICATION_FREQUENCY_AND_SAMPLE_RATE_UNLOCKED: + case NOTIFICATION_FREQUENCY_CORRECTION_CHANGE: + //no-op + break; //Request events from the consumer case REQUEST_CHANNEL_FREQUENCY_CORRECTION_CHANGE: setFrequencyCorrection(sourceEvent.getValue().longValue()); break; - default: mLog.error("Ignoring unrecognized source event: " + sourceEvent.getEvent() + " from [" + - sourceEvent.getSource().getClass() + "]"); + (sourceEvent.hasSource() ? sourceEvent.getSource().getClass() : "") + "]"); break; } } diff --git a/src/main/java/io/github/dsheirer/source/tuner/manager/TunerChannelSourceManager.java b/src/main/java/io/github/dsheirer/source/tuner/manager/TunerChannelSourceManager.java index 64733a83..d3f2e7d8 100644 --- a/src/main/java/io/github/dsheirer/source/tuner/manager/TunerChannelSourceManager.java +++ b/src/main/java/io/github/dsheirer/source/tuner/manager/TunerChannelSourceManager.java @@ -181,18 +181,39 @@ public class TunerChannelSourceManager extends ChannelSourceManager long bestIntegralFrequency = getIntegralFrequency(channels); - //See if we can reuse the current frequency + //Strategy 1: reuse the current frequency if it's a good integral and the channels fit if(isIntegralSpacing(currentCenterFrequency, bestIntegralFrequency) && isValidCenterFrequency(channels, currentCenterFrequency)) { return currentCenterFrequency; } - //Start by placing the first channel at the minimum location within the tuner bandwidth double usableHalfBandwidth = mTunerController.getUsableHalfBandwidth(); + + //Strategy 2: start by placing the center frequency exactly one channel width below the first channel frequency + //and iteratively increase it one channel width at a time. This optimally places the channels nearest to the + //center of the bandwidth + long start = channels.first().getFrequency() - (int)mPolyphaseChannelManager.getChannelBandwidth(); + + if(isValidCenterFrequency(channels, start)) + { + return start; + } + + while(start - channels.first().getMinFrequency() < usableHalfBandwidth) + { + start += (int)mPolyphaseChannelManager.getChannelBandwidth(); + + if(isValidCenterFrequency(channels, start)) + { + return start; + } + } + + //Strategy 3: start by placing the first channel at the minimum location within the tuner bandwidth double startFrequency = channels.first().getMinFrequency() + usableHalfBandwidth; - //Align the test frequency to the next greater integral frequency placing to place the first channel at the + //Align the test frequency to the next greater integral frequency placing the first channel at the // minimum extreme of the available bandwidth as a starting location double delta = Math.abs(startFrequency - bestIntegralFrequency); double adjustment = delta % mPolyphaseChannelManager.getChannelBandwidth(); @@ -221,7 +242,8 @@ public class TunerChannelSourceManager extends ChannelSourceManager currentChannel++; } - //If we can't find an integral center frequency, choose a frequency that will fit the channels + //Strategy 4: brute force walk across the spectrum 1 hertz at a time looking for a center frequency that will + //fit all channels long testFrequency = channels.first().getMinFrequency() + mTunerController.getUsableHalfBandwidth(); long minimumFrequency = testFrequency - availableTestBandwidth; diff --git a/src/main/java/io/github/dsheirer/source/tuner/test/TestTunerController.java b/src/main/java/io/github/dsheirer/source/tuner/test/TestTunerController.java index ec1bf0d6..0638232b 100644 --- a/src/main/java/io/github/dsheirer/source/tuner/test/TestTunerController.java +++ b/src/main/java/io/github/dsheirer/source/tuner/test/TestTunerController.java @@ -33,8 +33,8 @@ 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 = 100000; - public static final int DC_NOISE_BANDWIDTH = 5000; - public static final double USABLE_BANDWIDTH_PERCENTAGE = .90; + public static final int DC_NOISE_BANDWIDTH = 0; + public static final double USABLE_BANDWIDTH_PERCENTAGE = 1.00; public static final int SPECTRAL_FRAME_RATE = 20; public static final long SAMPLE_GENERATION_INTERVAL = 1000 / SPECTRAL_FRAME_RATE; From 28da8b2fa44d60d10189dad1bf3445613fe12bf0 Mon Sep 17 00:00:00 2001 From: Denny Date: Fri, 9 Feb 2018 05:56:20 -0500 Subject: [PATCH 56/87] [wip] reusable buffer support. --- .../IReusableBufferDisposedListener.java | 25 +++ .../IReusableComplexBufferListener.java | 23 ++ .../IReusableComplexBufferProvider.java | 36 +++ .../reusable/ReusableBufferAssembler.java | 136 +++++++++++ .../reusable/ReusableBufferBroadcaster.java | 67 ++++++ .../complex/reusable/ReusableBufferQueue.java | 74 ++++++ .../reusable/ReusableComplexBuffer.java | 211 ++++++++++++++++++ 7 files changed, 572 insertions(+) create mode 100644 src/main/java/io/github/dsheirer/sample/complex/reusable/IReusableBufferDisposedListener.java create mode 100644 src/main/java/io/github/dsheirer/sample/complex/reusable/IReusableComplexBufferListener.java create mode 100644 src/main/java/io/github/dsheirer/sample/complex/reusable/IReusableComplexBufferProvider.java create mode 100644 src/main/java/io/github/dsheirer/sample/complex/reusable/ReusableBufferAssembler.java create mode 100644 src/main/java/io/github/dsheirer/sample/complex/reusable/ReusableBufferBroadcaster.java create mode 100644 src/main/java/io/github/dsheirer/sample/complex/reusable/ReusableBufferQueue.java create mode 100644 src/main/java/io/github/dsheirer/sample/complex/reusable/ReusableComplexBuffer.java diff --git a/src/main/java/io/github/dsheirer/sample/complex/reusable/IReusableBufferDisposedListener.java b/src/main/java/io/github/dsheirer/sample/complex/reusable/IReusableBufferDisposedListener.java new file mode 100644 index 00000000..fd261e23 --- /dev/null +++ b/src/main/java/io/github/dsheirer/sample/complex/reusable/IReusableBufferDisposedListener.java @@ -0,0 +1,25 @@ +/******************************************************************************* + * 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 + * + ******************************************************************************/ +package io.github.dsheirer.sample.complex.reusable; + +public interface IReusableBufferDisposedListener +{ + /** + * Notifies the listener that the timestamped buffer has been disposed and can now be reused. + * @param reusableComplexBuffer that has been disposed + */ + void disposed(ReusableComplexBuffer reusableComplexBuffer); +} diff --git a/src/main/java/io/github/dsheirer/sample/complex/reusable/IReusableComplexBufferListener.java b/src/main/java/io/github/dsheirer/sample/complex/reusable/IReusableComplexBufferListener.java new file mode 100644 index 00000000..2f7a7306 --- /dev/null +++ b/src/main/java/io/github/dsheirer/sample/complex/reusable/IReusableComplexBufferListener.java @@ -0,0 +1,23 @@ +/******************************************************************************* + * 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 + * + ******************************************************************************/ +package io.github.dsheirer.sample.complex.reusable; + +import io.github.dsheirer.sample.Listener; + +public interface IReusableComplexBufferListener +{ + Listener getReusableComplexBufferListener(); +} diff --git a/src/main/java/io/github/dsheirer/sample/complex/reusable/IReusableComplexBufferProvider.java b/src/main/java/io/github/dsheirer/sample/complex/reusable/IReusableComplexBufferProvider.java new file mode 100644 index 00000000..96ad66dd --- /dev/null +++ b/src/main/java/io/github/dsheirer/sample/complex/reusable/IReusableComplexBufferProvider.java @@ -0,0 +1,36 @@ +/******************************************************************************* + * 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 + * + ******************************************************************************/ +package io.github.dsheirer.sample.complex.reusable; + +import io.github.dsheirer.sample.Listener; + +public interface IReusableComplexBufferProvider +{ + /** + * Adds the listener to receive complex buffer samples + */ + void addBufferListener(Listener listener); + + /** + * Removes the listener from receiving complex buffer samples + */ + void removeBufferListener(Listener listener); + + /** + * Indicates if there are any complex buffer listeners registered + */ + boolean hasBufferListeners(); +} diff --git a/src/main/java/io/github/dsheirer/sample/complex/reusable/ReusableBufferAssembler.java b/src/main/java/io/github/dsheirer/sample/complex/reusable/ReusableBufferAssembler.java new file mode 100644 index 00000000..a58b62f3 --- /dev/null +++ b/src/main/java/io/github/dsheirer/sample/complex/reusable/ReusableBufferAssembler.java @@ -0,0 +1,136 @@ +/******************************************************************************* + * 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 + * + ******************************************************************************/ +package io.github.dsheirer.sample.complex.reusable; + +import io.github.dsheirer.dsp.filter.channelizer.SampleTimestampManager; +import io.github.dsheirer.sample.Listener; +import org.apache.commons.lang3.Validate; + +import java.nio.FloatBuffer; + +/** + * Assembles complex float samples into a ComplexBuffer containing an array of floats. Monitors incoming sample count + * to accurately assign a relative timestamp to each assembled buffer. + */ +public class ReusableBufferAssembler +{ + private ReusableBufferQueue mReusableBufferQueue = new ReusableBufferQueue(); + private SampleTimestampManager mTimestampManager; + private FloatBuffer mBuffer; + private int mBufferSize; + private long mCurrentBufferTimestamp; + private Listener mListener; + + public ReusableBufferAssembler(int bufferSize, double sampleRate) + { + Validate.isTrue(bufferSize % 2 == 0); + mBufferSize = bufferSize; + mBuffer = FloatBuffer.allocate(mBufferSize); + mTimestampManager = new SampleTimestampManager(sampleRate); + } + + /** + * Updates the reference timestamp to use for all subsequent samples that are assembled. This timestamp should + * identify the relative timestamp of the first/next sample that is received by this assembler. + * @param timestamp to use relative to the incoming sample arrays. + */ + public void updateTimestamp(long timestamp) + { + mTimestampManager.setReferenceTimestamp(timestamp); + + if(mBuffer.position() == 0) + { + mCurrentBufferTimestamp = timestamp; + } + } + + public void dispose() + { + mListener = null; + mBuffer = null; + } + + /** + * Adds the samples to this assembler. As each buffer is assembled, it will be dispatched to the registered + * listener. + * + * @param samples to assemble + */ + public void receive(float[] samples) + { + if(mBuffer.remaining() >= samples.length) + { + mBuffer.put(samples); + mTimestampManager.increment(samples.length / 2); + + if(!mBuffer.hasRemaining()) + { + flush(); + } + } + else + { + int offset = 0; + + while(offset < samples.length) + { + int toCopy = mBuffer.remaining(); + + if((samples.length - offset) < toCopy) + { + toCopy = samples.length - offset; + } + + mBuffer.put(samples, offset, toCopy); + + offset += toCopy; + mTimestampManager.increment(toCopy / 2); + + if(!mBuffer.hasRemaining()) + { + flush(); + } + } + } + } + + /** + * Flushes the current buffer contents to the registered listener + */ + public void flush() + { + if(mListener != null && mBuffer.position() > 0) + { + mBuffer.rewind(); + ReusableComplexBuffer reusableComplexBuffer = mReusableBufferQueue.getBuffer(mBufferSize); + reusableComplexBuffer.reloadFrom(mBuffer, mCurrentBufferTimestamp); + reusableComplexBuffer.setUserCount(1); + mListener.receive(reusableComplexBuffer); + } + + mBuffer.rewind(); + mCurrentBufferTimestamp = mTimestampManager.getCurrentTimestamp(); + } + + /** + * Sets the listener to receive the output from this buffer assembler. Note: the listener is responsible for + * managing the user count for any buffers received from this buffer assembler. + */ + public void setListener(Listener listener) + { + mListener = listener; + } +} diff --git a/src/main/java/io/github/dsheirer/sample/complex/reusable/ReusableBufferBroadcaster.java b/src/main/java/io/github/dsheirer/sample/complex/reusable/ReusableBufferBroadcaster.java new file mode 100644 index 00000000..e2ac0f80 --- /dev/null +++ b/src/main/java/io/github/dsheirer/sample/complex/reusable/ReusableBufferBroadcaster.java @@ -0,0 +1,67 @@ +/******************************************************************************* + * 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 + * + ******************************************************************************/ +package io.github.dsheirer.sample.complex.reusable; + +import io.github.dsheirer.sample.Broadcaster; +import io.github.dsheirer.sample.Listener; +import org.slf4j.Logger; +import org.slf4j.LoggerFactory; + +public class ReusableBufferBroadcaster extends Broadcaster +{ + private final static Logger mLog = LoggerFactory.getLogger(ReusableBufferBroadcaster.class); + + /** + * Increments the user count for the reusable complex buffer and then broadcasts the buffer to all registered + * listeners. + * + * The total user count is established and applied to the buffer prior to dispatching. If we were to simply + * increment the user count prior to sending to each consumer, there is a possibility that the consumer could + * immediately decrement the user count and prematurely signal that the buffer is ready for disposal before we + * send the buffer to all consumers. + */ + @Override + public void broadcast(ReusableComplexBuffer reusableComplexBuffer) + { + //There is a minuscule possibility that the listener list will change from the time that we get the size until + //the for/each iterator is created below, therefore we have to accurately track the user count. If a listener + //is added in the time between getting the list size and creating the iterator, that listener will NOT receive + //a copy of the buffer, but will instead get the next buffer. Likewise, if a listener is removed, we have to + //decrement the user count on the buffer after iterating the listener list. + int listenerCount = mListeners.size(); + + reusableComplexBuffer.incrementUserCount(listenerCount); + + for(Listener listener : mListeners) + { + if(listenerCount > 0) + { + listener.receive(reusableComplexBuffer); + } + + listenerCount--; + } + + while(listenerCount > 0) + { + reusableComplexBuffer.decrementUserCount(); + listenerCount--; + } + + //Decrement user counter for this broadcaster + reusableComplexBuffer.decrementUserCount(); + } +} diff --git a/src/main/java/io/github/dsheirer/sample/complex/reusable/ReusableBufferQueue.java b/src/main/java/io/github/dsheirer/sample/complex/reusable/ReusableBufferQueue.java new file mode 100644 index 00000000..655c6967 --- /dev/null +++ b/src/main/java/io/github/dsheirer/sample/complex/reusable/ReusableBufferQueue.java @@ -0,0 +1,74 @@ +/******************************************************************************* + * 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 + * + ******************************************************************************/ +package io.github.dsheirer.sample.complex.reusable; + +import org.slf4j.Logger; +import org.slf4j.LoggerFactory; + +import java.util.Queue; +import java.util.concurrent.LinkedTransferQueue; + +public class ReusableBufferQueue implements IReusableBufferDisposedListener +{ + private final static Logger mLog = LoggerFactory.getLogger(ReusableBufferQueue.class); + private Queue mReusableBufferQueue = new LinkedTransferQueue<>(); + private int mBufferCount = 0; + private String mDebugName; + + public ReusableBufferQueue(String debugName) + { + mDebugName = debugName; + } + + public ReusableBufferQueue() + { + + } + + /** + * Implements buffer disposed listener interface. Disposed (ie user count = 0) buffers will automatically callback + * to this method to indicate when they are disposed. + * + * @param reusableComplexBuffer that has been disposed + */ + @Override + public void disposed(ReusableComplexBuffer reusableComplexBuffer) + { + mReusableBufferQueue.offer(reusableComplexBuffer); + } + + /** + * Returns a reusable buffer from an internal recycling queue, or creates a new buffer if there are currently no + * buffers available for reuse. + * + * @param size of the samples that will be loaded into the buffer + * @return a reusable buffer + */ + public ReusableComplexBuffer getBuffer(int size) + { + ReusableComplexBuffer buffer = mReusableBufferQueue.poll(); + + //Create a new buffer if we currently don't have any buffers to reuse + if(buffer == null) + { + buffer = new ReusableComplexBuffer(this, new float[size]); + mBufferCount++; + mLog.debug("Buffer Created - Count:" + mBufferCount + (mDebugName != null ? " [" + mDebugName + "]" : "")); + } + + return buffer; + } +} diff --git a/src/main/java/io/github/dsheirer/sample/complex/reusable/ReusableComplexBuffer.java b/src/main/java/io/github/dsheirer/sample/complex/reusable/ReusableComplexBuffer.java new file mode 100644 index 00000000..00345429 --- /dev/null +++ b/src/main/java/io/github/dsheirer/sample/complex/reusable/ReusableComplexBuffer.java @@ -0,0 +1,211 @@ +/******************************************************************************* + * 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 + * + ******************************************************************************/ +package io.github.dsheirer.sample.complex.reusable; + +import io.github.dsheirer.sample.complex.ComplexBuffer; +import org.apache.commons.lang3.Validate; + +import java.nio.FloatBuffer; +import java.util.concurrent.atomic.AtomicInteger; + +public class ReusableComplexBuffer extends ComplexBuffer +{ + private IReusableBufferDisposedListener mDisposalListener; + private long mTimestamp; + private AtomicInteger mUserCount = new AtomicInteger(); + + /** + * Creates a reusable, timestamped complex buffer using the specified time in milliseconds. + * + * @param disposalListener to be notified when all consumers/users are finished using the buffer + * @param samples of data + * @param timestamp in millis for the buffer + */ + public ReusableComplexBuffer(IReusableBufferDisposedListener disposalListener, float[] samples, long timestamp) + { + super(samples); + + Validate.notNull(disposalListener, "Timestamped Buffer Listener cannot be null"); + mDisposalListener = disposalListener; + mTimestamp = timestamp; + } + + /** + * Constructs a timestamped complex buffer using the current system time in milliseconds. + * + * @param disposalListener to be notified when all consumers are finished using the buffer + * @param samples of data + */ + public ReusableComplexBuffer(IReusableBufferDisposedListener disposalListener, float[] samples) + { + this(disposalListener, samples, System.currentTimeMillis()); + } + + /** + * Creates a copy of the samples from this buffer + */ + public float[] getSamplesCopy() + { + float[] samples = getSamples(); + float[] copy = new float[samples.length]; + System.arraycopy(samples, 0, copy, 0, samples.length); + return copy; + } + + /** + * Number of complex samples contained in this buffer + */ + public int getSampleLength() + { + return getSamples().length / 2; + } + + /** + * Reference timestamp for this complex buffer. Timestamp is relative to the first sample in the buffer. + * @return timestamp as milliseconds since epoch + */ + public long getTimestamp() + { + return mTimestamp; + } + + /** + * Reloads this buffer by copying the sample array into this buffer. Internal buffer length will be automatically + * resized to fit the new sample array length. + * + * This is convenient if the source of the sample array originates via a native buffer. + * @param samples to copy into this buffer + * @param timestamp for the samples in millis + */ + public void reloadFrom(float[] samples, long timestamp) + { + if(mUserCount.get() > 0) + { + throw new IllegalStateException("New data cannot be loaded into this reusable buffer while the user count " + + "is above zero - user count:" + mUserCount.get()); + } + + mUserCount.set(0); + + if(mSamples.length != samples.length) + { + mSamples = new float[samples.length]; + } + + System.arraycopy(samples, 0, mSamples, 0, samples.length); + + mTimestamp = timestamp; + } + + /** + * Reloads this buffer by filling this buffer from the float buffer (ie copying the buffer contents). Internal + * buffer length will be automatically resized to accommodate the float buffer's capacity/size. + * + * @param floatBuffer to copy into this buffer + * @param timestamp for the samples in millis + */ + public void reloadFrom(FloatBuffer floatBuffer, long timestamp) + { + if(mUserCount.get() > 0) + { + throw new IllegalStateException("New data cannot be loaded into this reusable buffer while the user count " + + "is above zero - user count:" + mUserCount.get()); + } + + mUserCount.set(0); + + if(mSamples.length != floatBuffer.capacity()) + { + mSamples = new float[floatBuffer.capacity()]; + } + + floatBuffer.rewind(); + floatBuffer.get(mSamples); + + mTimestamp = timestamp; + } + + /** + * Sets the number of users that will receive this buffer. Each user is expected to invoke the + * decrementUserCount() method to indicate that they are finished using this buffer so that this buffer can be + * reused once the user count is zero. + * + * @param userCount that will receive this reusable buffer + */ + public void setUserCount(int userCount) + { + mUserCount.set(userCount); + } + + /** + * Decrements the user count for this buffer to indicate that the user/receiver of this buffer is finished using + * the buffer. + * + * User count is established prior to sending this buffer to each listener and each listener invokes this method to + * indicate that they have finished processing the buffer so that when the user count reaches zero, this buffer + * can be reused. + * + * This method is thread-safe. + */ + public ReusableComplexBuffer decrementUserCount() + { + int currentCount = mUserCount.decrementAndGet(); + + if(currentCount == 0) + { + mDisposalListener.disposed(this); + } + else if(currentCount < 0) + { + throw new IllegalStateException("User count is below zero. This indicates that this buffer's decrement" + + " user count was invoked by more than the expected user count"); + } + + return this; + } + + /** + * Increments the user count to indicate that this buffer will be sent to another user. + * + * Users/receivers of this buffer that wish to forward the buffer to additional users should increment the expected + * user count prior to invoking the decrementUserCount() to indicate that the original receiver of the buffer is + * finished to avoid the user count reaching zero and the buffer being flagged as ready for reuse. + * + * This method is thread-safe. + */ + public ReusableComplexBuffer incrementUserCount() + { + mUserCount.incrementAndGet(); + return this; + } + + /** + * Increments the user count by the specified count to indicate that this buffer will be sent to additional users. + * + * Users/receivers of this buffer that wish to forward the buffer to additional users should increment the expected + * user count prior to invoking the decrementUserCount() to indicate that the original receiver of the buffer is + * finished to avoid the user count reaching zero and the buffer being flagged as ready for reuse. + * + * This method is thread-safe. + * + * @param additionalUserCount to add to the current user count + */ + public ReusableComplexBuffer incrementUserCount(int additionalUserCount) + { + mUserCount.addAndGet(additionalUserCount); + return this; + } +} From bfaacb309d87ec875384053b5b3c23f49ea8a346 Mon Sep 17 00:00:00 2001 From: Denny Date: Sat, 24 Mar 2018 06:59:41 -0400 Subject: [PATCH 57/87] 39 - WIP - incorporates the latest P25 C4FM decoder from master and updates the LSM decoder to follow the same design pattern. Adds LSM decoder support to the demodulator viewer application. The current symbol timing gain for LSM is set too high -- is using the C4FM gain and causes too much fluctuation in the timing error. --- .../dsheirer/buffer/FloatCircularBuffer.java | 120 ++++ .../filter/fir/complex/ComplexFIRFilter.java | 48 +- .../gain/ComplexFeedForwardGainControl.java | 60 +- .../psk/DQPSKDecisionDirectedDemodulator.java | 90 +++ ...isionDirectedDemodulatorInstrumented.java} | 24 +- ...DQPSKDecisionDirectedSymbolEvaluator.java} | 7 +- .../dsheirer/dsp/psk/DQPSKDemodulator.java | 162 ----- .../dsp/psk/DQPSKGardnerDemodulator.java | 90 +++ .../DQPSKGardnerDemodulatorInstrumented.java | 127 ++++ .../dsp/psk/DQPSKGardnerSymbolEvaluator.java | 158 +++++ ...valuator.java => IPSKSymbolEvaluator.java} | 18 +- .../dsp/psk/InterpolatingSampleBuffer.java | 53 +- ...InterpolatingSampleBufferInstrumented.java | 5 +- .../dsheirer/dsp/psk/LSMDemodulator.java | 614 ------------------ .../dsheirer/dsp/psk/PSKDemodulator.java | 137 ++++ .../gui/viewer/DemodulatorViewerFX.java | 77 ++- .../gui/viewer/PlaybackController.java | 16 +- .../instrument/gui/viewer/ViewerDesktop.java | 15 +- .../viewer/chart/ComplexSampleLineChart.java | 8 +- .../gui/viewer/decoder/DecoderPane.java | 16 +- .../viewer/decoder/DecoderPaneFactory.java | 19 +- .../gui/viewer/decoder/P25Phase1C4FMPane.java | 194 ++++++ ...5Phase1Pane.java => P25Phase1LSMPane.java} | 19 +- .../dsheirer/module/decode/Decoder.java | 15 + .../module/decode/DecoderFactory.java | 10 +- .../decode/p25/DecodeConfigP25Phase1.java | 6 +- .../module/decode/p25/P25Decoder.java | 125 +++- ...5_C4FMDecoder.java => P25DecoderC4FM.java} | 139 ++-- .../p25/P25DecoderC4FMInstrumented.java | 121 ++++ .../module/decode/p25/P25DecoderEditor.java | 14 +- .../module/decode/p25/P25DecoderLSM.java | 177 +++++ ...ed.java => P25DecoderLSMInstrumented.java} | 53 +- .../module/decode/p25/P25_LSMDecoder.java | 240 ------- .../complex/reusable/ReusableBufferQueue.java | 5 + .../reusable/ReusableComplexBuffer.java | 13 + .../source/IControllableFileSource.java | 2 +- .../dsheirer/source/tuner/TunerModel.java | 52 +- .../source/tuner/TunerSourceEditor.java | 5 +- .../source/wave/ComplexWaveSource.java | 4 +- .../spectrum/ChannelSpectrumPanel.java | 50 +- 40 files changed, 1817 insertions(+), 1291 deletions(-) create mode 100644 src/main/java/io/github/dsheirer/dsp/psk/DQPSKDecisionDirectedDemodulator.java rename src/main/java/io/github/dsheirer/dsp/psk/{DQPSKDemodulatorInstrumented.java => DQPSKDecisionDirectedDemodulatorInstrumented.java} (79%) rename src/main/java/io/github/dsheirer/dsp/psk/{DQPSKSymbolEvaluator.java => DQPSKDecisionDirectedSymbolEvaluator.java} (95%) delete mode 100644 src/main/java/io/github/dsheirer/dsp/psk/DQPSKDemodulator.java create mode 100644 src/main/java/io/github/dsheirer/dsp/psk/DQPSKGardnerDemodulator.java create mode 100644 src/main/java/io/github/dsheirer/dsp/psk/DQPSKGardnerDemodulatorInstrumented.java create mode 100644 src/main/java/io/github/dsheirer/dsp/psk/DQPSKGardnerSymbolEvaluator.java rename src/main/java/io/github/dsheirer/dsp/psk/{IDQPSKSymbolEvaluator.java => IPSKSymbolEvaluator.java} (71%) delete mode 100644 src/main/java/io/github/dsheirer/dsp/psk/LSMDemodulator.java create mode 100644 src/main/java/io/github/dsheirer/dsp/psk/PSKDemodulator.java create mode 100644 src/main/java/io/github/dsheirer/instrument/gui/viewer/decoder/P25Phase1C4FMPane.java rename src/main/java/io/github/dsheirer/instrument/gui/viewer/decoder/{P25Phase1Pane.java => P25Phase1LSMPane.java} (91%) rename src/main/java/io/github/dsheirer/module/decode/p25/{P25_C4FMDecoder.java => P25DecoderC4FM.java} (56%) create mode 100644 src/main/java/io/github/dsheirer/module/decode/p25/P25DecoderC4FMInstrumented.java create mode 100644 src/main/java/io/github/dsheirer/module/decode/p25/P25DecoderLSM.java rename src/main/java/io/github/dsheirer/module/decode/p25/{P25_C4FMDecoderInstrumented.java => P25DecoderLSMInstrumented.java} (59%) delete mode 100644 src/main/java/io/github/dsheirer/module/decode/p25/P25_LSMDecoder.java diff --git a/src/main/java/io/github/dsheirer/buffer/FloatCircularBuffer.java b/src/main/java/io/github/dsheirer/buffer/FloatCircularBuffer.java index e69de29b..640e6506 100644 --- a/src/main/java/io/github/dsheirer/buffer/FloatCircularBuffer.java +++ b/src/main/java/io/github/dsheirer/buffer/FloatCircularBuffer.java @@ -0,0 +1,120 @@ +/******************************************************************************* + * SDR Trunk + * Copyright (C) 2014 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 io.github.dsheirer.buffer; + +import java.util.Arrays; + +/** + * Circular sample buffer - allocates a buffer and stores samples in a circular + * fashion, overwriting older samples with newly arrived samples. Initially + * fills buffer with 0-valued samples + * + * Can be used as a delay-type buffer, to delay samples by the 'size' amount + * + * @author denny + */ +public class FloatCircularBuffer +{ + float[] mBuffer; + int mBufferPointer = 0; + + /** + * Creates a circular double buffer of the specified size and all entries filled with the specified initial value. + * + * @param size of the buffer + * @param initialFillValue to fill the buffer entries + */ + public FloatCircularBuffer(int size, float initialFillValue) + { + mBuffer = new float[size]; + Arrays.fill(mBuffer, initialFillValue); + } + + /** + * Creates a circular double buffer of the specified size and all entries filled with an initial value of zero. + * + * @param size of the buffer + */ + public FloatCircularBuffer(int size) + { + this(size, 0.0f); + } + + public float[] getBuffer() + { + return mBuffer; + } + + /** + * Puts the new value into the buffer and returns the oldest buffer value that it replaced + * + * @param newValue to add to the buffer + * @return oldest value that was overwritten by the new value + */ + public float get(float newValue) + { + float oldestSample = mBuffer[mBufferPointer]; + + mBuffer[mBufferPointer] = newValue; + + mBufferPointer++; + + if(mBufferPointer >= mBuffer.length) + { + mBufferPointer = 0; + } + + return oldestSample; + } + + public float[] getAll() + { + int valuePointer = 0; + float[] values = new float[mBuffer.length]; + int bufferPointer = mBufferPointer; + + while(valuePointer < values.length) + { + values[valuePointer++] = mBuffer[bufferPointer++]; + + if(bufferPointer >= mBuffer.length) + { + bufferPointer = 0; + } + } + + return values; + } + + /** + * Places the new value into the buffer, overwriting the oldest value + * + * @param newValue to add to the buffer + */ + public void put(float newValue) + { + mBuffer[mBufferPointer] = newValue; + + mBufferPointer++; + + if(mBufferPointer >= mBuffer.length) + { + mBufferPointer = 0; + } + } +} diff --git a/src/main/java/io/github/dsheirer/dsp/filter/fir/complex/ComplexFIRFilter.java b/src/main/java/io/github/dsheirer/dsp/filter/fir/complex/ComplexFIRFilter.java index 6ac9a164..64cb1d0f 100644 --- a/src/main/java/io/github/dsheirer/dsp/filter/fir/complex/ComplexFIRFilter.java +++ b/src/main/java/io/github/dsheirer/dsp/filter/fir/complex/ComplexFIRFilter.java @@ -18,10 +18,12 @@ package io.github.dsheirer.dsp.filter.fir.complex; import io.github.dsheirer.dsp.filter.fir.FIRFilter; import io.github.dsheirer.dsp.filter.fir.real.RealFIRFilter; import io.github.dsheirer.sample.complex.Complex; -import io.github.dsheirer.sample.complex.ComplexBuffer; +import io.github.dsheirer.sample.complex.reusable.ReusableBufferQueue; +import io.github.dsheirer.sample.complex.reusable.ReusableComplexBuffer; public class ComplexFIRFilter extends FIRFilter { + private ReusableBufferQueue mReusableBufferQueue = new ReusableBufferQueue(); private RealFIRFilter mIFilter; private RealFIRFilter mQFilter; @@ -61,24 +63,43 @@ public class ComplexFIRFilter extends FIRFilter return new Complex(i, q); } - /** - * Filters the complex samples from the buffer and returns a new complex buffer with the filtered output - * - * @param buffer with complex samples to filter - * @return new buffer containing filtered complex samples - */ - public ComplexBuffer filter(ComplexBuffer buffer) + public float[] filter(float[] samples) { - float[] samples = buffer.getSamples(); - float[] filtered = new float[samples.length]; + float[] filteredSamples = new float[samples.length]; for(int x = 0; x < samples.length; x += 2) { - filtered[x] = filterInphase(samples[x]); - filtered[x + 1] = filterQuadrature(samples[x + 1]); + filteredSamples[x] = filterInphase(samples[x]); + filteredSamples[x + 1] = filterQuadrature(samples[x + 1]); } - return new ComplexBuffer(filtered); + return filteredSamples; + } + + /** + * Filters the complex samples from the buffer and returns a new complex buffer with the filtered output + * + * @param originalBuffer with complex samples to filter + * @return new buffer containing filtered complex samples + */ + public ReusableComplexBuffer filter(ReusableComplexBuffer originalBuffer) + { + ReusableComplexBuffer filteredBuffer = mReusableBufferQueue.getBuffer(originalBuffer.getSampleLength()); + filteredBuffer.setTimestamp(originalBuffer.getTimestamp()); + + float[] samples = originalBuffer.getSamples(); + float[] filteredSamples = filteredBuffer.getSamples(); + + for(int x = 0; x < originalBuffer.getSampleLength(); x += 2) + { + filteredSamples[x] = filterInphase(samples[x]); + filteredSamples[x + 1] = filterQuadrature(samples[x + 1]); + } + + originalBuffer.decrementUserCount(); + filteredBuffer.incrementUserCount(); + + return filteredBuffer; } @Override @@ -86,5 +107,6 @@ public class ComplexFIRFilter extends FIRFilter { mIFilter.dispose(); mQFilter.dispose(); + mReusableBufferQueue.dispose(); } } diff --git a/src/main/java/io/github/dsheirer/dsp/gain/ComplexFeedForwardGainControl.java b/src/main/java/io/github/dsheirer/dsp/gain/ComplexFeedForwardGainControl.java index 215d7b46..560655b4 100644 --- a/src/main/java/io/github/dsheirer/dsp/gain/ComplexFeedForwardGainControl.java +++ b/src/main/java/io/github/dsheirer/dsp/gain/ComplexFeedForwardGainControl.java @@ -19,6 +19,8 @@ import io.github.dsheirer.buffer.FloatCircularBuffer; import io.github.dsheirer.sample.complex.Complex; import io.github.dsheirer.sample.complex.ComplexBuffer; import io.github.dsheirer.sample.complex.ComplexSampleListener; +import io.github.dsheirer.sample.complex.reusable.ReusableBufferQueue; +import io.github.dsheirer.sample.complex.reusable.ReusableComplexBuffer; public class ComplexFeedForwardGainControl implements ComplexSampleListener { @@ -26,8 +28,8 @@ public class ComplexFeedForwardGainControl implements ComplexSampleListener public static final float MINIMUM_ENVELOPE = 0.0001f; private ComplexSampleListener mListener; - private FloatCircularBuffer mEnvelopeHistory; + private ReusableBufferQueue mReusableBufferQueue = new ReusableBufferQueue(); private float mMaxEnvelope = 0.0f; private float mGain = 1.0f; @@ -107,19 +109,59 @@ public class ComplexFeedForwardGainControl implements ComplexSampleListener mListener = listener; } - public ComplexBuffer filter(ComplexBuffer complexBuffer) + public float[] filter(float[] complesSamples) { - float[] samples = complexBuffer.getSamples(); - float[] processed = new float[samples.length]; + mMaxEnvelope = MINIMUM_ENVELOPE; + float currentEnvelope; - for(int x = 0; x < samples.length; x += 2) + for(int x = 0; x < complesSamples.length; x += 2) { - process(samples[x], samples[x + 1]); + currentEnvelope = Complex.envelope(complesSamples[x], complesSamples[x + 1]); - processed[x] = samples[x] * mGain; - processed[x + 1] = samples[x + 1] * mGain; + if(currentEnvelope > mMaxEnvelope) + { + mMaxEnvelope = currentEnvelope; + } } - return new ComplexBuffer(processed); + adjustGain(); + + float[] processed = new float[complesSamples.length]; + + for(int x = 0; x < complesSamples.length; x += 2) + { + processed[x] = complesSamples[x] * mGain; + processed[x + 1] = complesSamples[x + 1] * mGain; + } + + return processed; + } + + /** + * Applies gain to the complex sample buffer + * @param buffer to apply gain + * @return buffer with gain applied samples + */ + public ComplexBuffer filter(ComplexBuffer buffer) + { + float[] filtered = filter(buffer.getSamples()); + return new ComplexBuffer(filtered); + } + + /** + * Applies gain to the complex sample buffer + * @param buffer to apply gain + * @return reusable buffer with the user count incremented to 1 + */ + public ReusableComplexBuffer filter(ReusableComplexBuffer buffer) + { + float[] filtered = filter(buffer.getSamples()); + + ReusableComplexBuffer filteredBuffer = mReusableBufferQueue.getBuffer(filtered.length); + + filteredBuffer.reloadFrom(filtered, buffer.getTimestamp()); + filteredBuffer.incrementUserCount(); + + return filteredBuffer; } } diff --git a/src/main/java/io/github/dsheirer/dsp/psk/DQPSKDecisionDirectedDemodulator.java b/src/main/java/io/github/dsheirer/dsp/psk/DQPSKDecisionDirectedDemodulator.java new file mode 100644 index 00000000..7eebc342 --- /dev/null +++ b/src/main/java/io/github/dsheirer/dsp/psk/DQPSKDecisionDirectedDemodulator.java @@ -0,0 +1,90 @@ +/******************************************************************************* + * 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 + * + ******************************************************************************/ +package io.github.dsheirer.dsp.psk; + +import io.github.dsheirer.dsp.psk.pll.IPhaseLockedLoop; +import io.github.dsheirer.dsp.symbol.Dibit; +import io.github.dsheirer.sample.complex.Complex; + +public class DQPSKDecisionDirectedDemodulator extends PSKDemodulator +{ + protected DQPSKDecisionDirectedSymbolEvaluator mSymbolEvaluator = new DQPSKDecisionDirectedSymbolEvaluator(); + private Complex mPreviousPrecedingSample = new Complex(0, 0); + private Complex mPreviousCurrentSample = new Complex(0, 0); + private Complex mPrecedingSample = new Complex(0, 0); + private Complex mCurrentSample = new Complex(0, 0); + private Complex mPrecedingSymbol = new Complex(0, 0); + protected Complex mCurrentSymbol = new Complex(0, 0); + + /** + * Decoder for Differential Quaternary Phase Shift Keying (DQPSK). This decoder uses both a Costas Loop (PLL) and + * a decision-directed DQPSK symbol timing and phase error detector to automatically align to the incoming carrier + * frequency and to adjust for any changes in symbol timing. + * + * This detector is optimized for constant amplitude DQPSK symbols like C4FM. + * + * @param phaseLockedLoop for tracking carrier frequency error + * @param interpolatingSampleBuffer to hold samples for interpolating a symbol + */ + public DQPSKDecisionDirectedDemodulator(IPhaseLockedLoop phaseLockedLoop, InterpolatingSampleBuffer interpolatingSampleBuffer) + { + super(interpolatingSampleBuffer, phaseLockedLoop); + } + + /** + * Calculates a symbol from the interpolating buffer + */ + protected void calculateSymbol() + { + //Get preceding sample and an interpolated current sample from the interpolating buffer + mPrecedingSample = getInterpolatingSampleBuffer().getPrecedingSample(); + mCurrentSample = getInterpolatingSampleBuffer().getCurrentSample(); + + //Differential decode preceding and current symbols by calculating the angular rotation between the previous and + //current samples (current sample x complex conjugate of previous sample). + + //Note: preceding symbol is a preceding measurement of the current symbol that is simply used as a reference + //point to determine vector rotation to the current symbol -- it is not the true predecessor symbol + mPrecedingSymbol.setInphase(Complex.multiplyInphase(mPrecedingSample.inphase(), mPrecedingSample.quadrature(), + mPreviousPrecedingSample.inphase(), -mPreviousPrecedingSample.quadrature())); + mPrecedingSymbol.setQuadrature(Complex.multiplyQuadrature(mPrecedingSample.inphase(), mPrecedingSample.quadrature(), + mPreviousPrecedingSample.inphase(), -mPreviousPrecedingSample.quadrature())); + + mCurrentSymbol.setInphase(Complex.multiplyInphase(mCurrentSample.inphase(), mCurrentSample.quadrature(), + mPreviousCurrentSample.inphase(), -mPreviousCurrentSample.quadrature())); + mCurrentSymbol.setQuadrature(Complex.multiplyQuadrature(mCurrentSample.inphase(), mCurrentSample.quadrature(), + mPreviousCurrentSample.inphase(), -mPreviousCurrentSample.quadrature())); + + //Set gain to unity before we calculate the error value + mPrecedingSymbol.normalize(); + mCurrentSymbol.normalize(); + + //Apply symbols to evaluator to calculate phase and timing error and make a symbol decision + mSymbolEvaluator.setSymbol(mPrecedingSymbol, mCurrentSymbol); + + //Update the symbol timing error + getInterpolatingSampleBuffer().resetAndAdjust(mSymbolEvaluator.getTimingError()); + + //Update the costas loop (PLL) with any measured phase error + getPLL().adjust(clip(mSymbolEvaluator.getPhaseError(), 0.5f)); + + //Store current samples/symbols to use for the next symbol period + mPreviousPrecedingSample.setValues(mPrecedingSample); + mPreviousCurrentSample.setValues(mCurrentSample); + + broadcast(mSymbolEvaluator.getSymbolDecision()); + } +} diff --git a/src/main/java/io/github/dsheirer/dsp/psk/DQPSKDemodulatorInstrumented.java b/src/main/java/io/github/dsheirer/dsp/psk/DQPSKDecisionDirectedDemodulatorInstrumented.java similarity index 79% rename from src/main/java/io/github/dsheirer/dsp/psk/DQPSKDemodulatorInstrumented.java rename to src/main/java/io/github/dsheirer/dsp/psk/DQPSKDecisionDirectedDemodulatorInstrumented.java index cc90a742..819a9124 100644 --- a/src/main/java/io/github/dsheirer/dsp/psk/DQPSKDemodulatorInstrumented.java +++ b/src/main/java/io/github/dsheirer/dsp/psk/DQPSKDecisionDirectedDemodulatorInstrumented.java @@ -20,11 +20,11 @@ import io.github.dsheirer.dsp.psk.pll.IPhaseLockedLoop; import io.github.dsheirer.sample.Listener; import io.github.dsheirer.sample.complex.Complex; -public class DQPSKDemodulatorInstrumented extends DQPSKDemodulator +public class DQPSKDecisionDirectedDemodulatorInstrumented extends DQPSKDecisionDirectedDemodulator { private Listener mSymbolDecisionDataListener; private Listener mSamplesPerSymbolListener; - private Listener mSymbolListener; + private Listener mComplexSymbolListener; private Listener mPLLErrorListener; private Listener mPLLFrequencyListener; private double mSampleRate; @@ -40,9 +40,9 @@ public class DQPSKDemodulatorInstrumented extends DQPSKDemodulator * @param interpolatingSampleBuffer * @param sampleRate of the incoming complex sample stream */ - public DQPSKDemodulatorInstrumented(IPhaseLockedLoop phaseLockedLoop, - InterpolatingSampleBufferInstrumented interpolatingSampleBuffer, - double sampleRate) + public DQPSKDecisionDirectedDemodulatorInstrumented(IPhaseLockedLoop phaseLockedLoop, + InterpolatingSampleBufferInstrumented interpolatingSampleBuffer, + double sampleRate) { super(phaseLockedLoop, interpolatingSampleBuffer); mSampleRate = sampleRate; @@ -57,20 +57,20 @@ public class DQPSKDemodulatorInstrumented extends DQPSKDemodulator if(mSymbolDecisionDataListener != null) { mSymbolDecisionDataListener.receive( - ((InterpolatingSampleBufferInstrumented)mInterpolatingSampleBuffer).getSymbolDecisionData()); + ((InterpolatingSampleBufferInstrumented)getInterpolatingSampleBuffer()).getSymbolDecisionData()); } super.calculateSymbol(); if(mSamplesPerSymbolListener != null) { - mSamplesPerSymbolListener.receive((double)mInterpolatingSampleBuffer.getSamplingPoint()); + mSamplesPerSymbolListener.receive((double)getInterpolatingSampleBuffer().getSamplingPoint()); } //Send to an external constellation symbol listener when registered - if(mSymbolListener != null) + if(mComplexSymbolListener != null) { - mSymbolListener.receive(mCurrentSymbol); + mComplexSymbolListener.receive(mCurrentSymbol); } if(mPLLErrorListener != null) @@ -80,7 +80,7 @@ public class DQPSKDemodulatorInstrumented extends DQPSKDemodulator if(mPLLFrequencyListener != null) { - double loopFrequency = ((CostasLoop)mPLL).getLoopFrequency(); + double loopFrequency = ((CostasLoop)getPLL()).getLoopFrequency(); loopFrequency *= mSampleRate / (2.0 * Math.PI); @@ -107,9 +107,9 @@ public class DQPSKDemodulatorInstrumented extends DQPSKDemodulator /** * Registers the listener to receive decoded QPSK symbols */ - public void setSymbolListener(Listener listener) + public void setComplexSymbolListener(Listener listener) { - mSymbolListener = listener; + mComplexSymbolListener = listener; } /** diff --git a/src/main/java/io/github/dsheirer/dsp/psk/DQPSKSymbolEvaluator.java b/src/main/java/io/github/dsheirer/dsp/psk/DQPSKDecisionDirectedSymbolEvaluator.java similarity index 95% rename from src/main/java/io/github/dsheirer/dsp/psk/DQPSKSymbolEvaluator.java rename to src/main/java/io/github/dsheirer/dsp/psk/DQPSKDecisionDirectedSymbolEvaluator.java index cab0fd22..077010dc 100644 --- a/src/main/java/io/github/dsheirer/dsp/psk/DQPSKSymbolEvaluator.java +++ b/src/main/java/io/github/dsheirer/dsp/psk/DQPSKDecisionDirectedSymbolEvaluator.java @@ -18,7 +18,7 @@ package io.github.dsheirer.dsp.psk; import io.github.dsheirer.dsp.symbol.Dibit; import io.github.dsheirer.sample.complex.Complex; -public class DQPSKSymbolEvaluator implements IDQPSKSymbolEvaluator +public class DQPSKDecisionDirectedSymbolEvaluator implements IPSKSymbolEvaluator { private static final Complex ROTATE_FROM_PLUS_135 = Complex.fromAngle(-3.0 * Math.PI / 4.0); private static final Complex ROTATE_FROM_PLUS_45 = Complex.fromAngle(-1.0 * Math.PI / 4.0); @@ -32,7 +32,7 @@ public class DQPSKSymbolEvaluator implements IDQPSKSymbolEvaluator private Dibit mSymbolDecision = Dibit.D00_PLUS_1; /** - * Decision-directed symbol phase and timing error detector and symbol decision slicer. + * Differential QPSK Decision-directed symbol phase and timing error detector and symbol decision slicer. * * Symbol decision is based on the closest reference quadrant for the sampled symbol. * @@ -40,7 +40,7 @@ public class DQPSKSymbolEvaluator implements IDQPSKSymbolEvaluator * symbol. Timing error is further corrected to compensate for clock-wise or counter-clock-wise vector rotation * relative to the immediately preceding symbol. */ - public DQPSKSymbolEvaluator() + public DQPSKDecisionDirectedSymbolEvaluator() { } @@ -57,7 +57,6 @@ public class DQPSKSymbolEvaluator implements IDQPSKSymbolEvaluator * since it's only a reference * @param current symbol to be evaluated - interpolated */ - @Override public void setSymbol(Complex preceding, Complex current) { mSymbolError.setValues(current); diff --git a/src/main/java/io/github/dsheirer/dsp/psk/DQPSKDemodulator.java b/src/main/java/io/github/dsheirer/dsp/psk/DQPSKDemodulator.java deleted file mode 100644 index f8b6aabd..00000000 --- a/src/main/java/io/github/dsheirer/dsp/psk/DQPSKDemodulator.java +++ /dev/null @@ -1,162 +0,0 @@ -/******************************************************************************* - * 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 - * - ******************************************************************************/ -package io.github.dsheirer.dsp.psk; - -import io.github.dsheirer.dsp.psk.pll.IPhaseLockedLoop; -import io.github.dsheirer.dsp.symbol.Dibit; -import io.github.dsheirer.sample.Listener; -import io.github.dsheirer.sample.complex.Complex; -import io.github.dsheirer.sample.complex.ComplexBuffer; -import io.github.dsheirer.sample.complex.ComplexSampleListener; -import org.slf4j.Logger; -import org.slf4j.LoggerFactory; - -public class DQPSKDemodulator implements ComplexSampleListener -{ - private final static Logger mLog = LoggerFactory.getLogger(DQPSKDemodulator.class); - protected InterpolatingSampleBuffer mInterpolatingSampleBuffer; - protected IPhaseLockedLoop mPLL; - protected DQPSKSymbolEvaluator mSymbolEvaluator = new DQPSKSymbolEvaluator(); - private Complex mReceivedSample = new Complex(0, 0); - private Complex mPreviousPrecedingSample = new Complex(0, 0); - private Complex mPreviousCurrentSample = new Complex(0, 0); - private Complex mPrecedingSample = new Complex(0, 0); - private Complex mCurrentSample = new Complex(0, 0); - private Complex mPrecedingSymbol = new Complex(0, 0); - protected Complex mCurrentSymbol = new Complex(0, 0); - private Listener mDibitListener; - - /** - * Decoder for Differential Quaternary Phase Shift Keying (DQPSK). This decoder uses both a Costas Loop (PLL) and - * a custom DQPSK symbol timing error detector to automatically align to the incoming carrier frequency and to - * adjust for any changes in symbol timing. - * - * This detector is optimized for constant amplitude DQPSK symbols like C4FM. - * - * @param phaseLockedLoop for tracking carrier frequency error - * @param interpolatingSampleBuffer to hold samples for interpolating a symbol - */ - public DQPSKDemodulator(IPhaseLockedLoop phaseLockedLoop, InterpolatingSampleBuffer interpolatingSampleBuffer) - { - mPLL = phaseLockedLoop; - mInterpolatingSampleBuffer = interpolatingSampleBuffer; - } - - /** - * Processes a (filtered) buffer containing complex samples for decoding - * @param complexBuffer with complex samples - */ - public void receive(ComplexBuffer complexBuffer) - { - float[] samples = complexBuffer.getSamples(); - - for(int x = 0; x < samples.length; x += 2) - { - receive(samples[x], samples[x + 1]); - } - } - - /** - * Processes a complex sample for decoding. Once sufficient samples are buffered, a symbol decision is made. - * @param inphase value for the sample - * @param quadrature value for the sample - */ - @Override - public void receive(float inphase, float quadrature) - { - //Update current sample with values - mReceivedSample.setValues(inphase, quadrature); - - //Mix current sample with costas loop to remove any rotation that is present from a mis-tuned carrier frequency - mReceivedSample.multiply(mPLL.incrementAndGetCurrentVector()); - - //Store the sample in the interpolating buffer - mInterpolatingSampleBuffer.receive(mReceivedSample); - - //Calculate the symbol once we've stored enough samples - if(mInterpolatingSampleBuffer.hasSymbol()) - { - calculateSymbol(); - } - } - - /** - * Calculates a symbol from the interpolating buffer - */ - protected void calculateSymbol() - { - //Get preceding sample and an interpolated current sample from the interpolating buffer - mPrecedingSample = mInterpolatingSampleBuffer.getPrecedingSample(); - mCurrentSample = mInterpolatingSampleBuffer.getCurrentSample(); - - //Calculate preceding, current and following symbols as the delta rotation compared to the previous samples - mPrecedingSymbol.setInphase(Complex.multiplyInphase(mPrecedingSample.inphase(), mPrecedingSample.quadrature(), - mPreviousPrecedingSample.inphase(), -mPreviousPrecedingSample.quadrature())); - mPrecedingSymbol.setQuadrature(Complex.multiplyQuadrature(mPrecedingSample.inphase(), mPrecedingSample.quadrature(), - mPreviousPrecedingSample.inphase(), -mPreviousPrecedingSample.quadrature())); - - mCurrentSymbol.setInphase(Complex.multiplyInphase(mCurrentSample.inphase(), mCurrentSample.quadrature(), - mPreviousCurrentSample.inphase(), -mPreviousCurrentSample.quadrature())); - mCurrentSymbol.setQuadrature(Complex.multiplyQuadrature(mCurrentSample.inphase(), mCurrentSample.quadrature(), - mPreviousCurrentSample.inphase(), -mPreviousCurrentSample.quadrature())); - - //Set gain to unity before we calculate the error value - mPrecedingSymbol.normalize(); - mCurrentSymbol.normalize(); - - mSymbolEvaluator.setSymbol(mPrecedingSymbol, mCurrentSymbol); - - mInterpolatingSampleBuffer.resetAndAdjust(mSymbolEvaluator.getTimingError()); - - //Store current samples/symbols to use for the next period - mPreviousPrecedingSample.setValues(mPrecedingSample); - mPreviousCurrentSample.setValues(mCurrentSample); - - //Calculate the phase error of the current symbol relative to the expected constellation and provide - //feedback to the PLL - mPLL.adjust(clip(mSymbolEvaluator.getPhaseError(), 0.5f)); - - if(mDibitListener != null) - { - mDibitListener.receive(mSymbolEvaluator.getSymbolDecision()); - } - } - - /** - * Registers the listener to receive dibit symbol decisions from this demodulator - */ - public void setDibitListener(Listener listener) - { - mDibitListener = listener; - } - - /** - * Constrains value to the range of ( -maximum <> maximum ) - */ - public static float clip(float value, float maximum) - { - if(value > maximum) - { - return maximum; - } - else if(value < -maximum) - { - return -maximum; - } - - return value; - } -} diff --git a/src/main/java/io/github/dsheirer/dsp/psk/DQPSKGardnerDemodulator.java b/src/main/java/io/github/dsheirer/dsp/psk/DQPSKGardnerDemodulator.java new file mode 100644 index 00000000..4e8033ee --- /dev/null +++ b/src/main/java/io/github/dsheirer/dsp/psk/DQPSKGardnerDemodulator.java @@ -0,0 +1,90 @@ +/******************************************************************************* + * 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 + * + ******************************************************************************/ +package io.github.dsheirer.dsp.psk; + +import io.github.dsheirer.dsp.psk.pll.IPhaseLockedLoop; +import io.github.dsheirer.dsp.symbol.Dibit; +import io.github.dsheirer.sample.complex.Complex; + +public class DQPSKGardnerDemodulator extends PSKDemodulator +{ + protected DQPSKGardnerSymbolEvaluator mSymbolEvaluator = new DQPSKGardnerSymbolEvaluator(); + private Complex mPreviousCurrentSample = new Complex(0, 0); + private Complex mPreviousMiddleSample = new Complex(0, 0); + private Complex mMiddleSymbol = new Complex(0, 0); + protected Complex mCurrentSymbol = new Complex(0, 0); + + /** + * Implements a Differential QPSK demodulator using a Costas Loop (PLL) and a Gardner timing error detector. + * + * This decoder is optimized for P25 Linear Simulcast Modulation (LSM). + * + * @param phaseLockedLoop for tracking carrier frequency error + * @param interpolatingSampleBuffer to hold samples for interpolating a symbol + */ + public DQPSKGardnerDemodulator(IPhaseLockedLoop phaseLockedLoop, InterpolatingSampleBuffer interpolatingSampleBuffer) + { + super(interpolatingSampleBuffer, phaseLockedLoop); + } + + public void dispose() + { + } + + @Override + protected void calculateSymbol() + { + //Note: the interpolating sample buffer holds 2 symbols worth of samples and the current sample method points + //to the sample at the mid-point between those 2 symbol periods and the middle sample method points to the + //sample that is half a symbol period after the current sample. Since we need a middle sample and a current + //symbol sample for the gardner calculation, we'll treat the interpolating buffer's current sample as the + //gardner mid-point and we'll treat the interpolating buffer's mid-point sample as the current symbol + //sample (ie flip-flopped) + Complex middleSample = getInterpolatingSampleBuffer().getCurrentSample(); + Complex currentSample = getInterpolatingSampleBuffer().getMiddleSample(); + + //Differential decode middle and current symbols by calculating the angular rotation between the previous and + //current samples (current sample x complex conjugate of previous sample). + mMiddleSymbol.setInphase(Complex.multiplyInphase(middleSample.inphase(), middleSample.quadrature(), + mPreviousMiddleSample.inphase(), -mPreviousMiddleSample.quadrature())); + mMiddleSymbol.setQuadrature(Complex.multiplyQuadrature(middleSample.inphase(), middleSample.quadrature(), + mPreviousMiddleSample.inphase(), -mPreviousMiddleSample.quadrature())); + + mCurrentSymbol.setInphase(Complex.multiplyInphase(currentSample.inphase(), currentSample.quadrature(), + mPreviousCurrentSample.inphase(), -mPreviousCurrentSample.quadrature())); + mCurrentSymbol.setQuadrature(Complex.multiplyQuadrature(currentSample.inphase(), currentSample.quadrature(), + mPreviousCurrentSample.inphase(), -mPreviousCurrentSample.quadrature())); + + //Set gain to unity before we calculate the error value + mMiddleSymbol.normalize(); + mCurrentSymbol.normalize(); + + //Pass symbols to evaluator to determine timing and phase error and make symbol decision + mSymbolEvaluator.setSymbols(mMiddleSymbol, mCurrentSymbol); + + //Update symbol timing error + getInterpolatingSampleBuffer().resetAndAdjust(mSymbolEvaluator.getTimingError()); + + //Update PLL phase error + getPLL().adjust(mSymbolEvaluator.getPhaseError()); + + //Store current samples/symbols for next symbol calculation + mPreviousMiddleSample.setValues(middleSample); + mPreviousCurrentSample.setValues(currentSample); + + broadcast(mSymbolEvaluator.getSymbolDecision()); + } +} diff --git a/src/main/java/io/github/dsheirer/dsp/psk/DQPSKGardnerDemodulatorInstrumented.java b/src/main/java/io/github/dsheirer/dsp/psk/DQPSKGardnerDemodulatorInstrumented.java new file mode 100644 index 00000000..272b0534 --- /dev/null +++ b/src/main/java/io/github/dsheirer/dsp/psk/DQPSKGardnerDemodulatorInstrumented.java @@ -0,0 +1,127 @@ +/******************************************************************************* + * 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 + * + ******************************************************************************/ +package io.github.dsheirer.dsp.psk; + +import io.github.dsheirer.dsp.psk.pll.CostasLoop; +import io.github.dsheirer.dsp.psk.pll.IPhaseLockedLoop; +import io.github.dsheirer.sample.Listener; +import io.github.dsheirer.sample.complex.Complex; + +public class DQPSKGardnerDemodulatorInstrumented extends DQPSKGardnerDemodulator +{ + private Listener mSymbolDecisionDataListener; + private Listener mSamplesPerSymbolListener; + private Listener mComplexSymbolListener; + private Listener mPLLErrorListener; + private Listener mPLLFrequencyListener; + private double mSampleRate; + + /** + * Implements a Differential QPSK demodulator using a Costas Loop (PLL) and a Gardner timing error detector. + * + * This decoder is optimized for P25 Linear Simulcast Modulation (LSM). + * + * @param phaseLockedLoop for tracking carrier frequency error + * @param interpolatingSampleBuffer to hold samples for interpolating a symbol + */ + public DQPSKGardnerDemodulatorInstrumented(IPhaseLockedLoop phaseLockedLoop, + InterpolatingSampleBuffer interpolatingSampleBuffer, + double sampleRate) + { + super(phaseLockedLoop, interpolatingSampleBuffer); + mSampleRate = sampleRate; + } + + /** + * Overrides the parent class symbol calculation to capture eye diagram data + */ + protected void calculateSymbol() + { + //Eye diagram listener + if(mSymbolDecisionDataListener != null) + { + mSymbolDecisionDataListener.receive( + ((InterpolatingSampleBufferInstrumented)getInterpolatingSampleBuffer()).getSymbolDecisionData()); + } + + super.calculateSymbol(); + + if(mSamplesPerSymbolListener != null) + { + mSamplesPerSymbolListener.receive((double)getInterpolatingSampleBuffer().getSamplingPoint()); + } + + //Send to an external constellation symbol listener when registered + if(mComplexSymbolListener != null) + { + mComplexSymbolListener.receive(mCurrentSymbol); + } + + if(mPLLErrorListener != null) + { + mPLLErrorListener.receive((double)mSymbolEvaluator.getPhaseError()); + } + + if(mPLLFrequencyListener != null) + { + double loopFrequency = ((CostasLoop)getPLL()).getLoopFrequency(); + + loopFrequency *= mSampleRate / (2.0 * Math.PI); + + mPLLFrequencyListener.receive(loopFrequency); + } + } + + /** + * Registers a listener to receive symbol decision data to produce an eye diagram. + */ + public void setSymbolDecisionDataListener(Listener listener) + { + mSymbolDecisionDataListener = listener; + } + + /** + * Registers the listener to receive PLL error values + */ + public void setSamplesPerSymbolListener(Listener listener) + { + mSamplesPerSymbolListener = listener; + } + + /** + * Registers the listener to receive decoded QPSK symbols + */ + public void setComplexSymbolListener(Listener listener) + { + mComplexSymbolListener = listener; + } + + /** + * Registers the listener to receive PLL error values + */ + public void setPLLErrorListener(Listener listener) + { + mPLLErrorListener = listener; + } + + /** + * Registers the listener to receive PLL error values + */ + public void setPLLFrequencyListener(Listener listener) + { + mPLLFrequencyListener = listener; + } +} diff --git a/src/main/java/io/github/dsheirer/dsp/psk/DQPSKGardnerSymbolEvaluator.java b/src/main/java/io/github/dsheirer/dsp/psk/DQPSKGardnerSymbolEvaluator.java new file mode 100644 index 00000000..e54ae90a --- /dev/null +++ b/src/main/java/io/github/dsheirer/dsp/psk/DQPSKGardnerSymbolEvaluator.java @@ -0,0 +1,158 @@ +/******************************************************************************* + * 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 + * + ******************************************************************************/ +package io.github.dsheirer.dsp.psk; + +import io.github.dsheirer.dsp.symbol.Dibit; +import io.github.dsheirer.sample.complex.Complex; + +public class DQPSKGardnerSymbolEvaluator implements IPSKSymbolEvaluator +{ + private float mPhaseError = 0.0f; + private float mTimingError = 0.0f; + private Dibit mSymbolDecision = Dibit.D00_PLUS_1; + private Complex mPreviousSymbol = new Complex(0, 0); + + /** + * Differential QPSK Decision-directed symbol phase and timing error detector and symbol decision slicer. + * + * Symbol decision is based on the closest reference quadrant for the sampled symbol. + * + * Phase error is calculated as the angular distance of the sampled symbol from the reference symbol. + * + * Timing error is calculated using the Gardner method by comparing the previous symbol to the current symbol and + * amplifying the delta between the two using the intra-symbol sample to form the timing error. + */ + public DQPSKGardnerSymbolEvaluator() + { + } + + /** + * Sets the middle and current symbols to be evaluated for phase and timing errors and to determine the + * transmitted symbol relative to the closest reference symbol. After invoking this method, you can access the + * phase and timing errors and the symbol decision via their respective accessor methods. + * + * Phase and timing error values are calculated by first determining the symbol and then calculating the phase + * and timing errors relative to the reference symbol. The timing error is corrected with the appropriate sign + * relative to the angular vector rotation so that the error value indicates the correct error direction. + * + * @param middle interpolated differentially-decoded sample that falls midway between previous/current symbols + * @param current interpolated differentially-decoded symbol + */ + public void setSymbols(Complex middle, Complex current) + { + /* Gardner timing error calculations */ + float errorInphase = (mPreviousSymbol.inphase() - current.inphase()) * middle.inphase(); + float errorQuadrature = (mPreviousSymbol.quadrature() - current.quadrature()) * middle.quadrature(); + mTimingError = normalize(errorInphase + errorQuadrature, 1.0f); + + //Calculate phase error and determine the transmitted dibit from the symbol + if(Math.abs(current.inphase()) > Math.abs(current.quadrature())) + { + if(current.inphase() > 0) + { + mSymbolDecision = Dibit.D10_MINUS_1; + mPhaseError = -current.quadrature(); + } + else + { + mSymbolDecision = Dibit.D01_PLUS_3; + mPhaseError = current.quadrature(); + } + } + else + { + if(current.quadrature() > 0) + { + mSymbolDecision = Dibit.D00_PLUS_1; + mPhaseError = current.inphase(); + } + else + { + mSymbolDecision = Dibit.D11_MINUS_3; + mPhaseError = -current.inphase(); + } + } + + mPreviousSymbol.setValues(current); + } + + /** + * Constrains value to the range of ( -maximum <> maximum ) + */ + public static float clip(float value, float maximum) + { + if(value > maximum) + { + return maximum; + } + else if(value < -maximum) + { + return -maximum; + } + + return value; + } + + /** + * Constrains timing error to +/- the maximum value and corrects any + * floating point invalid numbers + */ + private float normalize(float error, float maximum) + { + if(Float.isNaN(error)) + { + return 0.0f; + } + else + { + return clip(error, maximum); + } + } + + + /** + * Phase error of the symbol relative to the nearest reference symbol. + * + * @return phase error in radians of distance from the reference symbol. + */ + @Override + public float getPhaseError() + { + return mPhaseError; + } + + /** + * Timing error of the symbol relative to the nearest reference symbol. + * + * @return timing error in radians of angular distance from the reference symbol recognizing that the symbol + * originates at zero radians and rotates toward the intended reference symbol, therefore the error value indicates + * if the symbol was sampled early (-) or late (+) relative to the reference symbol. + */ + @Override + public float getTimingError() + { + return mTimingError; + } + + /** + * Reference symbol that is closest to the transmitted/sampled symbol. + */ + @Override + public Dibit getSymbolDecision() + { + return mSymbolDecision; + } +} diff --git a/src/main/java/io/github/dsheirer/dsp/psk/IDQPSKSymbolEvaluator.java b/src/main/java/io/github/dsheirer/dsp/psk/IPSKSymbolEvaluator.java similarity index 71% rename from src/main/java/io/github/dsheirer/dsp/psk/IDQPSKSymbolEvaluator.java rename to src/main/java/io/github/dsheirer/dsp/psk/IPSKSymbolEvaluator.java index d0213a0d..fa1d9d12 100644 --- a/src/main/java/io/github/dsheirer/dsp/psk/IDQPSKSymbolEvaluator.java +++ b/src/main/java/io/github/dsheirer/dsp/psk/IPSKSymbolEvaluator.java @@ -15,20 +15,8 @@ ******************************************************************************/ package io.github.dsheirer.dsp.psk; -import io.github.dsheirer.dsp.symbol.Dibit; -import io.github.dsheirer.sample.complex.Complex; - - -public interface IDQPSKSymbolEvaluator +public interface IPSKSymbolEvaluator { - /** - * Sets the sampled symbol to be evaluated. Note: the symbol's gain value MUST be normalized to the unit circle. - * - * @param preceding symbol used to detect (counter-)clock-wise rotation - * @param symbol currently being sampled - */ - void setSymbol(Complex preceding, Complex symbol); - /** * Phase error of the symbol relative to the closest reference symbol * @return angular phase error in radians @@ -43,8 +31,8 @@ public interface IDQPSKSymbolEvaluator float getTimingError(); /** - * Dibit decision indicating the reference symbol location closest to the symbol. + * Symbol decision indicating the reference symbol location closest to the symbol. * @return symbol decision */ - Dibit getSymbolDecision(); + T getSymbolDecision(); } diff --git a/src/main/java/io/github/dsheirer/dsp/psk/InterpolatingSampleBuffer.java b/src/main/java/io/github/dsheirer/dsp/psk/InterpolatingSampleBuffer.java index 15adbd9a..5b64f338 100644 --- a/src/main/java/io/github/dsheirer/dsp/psk/InterpolatingSampleBuffer.java +++ b/src/main/java/io/github/dsheirer/dsp/psk/InterpolatingSampleBuffer.java @@ -24,6 +24,7 @@ public class InterpolatingSampleBuffer private Complex mPrecedingSample = new Complex(0,0); private Complex mCurrentSample = new Complex(0,0); + private Complex mMiddleSample = new Complex(0,0); private float[] mDelayLineInphase; private float[] mDelayLineQuadrature; @@ -42,8 +43,9 @@ public class InterpolatingSampleBuffer /** * Buffer to store complex sample data and produce interpolated samples. * @param samplesPerSymbol + * @param symbolTimingGain for the symbol timing error adjustments */ - public InterpolatingSampleBuffer(float samplesPerSymbol) + public InterpolatingSampleBuffer(float samplesPerSymbol, float symbolTimingGain) { mSamplingPoint = samplesPerSymbol; mDetectedSamplesPerSymbol = samplesPerSymbol; @@ -52,6 +54,7 @@ public class InterpolatingSampleBuffer mTwiceSamplesPerSymbol = (int)Math.floor(2.0 * samplesPerSymbol); mDelayLineInphase = new float[2 * mTwiceSamplesPerSymbol]; mDelayLineQuadrature = new float[2 * mTwiceSamplesPerSymbol]; + mDetectedSamplesPerSymbolGain = symbolTimingGain * mSampleCounterGain * mSampleCounterGain; } /** @@ -125,18 +128,21 @@ public class InterpolatingSampleBuffer } /** - * Un-interpolated sample that precedes the current interpolated sampling point. The current sampling point falls - * somewhere between sample index 3 and index 4, therefore the preceding sample is located at index 3. + * Un-interpolated sample that precedes the current interpolated sampling point. The current interpolated sampling + * point falls somewhere between sample index 3 and index 4 and the sample is interpolated from sample + * indexes 0 - 7, therefore the uninterpolated sample that immediately precedes the current sample is + * located at index 3. */ public Complex getPrecedingSample() { - mPrecedingSample.setValues(mDelayLineInphase[mDelayLinePointer + 4], mDelayLineQuadrature[mDelayLinePointer + 3]); + mPrecedingSample.setValues(mDelayLineInphase[mDelayLinePointer + 3], mDelayLineQuadrature[mDelayLinePointer + 3]); return mPrecedingSample; } /** - * Interpolated middle sample for the symbol. Note: this method should only be invoked after testing for a - * complete symbol with the hasSymbol() method. + * Interpolated current sample for the symbol. + * + * Note: this method should only be invoked after testing for a complete symbol with the hasSymbol() method. */ public Complex getCurrentSample() { @@ -145,6 +151,21 @@ public class InterpolatingSampleBuffer return mCurrentSample; } + /** + * Interpolated sample that is 1/2 symbol away from (after) the current sample. + * + * Note: this method should only be invoked after testing for a complete symbol with the hasSymbol() method. + */ + public Complex getMiddleSample() + { + float halfDetectedSamplesPerSymbol = mDetectedSamplesPerSymbol / 2.0f; + + //Interpolated sample that is half a symbol away from (occurred before) the current sample. + mMiddleSample.setValues(getInphase(halfDetectedSamplesPerSymbol), + getQuadrature(halfDetectedSamplesPerSymbol)); + return mMiddleSample; + } + /** * Returns the interpolated inphase value for the specified offset * @param interpolation into the buffer to calculate the interpolated sample @@ -152,7 +173,15 @@ public class InterpolatingSampleBuffer */ public float getInphase(float interpolation) { - return mInterpolator.filter(mDelayLineInphase, mDelayLinePointer + 1, interpolation); + if(interpolation < 1.0f) + { + return mInterpolator.filter(mDelayLineInphase, mDelayLinePointer, interpolation); + } + else + { + int offset = (int)Math.floor(interpolation); + return mInterpolator.filter(mDelayLineInphase, mDelayLinePointer + offset, interpolation - offset); + } } /** @@ -162,6 +191,14 @@ public class InterpolatingSampleBuffer */ public float getQuadrature(float interpolation) { - return mInterpolator.filter(mDelayLineQuadrature, mDelayLinePointer + 1, interpolation); + if(interpolation < 1.0f) + { + return mInterpolator.filter(mDelayLineQuadrature, mDelayLinePointer, interpolation); + } + else + { + int offset = (int)Math.floor(interpolation); + return mInterpolator.filter(mDelayLineQuadrature, mDelayLinePointer + offset, interpolation - offset); + } } } diff --git a/src/main/java/io/github/dsheirer/dsp/psk/InterpolatingSampleBufferInstrumented.java b/src/main/java/io/github/dsheirer/dsp/psk/InterpolatingSampleBufferInstrumented.java index 6c71fc25..dcdef399 100644 --- a/src/main/java/io/github/dsheirer/dsp/psk/InterpolatingSampleBufferInstrumented.java +++ b/src/main/java/io/github/dsheirer/dsp/psk/InterpolatingSampleBufferInstrumented.java @@ -21,10 +21,9 @@ public class InterpolatingSampleBufferInstrumented extends InterpolatingSampleBu { private SymbolDecisionData mSymbolDecisionData; - public InterpolatingSampleBufferInstrumented(float samplesPerSymbol) + public InterpolatingSampleBufferInstrumented(float samplesPerSymbol, float symbolTimingGain) { - super(samplesPerSymbol); - + super(samplesPerSymbol, symbolTimingGain); mSymbolDecisionData = new SymbolDecisionData((int)samplesPerSymbol); } diff --git a/src/main/java/io/github/dsheirer/dsp/psk/LSMDemodulator.java b/src/main/java/io/github/dsheirer/dsp/psk/LSMDemodulator.java deleted file mode 100644 index 0536f7f2..00000000 --- a/src/main/java/io/github/dsheirer/dsp/psk/LSMDemodulator.java +++ /dev/null @@ -1,614 +0,0 @@ -package io.github.dsheirer.dsp.psk; - -/******************************************************************************* - * SDR Trunk - * Copyright (C) 2014 - 2018 Dennis Sheirer - * - * Gardner detector, Costas loop and Interpolator derived from: - * - * OP25 - gardner_costas_cc_impl.cc - * Copyright 2010, 2011, 2012, 2013 KA1RBI (gardner_costas_cc_impl.cc) - * - * OP25 - scope.py - * Copyright 2008-2011 Steve Glass - * Copyright 2011, 2012, 2013 KA1RBI - * - * GNURadio - control_loop and mpsk_receiver_cc - * Copyright 2011,2013 Free Software Foundation, Inc. - * - * 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 - ******************************************************************************/ - -import io.github.dsheirer.buffer.FloatAveragingBuffer; -import io.github.dsheirer.dsp.filter.interpolator.RealInterpolator; -import io.github.dsheirer.dsp.psk.pll.IPhaseLockedLoop; -import io.github.dsheirer.dsp.symbol.Dibit; -import io.github.dsheirer.instrument.Instrumentable; -import io.github.dsheirer.instrument.tap.Tap; -import io.github.dsheirer.instrument.tap.TapGroup; -import io.github.dsheirer.instrument.tap.stream.EyeDiagramData; -import io.github.dsheirer.instrument.tap.stream.EyeDiagramDataTap; -import io.github.dsheirer.sample.Listener; -import io.github.dsheirer.sample.complex.Complex; -import io.github.dsheirer.sample.complex.ComplexSampleListener; -import io.github.dsheirer.source.ISourceEventListener; - -import java.util.ArrayList; -import java.util.Arrays; -import java.util.List; - -/** - * Implements a LSM (Pi/4) demodulator using a Gardner Detector to determine - * optimal sampling timing and a Costas Loop as a phase locked loop synchronized - * with the incoming signal carrier frequency. - * - * Sample Rate: 48000 - * Symbol Rate: 4800 - */ -public class LSMDemodulator implements Instrumentable, ComplexSampleListener -{ - private List mAvailableTaps; - - /* 45 degree rotation angle */ - public static final float THETA = (float)(Math.PI / 4.0d); - - /* 45 degree point */ - public static final Complex POINT_45_DEGREES = - new Complex( (float)Math.sin( THETA ), (float)Math.cos( THETA ) ); - - private Listener mSymbolListener; - - private GardnerSymbolTiming mGardnerDetector = new GardnerSymbolTiming(); - - private CostasLoop mCostasLoop = new CostasLoop(); - - private ISourceEventListener mFrequencyChangeListener; - - private EyeDiagramDataTap mEyeDiagramDataTap; - - public LSMDemodulator() - { - } - - public void dispose() - { - mSymbolListener = null; - mFrequencyChangeListener = null; - } - - public void setSymbolListener( Listener listener ) - { - mSymbolListener = listener; - } - - public void removeSymbolListener() - { - mSymbolListener = null; - } - - @Override - public void receive( float inphase, float quadrature ) - { - mGardnerDetector.receive( inphase, quadrature ); - } - - /** - * Applies a phase correction value to the costas loop to correct when a - * phase lock error is detected in the binary output stream. - * - * @param correction - value in radians - */ - public void correctPhaseError( double correction ) - { - mCostasLoop.correctPhaseError( correction ); - } - - /** - * Constrains value to the range of ( -maximum <> maximum ) - */ - public static float clip( float value, float maximum ) - { - if( value > maximum ) - { - return maximum; - } - else if( value < -maximum ) - { - return -maximum; - } - - return value; - } - - /** - * Constrains timing error to +/- the maximum value and corrects any - * floating point invalid numbers - */ - private float normalize( float error, float maximum ) - { - if( Float.isNaN( error ) ) - { - return 0.0f; - } - else - { - return clip( error, maximum ); - } - } - - public void addListener( ISourceEventListener listener ) - { - mFrequencyChangeListener = listener; - } - - public void removeListener( ISourceEventListener listener ) - { - mFrequencyChangeListener = null; - } - - /** - * Processes the current loop frequency of the costas loop and broadcasts - * tuner frequency offset adjustments 10 times a second as needed to keep - * the frequency offset below half of the maximum frequency - */ - public class FrequencyControl - { - /* Set the trigger threshold at half of the maximum costas loop control - * frequency */ - private static final float CORRECTION_THRESHOLD_FREQUENCY = - (2.0f * (float)Math.PI * 600.0f) / 48000.0f; - - private FloatAveragingBuffer mBuffer = new FloatAveragingBuffer(40); - - private long mFrequencyError = 0; - - private int mCounter = 0; - - public void receive(float frequency) - { -// mCounter++; -// -// if( mCounter >= 480 ) -// { -// float average = mBuffer.get( frequency ); -// mLog.debug( "Avg:" + average + " Threshold:" + CORRECTION_THRESHOLD_FREQUENCY); -// -// boolean correctionNeeded = false; -// -// if( average > CORRECTION_THRESHOLD_FREQUENCY ) -// { -// mFrequencyError--; -// -// correctionNeeded = true; -// } -// else if( average < -CORRECTION_THRESHOLD_FREQUENCY ) -// { -// mFrequencyError++; -// -// correctionNeeded = true; -// } -// -// if( correctionNeeded && mFrequencyChangeListener != null ) -// { -// mLog.debug( "Issuing Frequency Correction: " + mFrequencyError ); -// mFrequencyChangeListener.process( -// new SourceEvent( Attribute.FREQUENCY_ERROR, -// mFrequencyError ) ); -// } -// -// mCounter = 0; -// } -// else -// { -// mBuffer.get( frequency ); -// } - } - } - - /** - * Gardner symbol timing detector - */ - public class GardnerSymbolTiming implements ComplexSampleListener - { - public static final int HALF_SAMPLES_PER_SYMBOL = 5; - public static final int SAMPLES_PER_SYMBOL = 10; - public static final int TWICE_SAMPLES_PER_SYMBOL = 20; - - private float[] mDelayLineInphase = new float[2 * TWICE_SAMPLES_PER_SYMBOL]; - private float[] mDelayLineQuadrature = new float[2 * TWICE_SAMPLES_PER_SYMBOL]; - - private int mDelayLinePointer = 0; - - /* Sampling point */ - private float mMu = 10.0f; - private float mGainMu = 0.05f; - - /* Samples per symbol */ - private float mOmega = 10.0f; - private float mGainOmega = 0.1f * mGainMu * mGainMu; - private float mOmegaRel = 0.005f; - private float mOmegaMid = 10.0f; - - private RealInterpolator mInterpolator = new RealInterpolator(1.0f); - - private Complex mPreviousSample = new Complex(0, 0); - private Complex mPreviousMiddleSample = new Complex(0, 0); - private Complex mPreviousSymbol = new Complex(0, 0); - - /** - * Provides symbol sampling timing control - */ - public GardnerSymbolTiming() - { - } - - @Override - public void receive(float inphase, float quadrature) - { - /* Count down samples per symbol until we calculate the symbol */ - mMu--; - - mCostasLoop.increment(); - - /* Mix incoming sample with costas loop to remove any rotation - * that is present from a mis-tuned carrier frequency */ - - Complex costas = mCostasLoop.getCurrentVector(); - - float derotatedInphase = Complex.multiplyInphase(inphase, quadrature, costas.inphase(), costas.quadrature()); - float derotatedQuadrature = Complex.multiplyQuadrature(inphase, quadrature, costas.inphase(), costas.quadrature()); - - /* Fill up the delay line to use with the interpolator */ - mDelayLineInphase[mDelayLinePointer] = derotatedInphase; - mDelayLineInphase[mDelayLinePointer + TWICE_SAMPLES_PER_SYMBOL] = derotatedInphase; - - mDelayLineQuadrature[mDelayLinePointer] = derotatedQuadrature; - mDelayLineQuadrature[mDelayLinePointer + TWICE_SAMPLES_PER_SYMBOL] = derotatedQuadrature; - - /* Increment pointer and keep pointer in bounds */ - mDelayLinePointer = (mDelayLinePointer + 1) % TWICE_SAMPLES_PER_SYMBOL; - - /* Calculate the symbol once we've stored enough samples */ - if(mMu <= 1.0f) - { - float half_omega = mOmega / 2.0f; - int half_sps = (int)Math.floor(half_omega); - float half_mu = mMu + half_omega - (float)half_sps; - - if(half_mu > 1.0) - { - half_mu -= 1.0; - half_sps += 1; - } - - /* Calculate interpolated middle sample and current sample */ - float middleSampleInphase = mInterpolator.filter( - mDelayLineInphase, mDelayLinePointer, mMu); - float middleSampleQuadrature = mInterpolator.filter( - mDelayLineQuadrature, mDelayLinePointer, mMu); - - int index = mDelayLinePointer + half_sps; - - float currentSampleInphase = mInterpolator.filter( - mDelayLineInphase, index, half_mu); - float currentSampleQuadrature = mInterpolator.filter( - mDelayLineQuadrature, index, half_mu); - - /* Multiply current sample and conjugate (negated quadrature) of - * previous sample to get symbols to use for gardner error feedback */ - Complex middleSymbol = Complex.multiply(middleSampleInphase, - middleSampleQuadrature, mPreviousMiddleSample.conjugate()); - Complex currentSymbol = Complex.multiply(currentSampleInphase, - currentSampleQuadrature, mPreviousSample.conjugate()); - - /* Set gain to unity */ - middleSymbol.normalize(); - currentSymbol.normalize(); - - /* Gardner timing error calculations */ - float errorInphase = (mPreviousSymbol.inphase() - - currentSymbol.inphase()) * middleSymbol.inphase(); - - float errorQuadrature = (mPreviousSymbol.quadrature() - - currentSymbol.quadrature()) * middleSymbol.quadrature(); - - float gardnerError = normalize(errorInphase + errorQuadrature, 1.0f); - - if(mEyeDiagramDataTap != null) - { - mEyeDiagramDataTap.receive( - new EyeDiagramData( - Arrays.copyOfRange(mDelayLineInphase, 0, 20), - Arrays.copyOfRange(mDelayLineQuadrature, 0, 20), - mMu, (float)half_sps + half_mu, gardnerError)); - } - - /* mOmega is samples per symbol and is constrained to floating - * between +/- .005 of the nominal 10.0 samples per symbol */ - mOmega = mOmega + mGainOmega * gardnerError; - mOmega = mOmegaMid + clip(mOmega - mOmegaMid, mOmegaRel); - - /* Adjust sample timing based on error of current sample */ - mMu += mOmega + (mGainMu * gardnerError); - - /* Store current samples/symbols to use for the next period */ - mPreviousSample.setValues(currentSampleInphase, currentSampleQuadrature); - mPreviousMiddleSample.setValues(middleSampleInphase, middleSampleQuadrature); - mPreviousSymbol = currentSymbol; - - /* Update costas loop using phase error present in current - * symbol. The symbol is rotated from star orientation to polar - * orientation to simplify error calculation */ - mCostasLoop.receive(currentSymbol); -// mCostasLoop.receive( -// ComplexSample.multiply( currentSymbol, POINT_45_DEGREES ) ); - - /* Dispatch the differentiated symbol to the registered listener */ - if(mSymbolListener != null) - { - mSymbolListener.receive(currentSymbol); - } - } - } - } - - /** - * Costas Loop - phase locked loop synchronized to the frequency offset of - * the incoming signal to enable the signal to be mixed down to zero offset - * for proper synchronization. The mFrequency value indicates the detected - * frequency offset. We attempt to keep that value close to zero by issuing - * frequency adjustments to the tuner channel source. - * - * Most of the costas loop code was ported from gnuradio/control_loop and - * gnuradio/mpsk_receiver_cc using initialization values from KA1RBI's - * OP25/cqpsk.py - */ - public class CostasLoop implements Listener, IPhaseLockedLoop - { - public static final double TWO_PI = 2.0 * Math.PI; - - private static final float MAXIMUM_FREQUENCY = - (2400.0f * (float)TWO_PI) / 48000.0f; - - /* http://www.trondeau.com/blog/2011/8/13/control-loop-gain-values.html */ - private float mDamping = (float)Math.sqrt(2.0) / 2.0f; - - /* Use denominator between 100 (most) and 400 (least) to adjust costas - * loop control level */ - private float mLoopBandwidth = (float)(TWO_PI / 400.0d); - - private float mAlphaGain = (4.0f * mDamping * mLoopBandwidth) / - (1.0f + (2.0f * mDamping * mLoopBandwidth) + - (mLoopBandwidth * mLoopBandwidth)); - - private float mBetaGain = (4.0f * mLoopBandwidth * mLoopBandwidth) / - (1.0f + (2.0f * mDamping * mLoopBandwidth) + - (mLoopBandwidth * mLoopBandwidth)); - - private float mLoopPhase = 0.0f; - - private float mLoopFrequency = 0.0f; - - private FrequencyControl mFrequencyControl = new FrequencyControl(); - - public CostasLoop() - { - } - - /** - * Applies a phase correction value to the current phasor. Use this - * method to apply correction when a +/- 90, or 180 degree phase error - * is detected in the binary output stream. - * - * If the supplied correction value places the loop frequency outside - * of the max frequency, then the frequency will be corrected 360 - * degrees in the opposite direction to maintain within the max - * frequency bounds. - * - * @param correction - correction value in radians - */ - public void correctPhaseError(double correction) - { - mLoopFrequency += correction; - - if(mLoopFrequency > MAXIMUM_FREQUENCY) - { - mLoopFrequency -= 2.0d * MAXIMUM_FREQUENCY; - } - - if(mLoopFrequency < -MAXIMUM_FREQUENCY) - { - mLoopFrequency += 2.0d * MAXIMUM_FREQUENCY; - } - } - - /** - * Increments the phase of the loop - */ - public void increment() - { - mLoopPhase += mLoopFrequency; - - /* Keep the loop phase in bounds */ - unwrapPhase(); - } - - @Override - public void reset() - { - mLoopPhase = 0.0f; - mLoopFrequency = 0.0f; - } - - @Override - public void adjust(double error) - { - adjust(error); - } - - @Override - public void correctInversion(double correction) - { - correctPhaseError(correction); - } - - private void unwrapPhase() - { - while(mLoopPhase > TWO_PI) - { - mLoopPhase -= TWO_PI; - } - - while(mLoopPhase < -TWO_PI) - { - mLoopPhase += TWO_PI; - } - } - - /** - * Current vector of the loop rotated by THETA degrees. - */ - public Complex getCurrentVector() - { -// return ComplexSample.fromAngle( mLoopPhase + THETA ); - return Complex.fromAngle(mLoopPhase); - } - - @Override - public Complex incrementAndGetCurrentVector() - { - return null; - } - - @Override - public void receive(Complex complex) - { - /* Calculate phase error */ - float phaseError = getPhaseError(complex); - - Dibit d = mQPSKSymbolDecoder.decode(complex); -// mLog.debug( "Dibit: " + d.name() + " Error: " + phaseError ); - - /* Adjust for phase error */ - adjust(phaseError); - } - - /** - * Updates the costas loop frequency and phase to adjust for the phase - * error value - * - * @param phase_error - (-)= late and (+)= early - */ - private void adjust(float phase_error) - { - mLoopFrequency += mBetaGain * phase_error; - mLoopPhase += mLoopFrequency + mAlphaGain * phase_error; - - /* Maintain phase between +/- 2 * PI */ - unwrapPhase(); - - /* Limit frequency to +/- maximum loop frequency */ - limitFrequency(); - } - - /** - * Constrains the frequency within the bounds of +/- loop frequency - */ - private void limitFrequency() - { - /* Check for and issue tuner offset correction */ - mFrequencyControl.receive(mLoopFrequency); - - if(mLoopFrequency > MAXIMUM_FREQUENCY) - { - mLoopFrequency = MAXIMUM_FREQUENCY; - } - - if(mLoopFrequency < -MAXIMUM_FREQUENCY) - { - mLoopFrequency = -MAXIMUM_FREQUENCY; - } - } - - /** - * Detects rotational error present when the costas loop is not aligned - * with the carrier frequency. Provides error feedback to adjust - * mixer frequency. - */ - public float getPhaseError(Complex complex) - { - float phase_error = 0; - - if(Math.abs(complex.inphase()) > Math.abs(complex.quadrature())) - { - if(complex.inphase() > 0) - { - phase_error = -complex.quadrature(); - } - else - { - phase_error = complex.quadrature(); - } - } - else - { - if(complex.quadrature() > 0) - { - phase_error = complex.inphase(); - } - else - { - phase_error = -complex.inphase(); - } - } - - return phase_error; - } - } - - @Override - public List getTapGroups() - { - if(mAvailableTaps == null) - { - mAvailableTaps = new ArrayList<>(); - - TapGroup group = new TapGroup("LSM Demodulator"); - - group.add(new EyeDiagramDataTap("Eye Diagram", 0, 4800)); - - mAvailableTaps.add(group); - } - - return mAvailableTaps; - } - - @Override - public void registerTap(Tap tap) - { - if(tap instanceof EyeDiagramDataTap) - { - mEyeDiagramDataTap = (EyeDiagramDataTap)tap; - } - } - - @Override - public void unregisterTap(Tap tap) - { - if(tap instanceof EyeDiagramDataTap) - { - mEyeDiagramDataTap = null; - } - } -} diff --git a/src/main/java/io/github/dsheirer/dsp/psk/PSKDemodulator.java b/src/main/java/io/github/dsheirer/dsp/psk/PSKDemodulator.java new file mode 100644 index 00000000..c187a408 --- /dev/null +++ b/src/main/java/io/github/dsheirer/dsp/psk/PSKDemodulator.java @@ -0,0 +1,137 @@ +/******************************************************************************* + * 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 + * + ******************************************************************************/ +package io.github.dsheirer.dsp.psk; + +import io.github.dsheirer.dsp.psk.pll.IPhaseLockedLoop; +import io.github.dsheirer.sample.Listener; +import io.github.dsheirer.sample.complex.Complex; +import io.github.dsheirer.sample.complex.ComplexSampleListener; +import io.github.dsheirer.sample.complex.reusable.ReusableComplexBuffer; + +public abstract class PSKDemodulator implements ComplexSampleListener +{ + private InterpolatingSampleBuffer mInterpolatingSampleBuffer; + private IPhaseLockedLoop mPLL; + private Complex mReceivedSample = new Complex(0, 0); + private Listener mSymbolListener; + + /** + * Abstract Phase Shift Keyed (PSK) demodulator + * @param interpolatingSampleBuffer to store complex samples and provide access to interpolated samples + * @param phaseLockedLoop to track and remove frequency offset in the incoming samples + */ + public PSKDemodulator(InterpolatingSampleBuffer interpolatingSampleBuffer, IPhaseLockedLoop phaseLockedLoop) + { + mInterpolatingSampleBuffer = interpolatingSampleBuffer; + mPLL = phaseLockedLoop; + } + + /** + * Registers the listener to receive symbol decisions from this demodulator + */ + public void setSymbolListener(Listener listener) + { + mSymbolListener = listener; + } + + /** + * Broadcasts the symbol decision to the registered symbol listener + * @param symbol + */ + protected void broadcast(T symbol) + { + if(mSymbolListener != null) + { + mSymbolListener.receive(symbol); + } + } + + /** + * Interpolating sample buffer for receiving the incoming complex sample stream and providing access to + * indexed and interpolated samples + */ + protected InterpolatingSampleBuffer getInterpolatingSampleBuffer() + { + return mInterpolatingSampleBuffer; + } + + /** + * Phase Locked Loop (PLL) + */ + protected IPhaseLockedLoop getPLL() + { + return mPLL; + } + + /** + * Processes a (filtered) buffer containing complex samples for decoding + * @param reusableComplexBuffer with complex samples + */ + public void receive(ReusableComplexBuffer reusableComplexBuffer) + { + float[] samples = reusableComplexBuffer.getSamples(); + + for(int x = 0; x < samples.length; x += 2) + { + receive(samples[x], samples[x + 1]); + } + + reusableComplexBuffer.decrementUserCount(); + } + + /** + * Processes a complex sample for decoding. Once sufficient samples are buffered, a symbol decision is made. + * @param inphase value for the sample + * @param quadrature value for the sample + */ + @Override + public void receive(float inphase, float quadrature) + { + //Update current sample with values + mReceivedSample.setValues(inphase, quadrature); + + //Mix current sample with costas loop to remove any rotation that is present from a mis-tuned carrier frequency + mReceivedSample.multiply(mPLL.incrementAndGetCurrentVector()); + + //Store the sample in the interpolating buffer + mInterpolatingSampleBuffer.receive(mReceivedSample); + + //Calculate the symbol once we've stored enough samples + if(mInterpolatingSampleBuffer.hasSymbol()) + { + calculateSymbol(); + } + } + + protected abstract void calculateSymbol(); + + /** + * Constrains value to the range of ( -maximum <> maximum ) + */ + public static float clip(float value, float maximum) + { + if(value > maximum) + { + return maximum; + } + else if(value < -maximum) + { + return -maximum; + } + + return value; + } +} diff --git a/src/main/java/io/github/dsheirer/instrument/gui/viewer/DemodulatorViewerFX.java b/src/main/java/io/github/dsheirer/instrument/gui/viewer/DemodulatorViewerFX.java index 5e9745a6..5af21746 100644 --- a/src/main/java/io/github/dsheirer/instrument/gui/viewer/DemodulatorViewerFX.java +++ b/src/main/java/io/github/dsheirer/instrument/gui/viewer/DemodulatorViewerFX.java @@ -17,6 +17,7 @@ package io.github.dsheirer.instrument.gui.viewer; import io.github.dsheirer.instrument.gui.viewer.decoder.DecoderPaneFactory; import io.github.dsheirer.module.decode.DecoderType; +import io.github.dsheirer.module.decode.p25.P25Decoder; import javafx.application.Application; import javafx.application.Platform; import javafx.event.ActionEvent; @@ -50,7 +51,7 @@ public class DemodulatorViewerFX extends Application public void start(Stage primaryStage) throws Exception { mStage = primaryStage; - setTitle(null); + setTitle(); BorderPane borderPane = new BorderPane(); borderPane.setTop(getMenuBar()); @@ -62,6 +63,11 @@ public class DemodulatorViewerFX extends Application mStage.show(); } + private void setTitle() + { + mStage.setTitle("Demodulator Viewer FX"); + } + private void setTitle(DecoderType decoderType) { if(decoderType != null) @@ -70,7 +76,19 @@ public class DemodulatorViewerFX extends Application } else { - mStage.setTitle("Demodulator Viewer FX"); + setTitle(); + } + } + + private void setTitle(P25Decoder.Modulation modulation) + { + if(modulation != null) + { + mStage.setTitle("Demodulator Viewer FX - P25 " + modulation.getLabel()); + } + else + { + setTitle(); } } @@ -136,19 +154,52 @@ public class DemodulatorViewerFX extends Application { if(DecoderPaneFactory.isSupported(decoderType)) { - MenuItem decoderMenuItem = new MenuItem(decoderType.getShortDisplayString()); - - decoderMenuItem.setOnAction(new EventHandler() + if(decoderType == DecoderType.P25_PHASE1) { - @Override - public void handle(ActionEvent event) - { - getViewerDesktop().setDecoder(decoderType); - setTitle(decoderType); - } - }); + MenuItem c4fmDecoderMenuItem = new MenuItem("P25 Phase 1 C4FM"); - decoderMenu.getItems().add(decoderMenuItem); + c4fmDecoderMenuItem.setOnAction(new EventHandler() + { + @Override + public void handle(ActionEvent event) + { + getViewerDesktop().setP25Phase1Decoder(P25Decoder.Modulation.C4FM); + setTitle(decoderType); + } + }); + + decoderMenu.getItems().add(c4fmDecoderMenuItem); + + MenuItem lsmDecoderMenuItem = new MenuItem("P25 Phase 1 LSM"); + + lsmDecoderMenuItem.setOnAction(new EventHandler() + { + @Override + public void handle(ActionEvent event) + { + getViewerDesktop().setP25Phase1Decoder(P25Decoder.Modulation.CQPSK); + setTitle(decoderType); + } + }); + + decoderMenu.getItems().add(lsmDecoderMenuItem); + } + else + { + MenuItem decoderMenuItem = new MenuItem(decoderType.getShortDisplayString()); + + decoderMenuItem.setOnAction(new EventHandler() + { + @Override + public void handle(ActionEvent event) + { + getViewerDesktop().setDecoder(decoderType); + setTitle(decoderType); + } + }); + + decoderMenu.getItems().add(decoderMenuItem); + } } } diff --git a/src/main/java/io/github/dsheirer/instrument/gui/viewer/PlaybackController.java b/src/main/java/io/github/dsheirer/instrument/gui/viewer/PlaybackController.java index 1ea7a687..6462901f 100644 --- a/src/main/java/io/github/dsheirer/instrument/gui/viewer/PlaybackController.java +++ b/src/main/java/io/github/dsheirer/instrument/gui/viewer/PlaybackController.java @@ -16,9 +16,9 @@ package io.github.dsheirer.instrument.gui.viewer; import io.github.dsheirer.instrument.gui.viewer.decoder.DecoderPane; -import io.github.dsheirer.sample.Broadcaster; import io.github.dsheirer.sample.Listener; -import io.github.dsheirer.sample.complex.ComplexBuffer; +import io.github.dsheirer.sample.complex.reusable.ReusableBufferBroadcaster; +import io.github.dsheirer.sample.complex.reusable.ReusableComplexBuffer; import io.github.dsheirer.source.IControllableFileSource; import io.github.dsheirer.source.IFrameLocationListener; import io.github.dsheirer.source.wave.ComplexWaveSource; @@ -40,7 +40,7 @@ import javax.sound.sampled.UnsupportedAudioFileException; import java.io.File; import java.io.IOException; -public class PlaybackController extends HBox implements IFrameLocationListener, Listener +public class PlaybackController extends HBox implements IFrameLocationListener, Listener { private final static Logger mLog = LoggerFactory.getLogger(PlaybackController.class); @@ -56,7 +56,7 @@ public class PlaybackController extends HBox implements IFrameLocationListener, private DecoderPane mSampleRateListener; private IControllableFileSource mControllableFileSource; - private Broadcaster mComplexBufferBroadcaster = new Broadcaster<>(); + private ReusableBufferBroadcaster mComplexBufferBroadcaster = new ReusableBufferBroadcaster(); public PlaybackController() { @@ -85,7 +85,7 @@ public class PlaybackController extends HBox implements IFrameLocationListener, { mControllableFileSource = new ComplexWaveSource(file); mControllableFileSource.setListener(this); - ((ComplexWaveSource)mControllableFileSource).setListener((Listener)this); + ((ComplexWaveSource)mControllableFileSource).setListener((Listener)this); mControllableFileSource.open(); if(mSampleRateListener != null) @@ -136,7 +136,7 @@ public class PlaybackController extends HBox implements IFrameLocationListener, /** * Adds listener to receive complex buffers from this playback */ - public void addListener(Listener listener) + public void addListener(Listener listener) { mComplexBufferBroadcaster.addListener(listener); } @@ -144,7 +144,7 @@ public class PlaybackController extends HBox implements IFrameLocationListener, /** * Removes the listener from receiving complex buffers from this playback */ - public void removeListener(Listener listener) + public void removeListener(Listener listener) { mComplexBufferBroadcaster.removeListener(listener); } @@ -304,7 +304,7 @@ public class PlaybackController extends HBox implements IFrameLocationListener, } @Override - public void receive(ComplexBuffer complexBuffer) + public void receive(ReusableComplexBuffer complexBuffer) { mComplexBufferBroadcaster.broadcast(complexBuffer); } diff --git a/src/main/java/io/github/dsheirer/instrument/gui/viewer/ViewerDesktop.java b/src/main/java/io/github/dsheirer/instrument/gui/viewer/ViewerDesktop.java index bdd01f5d..fcdef3f9 100644 --- a/src/main/java/io/github/dsheirer/instrument/gui/viewer/ViewerDesktop.java +++ b/src/main/java/io/github/dsheirer/instrument/gui/viewer/ViewerDesktop.java @@ -18,6 +18,7 @@ package io.github.dsheirer.instrument.gui.viewer; import io.github.dsheirer.instrument.gui.viewer.decoder.DecoderPane; import io.github.dsheirer.instrument.gui.viewer.decoder.DecoderPaneFactory; import io.github.dsheirer.module.decode.DecoderType; +import io.github.dsheirer.module.decode.p25.P25Decoder; import javafx.scene.layout.BorderPane; import java.io.File; @@ -40,14 +41,22 @@ public class ViewerDesktop extends BorderPane } public void setDecoder(DecoderType decoderType) + { + setDecoderPane(DecoderPaneFactory.getDecoderPane(decoderType)); + } + + public void setP25Phase1Decoder(P25Decoder.Modulation modulation) + { + setDecoderPane(DecoderPaneFactory.getP25DecoderPane(modulation)); + } + + private void setDecoderPane(DecoderPane decoderPane) { getPlaybackController().removeListener(getDecoderPane()); getChildren().remove(getDecoderPane()); - - mDecoderPane = DecoderPaneFactory.getDecoderPane(decoderType); + mDecoderPane = decoderPane; setCenter(getDecoderPane()); getPlaybackController().addListener(getDecoderPane()); - getPlaybackController().setSampleRateListener(getDecoderPane()); } diff --git a/src/main/java/io/github/dsheirer/instrument/gui/viewer/chart/ComplexSampleLineChart.java b/src/main/java/io/github/dsheirer/instrument/gui/viewer/chart/ComplexSampleLineChart.java index d2b25b4b..bec81eef 100644 --- a/src/main/java/io/github/dsheirer/instrument/gui/viewer/chart/ComplexSampleLineChart.java +++ b/src/main/java/io/github/dsheirer/instrument/gui/viewer/chart/ComplexSampleLineChart.java @@ -19,7 +19,7 @@ import io.github.dsheirer.buffer.ComplexCircularBuffer; import io.github.dsheirer.dsp.gain.ComplexGain; import io.github.dsheirer.sample.Listener; import io.github.dsheirer.sample.complex.Complex; -import io.github.dsheirer.sample.complex.ComplexBuffer; +import io.github.dsheirer.sample.complex.reusable.ReusableComplexBuffer; import javafx.beans.property.IntegerProperty; import javafx.beans.property.SimpleIntegerProperty; import javafx.beans.value.ChangeListener; @@ -32,7 +32,7 @@ import javafx.scene.chart.XYChart; import org.slf4j.Logger; import org.slf4j.LoggerFactory; -public class ComplexSampleLineChart extends LineChart implements Listener +public class ComplexSampleLineChart extends LineChart implements Listener { private final static Logger mLog = LoggerFactory.getLogger(ComplexSampleLineChart.class); @@ -92,7 +92,7 @@ public class ComplexSampleLineChart extends LineChart implements Listener +public class DecoderPane extends VBox implements Listener { - private Broadcaster mComplexBufferBroadcaster = new Broadcaster<>(); + private ReusableBufferBroadcaster mComplexBufferBroadcaster = new ReusableBufferBroadcaster(); private DecoderType mDecoderType; public DecoderPane(DecoderType decoderType) @@ -41,17 +41,17 @@ public class DecoderPane extends VBox implements Listener } @Override - public void receive(ComplexBuffer complexBuffer) + public void receive(ReusableComplexBuffer reusableComplexBuffer) { - mComplexBufferBroadcaster.broadcast(complexBuffer); + mComplexBufferBroadcaster.broadcast(reusableComplexBuffer); } - public void addListener(Listener listener) + public void addListener(Listener listener) { mComplexBufferBroadcaster.addListener(listener); } - public void removeListener(Listener listener) + public void removeListener(Listener listener) { mComplexBufferBroadcaster.removeListener(listener); } diff --git a/src/main/java/io/github/dsheirer/instrument/gui/viewer/decoder/DecoderPaneFactory.java b/src/main/java/io/github/dsheirer/instrument/gui/viewer/decoder/DecoderPaneFactory.java index b069204e..03590922 100644 --- a/src/main/java/io/github/dsheirer/instrument/gui/viewer/decoder/DecoderPaneFactory.java +++ b/src/main/java/io/github/dsheirer/instrument/gui/viewer/decoder/DecoderPaneFactory.java @@ -16,6 +16,7 @@ package io.github.dsheirer.instrument.gui.viewer.decoder; import io.github.dsheirer.module.decode.DecoderType; +import io.github.dsheirer.module.decode.p25.P25Decoder; import java.util.EnumSet; @@ -31,7 +32,23 @@ public class DecoderPaneFactory switch(decoderType) { case P25_PHASE1: - return new P25Phase1Pane(); + throw new IllegalArgumentException("Use the getP25DecoderPane() method for P25 decoder type"); + } + + return getDefaultPane(); + } + + /** + * Creates a decoder pane for the P25 decoder type + */ + public static DecoderPane getP25DecoderPane(P25Decoder.Modulation modulation) + { + switch(modulation) + { + case C4FM: + return new P25Phase1C4FMPane(); + case CQPSK: + return new P25Phase1LSMPane(); } return getDefaultPane(); diff --git a/src/main/java/io/github/dsheirer/instrument/gui/viewer/decoder/P25Phase1C4FMPane.java b/src/main/java/io/github/dsheirer/instrument/gui/viewer/decoder/P25Phase1C4FMPane.java new file mode 100644 index 00000000..66ac5153 --- /dev/null +++ b/src/main/java/io/github/dsheirer/instrument/gui/viewer/decoder/P25Phase1C4FMPane.java @@ -0,0 +1,194 @@ +/******************************************************************************* + * 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 + * + ******************************************************************************/ +package io.github.dsheirer.instrument.gui.viewer.decoder; + +import io.github.dsheirer.instrument.gui.viewer.chart.ComplexSampleLineChart; +import io.github.dsheirer.instrument.gui.viewer.chart.DoubleLineChart; +import io.github.dsheirer.instrument.gui.viewer.chart.EyeDiagramChart; +import io.github.dsheirer.instrument.gui.viewer.chart.PhaseLineChart; +import io.github.dsheirer.instrument.gui.viewer.chart.SymbolChart; +import io.github.dsheirer.message.Message; +import io.github.dsheirer.module.decode.DecoderType; +import io.github.dsheirer.module.decode.p25.P25DecoderC4FMInstrumented; +import io.github.dsheirer.sample.Listener; +import io.github.dsheirer.sample.complex.reusable.ReusableBufferBroadcaster; +import javafx.scene.layout.HBox; +import javafx.scene.layout.Priority; +import org.slf4j.Logger; +import org.slf4j.LoggerFactory; + +public class P25Phase1C4FMPane extends DecoderPane +{ + private final static Logger mLog = LoggerFactory.getLogger(P25Phase1C4FMPane.class); + + private HBox mSampleChartBox; + private ComplexSampleLineChart mSampleLineChart; + private EyeDiagramChart mEyeDiagramChart; + private HBox mDecoderChartBox; + private SymbolChart mSymbolChart; + private PhaseLineChart mPLLPhaseErrorLineChart; + private DoubleLineChart mPLLFrequencyLineChart; + private DoubleLineChart mSamplesPerSymbolLineChart; + private ReusableBufferBroadcaster mFilteredBufferBroadcaster = new ReusableBufferBroadcaster(); + private P25DecoderC4FMInstrumented mDecoder = new P25DecoderC4FMInstrumented(null); + + public P25Phase1C4FMPane() + { + super(DecoderType.P25_PHASE1); + init(); + } + + private void init() + { + addListener(getDecoder()); + + getDecoder().setFilteredBufferListener(mFilteredBufferBroadcaster); + getDecoder().setComplexSymbolListener(getSymbolChart()); + getDecoder().setPLLPhaseErrorListener(getPLLPhaseErrorLineChart()); + getDecoder().setPLLFrequencyListener(getPLLFrequencyLineChart()); + getDecoder().setSymbolDecisionDataListener(getEyeDiagramChart()); + getDecoder().setSamplesPerSymbolListener(getSamplesPerSymbolLineChart()); + mFilteredBufferBroadcaster.addListener(getSampleLineChart()); + + HBox.setHgrow(getSampleChartBox(), Priority.ALWAYS); + HBox.setHgrow(getDecoderChartBox(), Priority.ALWAYS); + getChildren().addAll(getSampleChartBox(), getDecoderChartBox()); + + mDecoder.setMessageListener(new Listener() + { + @Override + public void receive(Message message) + { + mLog.debug(message.getMessage()); + } + }); + + } + + @Override + public void setSampleRate(double sampleRate) + { + mLog.debug("Configuring for sample rate: " + sampleRate); + + mDecoder.setSampleRate(sampleRate); + double samplesPerSymbol = sampleRate / 4800.0; + + getSampleLineChart().setSamplesPerSymbol((int)samplesPerSymbol); + } + + private P25DecoderC4FMInstrumented getDecoder() + { + return mDecoder; + } + + private SymbolChart getSymbolChart() + { + if(mSymbolChart == null) + { + mSymbolChart = new SymbolChart(10); + } + + return mSymbolChart; + } + + private HBox getDecoderChartBox() + { + if(mDecoderChartBox == null) + { + mDecoderChartBox = new HBox(); + mDecoderChartBox.setMaxHeight(Double.MAX_VALUE); + getSymbolChart().setMaxWidth(Double.MAX_VALUE); + getPLLPhaseErrorLineChart().setMaxWidth(Double.MAX_VALUE); + getPLLFrequencyLineChart().setMaxWidth(Double.MAX_VALUE); + HBox.setHgrow(getSymbolChart(), Priority.ALWAYS); + HBox.setHgrow(getPLLPhaseErrorLineChart(), Priority.ALWAYS); + HBox.setHgrow(getPLLFrequencyLineChart(), Priority.ALWAYS); + mDecoderChartBox.getChildren().addAll(getSymbolChart(), getPLLPhaseErrorLineChart(), + getPLLFrequencyLineChart()); + } + + return mDecoderChartBox; + } + + private HBox getSampleChartBox() + { + if(mSampleChartBox == null) + { + mSampleChartBox = new HBox(); + mSampleChartBox.setMaxHeight(Double.MAX_VALUE); + getSampleLineChart().setMaxWidth(Double.MAX_VALUE); + getEyeDiagramChart().setMaxWidth(Double.MAX_VALUE); + getSamplesPerSymbolLineChart().setMaxWidth(Double.MAX_VALUE); + HBox.setHgrow(getSampleLineChart(), Priority.ALWAYS); + HBox.setHgrow(getEyeDiagramChart(), Priority.ALWAYS); + HBox.setHgrow(getSamplesPerSymbolLineChart(), Priority.ALWAYS); + mSampleChartBox.getChildren().addAll(getSampleLineChart(), getEyeDiagramChart(), + getSamplesPerSymbolLineChart()); + } + + return mSampleChartBox; + } + + private ComplexSampleLineChart getSampleLineChart() + { + if(mSampleLineChart == null) + { + mSampleLineChart = new ComplexSampleLineChart(100, 10); + } + + return mSampleLineChart; + } + + private EyeDiagramChart getEyeDiagramChart() + { + if(mEyeDiagramChart == null) + { + mEyeDiagramChart = new EyeDiagramChart(10); + } + + return mEyeDiagramChart; + } + + private PhaseLineChart getPLLPhaseErrorLineChart() + { + if(mPLLPhaseErrorLineChart == null) + { + mPLLPhaseErrorLineChart = new PhaseLineChart(40); + } + + return mPLLPhaseErrorLineChart; + } + + private DoubleLineChart getPLLFrequencyLineChart() + { + if(mPLLFrequencyLineChart == null) + { + mPLLFrequencyLineChart = new DoubleLineChart( "PLL Frequency", -500, 500, 50, 40); + } + + return mPLLFrequencyLineChart; + } + + private DoubleLineChart getSamplesPerSymbolLineChart() + { + if(mSamplesPerSymbolLineChart == null) + { + mSamplesPerSymbolLineChart = new DoubleLineChart( "Sample Point", 9.5, 11.5, 0.1, 40); + } + + return mSamplesPerSymbolLineChart; + } +} diff --git a/src/main/java/io/github/dsheirer/instrument/gui/viewer/decoder/P25Phase1Pane.java b/src/main/java/io/github/dsheirer/instrument/gui/viewer/decoder/P25Phase1LSMPane.java similarity index 91% rename from src/main/java/io/github/dsheirer/instrument/gui/viewer/decoder/P25Phase1Pane.java rename to src/main/java/io/github/dsheirer/instrument/gui/viewer/decoder/P25Phase1LSMPane.java index 526d3e7f..a69538a7 100644 --- a/src/main/java/io/github/dsheirer/instrument/gui/viewer/decoder/P25Phase1Pane.java +++ b/src/main/java/io/github/dsheirer/instrument/gui/viewer/decoder/P25Phase1LSMPane.java @@ -22,18 +22,17 @@ import io.github.dsheirer.instrument.gui.viewer.chart.PhaseLineChart; import io.github.dsheirer.instrument.gui.viewer.chart.SymbolChart; import io.github.dsheirer.message.Message; import io.github.dsheirer.module.decode.DecoderType; -import io.github.dsheirer.module.decode.p25.P25_C4FMDecoderInstrumented; -import io.github.dsheirer.sample.Broadcaster; +import io.github.dsheirer.module.decode.p25.P25DecoderLSMInstrumented; import io.github.dsheirer.sample.Listener; -import io.github.dsheirer.sample.complex.ComplexBuffer; +import io.github.dsheirer.sample.complex.reusable.ReusableBufferBroadcaster; import javafx.scene.layout.HBox; import javafx.scene.layout.Priority; import org.slf4j.Logger; import org.slf4j.LoggerFactory; -public class P25Phase1Pane extends DecoderPane +public class P25Phase1LSMPane extends DecoderPane { - private final static Logger mLog = LoggerFactory.getLogger(P25Phase1Pane.class); + private final static Logger mLog = LoggerFactory.getLogger(P25Phase1LSMPane.class); private HBox mSampleChartBox; private ComplexSampleLineChart mSampleLineChart; @@ -43,10 +42,10 @@ public class P25Phase1Pane extends DecoderPane private PhaseLineChart mPLLPhaseErrorLineChart; private DoubleLineChart mPLLFrequencyLineChart; private DoubleLineChart mSamplesPerSymbolLineChart; - private Broadcaster mFilteredBufferBroadcaster = new Broadcaster<>(); - private P25_C4FMDecoderInstrumented mDecoder = new P25_C4FMDecoderInstrumented(null); + private ReusableBufferBroadcaster mFilteredBufferBroadcaster = new ReusableBufferBroadcaster(); + private P25DecoderLSMInstrumented mDecoder = new P25DecoderLSMInstrumented(null); - public P25Phase1Pane() + public P25Phase1LSMPane() { super(DecoderType.P25_PHASE1); init(); @@ -57,7 +56,7 @@ public class P25Phase1Pane extends DecoderPane addListener(getDecoder()); getDecoder().setFilteredBufferListener(mFilteredBufferBroadcaster); - getDecoder().setSymbolListener(getSymbolChart()); + getDecoder().setComplexSymbolListener(getSymbolChart()); getDecoder().setPLLPhaseErrorListener(getPLLPhaseErrorLineChart()); getDecoder().setPLLFrequencyListener(getPLLFrequencyLineChart()); getDecoder().setSymbolDecisionDataListener(getEyeDiagramChart()); @@ -90,7 +89,7 @@ public class P25Phase1Pane extends DecoderPane getSampleLineChart().setSamplesPerSymbol((int)samplesPerSymbol); } - private P25_C4FMDecoderInstrumented getDecoder() + private P25DecoderLSMInstrumented getDecoder() { return mDecoder; } diff --git a/src/main/java/io/github/dsheirer/module/decode/Decoder.java b/src/main/java/io/github/dsheirer/module/decode/Decoder.java index f8f4953b..00859c78 100644 --- a/src/main/java/io/github/dsheirer/module/decode/Decoder.java +++ b/src/main/java/io/github/dsheirer/module/decode/Decoder.java @@ -41,6 +41,21 @@ public abstract class Decoder extends Module implements IMessageProvider mMessageListener = null; } + public void start() + { + //no-op + } + + public void stop() + { + //no-op + } + + public void reset() + { + //no-op + } + /** * Identifies the decoder type (ie protocol) */ diff --git a/src/main/java/io/github/dsheirer/module/decode/DecoderFactory.java b/src/main/java/io/github/dsheirer/module/decode/DecoderFactory.java index 0c19e47c..146e8f3a 100644 --- a/src/main/java/io/github/dsheirer/module/decode/DecoderFactory.java +++ b/src/main/java/io/github/dsheirer/module/decode/DecoderFactory.java @@ -75,10 +75,10 @@ import io.github.dsheirer.module.decode.nbfm.NBFMDecoder; import io.github.dsheirer.module.decode.nbfm.NBFMDecoderEditor; import io.github.dsheirer.module.decode.p25.DecodeConfigP25Phase1; import io.github.dsheirer.module.decode.p25.P25Decoder.Modulation; +import io.github.dsheirer.module.decode.p25.P25DecoderC4FM; import io.github.dsheirer.module.decode.p25.P25DecoderEditor; +import io.github.dsheirer.module.decode.p25.P25DecoderLSM; import io.github.dsheirer.module.decode.p25.P25DecoderState; -import io.github.dsheirer.module.decode.p25.P25_C4FMDecoder; -import io.github.dsheirer.module.decode.p25.P25_LSMDecoder; import io.github.dsheirer.module.decode.p25.audio.P25AudioModule; import io.github.dsheirer.module.decode.p25.message.filter.P25MessageFilterSet; import io.github.dsheirer.module.decode.passport.DecodeConfigPassport; @@ -247,12 +247,12 @@ public class DecoderFactory switch(modulation) { case C4FM: - modules.add(new P25_C4FMDecoder(aliasList)); - modules.add(new P25DecoderState(aliasList, channelType, P25Decoder.Modulation.C4FM, + modules.add(new P25DecoderC4FM(aliasList)); + modules.add(new P25DecoderState(aliasList, channelType, Modulation.C4FM, p25Config.getIgnoreDataCalls())); break; case CQPSK: - modules.add(new P25_LSMDecoder(aliasList)); + modules.add(new P25DecoderLSM(aliasList)); modules.add(new P25DecoderState(aliasList, channelType, Modulation.CQPSK, p25Config.getIgnoreDataCalls())); break; default: diff --git a/src/main/java/io/github/dsheirer/module/decode/p25/DecodeConfigP25Phase1.java b/src/main/java/io/github/dsheirer/module/decode/p25/DecodeConfigP25Phase1.java index 03aec81b..d0dc5620 100644 --- a/src/main/java/io/github/dsheirer/module/decode/p25/DecodeConfigP25Phase1.java +++ b/src/main/java/io/github/dsheirer/module/decode/p25/DecodeConfigP25Phase1.java @@ -25,7 +25,7 @@ import io.github.dsheirer.module.decode.config.DecodeConfiguration; public class DecodeConfigP25Phase1 extends DecodeConfiguration { - private P25_LSMDecoder.Modulation mModulation = P25Decoder.Modulation.C4FM; + private P25DecoderLSM.Modulation mModulation = P25Decoder.Modulation.C4FM; private int mCallTimeout = 1; private int mTrafficChannelPoolSize = TRAFFIC_CHANNEL_LIMIT_DEFAULT; @@ -48,12 +48,12 @@ public class DecodeConfigP25Phase1 extends DecodeConfiguration } @JacksonXmlProperty(isAttribute = true, localName = "modulation") - public P25_LSMDecoder.Modulation getModulation() + public P25DecoderLSM.Modulation getModulation() { return mModulation; } - public void setModulation(P25_LSMDecoder.Modulation modulation) + public void setModulation(P25DecoderLSM.Modulation modulation) { mModulation = modulation; } diff --git a/src/main/java/io/github/dsheirer/module/decode/p25/P25Decoder.java b/src/main/java/io/github/dsheirer/module/decode/p25/P25Decoder.java index e85dec3c..dfcf230b 100644 --- a/src/main/java/io/github/dsheirer/module/decode/p25/P25Decoder.java +++ b/src/main/java/io/github/dsheirer/module/decode/p25/P25Decoder.java @@ -18,26 +18,70 @@ package io.github.dsheirer.module.decode.p25; import io.github.dsheirer.alias.AliasList; import io.github.dsheirer.module.decode.Decoder; import io.github.dsheirer.module.decode.DecoderType; +import io.github.dsheirer.sample.Listener; +import io.github.dsheirer.sample.complex.reusable.IReusableComplexBufferListener; +import io.github.dsheirer.sample.complex.reusable.ReusableComplexBuffer; import io.github.dsheirer.source.ISourceEventListener; import io.github.dsheirer.source.ISourceEventProvider; +import io.github.dsheirer.source.SourceEvent; -public abstract class P25Decoder extends Decoder implements ISourceEventListener, ISourceEventProvider, Instrumentable +public abstract class P25Decoder extends Decoder implements ISourceEventListener, ISourceEventProvider, + IReusableComplexBufferListener, Listener { + private double mSampleRate; private P25MessageProcessor mMessageProcessor; private AliasList mAliasList; + private Listener mSourceEventListener; + private double mSymbolRate; - public P25Decoder(AliasList aliasList) + public P25Decoder(double symbolRate, AliasList aliasList) { + mSymbolRate = symbolRate; mAliasList = aliasList; mMessageProcessor = new P25MessageProcessor(mAliasList); mMessageProcessor.setMessageListener(getMessageListener()); } + protected double getSymbolRate() + { + return mSymbolRate; + } + protected AliasList getAliasList() { return mAliasList; } + /** + * Current sample rate for this decoder + */ + protected double getSampleRate() + { + return mSampleRate; + } + + /** + * Sets current sample rate for this decoder + */ + public void setSampleRate(double sampleRate) + { + if(sampleRate <= getSymbolRate() * 2) + { + throw new IllegalArgumentException("Sample rate [" + sampleRate + "] must be >9600 (2 * " + + getSymbolRate() + " symbol rate)"); + } + + mSampleRate = sampleRate; + } + + /** + * Samples per symbol based on current sample rate and symbol rate. + */ + public float getSamplesPerSymbol() + { + return (float)(getSampleRate() / getSymbolRate()); + } + public void dispose() { super.dispose(); @@ -46,6 +90,83 @@ public abstract class P25Decoder extends Decoder implements ISourceEventListener mMessageProcessor = null; } + /** + * Sets the source event listener to receive source events from this decoder. + */ + @Override + public void setSourceEventListener(Listener listener) + { + mSourceEventListener = listener; + } + + /** + * Removes a registered source event listener from receiving source events from this decoder + */ + @Override + public void removeSourceEventListener() + { + mSourceEventListener = null; + } + + @Override + public Listener getSourceEventListener() + { + return new Listener() + { + @Override + public void receive(SourceEvent sourceEvent) + { + process(sourceEvent); + } + }; + } + + /** + * Sub-class processing of received source events + * @param sourceEvent + */ + protected abstract void process(SourceEvent sourceEvent); + + /** + * Broadcasts the source event to an optional registered listener. This method should primarily be used to + * issue frequency correction requests to the channel source. + * @param sourceEvent to broadcast + */ + protected void broadcast(SourceEvent sourceEvent) + { + if(mSourceEventListener != null) + { + mSourceEventListener.receive(sourceEvent); + } + } + + /** + * Listener interface to receive reusable complex buffers + */ + @Override + public Listener getReusableComplexBufferListener() + { + return P25Decoder.this; + } + + /** + * Starts the decoder + */ + @Override + public void start() + { + //No-op + } + + /** + * Stops the decoder + */ + @Override + public void stop() + { + //No-op + } + @Override public DecoderType getDecoderType() { diff --git a/src/main/java/io/github/dsheirer/module/decode/p25/P25_C4FMDecoder.java b/src/main/java/io/github/dsheirer/module/decode/p25/P25DecoderC4FM.java similarity index 56% rename from src/main/java/io/github/dsheirer/module/decode/p25/P25_C4FMDecoder.java rename to src/main/java/io/github/dsheirer/module/decode/p25/P25DecoderC4FM.java index 6923bc37..cc787e5a 100644 --- a/src/main/java/io/github/dsheirer/module/decode/p25/P25_C4FMDecoder.java +++ b/src/main/java/io/github/dsheirer/module/decode/p25/P25DecoderC4FM.java @@ -20,79 +20,88 @@ import io.github.dsheirer.dsp.filter.FilterFactory; import io.github.dsheirer.dsp.filter.design.FilterDesignException; import io.github.dsheirer.dsp.filter.fir.FIRFilterSpecification; import io.github.dsheirer.dsp.filter.fir.complex.ComplexFIRFilter; -import io.github.dsheirer.dsp.psk.DQPSKDemodulator; +import io.github.dsheirer.dsp.psk.DQPSKDecisionDirectedDemodulator; import io.github.dsheirer.dsp.psk.InterpolatingSampleBuffer; -import io.github.dsheirer.dsp.psk.InterpolatingSampleBufferInstrumented; import io.github.dsheirer.dsp.psk.pll.CostasLoop; -import io.github.dsheirer.sample.Listener; -import io.github.dsheirer.sample.complex.ComplexBuffer; -import io.github.dsheirer.sample.complex.IComplexBufferListener; -import io.github.dsheirer.source.tuner.frequency.FrequencyChangeEvent; +import io.github.dsheirer.sample.complex.reusable.ReusableComplexBuffer; +import io.github.dsheirer.source.SourceEvent; import org.slf4j.Logger; import org.slf4j.LoggerFactory; import java.util.HashMap; import java.util.Map; -import java.util.concurrent.ScheduledExecutorService; -public class P25_C4FMDecoder extends P25Decoder implements IComplexBufferListener, Listener +public class P25DecoderC4FM extends P25Decoder { - private final static Logger mLog = LoggerFactory.getLogger(P25_C4FMDecoder.class); - protected static final double SYMBOL_RATE = 4800; + private final static Logger mLog = LoggerFactory.getLogger(P25DecoderC4FM.class); + protected static final float SYMBOL_TIMING_GAIN = 0.1f; protected InterpolatingSampleBuffer mInterpolatingSampleBuffer; - protected DQPSKDemodulator mQPSKDemodulator; + protected DQPSKDecisionDirectedDemodulator mQPSKDemodulator; protected CostasLoop mCostasLoop; protected P25MessageFramer mMessageFramer; - protected double mSampleRate; private Map mBasebandFilters = new HashMap<>(); private ComplexFIRFilter mBasebandFilter; - public P25_C4FMDecoder(AliasList aliasList) + /** + * P25 Phase 1 - standard C4FM modulation decoder. Uses Differential QPSK decoding with a Costas PLL and a + * decision-directed phase and timing error detector. + * + * @param aliasList + */ + public P25DecoderC4FM(AliasList aliasList) { - super(aliasList); + super(4800.0, aliasList); setSampleRate(48000.0); } public void setSampleRate(double sampleRate) { - if(sampleRate <= SYMBOL_RATE * 2) - { - throw new IllegalArgumentException("Sample rate must be at least twice the symbol rate [4800]"); - } + super.setSampleRate(sampleRate); - mSampleRate = sampleRate; mBasebandFilter = new ComplexFIRFilter(getBasebandFilter(), 1.0f); - mCostasLoop = new CostasLoop(mSampleRate, SYMBOL_RATE); + mCostasLoop = new CostasLoop(getSampleRate(), getSymbolRate()); + mInterpolatingSampleBuffer = new InterpolatingSampleBuffer(getSamplesPerSymbol(), SYMBOL_TIMING_GAIN); - mInterpolatingSampleBuffer = new InterpolatingSampleBufferInstrumented((float)(sampleRate / SYMBOL_RATE)); - mQPSKDemodulator = new DQPSKDemodulator(mCostasLoop, mInterpolatingSampleBuffer); + mQPSKDemodulator = new DQPSKDecisionDirectedDemodulator(mCostasLoop, mInterpolatingSampleBuffer); - //Message framer can trigger a symbol-inversion correction to the PLL when detected + //Message framer can issue a symbol-inversion correction request to the PLL when detected mMessageFramer = new P25MessageFramer(getAliasList(), mCostasLoop); mMessageFramer.setListener(getMessageProcessor()); - mQPSKDemodulator.setDibitListener(mMessageFramer); + mQPSKDemodulator.setSymbolListener(mMessageFramer); } + /** + * Primary method for processing incoming complex sample buffers + * @param reusableComplexBuffer containing channelized complex samples + */ @Override - public void receive(ComplexBuffer complexBuffer) + public void receive(ReusableComplexBuffer reusableComplexBuffer) { - ComplexBuffer filtered = filter(complexBuffer); + //User accounting of the incoming buffer is handled by the filter + ReusableComplexBuffer filtered = filter(reusableComplexBuffer); + + //User accounting of the filtered buffer is handled by the demodulator mQPSKDemodulator.receive(filtered); } - protected ComplexBuffer filter(ComplexBuffer complexBuffer) + /** + * Filters the complex buffer and returns a new reusable complex buffer with the filtered contents. + * @param reusableComplexBuffer to filter + * @return filtered complex buffer + */ + protected ReusableComplexBuffer filter(ReusableComplexBuffer reusableComplexBuffer) { - return mBasebandFilter.filter(complexBuffer); - } - - private double getSampleRate() - { - return mSampleRate; + //User accounting of the incoming buffer is handled by the filter + return mBasebandFilter.filter(reusableComplexBuffer); } + /** + * Constructs a baseband filter for this decoder using the current sample rate + */ private float[] getBasebandFilter() { + //Attempt to reuse a cached (ie already-designed) filter if available float[] filter = mBasebandFilters.get(getSampleRate()); if(filter == null) @@ -110,19 +119,19 @@ public class P25_C4FMDecoder extends P25Decoder implements IComplexBufferListene try { filter = FilterFactory.getTaps(specification); - } catch(FilterDesignException fde) { mLog.error("Couldn't design low pass baseband filter for sample rate: " + getSampleRate()); } + if(filter != null) { mBasebandFilters.put(getSampleRate(), filter); } else { - throw new IllegalStateException("Couldn't design a C4FM symbol filter for sample rate: " + mSampleRate); + throw new IllegalStateException("Couldn't design a C4FM baseband filter for sample rate: " + getSampleRate()); } } @@ -141,60 +150,36 @@ public class P25_C4FMDecoder extends P25Decoder implements IComplexBufferListene } @Override - public void setFrequencyChangeListener(Listener listener) + protected void process(SourceEvent sourceEvent) { - } - - @Override - public void removeFrequencyChangeListener() - { - } - - @Override - public Listener getFrequencyChangeListener() - { - return new Listener() + switch(sourceEvent.getEvent()) { - @Override - public void receive(FrequencyChangeEvent frequencyChangeEvent) - { - switch(frequencyChangeEvent.getEvent()) - { - case NOTIFICATION_FREQUENCY_CHANGE: - case NOTIFICATION_FREQUENCY_CORRECTION_CHANGE: - case NOTIFICATION_CHANNEL_FREQUENCY_CORRECTION_CHANGE: - case NOTIFICATION_SAMPLE_RATE_CHANGE: - mCostasLoop.reset(); - break; - } - } - }; - } - - @Override - public Listener getComplexBufferListener() - { - return P25_C4FMDecoder.this; + case NOTIFICATION_FREQUENCY_CHANGE: + case NOTIFICATION_FREQUENCY_CORRECTION_CHANGE: + case NOTIFICATION_CHANNEL_FREQUENCY_CORRECTION_CHANGE: + mCostasLoop.reset(); + break; + case NOTIFICATION_SAMPLE_RATE_CHANGE: + mCostasLoop.reset(); + setSampleRate(sourceEvent.getValue().doubleValue()); + break; + } } + /** + * P25 modulation supported by this decoder + */ public Modulation getModulation() { return Modulation.C4FM; } + /** + * Resets this decoder to prepare for processing a new channel + */ @Override public void reset() { mCostasLoop.reset(); } - - @Override - public void start(ScheduledExecutorService executor) - { - } - - @Override - public void stop() - { - } } diff --git a/src/main/java/io/github/dsheirer/module/decode/p25/P25DecoderC4FMInstrumented.java b/src/main/java/io/github/dsheirer/module/decode/p25/P25DecoderC4FMInstrumented.java new file mode 100644 index 00000000..c7b329bc --- /dev/null +++ b/src/main/java/io/github/dsheirer/module/decode/p25/P25DecoderC4FMInstrumented.java @@ -0,0 +1,121 @@ +/******************************************************************************* + * 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 + * + ******************************************************************************/ +package io.github.dsheirer.module.decode.p25; + +import io.github.dsheirer.alias.AliasList; +import io.github.dsheirer.dsp.psk.DQPSKDecisionDirectedDemodulatorInstrumented; +import io.github.dsheirer.dsp.psk.InterpolatingSampleBufferInstrumented; +import io.github.dsheirer.dsp.psk.SymbolDecisionData; +import io.github.dsheirer.sample.Listener; +import io.github.dsheirer.sample.complex.Complex; +import io.github.dsheirer.sample.complex.reusable.ReusableComplexBuffer; + +public class P25DecoderC4FMInstrumented extends P25DecoderC4FM +{ + private Listener mPLLPhaseErrorListener; + private Listener mPLLFrequencyListener; + private Listener mSamplesPerSymbolListener; + private Listener mComplexSymbolListener; + private Listener mFilteredSymbolListener; + private Listener mSymbolDecisionDataListener; + + /** + * Instrumented version of the P25 C4FM decoder that supports registering listeners to provide access to data as + * it is being processed by the decoder. + * + * @param aliasList + */ + public P25DecoderC4FMInstrumented(AliasList aliasList) + { + super(aliasList); + } + + /** + * Overrides the filter method so that we can capture the filtered samples for instrumentation + */ + protected ReusableComplexBuffer filter(ReusableComplexBuffer reusableComplexBuffer) + { + ReusableComplexBuffer filtered = super.filter(reusableComplexBuffer); + + //Increment user count before we hand to another listener or return from this method + filtered.incrementUserCount(); + + if(mFilteredSymbolListener != null) + { + filtered.incrementUserCount(); + mFilteredSymbolListener.receive(filtered); + } + + return filtered; + } + + /** + * Overrides this method so we can correctly configure for instrumented operations + */ + public void setSampleRate(double sampleRate) + { + super.setSampleRate(sampleRate); + + InterpolatingSampleBufferInstrumented instrumentedBuffer = + new InterpolatingSampleBufferInstrumented(getSamplesPerSymbol(), SYMBOL_TIMING_GAIN); + mInterpolatingSampleBuffer = instrumentedBuffer; + + DQPSKDecisionDirectedDemodulatorInstrumented instrumented = new DQPSKDecisionDirectedDemodulatorInstrumented(mCostasLoop, instrumentedBuffer, getSampleRate()); + mQPSKDemodulator = instrumented; + + instrumented.setComplexSymbolListener(mComplexSymbolListener); + instrumented.setPLLErrorListener(mPLLPhaseErrorListener); + instrumented.setPLLFrequencyListener(mPLLFrequencyListener); + instrumented.setSymbolDecisionDataListener(mSymbolDecisionDataListener); + instrumented.setSamplesPerSymbolListener(mSamplesPerSymbolListener); + instrumented.setSymbolListener(mMessageFramer); + } + + public void setComplexSymbolListener(Listener listener) + { + mComplexSymbolListener = listener; + ((DQPSKDecisionDirectedDemodulatorInstrumented)mQPSKDemodulator).setComplexSymbolListener(listener); + } + + public void setPLLPhaseErrorListener(Listener listener) + { + mPLLPhaseErrorListener = listener; + ((DQPSKDecisionDirectedDemodulatorInstrumented)mQPSKDemodulator).setPLLErrorListener(listener); + } + + public void setPLLFrequencyListener(Listener listener) + { + mPLLFrequencyListener = listener; + ((DQPSKDecisionDirectedDemodulatorInstrumented)mQPSKDemodulator).setPLLFrequencyListener(listener); + } + + public void setFilteredBufferListener(Listener listener) + { + mFilteredSymbolListener = listener; + } + + public void setSymbolDecisionDataListener(Listener listener) + { + mSymbolDecisionDataListener = listener; + ((DQPSKDecisionDirectedDemodulatorInstrumented)mQPSKDemodulator).setSymbolDecisionDataListener(listener); + } + + public void setSamplesPerSymbolListener(Listener listener) + { + mSamplesPerSymbolListener = listener; + ((DQPSKDecisionDirectedDemodulatorInstrumented)mQPSKDemodulator).setSamplesPerSymbolListener(listener); + } +} \ No newline at end of file diff --git a/src/main/java/io/github/dsheirer/module/decode/p25/P25DecoderEditor.java b/src/main/java/io/github/dsheirer/module/decode/p25/P25DecoderEditor.java index e283a708..d6d4df83 100644 --- a/src/main/java/io/github/dsheirer/module/decode/p25/P25DecoderEditor.java +++ b/src/main/java/io/github/dsheirer/module/decode/p25/P25DecoderEditor.java @@ -31,7 +31,11 @@ import net.miginfocom.swing.MigLayout; import org.slf4j.Logger; import org.slf4j.LoggerFactory; -import javax.swing.*; +import javax.swing.DefaultComboBoxModel; +import javax.swing.JCheckBox; +import javax.swing.JComboBox; +import javax.swing.JLabel; +import javax.swing.JSlider; import javax.swing.event.ChangeEvent; import javax.swing.event.ChangeListener; import java.awt.event.ActionEvent; @@ -43,7 +47,7 @@ public class P25DecoderEditor extends ValidatingEditor private static final long serialVersionUID = 1L; - private JComboBox mComboModulation; + private JComboBox mComboModulation; private JCheckBox mIgnoreDataCalls; private JLabel mTrafficChannelPoolSizeLabel; private JSlider mTrafficChannelPoolSize; @@ -57,9 +61,9 @@ public class P25DecoderEditor extends ValidatingEditor { setLayout( new MigLayout( "insets 0 0 0 0,wrap 3", "[right][grow,fill][]", "" ) ); - mComboModulation = new JComboBox(); - mComboModulation.setModel( new DefaultComboBoxModel( - P25_LSMDecoder.Modulation.values() ) ); + mComboModulation = new JComboBox(); + mComboModulation.setModel( new DefaultComboBoxModel( + P25DecoderLSM.Modulation.values() ) ); mComboModulation.setEnabled( false ); mComboModulation.addActionListener( new ActionListener() { diff --git a/src/main/java/io/github/dsheirer/module/decode/p25/P25DecoderLSM.java b/src/main/java/io/github/dsheirer/module/decode/p25/P25DecoderLSM.java new file mode 100644 index 00000000..74a8cceb --- /dev/null +++ b/src/main/java/io/github/dsheirer/module/decode/p25/P25DecoderLSM.java @@ -0,0 +1,177 @@ +/******************************************************************************* + * 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 + * + ******************************************************************************/ +package io.github.dsheirer.module.decode.p25; + +import io.github.dsheirer.alias.AliasList; +import io.github.dsheirer.dsp.filter.FilterFactory; +import io.github.dsheirer.dsp.filter.Window.WindowType; +import io.github.dsheirer.dsp.filter.fir.complex.ComplexFIRFilter; +import io.github.dsheirer.dsp.gain.ComplexFeedForwardGainControl; +import io.github.dsheirer.dsp.psk.DQPSKGardnerDemodulator; +import io.github.dsheirer.dsp.psk.InterpolatingSampleBuffer; +import io.github.dsheirer.dsp.psk.pll.CostasLoop; +import io.github.dsheirer.sample.complex.reusable.ReusableComplexBuffer; +import io.github.dsheirer.source.SourceEvent; +import org.slf4j.Logger; +import org.slf4j.LoggerFactory; + +import java.util.HashMap; +import java.util.Map; + +public class P25DecoderLSM extends P25Decoder +{ + private final static Logger mLog = LoggerFactory.getLogger(P25DecoderLSM.class); + protected static final float SYMBOL_TIMING_GAIN = 0.1f; + + private Map mBasebandFilters = new HashMap<>(); + private ComplexFIRFilter mBasebandFilter; + private ComplexFeedForwardGainControl mAGC = new ComplexFeedForwardGainControl(32); + protected DQPSKGardnerDemodulator mQPSKDemodulator; + protected P25MessageFramer mMessageFramer; + protected CostasLoop mCostasLoop; + protected InterpolatingSampleBuffer mInterpolatingSampleBuffer; + + /** + * P25 Phase 1 - linear simulcast modulation (LSM) decoder. Uses Differential QPSK decoding with a Costas PLL and + * a gardner timing error detector. + * + * @param aliasList + */ + public P25DecoderLSM(AliasList aliasList) + { + super(4800.0, aliasList); + setSampleRate(48000.0); + } + + /** + * Sets or changes the channel sample rate + * @param sampleRate in hertz + */ + public void setSampleRate(double sampleRate) + { + super.setSampleRate(sampleRate); + + mBasebandFilter = new ComplexFIRFilter(getBasebandFilter(), 1.0f); + + mCostasLoop = new CostasLoop(getSampleRate(), getSymbolRate()); + mInterpolatingSampleBuffer = new InterpolatingSampleBuffer(getSamplesPerSymbol(), SYMBOL_TIMING_GAIN); + + mQPSKDemodulator = new DQPSKGardnerDemodulator(mCostasLoop, mInterpolatingSampleBuffer); + + //Message framer can issue a symbol-inversion correction request to the PLL when detected + mMessageFramer = new P25MessageFramer(getAliasList(), mCostasLoop); + mMessageFramer.setListener(getMessageProcessor()); + mQPSKDemodulator.setSymbolListener(mMessageFramer); + } + + /** + * Primary method for receiving incoming channel samples + */ + @Override + public void receive(ReusableComplexBuffer reusableComplexBuffer) + { + //The filter will decrement the user count when finished + ReusableComplexBuffer basebandFiltered = filter(reusableComplexBuffer); + + //AGC will decrement the user count when finished + ReusableComplexBuffer gainApplied = mAGC.filter(basebandFiltered); + + //Decoder will decrement the user count when finished + mQPSKDemodulator.receive(gainApplied); + } + + /** + * Filters the complex buffer and returns a new reusable complex buffer with the filtered contents. + * @param reusableComplexBuffer to filter + * @return filtered complex buffer + */ + protected ReusableComplexBuffer filter(ReusableComplexBuffer reusableComplexBuffer) + { + //User accounting of the incoming buffer is handled by the filter + return mBasebandFilter.filter(reusableComplexBuffer); + } + + /** + * Processes source events - updates buffer and decoder when sample rate is established/changed and resets the + * PLL after frequency changes/corrections + */ + @Override + protected void process(SourceEvent sourceEvent) + { + switch(sourceEvent.getEvent()) + { + case NOTIFICATION_FREQUENCY_CHANGE: + case NOTIFICATION_FREQUENCY_CORRECTION_CHANGE: + case NOTIFICATION_CHANNEL_FREQUENCY_CORRECTION_CHANGE: + mCostasLoop.reset(); + break; + case NOTIFICATION_SAMPLE_RATE_CHANGE: + mCostasLoop.reset(); + setSampleRate(sourceEvent.getValue().doubleValue()); + break; + } + } + + /** + * Resets this decoder to prepare for processing a new channel + */ + @Override + public void reset() + { + mCostasLoop.reset(); + } + + public void dispose() + { + super.dispose(); + + mBasebandFilter.dispose(); + mBasebandFilter = null; + + mAGC.dispose(); + mAGC = null; + + mQPSKDemodulator.dispose(); + mQPSKDemodulator = null; + + mMessageFramer.dispose(); + mMessageFramer = null; + } + + public Modulation getModulation() + { + return Modulation.CQPSK; + } + + /** + * Constructs a baseband filter for this decoder using the current sample rate + */ + private float[] getBasebandFilter() + { + //Attempt to reuse a cached (ie already-designed) filter if available + float[] filter = mBasebandFilters.get(getSampleRate()); + + if(filter == null) + { + filter = FilterFactory.getLowPass(48000, 7250, 8000, 60, + WindowType.HANN, true); + + mBasebandFilters.put(getSampleRate(), filter); + } + + return filter; + } +} diff --git a/src/main/java/io/github/dsheirer/module/decode/p25/P25_C4FMDecoderInstrumented.java b/src/main/java/io/github/dsheirer/module/decode/p25/P25DecoderLSMInstrumented.java similarity index 59% rename from src/main/java/io/github/dsheirer/module/decode/p25/P25_C4FMDecoderInstrumented.java rename to src/main/java/io/github/dsheirer/module/decode/p25/P25DecoderLSMInstrumented.java index 903396a7..179d3503 100644 --- a/src/main/java/io/github/dsheirer/module/decode/p25/P25_C4FMDecoderInstrumented.java +++ b/src/main/java/io/github/dsheirer/module/decode/p25/P25DecoderLSMInstrumented.java @@ -16,23 +16,31 @@ package io.github.dsheirer.module.decode.p25; import io.github.dsheirer.alias.AliasList; -import io.github.dsheirer.dsp.psk.DQPSKDemodulatorInstrumented; +import io.github.dsheirer.dsp.psk.DQPSKGardnerDemodulatorInstrumented; import io.github.dsheirer.dsp.psk.InterpolatingSampleBufferInstrumented; import io.github.dsheirer.dsp.psk.SymbolDecisionData; import io.github.dsheirer.sample.Listener; import io.github.dsheirer.sample.complex.Complex; -import io.github.dsheirer.sample.complex.ComplexBuffer; +import io.github.dsheirer.sample.complex.reusable.ReusableComplexBuffer; -public class P25_C4FMDecoderInstrumented extends P25_C4FMDecoder +public class P25DecoderLSMInstrumented extends P25DecoderLSM { private Listener mPLLPhaseErrorListener; private Listener mPLLFrequencyListener; private Listener mSamplesPerSymbolListener; - private Listener mSymbolListener; - private Listener mFilteredSymbolListener; + private Listener mComplexSymbolListener; + private Listener mFilteredSymbolListener; private Listener mSymbolDecisionDataListener; - public P25_C4FMDecoderInstrumented(AliasList aliasList) + /** + * P25 Phase 1 - linear simulcast modulation (LSM) decoder. Uses Differential QPSK decoding with a Costas PLL and + * a gardner timing error detector. + * + * Instrumented decoder instance. + * + * @param aliasList + */ + public P25DecoderLSMInstrumented(AliasList aliasList) { super(aliasList); } @@ -40,12 +48,16 @@ public class P25_C4FMDecoderInstrumented extends P25_C4FMDecoder /** * Overrides the filter method so that we can capture the filtered samples for instrumentation */ - protected ComplexBuffer filter(ComplexBuffer complexBuffer) + protected ReusableComplexBuffer filter(ReusableComplexBuffer reusableComplexBuffer) { - ComplexBuffer filtered = super.filter(complexBuffer); + ReusableComplexBuffer filtered = super.filter(reusableComplexBuffer); + + //Increment user count before we hand to another listener or return from this method + filtered.incrementUserCount(); if(mFilteredSymbolListener != null) { + filtered.incrementUserCount(); mFilteredSymbolListener.receive(filtered); } @@ -58,39 +70,40 @@ public class P25_C4FMDecoderInstrumented extends P25_C4FMDecoder public void setSampleRate(double sampleRate) { super.setSampleRate(sampleRate); - InterpolatingSampleBufferInstrumented instrumentedBuffer = new InterpolatingSampleBufferInstrumented((float)(sampleRate / SYMBOL_RATE)); + + InterpolatingSampleBufferInstrumented instrumentedBuffer = new InterpolatingSampleBufferInstrumented(getSamplesPerSymbol(), SYMBOL_TIMING_GAIN); mInterpolatingSampleBuffer = instrumentedBuffer; - DQPSKDemodulatorInstrumented instrumented = new DQPSKDemodulatorInstrumented(mCostasLoop, instrumentedBuffer, mSampleRate); + DQPSKGardnerDemodulatorInstrumented instrumented = new DQPSKGardnerDemodulatorInstrumented(mCostasLoop, instrumentedBuffer, getSampleRate()); mQPSKDemodulator = instrumented; - instrumented.setSymbolListener(mSymbolListener); + instrumented.setComplexSymbolListener(mComplexSymbolListener); instrumented.setPLLErrorListener(mPLLPhaseErrorListener); instrumented.setPLLFrequencyListener(mPLLFrequencyListener); instrumented.setSymbolDecisionDataListener(mSymbolDecisionDataListener); instrumented.setSamplesPerSymbolListener(mSamplesPerSymbolListener); - instrumented.setDibitListener(mMessageFramer); + instrumented.setSymbolListener(mMessageFramer); } - public void setSymbolListener(Listener listener) + public void setComplexSymbolListener(Listener listener) { - mSymbolListener = listener; - ((DQPSKDemodulatorInstrumented)mQPSKDemodulator).setSymbolListener(listener); + mComplexSymbolListener = listener; + ((DQPSKGardnerDemodulatorInstrumented)mQPSKDemodulator).setComplexSymbolListener(listener); } public void setPLLPhaseErrorListener(Listener listener) { mPLLPhaseErrorListener = listener; - ((DQPSKDemodulatorInstrumented)mQPSKDemodulator).setPLLErrorListener(listener); + ((DQPSKGardnerDemodulatorInstrumented)mQPSKDemodulator).setPLLErrorListener(listener); } public void setPLLFrequencyListener(Listener listener) { mPLLFrequencyListener = listener; - ((DQPSKDemodulatorInstrumented)mQPSKDemodulator).setPLLFrequencyListener(listener); + ((DQPSKGardnerDemodulatorInstrumented)mQPSKDemodulator).setPLLFrequencyListener(listener); } - public void setFilteredBufferListener(Listener listener) + public void setFilteredBufferListener(Listener listener) { mFilteredSymbolListener = listener; } @@ -98,12 +111,12 @@ public class P25_C4FMDecoderInstrumented extends P25_C4FMDecoder public void setSymbolDecisionDataListener(Listener listener) { mSymbolDecisionDataListener = listener; - ((DQPSKDemodulatorInstrumented)mQPSKDemodulator).setSymbolDecisionDataListener(listener); + ((DQPSKGardnerDemodulatorInstrumented)mQPSKDemodulator).setSymbolDecisionDataListener(listener); } public void setSamplesPerSymbolListener(Listener listener) { mSamplesPerSymbolListener = listener; - ((DQPSKDemodulatorInstrumented)mQPSKDemodulator).setSamplesPerSymbolListener(listener); + ((DQPSKGardnerDemodulatorInstrumented)mQPSKDemodulator).setSamplesPerSymbolListener(listener); } } \ No newline at end of file diff --git a/src/main/java/io/github/dsheirer/module/decode/p25/P25_LSMDecoder.java b/src/main/java/io/github/dsheirer/module/decode/p25/P25_LSMDecoder.java deleted file mode 100644 index 20460d76..00000000 --- a/src/main/java/io/github/dsheirer/module/decode/p25/P25_LSMDecoder.java +++ /dev/null @@ -1,240 +0,0 @@ -/******************************************************************************* - * 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 - * - ******************************************************************************/ -package io.github.dsheirer.module.decode.p25; - -import io.github.dsheirer.alias.AliasList; -import io.github.dsheirer.dsp.filter.FilterFactory; -import io.github.dsheirer.dsp.filter.Window.WindowType; -import io.github.dsheirer.dsp.filter.fir.complex.ComplexFIRFilter_CB_CB; -import io.github.dsheirer.dsp.gain.ComplexFeedForwardGainControl; -import io.github.dsheirer.dsp.psk.LSMDemodulator; -import io.github.dsheirer.dsp.psk.QPSKSymbolDecoder; -import io.github.dsheirer.instrument.tap.TapGroup; -import io.github.dsheirer.sample.Listener; -import io.github.dsheirer.sample.complex.ComplexBuffer; -import io.github.dsheirer.sample.complex.ComplexBufferToStreamConverter; -import io.github.dsheirer.sample.complex.reusable.IReusableComplexBufferListener; -import io.github.dsheirer.sample.complex.reusable.ReusableComplexBuffer; -import io.github.dsheirer.source.SourceEvent; -import org.slf4j.Logger; -import org.slf4j.LoggerFactory; - -import java.util.List; - -public class P25_LSMDecoder extends P25Decoder implements IReusableComplexBufferListener -{ - private final static Logger mLog = LoggerFactory.getLogger(P25_LSMDecoder.class); - - /* Instrumentation Taps */ - private static final String INSTRUMENT_BASEBAND_FILTER_OUTPUT = "Tap Point: Baseband Filter Output"; - private static final String INSTRUMENT_AGC_OUTPUT = "Tap Point: AGC Output"; - private static final String INSTRUMENT_LSM_DEMODULATOR_OUTPUT = "Tap Point: LSM Demodulator Output"; - private static final String INSTRUMENT_QPSK_SLICER_OUTPUT = "Tap Point: QPSK Slicer Output"; - - private List mAvailableTaps; - - private ComplexFIRFilter_CB_CB mBasebandFilter; - private ComplexBufferToStreamConverter mStreamConverter = new ComplexBufferToStreamConverter(); - private ComplexFeedForwardGainControl mAGC = - new ComplexFeedForwardGainControl(32); - private LSMDemodulator mLSMDemodulator = new LSMDemodulator(); - private QPSKPolarSlicer mQPSKSlicer = new QPSKPolarSlicer(); - private P25MessageFramer mMessageFramer; - private ReusableBufferListener mReusableBufferListener = new ReusableBufferListener(); - - public P25_LSMDecoder(AliasList aliasList) - { - super(aliasList); - - mBasebandFilter = new ComplexFIRFilter_CB_CB(FilterFactory.getLowPass( - 48000, 7250, 8000, 60, WindowType.HANN, true), 1.0f); - - mBasebandFilter.setListener(mStreamConverter); - - mStreamConverter.setListener(mAGC); - - mAGC.setListener(mLSMDemodulator); - - mLSMDemodulator.setSymbolListener(mQPSKSlicer); - - mMessageFramer = new P25MessageFramer(aliasList, mLSMDemodulator); - mQPSKSlicer.addListener(mMessageFramer); - - mMessageFramer.setListener(getMessageProcessor()); - } - - public void dispose() - { - super.dispose(); - - mBasebandFilter.dispose(); - mBasebandFilter = null; - - mStreamConverter.dispose(); - mStreamConverter = null; - - mAGC.dispose(); - mAGC = null; - - mLSMDemodulator.dispose(); - mLSMDemodulator = null; - - mQPSKSlicer.dispose(); - mQPSKSlicer = null; - - mMessageFramer.dispose(); - mMessageFramer = null; - } - - @Override - public Listener getReusableComplexBufferListener() - { - return mReusableBufferListener; - } - - public Modulation getModulation() - { - return Modulation.CQPSK; - } - - /** - * Provides a list of instrumentation taps for monitoring internal processing - */ - @Override - public List getTapGroups() - { - if(mAvailableTaps == null) - { - mAvailableTaps = new ArrayList<>(); - - TapGroup group = new TapGroup("P25 LSM Decoder"); - - group.add(new ComplexTap(INSTRUMENT_BASEBAND_FILTER_OUTPUT, 0, 1.0f)); - group.add(new ComplexTap(INSTRUMENT_AGC_OUTPUT, 0, 1.0f)); - group.add(new QPSKTap(INSTRUMENT_LSM_DEMODULATOR_OUTPUT, 0, 1.0f)); - group.add(new DibitTap(INSTRUMENT_QPSK_SLICER_OUTPUT, 0, 0.1f)); - - mAvailableTaps.add(group); - - if(mLSMDemodulator != null) - { - mAvailableTaps.addAll(mLSMDemodulator.getTapGroups()); - } - } - - return mAvailableTaps; - } - - /** - * Adds the instrumentation tap - */ - @Override - public void registerTap(Tap tap) - { - if(mLSMDemodulator != null) - { - mLSMDemodulator.registerTap(tap); - } - - switch(tap.getName()) - { - case INSTRUMENT_BASEBAND_FILTER_OUTPUT: - ComplexSampleTap baseband = (ComplexSampleTap)tap; - mStreamConverter.setListener(baseband); - baseband.setListener(mAGC); - break; - case INSTRUMENT_AGC_OUTPUT: - ComplexSampleTap agcSymbol = (ComplexSampleTap)tap; - mAGC.setListener(agcSymbol); - agcSymbol.setListener(mLSMDemodulator); - break; - case INSTRUMENT_LSM_DEMODULATOR_OUTPUT: - QPSKTap qpsk = (QPSKTap)tap; - mLSMDemodulator.setSymbolListener(qpsk); - qpsk.setListener(mQPSKSlicer); - break; - case INSTRUMENT_QPSK_SLICER_OUTPUT: - mQPSKSlicer.addListener((DibitTap)tap); - break; - default: - throw new IllegalArgumentException("Unrecognized tap: " + - tap.getName()); - } - } - - /** - * Removes the instrumentation tap - */ - @Override - public void unregisterTap(Tap tap) - { - if(mLSMDemodulator != null) - { - mLSMDemodulator.unregisterTap(tap); - } - - switch(tap.getName()) - { - case INSTRUMENT_BASEBAND_FILTER_OUTPUT: - mStreamConverter.setListener(mAGC); - break; - case INSTRUMENT_AGC_OUTPUT: - mAGC.setListener(mLSMDemodulator); - break; - case INSTRUMENT_LSM_DEMODULATOR_OUTPUT: - mLSMDemodulator.setSymbolListener(mQPSKSlicer); - break; - case INSTRUMENT_QPSK_SLICER_OUTPUT: - mQPSKSlicer.removeListener((DibitTap)tap); - break; - default: - throw new IllegalArgumentException("Unrecognized tap: " + - tap.getName()); - } - } - - @Override - public Listener getSourceEventListener() - { - return null; - } - - @Override - public void setSourceEventListener(Listener listener) - { - - } - - @Override - public void removeSourceEventListener() - { - - } - - public class ReusableBufferListener implements Listener - { - @Override - public void receive(ReusableComplexBuffer reusableComplexBuffer) - { - float[] samples = reusableComplexBuffer.getSamplesCopy(); - - //TODO: redesign the filter chain so that we can simply pass a float array ... - - mBasebandFilter.receive(new ComplexBuffer(samples)); - reusableComplexBuffer.decrementUserCount(); - } - } -} diff --git a/src/main/java/io/github/dsheirer/sample/complex/reusable/ReusableBufferQueue.java b/src/main/java/io/github/dsheirer/sample/complex/reusable/ReusableBufferQueue.java index 655c6967..89f7e81b 100644 --- a/src/main/java/io/github/dsheirer/sample/complex/reusable/ReusableBufferQueue.java +++ b/src/main/java/io/github/dsheirer/sample/complex/reusable/ReusableBufferQueue.java @@ -38,6 +38,11 @@ public class ReusableBufferQueue implements IReusableBufferDisposedListener } + public void dispose() + { + mReusableBufferQueue.clear(); + } + /** * Implements buffer disposed listener interface. Disposed (ie user count = 0) buffers will automatically callback * to this method to indicate when they are disposed. diff --git a/src/main/java/io/github/dsheirer/sample/complex/reusable/ReusableComplexBuffer.java b/src/main/java/io/github/dsheirer/sample/complex/reusable/ReusableComplexBuffer.java index 00345429..151852af 100644 --- a/src/main/java/io/github/dsheirer/sample/complex/reusable/ReusableComplexBuffer.java +++ b/src/main/java/io/github/dsheirer/sample/complex/reusable/ReusableComplexBuffer.java @@ -30,6 +30,10 @@ public class ReusableComplexBuffer extends ComplexBuffer /** * Creates a reusable, timestamped complex buffer using the specified time in milliseconds. * + * NOTE: reusability of this buffer requires strict user count tracking. Each component that receives this + * buffer should not modify the buffer contents. Each component should also increment the user count before + * sending this buffer to another component and should decrement the user count when finished using this buffer. + * * @param disposalListener to be notified when all consumers/users are finished using the buffer * @param samples of data * @param timestamp in millis for the buffer @@ -46,6 +50,10 @@ public class ReusableComplexBuffer extends ComplexBuffer /** * Constructs a timestamped complex buffer using the current system time in milliseconds. * + * NOTE: reusability of this buffer requires strict user count tracking. Each component that receives this + * buffer should not modify the buffer contents. Each component should also increment the user count before + * sending this buffer to another component and should decrement the user count when finished using this buffer. + * * @param disposalListener to be notified when all consumers are finished using the buffer * @param samples of data */ @@ -82,6 +90,11 @@ public class ReusableComplexBuffer extends ComplexBuffer return mTimestamp; } + public void setTimestamp(long timestamp) + { + mTimestamp = timestamp; + } + /** * Reloads this buffer by copying the sample array into this buffer. Internal buffer length will be automatically * resized to fit the new sample array length. diff --git a/src/main/java/io/github/dsheirer/source/IControllableFileSource.java b/src/main/java/io/github/dsheirer/source/IControllableFileSource.java index 59868d66..583b8007 100644 --- a/src/main/java/io/github/dsheirer/source/IControllableFileSource.java +++ b/src/main/java/io/github/dsheirer/source/IControllableFileSource.java @@ -40,7 +40,7 @@ public interface IControllableFileSource public long getFrameCount() throws IOException; - public int getSampleRate(); + public double getSampleRate(); public void setListener(IFrameLocationListener listener); diff --git a/src/main/java/io/github/dsheirer/source/tuner/TunerModel.java b/src/main/java/io/github/dsheirer/source/tuner/TunerModel.java index a88db737..615e20bb 100644 --- a/src/main/java/io/github/dsheirer/source/tuner/TunerModel.java +++ b/src/main/java/io/github/dsheirer/source/tuner/TunerModel.java @@ -77,11 +77,11 @@ public class TunerModel extends AbstractTableModel implements Listener Tuners ) - { - for( Tuner tuner: Tuners ) - { - addTuner( tuner ); - } - } + public void addTuners(List Tuners) + { + for(Tuner tuner : Tuners) + { + addTuner(tuner); + } + } /** * Adds the Tuner to this model @@ -284,17 +284,17 @@ public class TunerModel extends AbstractTableModel implements Listener it = mTuners.iterator(); - - Tuner tuner; + TunerChannelSource retVal = null; - if(config.hasPreferredTuner()) + TunerChannel tunerChannel = config.getTunerChannel(); + + tunerChannel.setBandwidth(bandwidth); + + Iterator it = mTuners.iterator(); + + Tuner tuner; + + if(config.hasPreferredTuner()) { tuner = getTuner(config.getPreferredTuner()); @@ -302,7 +302,7 @@ public class TunerModel extends AbstractTableModel implements Listener setLayout(new MigLayout("insets 0 0 0 0", "[left]", "")); mFrequencyControl = new JFrequencyControl(); mFrequencyControl.setEnabled(false); - mFrequencyControl.addListener(new IFrequencyChangeProcessor() + mFrequencyControl.addListener(new ISourceEventProcessor() { @Override - public void frequencyChanged(FrequencyChangeEvent event) + public void process(SourceEvent event) throws SourceException { setModified(true); } diff --git a/src/main/java/io/github/dsheirer/source/wave/ComplexWaveSource.java b/src/main/java/io/github/dsheirer/source/wave/ComplexWaveSource.java index 63453767..0abc2824 100644 --- a/src/main/java/io/github/dsheirer/source/wave/ComplexWaveSource.java +++ b/src/main/java/io/github/dsheirer/source/wave/ComplexWaveSource.java @@ -217,7 +217,9 @@ public class ComplexWaveSource extends ComplexSource implements IControllableFil float[] samples = ConversionUtils.convertFromSigned16BitSamples(buffer); ReusableComplexBuffer reusableBuffer = mReusableBufferQueue.getBuffer(samples.length); - mListener.receive(reusableBuffer.incrementUserCount()); + reusableBuffer.reloadFrom(samples, System.currentTimeMillis()); + reusableBuffer.incrementUserCount(); + mListener.receive(reusableBuffer); } } } diff --git a/src/main/java/io/github/dsheirer/spectrum/ChannelSpectrumPanel.java b/src/main/java/io/github/dsheirer/spectrum/ChannelSpectrumPanel.java index 879a7963..0431ff26 100644 --- a/src/main/java/io/github/dsheirer/spectrum/ChannelSpectrumPanel.java +++ b/src/main/java/io/github/dsheirer/spectrum/ChannelSpectrumPanel.java @@ -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 @@ -26,15 +26,15 @@ import io.github.dsheirer.dsp.filter.smoothing.SmoothingFilter.SmoothingType; import io.github.dsheirer.module.ProcessingChain; import io.github.dsheirer.sample.Listener; import io.github.dsheirer.sample.SampleType; -import io.github.dsheirer.sample.complex.ComplexBuffer; +import io.github.dsheirer.sample.complex.reusable.ReusableBufferQueue; +import io.github.dsheirer.sample.complex.reusable.ReusableComplexBuffer; import io.github.dsheirer.sample.real.RealBuffer; import io.github.dsheirer.settings.ColorSetting.ColorSettingName; import io.github.dsheirer.settings.ColorSettingMenuItem; import io.github.dsheirer.settings.Setting; import io.github.dsheirer.settings.SettingChangeListener; import io.github.dsheirer.settings.SettingsManager; -import io.github.dsheirer.source.tuner.frequency.FrequencyChangeEvent; -import io.github.dsheirer.source.tuner.frequency.FrequencyChangeEvent.Event; +import io.github.dsheirer.source.SourceEvent; import io.github.dsheirer.spectrum.converter.DFTResultsConverter; import io.github.dsheirer.spectrum.converter.RealDecibelConverter; import io.github.dsheirer.spectrum.menu.AveragingItem; @@ -45,8 +45,13 @@ import io.github.dsheirer.spectrum.menu.SmoothingItem; import io.github.dsheirer.spectrum.menu.SmoothingTypeItem; import net.miginfocom.swing.MigLayout; -import javax.swing.*; -import java.awt.*; +import javax.swing.JLayeredPane; +import javax.swing.JMenu; +import javax.swing.JPanel; +import javax.swing.JPopupMenu; +import javax.swing.JSeparator; +import javax.swing.SwingUtilities; +import java.awt.Component; import java.awt.event.ComponentEvent; import java.awt.event.ComponentListener; import java.awt.event.MouseEvent; @@ -54,22 +59,20 @@ import java.awt.event.MouseListener; import java.awt.event.MouseMotionListener; import java.util.concurrent.atomic.AtomicBoolean; -public class ChannelSpectrumPanel extends JPanel - implements ChannelEventListener, - Listener, - SettingChangeListener, - SpectralDisplayAdjuster +public class ChannelSpectrumPanel extends JPanel implements ChannelEventListener, + Listener, SettingChangeListener, SpectralDisplayAdjuster { - private static final long serialVersionUID = 1L; + private static final long serialVersionUID = 1L; - private DFTProcessor mDFTProcessor = new DFTProcessor( SampleType.REAL ); + private DFTProcessor mDFTProcessor = new DFTProcessor(SampleType.REAL); private DFTResultsConverter mDFTConverter = new RealDecibelConverter(); private JLayeredPane mLayeredPane; private SpectrumPanel mSpectrumPanel; private ChannelOverlayPanel mOverlayPanel; - + private ReusableBufferQueue mReusableBufferQueue = new ReusableBufferQueue(); + private Channel mCurrentChannel; - + private int mSampleBufferSize = 2400; private HalfBandFilter_RB_RB mDecimatingFilter = new HalfBandFilter_RB_RB( @@ -98,11 +101,10 @@ public class ChannelSpectrumPanel extends JPanel mDFTProcessor.addConverter( mDFTConverter ); mDFTConverter.addListener( mSpectrumPanel ); - /* Set the DFTProcessor to the decimated 24kHz sample rate */ - mDFTProcessor.frequencyChanged( - new FrequencyChangeEvent( Event.NOTIFICATION_SAMPLE_RATE_CHANGE, 24000 ) ); - - initGui(); + /* Set the DFTProcessor to the decimated 24kHz sample rate */ + mDFTProcessor.process(SourceEvent.sampleRateChange(24000.0)); + + initGui(); } public void dispose() @@ -272,9 +274,11 @@ public class ChannelSpectrumPanel extends JPanel { RealBuffer decimated = mDecimatingFilter.filter( buffer ); - //Hack: we're placing real samples in a complex buffer that the DFT - //processor is expecting. - mDFTProcessor.receive( new ComplexBuffer( decimated.getSamples() ) ); + //Hack: we're placing real samples in a complex buffer that the DFT + //processor is expecting. + ReusableComplexBuffer reusableComplexBuffer = mReusableBufferQueue.getBuffer(decimated.getSamples().length); + reusableComplexBuffer.reloadFrom(decimated.getSamples(), System.currentTimeMillis()); + mDFTProcessor.receive(reusableComplexBuffer.incrementUserCount()); } /** From c36f6901afa7f1ff78715b986c9f874ed6eca558 Mon Sep 17 00:00:00 2001 From: Denny Date: Sat, 24 Mar 2018 11:03:18 -0400 Subject: [PATCH 58/87] 39 - Adds P25/LSM support to the DemodulatorViewerFX application and adds Feed forward gain support to the C4FM DQPSK demodulator which seems to improve output from the interpolating sample buffer at each symbol period. Updates the symbol timing gain for C4FM to decrease the gain a bit since the feed forward gain is providing a better interpolated symbol. --- .../filter/fir/complex/ComplexFIRFilter.java | 6 +-- .../gain/ComplexFeedForwardGainControl.java | 33 +++++++++++++-- .../DQPSKDecisionDirectedSymbolEvaluator.java | 22 +++++----- .../dsp/psk/DQPSKGardnerSymbolEvaluator.java | 40 ++++++++++++------ .../dsp/psk/InterpolatingSampleBuffer.java | 10 +++-- .../viewer/chart/ComplexPhaseLineChart.java | 8 ++-- .../gui/viewer/chart/EyeDiagramChart.java | 4 +- .../viewer/chart/FMDemodulatedLineChart.java | 8 ++-- .../gui/viewer/decoder/P25Phase1C4FMPane.java | 2 +- .../gui/viewer/decoder/P25Phase1LSMPane.java | 2 +- .../module/decode/p25/P25DecoderC4FM.java | 13 ++++-- .../p25/P25DecoderC4FMInstrumented.java | 5 +-- .../module/decode/p25/P25DecoderLSM.java | 4 +- .../decode/p25/P25DecoderLSMInstrumented.java | 5 +-- .../dsheirer/sample/complex/Complex.java | 42 ++++++++++++------- .../source/wave/ComplexWaveSource.java | 2 +- 16 files changed, 130 insertions(+), 76 deletions(-) diff --git a/src/main/java/io/github/dsheirer/dsp/filter/fir/complex/ComplexFIRFilter.java b/src/main/java/io/github/dsheirer/dsp/filter/fir/complex/ComplexFIRFilter.java index 64cb1d0f..6c28556e 100644 --- a/src/main/java/io/github/dsheirer/dsp/filter/fir/complex/ComplexFIRFilter.java +++ b/src/main/java/io/github/dsheirer/dsp/filter/fir/complex/ComplexFIRFilter.java @@ -23,7 +23,7 @@ import io.github.dsheirer.sample.complex.reusable.ReusableComplexBuffer; public class ComplexFIRFilter extends FIRFilter { - private ReusableBufferQueue mReusableBufferQueue = new ReusableBufferQueue(); + private ReusableBufferQueue mReusableBufferQueue = new ReusableBufferQueue("Complex FIR Filter"); private RealFIRFilter mIFilter; private RealFIRFilter mQFilter; @@ -84,13 +84,13 @@ public class ComplexFIRFilter extends FIRFilter */ public ReusableComplexBuffer filter(ReusableComplexBuffer originalBuffer) { - ReusableComplexBuffer filteredBuffer = mReusableBufferQueue.getBuffer(originalBuffer.getSampleLength()); + ReusableComplexBuffer filteredBuffer = mReusableBufferQueue.getBuffer(originalBuffer.getSamples().length); filteredBuffer.setTimestamp(originalBuffer.getTimestamp()); float[] samples = originalBuffer.getSamples(); float[] filteredSamples = filteredBuffer.getSamples(); - for(int x = 0; x < originalBuffer.getSampleLength(); x += 2) + for(int x = 0; x < originalBuffer.getSamples().length; x += 2) { filteredSamples[x] = filterInphase(samples[x]); filteredSamples[x + 1] = filterQuadrature(samples[x + 1]); diff --git a/src/main/java/io/github/dsheirer/dsp/gain/ComplexFeedForwardGainControl.java b/src/main/java/io/github/dsheirer/dsp/gain/ComplexFeedForwardGainControl.java index 560655b4..13c54eda 100644 --- a/src/main/java/io/github/dsheirer/dsp/gain/ComplexFeedForwardGainControl.java +++ b/src/main/java/io/github/dsheirer/dsp/gain/ComplexFeedForwardGainControl.java @@ -29,7 +29,7 @@ public class ComplexFeedForwardGainControl implements ComplexSampleListener private ComplexSampleListener mListener; private FloatCircularBuffer mEnvelopeHistory; - private ReusableBufferQueue mReusableBufferQueue = new ReusableBufferQueue(); + private ReusableBufferQueue mReusableBufferQueue = new ReusableBufferQueue("ComplexFeedForwardGainControl"); private float mMaxEnvelope = 0.0f; private float mGain = 1.0f; @@ -155,11 +155,36 @@ public class ComplexFeedForwardGainControl implements ComplexSampleListener */ public ReusableComplexBuffer filter(ReusableComplexBuffer buffer) { - float[] filtered = filter(buffer.getSamples()); + float[] samples = buffer.getSamples(); - ReusableComplexBuffer filteredBuffer = mReusableBufferQueue.getBuffer(filtered.length); + ReusableComplexBuffer filteredBuffer = mReusableBufferQueue.getBuffer(samples.length); + filteredBuffer.setTimestamp(buffer.getTimestamp()); - filteredBuffer.reloadFrom(filtered, buffer.getTimestamp()); + float[] filtered = filteredBuffer.getSamples(); + + mMaxEnvelope = MINIMUM_ENVELOPE; + + float currentEnvelope; + + for(int x = 0; x < samples.length; x += 2) + { + currentEnvelope = Complex.envelope(samples[x], samples[x + 1]); + + if(currentEnvelope > mMaxEnvelope) + { + mMaxEnvelope = currentEnvelope; + } + } + + adjustGain(); + + for(int x = 0; x < samples.length; x += 2) + { + filtered[x] = samples[x] * mGain; + filtered[x + 1] = samples[x + 1] * mGain; + } + + buffer.decrementUserCount(); filteredBuffer.incrementUserCount(); return filteredBuffer; diff --git a/src/main/java/io/github/dsheirer/dsp/psk/DQPSKDecisionDirectedSymbolEvaluator.java b/src/main/java/io/github/dsheirer/dsp/psk/DQPSKDecisionDirectedSymbolEvaluator.java index 077010dc..61c82abc 100644 --- a/src/main/java/io/github/dsheirer/dsp/psk/DQPSKDecisionDirectedSymbolEvaluator.java +++ b/src/main/java/io/github/dsheirer/dsp/psk/DQPSKDecisionDirectedSymbolEvaluator.java @@ -25,7 +25,7 @@ public class DQPSKDecisionDirectedSymbolEvaluator implements IPSKSymbolEvaluator private static final Complex ROTATE_FROM_MINUS_45 = Complex.fromAngle(1.0 * Math.PI / 4.0); private static final Complex ROTATE_FROM_MINUS_135 = Complex.fromAngle(3.0 * Math.PI / 4.0); - private Complex mSymbolError = new Complex(0, 0); + private Complex mEvaluationSymbol = new Complex(0, 0); private float mPhaseError = 0.0f; private float mTimingError = 0.0f; private float mTimingErrorPolarity = 1.0f; @@ -59,46 +59,46 @@ public class DQPSKDecisionDirectedSymbolEvaluator implements IPSKSymbolEvaluator */ public void setSymbol(Complex preceding, Complex current) { - mSymbolError.setValues(current); + mEvaluationSymbol.setValues(current); - if(mSymbolError.quadrature() > 0.0f) + if(mEvaluationSymbol.quadrature() > 0.0f) { - if(mSymbolError.inphase() > 0.0f) + if(mEvaluationSymbol.inphase() > 0.0f) { mSymbolDecision = Dibit.D00_PLUS_1; mTimingErrorPolarity = (preceding.quadrature() > current.quadrature() ? 1.0f : -1.0f); - mSymbolError.multiply(ROTATE_FROM_PLUS_45); + mEvaluationSymbol.multiply(ROTATE_FROM_PLUS_45); } else { mSymbolDecision = Dibit.D01_PLUS_3; mTimingErrorPolarity = (preceding.quadrature() < current.quadrature() ? 1.0f : -1.0f); - mSymbolError.multiply(ROTATE_FROM_PLUS_135); + mEvaluationSymbol.multiply(ROTATE_FROM_PLUS_135); } } else { - if(mSymbolError.inphase() > 0.0f) + if(mEvaluationSymbol.inphase() > 0.0f) { mSymbolDecision = Dibit.D10_MINUS_1; mTimingErrorPolarity = (preceding.quadrature() > current.quadrature() ? 1.0f : -1.0f); - mSymbolError.multiply(ROTATE_FROM_MINUS_45); + mEvaluationSymbol.multiply(ROTATE_FROM_MINUS_45); } else { mSymbolDecision = Dibit.D11_MINUS_3; mTimingErrorPolarity = (preceding.quadrature() < current.quadrature() ? 1.0f : -1.0f); - mSymbolError.multiply(ROTATE_FROM_MINUS_135); + mEvaluationSymbol.multiply(ROTATE_FROM_MINUS_135); } } //Since we've rotated the error symbol back to 0 radians, the quadrature value closely approximates the //arctan of the error angle relative to 0 radians and this provides our error value - mPhaseError = -mSymbolError.quadrature(); + mPhaseError = -mEvaluationSymbol.quadrature(); //Timing error is the same as phase error with the sign corrected according to the vector's angular rotation - mTimingError = mSymbolError.quadrature() * mTimingErrorPolarity; + mTimingError = mEvaluationSymbol.quadrature() * mTimingErrorPolarity; } /** diff --git a/src/main/java/io/github/dsheirer/dsp/psk/DQPSKGardnerSymbolEvaluator.java b/src/main/java/io/github/dsheirer/dsp/psk/DQPSKGardnerSymbolEvaluator.java index e54ae90a..e2e1b544 100644 --- a/src/main/java/io/github/dsheirer/dsp/psk/DQPSKGardnerSymbolEvaluator.java +++ b/src/main/java/io/github/dsheirer/dsp/psk/DQPSKGardnerSymbolEvaluator.java @@ -20,10 +20,16 @@ import io.github.dsheirer.sample.complex.Complex; public class DQPSKGardnerSymbolEvaluator implements IPSKSymbolEvaluator { + private static final Complex ROTATE_FROM_PLUS_135 = Complex.fromAngle(-3.0 * Math.PI / 4.0); + private static final Complex ROTATE_FROM_PLUS_45 = Complex.fromAngle(-1.0 * Math.PI / 4.0); + private static final Complex ROTATE_FROM_MINUS_45 = Complex.fromAngle(1.0 * Math.PI / 4.0); + private static final Complex ROTATE_FROM_MINUS_135 = Complex.fromAngle(3.0 * Math.PI / 4.0); + private float mPhaseError = 0.0f; private float mTimingError = 0.0f; private Dibit mSymbolDecision = Dibit.D00_PLUS_1; private Complex mPreviousSymbol = new Complex(0, 0); + private Complex mEvaluationSymbol = new Complex(0, 0); /** * Differential QPSK Decision-directed symbol phase and timing error detector and symbol decision slicer. @@ -53,40 +59,48 @@ public class DQPSKGardnerSymbolEvaluator implements IPSKSymbolEvaluator */ public void setSymbols(Complex middle, Complex current) { - /* Gardner timing error calculations */ + //Gardner timing error calculation float errorInphase = (mPreviousSymbol.inphase() - current.inphase()) * middle.inphase(); float errorQuadrature = (mPreviousSymbol.quadrature() - current.quadrature()) * middle.quadrature(); - mTimingError = normalize(errorInphase + errorQuadrature, 1.0f); + mTimingError = normalize(errorInphase + errorQuadrature, .3f); - //Calculate phase error and determine the transmitted dibit from the symbol - if(Math.abs(current.inphase()) > Math.abs(current.quadrature())) + //Store the current symbol to use in the next symbol calculation + mPreviousSymbol.setValues(current); + + //Phase error and symbol decision calculations ... + mEvaluationSymbol.setValues(current); + + if(mEvaluationSymbol.quadrature() > 0.0f) { - if(current.inphase() > 0) + if(mEvaluationSymbol.inphase() > 0.0f) { - mSymbolDecision = Dibit.D10_MINUS_1; - mPhaseError = -current.quadrature(); + mSymbolDecision = Dibit.D00_PLUS_1; + mEvaluationSymbol.multiply(ROTATE_FROM_PLUS_45); } else { mSymbolDecision = Dibit.D01_PLUS_3; - mPhaseError = current.quadrature(); + mEvaluationSymbol.multiply(ROTATE_FROM_PLUS_135); } + } else { - if(current.quadrature() > 0) + if(mEvaluationSymbol.inphase() > 0.0f) { - mSymbolDecision = Dibit.D00_PLUS_1; - mPhaseError = current.inphase(); + mSymbolDecision = Dibit.D10_MINUS_1; + mEvaluationSymbol.multiply(ROTATE_FROM_MINUS_45); } else { mSymbolDecision = Dibit.D11_MINUS_3; - mPhaseError = -current.inphase(); + mEvaluationSymbol.multiply(ROTATE_FROM_MINUS_135); } } - mPreviousSymbol.setValues(current); + //Since we've rotated the error symbol back to 0 radians, the quadrature value closely approximates the + //arctan of the error angle relative to 0 radians and this provides our error value + mPhaseError = -mEvaluationSymbol.quadrature(); } /** diff --git a/src/main/java/io/github/dsheirer/dsp/psk/InterpolatingSampleBuffer.java b/src/main/java/io/github/dsheirer/dsp/psk/InterpolatingSampleBuffer.java index 5b64f338..37ea8fe9 100644 --- a/src/main/java/io/github/dsheirer/dsp/psk/InterpolatingSampleBuffer.java +++ b/src/main/java/io/github/dsheirer/dsp/psk/InterpolatingSampleBuffer.java @@ -32,7 +32,7 @@ public class InterpolatingSampleBuffer private int mTwiceSamplesPerSymbol; private float mSamplingPoint; - private float mSampleCounterGain = 1.0f; + private float mSampleCounterGain = 0.5f; private float mDetectedSamplesPerSymbol; private float mDetectedSamplesPerSymbolGain = 0.1f * mSampleCounterGain * mSampleCounterGain; private float mMaximumSamplesPerSymbol; @@ -43,9 +43,9 @@ public class InterpolatingSampleBuffer /** * Buffer to store complex sample data and produce interpolated samples. * @param samplesPerSymbol - * @param symbolTimingGain for the symbol timing error adjustments + * @param sampleCounterGain for the symbol timing error adjustments */ - public InterpolatingSampleBuffer(float samplesPerSymbol, float symbolTimingGain) + public InterpolatingSampleBuffer(float samplesPerSymbol, float sampleCounterGain) { mSamplingPoint = samplesPerSymbol; mDetectedSamplesPerSymbol = samplesPerSymbol; @@ -54,7 +54,9 @@ public class InterpolatingSampleBuffer mTwiceSamplesPerSymbol = (int)Math.floor(2.0 * samplesPerSymbol); mDelayLineInphase = new float[2 * mTwiceSamplesPerSymbol]; mDelayLineQuadrature = new float[2 * mTwiceSamplesPerSymbol]; - mDetectedSamplesPerSymbolGain = symbolTimingGain * mSampleCounterGain * mSampleCounterGain; + + mSampleCounterGain = sampleCounterGain; + mDetectedSamplesPerSymbolGain = 0.1f * mSampleCounterGain * mSampleCounterGain; } /** diff --git a/src/main/java/io/github/dsheirer/instrument/gui/viewer/chart/ComplexPhaseLineChart.java b/src/main/java/io/github/dsheirer/instrument/gui/viewer/chart/ComplexPhaseLineChart.java index 68d86efb..3d7694a2 100644 --- a/src/main/java/io/github/dsheirer/instrument/gui/viewer/chart/ComplexPhaseLineChart.java +++ b/src/main/java/io/github/dsheirer/instrument/gui/viewer/chart/ComplexPhaseLineChart.java @@ -19,7 +19,7 @@ import io.github.dsheirer.buffer.ComplexCircularBuffer; import io.github.dsheirer.dsp.gain.ComplexGain; import io.github.dsheirer.sample.Listener; import io.github.dsheirer.sample.complex.Complex; -import io.github.dsheirer.sample.complex.ComplexBuffer; +import io.github.dsheirer.sample.complex.reusable.ReusableComplexBuffer; import javafx.beans.property.IntegerProperty; import javafx.beans.property.SimpleIntegerProperty; import javafx.beans.value.ChangeListener; @@ -31,7 +31,7 @@ import javafx.scene.chart.NumberAxis; import org.slf4j.Logger; import org.slf4j.LoggerFactory; -public class ComplexPhaseLineChart extends LineChart implements Listener +public class ComplexPhaseLineChart extends LineChart implements Listener { private final static Logger mLog = LoggerFactory.getLogger(ComplexPhaseLineChart.class); @@ -81,7 +81,7 @@ public class ComplexPhaseLineChart extends LineChart implements Listener +public class FMDemodulatedLineChart extends LineChart implements Listener { private final static Logger mLog = LoggerFactory.getLogger(FMDemodulatedLineChart.class); @@ -73,7 +73,7 @@ public class FMDemodulatedLineChart extends LineChart implements Listener mBasebandFilters = new HashMap<>(); private ComplexFIRFilter mBasebandFilter; @@ -61,7 +63,7 @@ public class P25DecoderC4FM extends P25Decoder mBasebandFilter = new ComplexFIRFilter(getBasebandFilter(), 1.0f); mCostasLoop = new CostasLoop(getSampleRate(), getSymbolRate()); - mInterpolatingSampleBuffer = new InterpolatingSampleBuffer(getSamplesPerSymbol(), SYMBOL_TIMING_GAIN); + mInterpolatingSampleBuffer = new InterpolatingSampleBuffer(getSamplesPerSymbol(), SAMPLE_COUNTER_GAIN); mQPSKDemodulator = new DQPSKDecisionDirectedDemodulator(mCostasLoop, mInterpolatingSampleBuffer); @@ -79,10 +81,13 @@ public class P25DecoderC4FM extends P25Decoder public void receive(ReusableComplexBuffer reusableComplexBuffer) { //User accounting of the incoming buffer is handled by the filter - ReusableComplexBuffer filtered = filter(reusableComplexBuffer); + ReusableComplexBuffer basebandFiltered = filter(reusableComplexBuffer); + + //User accounting of the incoming buffer is handled by the gain filter + ReusableComplexBuffer gainApplied = mAGC.filter(basebandFiltered); //User accounting of the filtered buffer is handled by the demodulator - mQPSKDemodulator.receive(filtered); + mQPSKDemodulator.receive(gainApplied); } /** diff --git a/src/main/java/io/github/dsheirer/module/decode/p25/P25DecoderC4FMInstrumented.java b/src/main/java/io/github/dsheirer/module/decode/p25/P25DecoderC4FMInstrumented.java index c7b329bc..46762450 100644 --- a/src/main/java/io/github/dsheirer/module/decode/p25/P25DecoderC4FMInstrumented.java +++ b/src/main/java/io/github/dsheirer/module/decode/p25/P25DecoderC4FMInstrumented.java @@ -50,9 +50,6 @@ public class P25DecoderC4FMInstrumented extends P25DecoderC4FM { ReusableComplexBuffer filtered = super.filter(reusableComplexBuffer); - //Increment user count before we hand to another listener or return from this method - filtered.incrementUserCount(); - if(mFilteredSymbolListener != null) { filtered.incrementUserCount(); @@ -70,7 +67,7 @@ public class P25DecoderC4FMInstrumented extends P25DecoderC4FM super.setSampleRate(sampleRate); InterpolatingSampleBufferInstrumented instrumentedBuffer = - new InterpolatingSampleBufferInstrumented(getSamplesPerSymbol(), SYMBOL_TIMING_GAIN); + new InterpolatingSampleBufferInstrumented(getSamplesPerSymbol(), SAMPLE_COUNTER_GAIN); mInterpolatingSampleBuffer = instrumentedBuffer; DQPSKDecisionDirectedDemodulatorInstrumented instrumented = new DQPSKDecisionDirectedDemodulatorInstrumented(mCostasLoop, instrumentedBuffer, getSampleRate()); diff --git a/src/main/java/io/github/dsheirer/module/decode/p25/P25DecoderLSM.java b/src/main/java/io/github/dsheirer/module/decode/p25/P25DecoderLSM.java index 74a8cceb..1fd4c683 100644 --- a/src/main/java/io/github/dsheirer/module/decode/p25/P25DecoderLSM.java +++ b/src/main/java/io/github/dsheirer/module/decode/p25/P25DecoderLSM.java @@ -34,7 +34,7 @@ import java.util.Map; public class P25DecoderLSM extends P25Decoder { private final static Logger mLog = LoggerFactory.getLogger(P25DecoderLSM.class); - protected static final float SYMBOL_TIMING_GAIN = 0.1f; + protected static final float SAMPLE_COUNTER_GAIN = 0.5f; private Map mBasebandFilters = new HashMap<>(); private ComplexFIRFilter mBasebandFilter; @@ -67,7 +67,7 @@ public class P25DecoderLSM extends P25Decoder mBasebandFilter = new ComplexFIRFilter(getBasebandFilter(), 1.0f); mCostasLoop = new CostasLoop(getSampleRate(), getSymbolRate()); - mInterpolatingSampleBuffer = new InterpolatingSampleBuffer(getSamplesPerSymbol(), SYMBOL_TIMING_GAIN); + mInterpolatingSampleBuffer = new InterpolatingSampleBuffer(getSamplesPerSymbol(), SAMPLE_COUNTER_GAIN); mQPSKDemodulator = new DQPSKGardnerDemodulator(mCostasLoop, mInterpolatingSampleBuffer); diff --git a/src/main/java/io/github/dsheirer/module/decode/p25/P25DecoderLSMInstrumented.java b/src/main/java/io/github/dsheirer/module/decode/p25/P25DecoderLSMInstrumented.java index 179d3503..41292d32 100644 --- a/src/main/java/io/github/dsheirer/module/decode/p25/P25DecoderLSMInstrumented.java +++ b/src/main/java/io/github/dsheirer/module/decode/p25/P25DecoderLSMInstrumented.java @@ -52,9 +52,6 @@ public class P25DecoderLSMInstrumented extends P25DecoderLSM { ReusableComplexBuffer filtered = super.filter(reusableComplexBuffer); - //Increment user count before we hand to another listener or return from this method - filtered.incrementUserCount(); - if(mFilteredSymbolListener != null) { filtered.incrementUserCount(); @@ -71,7 +68,7 @@ public class P25DecoderLSMInstrumented extends P25DecoderLSM { super.setSampleRate(sampleRate); - InterpolatingSampleBufferInstrumented instrumentedBuffer = new InterpolatingSampleBufferInstrumented(getSamplesPerSymbol(), SYMBOL_TIMING_GAIN); + InterpolatingSampleBufferInstrumented instrumentedBuffer = new InterpolatingSampleBufferInstrumented(getSamplesPerSymbol(), SAMPLE_COUNTER_GAIN); mInterpolatingSampleBuffer = instrumentedBuffer; DQPSKGardnerDemodulatorInstrumented instrumented = new DQPSKGardnerDemodulatorInstrumented(mCostasLoop, instrumentedBuffer, getSampleRate()); diff --git a/src/main/java/io/github/dsheirer/sample/complex/Complex.java b/src/main/java/io/github/dsheirer/sample/complex/Complex.java index 93c15627..f61f45a5 100644 --- a/src/main/java/io/github/dsheirer/sample/complex/Complex.java +++ b/src/main/java/io/github/dsheirer/sample/complex/Complex.java @@ -248,12 +248,35 @@ public class Complex implements Serializable } /** - * Returns the vector length to 1 (unit circle), - * avoiding square root multiplication + * Maintains the vector magnitude close to 1.0 (unit circle) when the magnitude is already + * close to 1.0, avoiding square root multiplication. If you need a faster way to set the + * magnitude to close to 1.0 without the square root of normalize(), use fastNormalize2(). */ public void fastNormalize() { - multiply((float)(1.95f - magnitudeSquared())); + multiply((float)(1.9999f - magnitudeSquared())); + } + + /** + * Sets vector magnitude to close to 1.0f without the magnitude square root operation + */ + public void normalizeFast() + { + float inphaseAbsolute = Math.abs(inphase()); + float quadratureAbsolute = Math.abs(quadrature()); + + float gain; + + if(inphaseAbsolute > quadratureAbsolute) + { + gain = 1.0f / (inphaseAbsolute + (0.4f * quadratureAbsolute)); + } + else + { + gain = 1.0f / (quadratureAbsolute + (0.4f * inphaseAbsolute)); + } + + multiply(gain); } public float left() @@ -434,17 +457,4 @@ public class Complex implements Serializable return quadratureAbsolute + (0.4f * inphaseAbsolute); } } - - public static void main(String[] args) - { - Complex ROTATE_FROM_MINUS_45 = Complex.fromAngle(1.0 * Math.PI / 4.0); - Complex ROTATE_FROM_PLUS_135 = Complex.fromAngle(-3.0 * Math.PI / 4.0); - mLog.debug("REF: " + ROTATE_FROM_MINUS_45.toString()); - - Complex symbol = new Complex(0.7665009f, -0.6422432f); - mLog.debug("SYM: " + symbol.toString()); - - symbol.multiply(ROTATE_FROM_MINUS_45); - mLog.debug("DER: " + symbol.toString()); - } } diff --git a/src/main/java/io/github/dsheirer/source/wave/ComplexWaveSource.java b/src/main/java/io/github/dsheirer/source/wave/ComplexWaveSource.java index 0abc2824..256a53f3 100644 --- a/src/main/java/io/github/dsheirer/source/wave/ComplexWaveSource.java +++ b/src/main/java/io/github/dsheirer/source/wave/ComplexWaveSource.java @@ -45,7 +45,7 @@ public class ComplexWaveSource extends ComplexSource implements IControllableFil private Listener mListener; private AudioInputStream mInputStream; private File mFile; - private ReusableBufferQueue mReusableBufferQueue = new ReusableBufferQueue(); + private ReusableBufferQueue mReusableBufferQueue = new ReusableBufferQueue("Complex Wave Source"); public ComplexWaveSource(File file) throws IOException { From 60ffb18cd99b5e5dc692d7bd4bac239456e081ad Mon Sep 17 00:00:00 2001 From: Denny Date: Sun, 25 Mar 2018 09:13:03 -0400 Subject: [PATCH 59/87] 39 [WIP] cleans up the reusable buffers and creates both real and complex variants. --- .idea/dictionaries/denny.xml | 1 + .../github/dsheirer/audio/AudioModule2.java | 179 +++++ .../AbstractComplexPolyphaseChannelizer.java | 2 +- .../filter/channelizer/ChannelizerViewer.java | 19 +- .../ComplexPolyphaseChannelizerM2.java | 2 +- ...or.java => ContinuousBufferProcessor.java} | 40 +- ...=> ContinuousReusableBufferProcessor.java} | 14 +- .../channelizer/PolyphaseChannelManager.java | 26 +- .../channelizer/PolyphaseChannelSource.java | 4 +- .../filter/channelizer/SynthesizerViewer.java | 27 +- .../output/ChannelOutputProcessor.java | 2 +- .../IPolyphaseChannelOutputProcessor.java | 2 +- .../output/OneChannelOutputProcessor.java | 2 +- .../output/TwoChannelOutputProcessor.java | 2 +- .../filter/fir/complex/ComplexFIRFilter.java | 10 +- .../dsp/filter/resample/AudioResampler.java | 70 -- .../dsp/filter/resample/RealResampler.java | 166 ++++ .../gain/ComplexFeedForwardGainControl.java | 8 +- .../dsp/mixer/AbstractOscillator.java | 4 +- .../dsheirer/dsp/mixer/IOscillator.java | 2 +- .../dsheirer/dsp/psk/PSKDemodulator.java | 2 +- .../gui/viewer/PlaybackController.java | 4 +- .../viewer/chart/ComplexPhaseLineChart.java | 2 +- .../viewer/chart/ComplexSampleLineChart.java | 2 +- .../viewer/chart/FMDemodulatedLineChart.java | 2 +- .../gui/viewer/decoder/DecoderPane.java | 4 +- .../gui/viewer/decoder/P25Phase1C4FMPane.java | 2 +- .../gui/viewer/decoder/P25Phase1LSMPane.java | 2 +- .../dsheirer/module/ProcessingChain.java | 4 +- .../module/decode/DecoderFactory.java | 14 +- .../module/decode/p25/P25Decoder.java | 4 +- .../module/decode/p25/P25DecoderC4FM.java | 2 +- .../p25/P25DecoderC4FMInstrumented.java | 2 +- .../module/decode/p25/P25DecoderLSM.java | 2 +- .../decode/p25/P25DecoderLSMInstrumented.java | 2 +- .../demodulate/am/AMDemodulatorModule.java | 4 +- .../demodulate/fm/FMDemodulatorModule.java | 4 +- .../wave/ComplexBufferWaveRecorder.java | 10 +- .../dsheirer/sample/ConversionUtils.java | 87 +- .../sample/adapter/ComplexShortAdapter.java | 2 +- .../sample/adapter/IComplexSampleAdapter.java | 2 +- .../AbstractReusableBufferQueue.java} | 55 +- .../IReusableBufferDisposedListener.java | 8 +- .../IReusableBufferProvider.java} | 8 +- .../IReusableComplexBufferListener.java | 6 +- ...erflowableReusableBufferTransferQueue.java | 12 +- .../ReusableBuffer.java} | 92 ++- .../ReusableBufferAssembler.java | 6 +- .../ReusableBufferBroadcaster.java | 25 +- .../sample/buffer/ReusableBufferQueue.java | 39 + .../sample/buffer/ReusableComplexBuffer.java | 59 ++ .../buffer/ReusableComplexBufferQueue.java | 39 + .../github/dsheirer/source/ComplexSource.java | 2 +- .../dsheirer/source/mixer/ComplexMixer.java | 8 +- .../source/tuner/TunerController.java | 18 +- .../tuner/channel/TunerChannelSource.java | 2 +- .../source/tuner/fcd/FCDTunerController.java | 4 +- .../tuner/manager/ChannelSourceManager.java | 3 +- .../manager/TunerChannelSourceManager.java | 4 +- .../tuner/rtl/RTL2832TunerController.java | 2 +- .../source/tuner/test/SampleGenerator.java | 13 +- .../tuner/test/TestTunerController.java | 2 +- .../tuner/usb/USBTransferProcessor.java | 7 +- .../source/tuner/usb/USBTunerController.java | 2 +- .../usb/converter/NativeBufferConverter.java | 9 +- .../source/wave/ComplexWaveSource.java | 8 +- .../spectrum/ChannelSpectrumPanel.java | 749 +++++++++--------- .../dsheirer/spectrum/DFTProcessor.java | 2 +- .../spectrum/OverflowableBufferStream.java | 9 +- .../spectrum/SpectralDisplayPanel.java | 5 +- 70 files changed, 1211 insertions(+), 728 deletions(-) create mode 100644 src/main/java/io/github/dsheirer/audio/AudioModule2.java rename src/main/java/io/github/dsheirer/dsp/filter/channelizer/{ScheduledBufferProcessor.java => ContinuousBufferProcessor.java} (80%) rename src/main/java/io/github/dsheirer/dsp/filter/channelizer/{ScheduledReusableBufferProcessor.java => ContinuousReusableBufferProcessor.java} (71%) delete mode 100644 src/main/java/io/github/dsheirer/dsp/filter/resample/AudioResampler.java create mode 100644 src/main/java/io/github/dsheirer/dsp/filter/resample/RealResampler.java rename src/main/java/io/github/dsheirer/sample/{complex/reusable/ReusableBufferQueue.java => buffer/AbstractReusableBufferQueue.java} (50%) rename src/main/java/io/github/dsheirer/sample/{complex/reusable => buffer}/IReusableBufferDisposedListener.java (81%) rename src/main/java/io/github/dsheirer/sample/{complex/reusable/IReusableComplexBufferProvider.java => buffer/IReusableBufferProvider.java} (83%) rename src/main/java/io/github/dsheirer/sample/{complex/reusable => buffer}/IReusableComplexBufferListener.java (84%) rename src/main/java/io/github/dsheirer/sample/{complex/reusable => buffer}/OverflowableReusableBufferTransferQueue.java (85%) rename src/main/java/io/github/dsheirer/sample/{complex/reusable/ReusableComplexBuffer.java => buffer/ReusableBuffer.java} (80%) rename src/main/java/io/github/dsheirer/sample/{complex/reusable => buffer}/ReusableBufferAssembler.java (95%) rename src/main/java/io/github/dsheirer/sample/{complex/reusable => buffer}/ReusableBufferBroadcaster.java (66%) create mode 100644 src/main/java/io/github/dsheirer/sample/buffer/ReusableBufferQueue.java create mode 100644 src/main/java/io/github/dsheirer/sample/buffer/ReusableComplexBuffer.java create mode 100644 src/main/java/io/github/dsheirer/sample/buffer/ReusableComplexBufferQueue.java diff --git a/.idea/dictionaries/denny.xml b/.idea/dictionaries/denny.xml index a9674196..35e8bffe 100644 --- a/.idea/dictionaries/denny.xml +++ b/.idea/dictionaries/denny.xml @@ -14,6 +14,7 @@ overflowable polyphase remez + resampler samplerate sdrtrunk sheirer diff --git a/src/main/java/io/github/dsheirer/audio/AudioModule2.java b/src/main/java/io/github/dsheirer/audio/AudioModule2.java new file mode 100644 index 00000000..c2f498ac --- /dev/null +++ b/src/main/java/io/github/dsheirer/audio/AudioModule2.java @@ -0,0 +1,179 @@ +/******************************************************************************* + * 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 + * + ******************************************************************************/ +package io.github.dsheirer.audio; + +import io.github.dsheirer.audio.squelch.ISquelchStateListener; +import io.github.dsheirer.audio.squelch.SquelchState; +import io.github.dsheirer.channel.metadata.Metadata; +import io.github.dsheirer.dsp.filter.resample.RealResampler; +import io.github.dsheirer.module.Module; +import io.github.dsheirer.sample.Listener; +import io.github.dsheirer.sample.buffer.ReusableBuffer; +import io.github.dsheirer.sample.real.IFilteredRealBufferListener; +import io.github.dsheirer.sample.real.RealBuffer; +import io.github.dsheirer.source.ISourceEventListener; +import io.github.dsheirer.source.SourceEvent; +import org.slf4j.Logger; +import org.slf4j.LoggerFactory; + +/** + * Provides packaging of demodulated audio sample buffers into audio packets for + * broadcast to registered audio packet listeners. Includes audio packet + * metadata in constructed audio packets. + * + * Incorporates audio squelch state listener to control if audio packets are + * broadcast or ignored. + */ +public class AudioModule2 extends Module implements IAudioPacketProvider, IFilteredRealBufferListener, + ISquelchStateListener, ISourceEventListener, Listener +{ + protected static final Logger mLog = LoggerFactory.getLogger(AudioModule2.class); + private static final double OUTPUT_SAMPLE_RATE = 8000.0; + private SquelchStateListener mSquelchStateListener = new SquelchStateListener(); + private SquelchState mSquelchState = SquelchState.SQUELCH; + private Listener mAudioPacketListener; + private Listener mResampledBufferListener; + private RealResampler mAudioResampler; + private Metadata mMetadata; + + public AudioModule2(Metadata metadata) + { + mMetadata = metadata; + + mResampledBufferListener = new Listener() + { + @Override + public void receive(ReusableBuffer reusableBuffer) + { + if(mAudioPacketListener != null) + { + AudioPacket packet = new AudioPacket(reusableBuffer.getSamplesCopy(), mMetadata.copyOf()); + reusableBuffer.decrementUserCount(); + mAudioPacketListener.receive(packet); + } + } + }; + + setSampleRate(25000.0); + } + + /** + * Sets the incoming sample rate + */ + private void setSampleRate(double sampleRate) + { + mAudioResampler = new RealResampler(sampleRate, OUTPUT_SAMPLE_RATE); + mAudioResampler.setListener(mResampledBufferListener); + } + + @Override + public void dispose() + { + mSquelchStateListener = null; + mAudioPacketListener = null; + } + + @Override + public void reset() + { + } + + @Override + public void start() + { + /* No start operations provided */ + } + + @Override + public void stop() + { + /* Issue an end audio packet in case a recorder is still rolling */ + if(mAudioPacketListener != null) + { + mAudioPacketListener.receive(new AudioPacket(AudioPacket.Type.END, mMetadata.copyOf())); + } + } + + /** + * Processes demodulated audio samples into audio packets with current audio + * metadata and sends to the registered listener + */ + @Override + public void receive(RealBuffer buffer) + { + if(mAudioResampler != null && mSquelchState == SquelchState.UNSQUELCH) + { + mAudioResampler.receive(buffer); + } + } + + @Override + public Listener getFilteredRealBufferListener() + { + return this; + } + + @Override + public void setAudioPacketListener(Listener listener) + { + mAudioPacketListener = listener; + } + + @Override + public void removeAudioPacketListener() + { + mAudioPacketListener = null; + } + + @Override + public Listener getSourceEventListener() + { + return new Listener() + { + @Override + public void receive(SourceEvent sourceEvent) + { + if(sourceEvent.getEvent() == SourceEvent.Event.NOTIFICATION_SAMPLE_RATE_CHANGE) + { + setSampleRate(sourceEvent.getValue().doubleValue()); + } + } + }; + } + + @Override + public Listener getSquelchStateListener() + { + return mSquelchStateListener; + } + + /** + * Wrapper for squelch state listener + */ + public class SquelchStateListener implements Listener + { + @Override + public void receive(SquelchState state) + { + if(state == SquelchState.SQUELCH && mAudioPacketListener != null) + { + mAudioPacketListener.receive(new AudioPacket(AudioPacket.Type.END, mMetadata.copyOf())); + } + + mSquelchState = state; + } + } +} diff --git a/src/main/java/io/github/dsheirer/dsp/filter/channelizer/AbstractComplexPolyphaseChannelizer.java b/src/main/java/io/github/dsheirer/dsp/filter/channelizer/AbstractComplexPolyphaseChannelizer.java index cbffb3f7..7de9ead9 100644 --- a/src/main/java/io/github/dsheirer/dsp/filter/channelizer/AbstractComplexPolyphaseChannelizer.java +++ b/src/main/java/io/github/dsheirer/dsp/filter/channelizer/AbstractComplexPolyphaseChannelizer.java @@ -20,7 +20,7 @@ package io.github.dsheirer.dsp.filter.channelizer; import io.github.dsheirer.sample.Broadcaster; import io.github.dsheirer.sample.Listener; -import io.github.dsheirer.sample.complex.reusable.ReusableComplexBuffer; +import io.github.dsheirer.sample.buffer.ReusableComplexBuffer; import io.github.dsheirer.source.ISourceEventListener; import io.github.dsheirer.source.SourceEvent; import org.slf4j.Logger; diff --git a/src/main/java/io/github/dsheirer/dsp/filter/channelizer/ChannelizerViewer.java b/src/main/java/io/github/dsheirer/dsp/filter/channelizer/ChannelizerViewer.java index 820ea2c0..c6d6c25f 100644 --- a/src/main/java/io/github/dsheirer/dsp/filter/channelizer/ChannelizerViewer.java +++ b/src/main/java/io/github/dsheirer/dsp/filter/channelizer/ChannelizerViewer.java @@ -18,7 +18,7 @@ package io.github.dsheirer.dsp.filter.channelizer; import io.github.dsheirer.sample.Listener; import io.github.dsheirer.sample.SampleType; -import io.github.dsheirer.sample.complex.reusable.ReusableComplexBuffer; +import io.github.dsheirer.sample.buffer.ReusableComplexBuffer; import io.github.dsheirer.settings.SettingsManager; import io.github.dsheirer.source.ISourceEventProcessor; import io.github.dsheirer.source.SourceEvent; @@ -258,7 +258,7 @@ public class ChannelizerViewer extends JFrame for(int x = 0; x < mChannelCount; x++) { - TunerChannelSource source = mTestTuner.getChannelSourceManager().getSpecialSource(x); + TunerChannelSource source = mTestTuner.getChannelSourceManager().getTestingSource(x); DiscreteChannelPanel channelPanel = new DiscreteChannelPanel(mSettingsManager, source, x); channelPanel.setDFTSize(mChannelPanelDFTSize); @@ -327,8 +327,7 @@ public class ChannelizerViewer extends JFrame @Override public void receive(ReusableComplexBuffer reusableComplexBuffer) { - mDFTProcessor.receive(reusableComplexBuffer.incrementUserCount()); - reusableComplexBuffer.decrementUserCount(); + mDFTProcessor.receive(reusableComplexBuffer); } @Override @@ -373,7 +372,8 @@ public class ChannelizerViewer extends JFrame mLog.debug("Samples:" + Arrays.toString(complexBuffer.getSamples())); } - mDFTProcessor.receive(complexBuffer.incrementUserCount()); + complexBuffer.incrementUserCount(); + mDFTProcessor.receive(complexBuffer); complexBuffer.decrementUserCount(); } }); @@ -420,7 +420,8 @@ public class ChannelizerViewer extends JFrame @Override public void receive(ReusableComplexBuffer reusableComplexBuffer) { - mDFTProcessor.receive(reusableComplexBuffer.incrementUserCount()); + reusableComplexBuffer.incrementUserCount(); + mDFTProcessor.receive(reusableComplexBuffer); reusableComplexBuffer.decrementUserCount(); } @@ -476,7 +477,8 @@ public class ChannelizerViewer extends JFrame mLog.debug("Samples:" + Arrays.toString(complexBuffer.getSamples())); } - mDFTProcessor.receive(complexBuffer.incrementUserCount()); + complexBuffer.incrementUserCount(); + mDFTProcessor.receive(complexBuffer); complexBuffer.decrementUserCount(); } }); @@ -502,7 +504,8 @@ public class ChannelizerViewer extends JFrame @Override public void receive(ReusableComplexBuffer reusableComplexBuffer) { - mDFTProcessor.receive(reusableComplexBuffer.incrementUserCount()); + reusableComplexBuffer.incrementUserCount(); + mDFTProcessor.receive(reusableComplexBuffer); reusableComplexBuffer.decrementUserCount(); } diff --git a/src/main/java/io/github/dsheirer/dsp/filter/channelizer/ComplexPolyphaseChannelizerM2.java b/src/main/java/io/github/dsheirer/dsp/filter/channelizer/ComplexPolyphaseChannelizerM2.java index ddb01ae7..4ed168e1 100644 --- a/src/main/java/io/github/dsheirer/dsp/filter/channelizer/ComplexPolyphaseChannelizerM2.java +++ b/src/main/java/io/github/dsheirer/dsp/filter/channelizer/ComplexPolyphaseChannelizerM2.java @@ -21,7 +21,7 @@ package io.github.dsheirer.dsp.filter.channelizer; import io.github.dsheirer.dsp.filter.FilterFactory; import io.github.dsheirer.dsp.filter.Window.WindowType; import io.github.dsheirer.dsp.filter.design.FilterDesignException; -import io.github.dsheirer.sample.complex.reusable.ReusableComplexBuffer; +import io.github.dsheirer.sample.buffer.ReusableComplexBuffer; import org.jtransforms.fft.FloatFFT_1D; import org.slf4j.Logger; import org.slf4j.LoggerFactory; diff --git a/src/main/java/io/github/dsheirer/dsp/filter/channelizer/ScheduledBufferProcessor.java b/src/main/java/io/github/dsheirer/dsp/filter/channelizer/ContinuousBufferProcessor.java similarity index 80% rename from src/main/java/io/github/dsheirer/dsp/filter/channelizer/ScheduledBufferProcessor.java rename to src/main/java/io/github/dsheirer/dsp/filter/channelizer/ContinuousBufferProcessor.java index 7c3ca3d7..8c6ec27c 100644 --- a/src/main/java/io/github/dsheirer/dsp/filter/channelizer/ScheduledBufferProcessor.java +++ b/src/main/java/io/github/dsheirer/dsp/filter/channelizer/ContinuousBufferProcessor.java @@ -28,15 +28,13 @@ import java.util.concurrent.ScheduledFuture; import java.util.concurrent.TimeUnit; import java.util.concurrent.atomic.AtomicBoolean; -public class ScheduledBufferProcessor implements Listener +public class ContinuousBufferProcessor implements Listener { - private final static Logger mLog = LoggerFactory.getLogger(ScheduledBufferProcessor.class); + private final static Logger mLog = LoggerFactory.getLogger(ContinuousBufferProcessor.class); private OverflowableTransferQueue mQueue; private Listener mListener; private ScheduledFuture mScheduledFuture; - private long mDistributionInterval; - private int mMaxBuffersPerInterval; private AtomicBoolean mRunning = new AtomicBoolean(); /** @@ -52,14 +50,10 @@ public class ScheduledBufferProcessor implements Listener * * @param maximumSize of the internal queue (overflow happens when this is exceeded) * @param resetThreshold of the internal queue (overflow reset happens once queue size falls below this threshold - * @param distributionInterval in milliseconds specifying how often the internal scheduled thread pool buffer - * distributor should run - * @param maxBuffersPerInterval maximum number of buffers to pull from the internal queue during each distribution - * interval */ - public ScheduledBufferProcessor(int maximumSize, int resetThreshold, long distributionInterval, int maxBuffersPerInterval) + public ContinuousBufferProcessor(int maximumSize, int resetThreshold) { - this(new OverflowableTransferQueue<>(maximumSize, resetThreshold), distributionInterval, maxBuffersPerInterval); + this(new OverflowableTransferQueue<>(maximumSize, resetThreshold)); } /** @@ -74,16 +68,10 @@ public class ScheduledBufferProcessor implements Listener * notifications of overflow and reset state. Queue sizing parameters are specified in the constructor. * * @param queue implmentation of an overflowable transfer queue - * @param distributionInterval in milliseconds specifying how often the internal scheduled thread pool buffer - * distributor should run - * @param maxBuffersPerInterval maximum number of buffers to pull from the internal queue during each distribution - * interval */ - public ScheduledBufferProcessor(OverflowableTransferQueue queue, long distributionInterval, int maxBuffersPerInterval) + public ContinuousBufferProcessor(OverflowableTransferQueue queue) { mQueue = queue; - mDistributionInterval = distributionInterval; - mMaxBuffersPerInterval = maxBuffersPerInterval; } /** @@ -116,6 +104,7 @@ public class ScheduledBufferProcessor implements Listener @Override public void receive(E e) { +// mLog.debug(" Queuing: " + e.toString()); mQueue.offer(e); } @@ -126,7 +115,7 @@ public class ScheduledBufferProcessor implements Listener { if(mRunning.compareAndSet(false, true)) { - mScheduledFuture = ThreadPool.SCHEDULED.scheduleAtFixedRate(new Processor(), 0, mDistributionInterval, TimeUnit.MILLISECONDS); + mScheduledFuture = ThreadPool.SCHEDULED.scheduleAtFixedRate(new Processor(), 0, 5, TimeUnit.MILLISECONDS); } } @@ -175,17 +164,22 @@ public class ScheduledBufferProcessor implements Listener { try { - mQueue.drainTo(mBuffers, mMaxBuffersPerInterval); + mQueue.drainTo(mBuffers, 10); if(mListener != null) { - for(E buffer: mBuffers) + while(!mBuffers.isEmpty()) { - mListener.receive(buffer); + for(E buffer: mBuffers) + { +// mLog.debug("Dispatching: " + buffer.toString() + " Count:" + mBuffers.size()); + mListener.receive(buffer); + } + + mBuffers.clear(); + mQueue.drainTo(mBuffers, 10); } } - - mBuffers.clear(); } catch(Throwable throwable) { diff --git a/src/main/java/io/github/dsheirer/dsp/filter/channelizer/ScheduledReusableBufferProcessor.java b/src/main/java/io/github/dsheirer/dsp/filter/channelizer/ContinuousReusableBufferProcessor.java similarity index 71% rename from src/main/java/io/github/dsheirer/dsp/filter/channelizer/ScheduledReusableBufferProcessor.java rename to src/main/java/io/github/dsheirer/dsp/filter/channelizer/ContinuousReusableBufferProcessor.java index 24eb7843..0439fb11 100644 --- a/src/main/java/io/github/dsheirer/dsp/filter/channelizer/ScheduledReusableBufferProcessor.java +++ b/src/main/java/io/github/dsheirer/dsp/filter/channelizer/ContinuousReusableBufferProcessor.java @@ -15,10 +15,10 @@ ******************************************************************************/ package io.github.dsheirer.dsp.filter.channelizer; -import io.github.dsheirer.sample.complex.reusable.OverflowableReusableBufferTransferQueue; -import io.github.dsheirer.sample.complex.reusable.ReusableComplexBuffer; +import io.github.dsheirer.sample.buffer.OverflowableReusableBufferTransferQueue; +import io.github.dsheirer.sample.buffer.ReusableBuffer; -public class ScheduledReusableBufferProcessor extends ScheduledBufferProcessor +public class ContinuousReusableBufferProcessor extends ContinuousBufferProcessor { /** * Scheduled Reusable Buffer Processor combines an internal overflowable buffer with a scheduled runnable processing @@ -33,13 +33,9 @@ public class ScheduledReusableBufferProcessor extends ScheduledBufferProcessor(maximumSize, resetThreshold)); } } diff --git a/src/main/java/io/github/dsheirer/dsp/filter/channelizer/PolyphaseChannelManager.java b/src/main/java/io/github/dsheirer/dsp/filter/channelizer/PolyphaseChannelManager.java index ee5ec599..d9118020 100644 --- a/src/main/java/io/github/dsheirer/dsp/filter/channelizer/PolyphaseChannelManager.java +++ b/src/main/java/io/github/dsheirer/dsp/filter/channelizer/PolyphaseChannelManager.java @@ -23,8 +23,8 @@ import io.github.dsheirer.dsp.filter.channelizer.output.TwoChannelOutputProcesso import io.github.dsheirer.dsp.filter.design.FilterDesignException; import io.github.dsheirer.sample.Broadcaster; import io.github.dsheirer.sample.Listener; -import io.github.dsheirer.sample.complex.reusable.IReusableComplexBufferProvider; -import io.github.dsheirer.sample.complex.reusable.ReusableComplexBuffer; +import io.github.dsheirer.sample.buffer.IReusableBufferProvider; +import io.github.dsheirer.sample.buffer.ReusableComplexBuffer; import io.github.dsheirer.source.ISourceEventProcessor; import io.github.dsheirer.source.SourceEvent; import io.github.dsheirer.source.SourceException; @@ -52,13 +52,13 @@ public class PolyphaseChannelManager implements ISourceEventProcessor private static final int POLYPHASE_FILTER_TAPS_PER_CHANNEL = 17; private Broadcaster mSourceEventBroadcaster = new Broadcaster<>(); - private IReusableComplexBufferProvider mReusableBufferProvider; + private IReusableBufferProvider mReusableBufferProvider; private List mChannelSources = new CopyOnWriteArrayList<>(); private ChannelCalculator mChannelCalculator; private ComplexPolyphaseChannelizerM2 mPolyphaseChannelizer; private ChannelSourceEventListener mChannelSourceEventListener = new ChannelSourceEventListener(); private BufferSourceEventMonitor mBufferSourceEventMonitor = new BufferSourceEventMonitor(); - private ScheduledBufferProcessor mBufferProcessor; + private ContinuousBufferProcessor mBufferProcessor; private Map mOutputProcessorFilters = new HashMap<>(); /** @@ -76,7 +76,8 @@ public class PolyphaseChannelManager implements ISourceEventProcessor * @param frequency of the provided complex buffer samples * @param sampleRate of the provided complex buffer samples */ - public PolyphaseChannelManager(IReusableComplexBufferProvider reusableBufferProvider, long frequency, double sampleRate) + public PolyphaseChannelManager(IReusableBufferProvider reusableBufferProvider, + long frequency, double sampleRate) { if(reusableBufferProvider == null) { @@ -95,7 +96,7 @@ public class PolyphaseChannelManager implements ISourceEventProcessor mChannelCalculator = new ChannelCalculator(sampleRate, channelCount, frequency, CHANNEL_OVERSAMPLING); - mBufferProcessor = new ScheduledBufferProcessor(500, 100, 50, 50); + mBufferProcessor = new ContinuousBufferProcessor(200, 50); mBufferProcessor.setListener(mBufferSourceEventMonitor); updateChannelizerSampleRate(sampleRate); @@ -160,7 +161,7 @@ public class PolyphaseChannelManager implements ISourceEventProcessor return channelSource; } - public TunerChannelSource getSpecialChannel(int index) + public TunerChannelSource getTestChannel(int index) { PolyphaseChannelSource channelSource = null; @@ -181,6 +182,7 @@ public class PolyphaseChannelManager implements ISourceEventProcessor } catch(IllegalArgumentException iae) { + mLog.error("Error", iae); } return channelSource; @@ -582,15 +584,19 @@ public class PolyphaseChannelManager implements ISourceEventProcessor if(mPolyphaseChannelizer != null) { - mPolyphaseChannelizer.receive(reusableComplexBuffer.incrementUserCount()); + //User count management is handled by the channelizer + mPolyphaseChannelizer.receive(reusableComplexBuffer); + } + else + { + reusableComplexBuffer.decrementUserCount(); } } catch(Throwable throwable) { mLog.error("Error", throwable); + reusableComplexBuffer.decrementUserCount(); } - - reusableComplexBuffer.decrementUserCount(); } } } \ No newline at end of file diff --git a/src/main/java/io/github/dsheirer/dsp/filter/channelizer/PolyphaseChannelSource.java b/src/main/java/io/github/dsheirer/dsp/filter/channelizer/PolyphaseChannelSource.java index 0e62e6c7..b6a6a99f 100644 --- a/src/main/java/io/github/dsheirer/dsp/filter/channelizer/PolyphaseChannelSource.java +++ b/src/main/java/io/github/dsheirer/dsp/filter/channelizer/PolyphaseChannelSource.java @@ -17,8 +17,8 @@ package io.github.dsheirer.dsp.filter.channelizer; import io.github.dsheirer.dsp.filter.channelizer.output.IPolyphaseChannelOutputProcessor; import io.github.dsheirer.sample.Listener; -import io.github.dsheirer.sample.complex.reusable.ReusableBufferAssembler; -import io.github.dsheirer.sample.complex.reusable.ReusableComplexBuffer; +import io.github.dsheirer.sample.buffer.ReusableBufferAssembler; +import io.github.dsheirer.sample.buffer.ReusableComplexBuffer; import io.github.dsheirer.source.SourceEvent; import io.github.dsheirer.source.tuner.channel.TunerChannel; import io.github.dsheirer.source.tuner.channel.TunerChannelSource; diff --git a/src/main/java/io/github/dsheirer/dsp/filter/channelizer/SynthesizerViewer.java b/src/main/java/io/github/dsheirer/dsp/filter/channelizer/SynthesizerViewer.java index 75d48dea..ff8ae0d9 100644 --- a/src/main/java/io/github/dsheirer/dsp/filter/channelizer/SynthesizerViewer.java +++ b/src/main/java/io/github/dsheirer/dsp/filter/channelizer/SynthesizerViewer.java @@ -23,8 +23,8 @@ 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.reusable.ReusableBufferQueue; -import io.github.dsheirer.sample.complex.reusable.ReusableComplexBuffer; +import io.github.dsheirer.sample.buffer.ReusableComplexBuffer; +import io.github.dsheirer.sample.buffer.ReusableComplexBufferQueue; import io.github.dsheirer.sample.real.IOverflowListener; import io.github.dsheirer.settings.SettingsManager; import io.github.dsheirer.source.SourceEvent; @@ -177,8 +177,7 @@ public class SynthesizerViewer extends JFrame @Override public void receive(ReusableComplexBuffer reusableComplexBuffer) { - mDFTProcessor.receive(reusableComplexBuffer.incrementUserCount()); - reusableComplexBuffer.decrementUserCount(); + mDFTProcessor.receive(reusableComplexBuffer); } } @@ -212,8 +211,7 @@ public class SynthesizerViewer extends JFrame @Override public void receive(ReusableComplexBuffer reusableComplexBuffer) { - mDFTProcessor.receive(reusableComplexBuffer.incrementUserCount()); - reusableComplexBuffer.decrementUserCount(); + mDFTProcessor.receive(reusableComplexBuffer); } } @@ -277,7 +275,7 @@ public class SynthesizerViewer extends JFrame private TwoChannelSynthesizerM2 mSynthesizer; private FS4DownConverter mFS4DownConverter = new FS4DownConverter(); private int mSamplesPerCycle = CHANNEL_SAMPLE_RATE / DATA_GENERATOR_FRAME_RATE; - private ReusableBufferQueue mReusableBufferQueue = new ReusableBufferQueue(); + private ReusableComplexBufferQueue mReusableComplexBufferQueue = new ReusableComplexBufferQueue("SynthesizerViewer"); public DataGenerationManager() { @@ -296,19 +294,22 @@ public class SynthesizerViewer extends JFrame @Override public void run() { - ReusableComplexBuffer channel1Buffer = mReusableBufferQueue.getBuffer(mSamplesPerCycle); + ReusableComplexBuffer channel1Buffer = mReusableComplexBufferQueue.getBuffer(mSamplesPerCycle); getChannel1ControlPanel().getOscillator().generateComplex(channel1Buffer); - ReusableComplexBuffer channel2Buffer = mReusableBufferQueue.getBuffer(mSamplesPerCycle); + ReusableComplexBuffer channel2Buffer = mReusableComplexBufferQueue.getBuffer(mSamplesPerCycle); getChannel2ControlPanel().getOscillator().generateComplex(channel2Buffer); float[] synthesized = mSynthesizer.process(channel1Buffer.getSamples(), channel2Buffer.getSamples()); mFS4DownConverter.mixComplex(synthesized); - ReusableComplexBuffer synthesizedBuffer = mReusableBufferQueue.getBuffer(synthesized.length); + ReusableComplexBuffer synthesizedBuffer = mReusableComplexBufferQueue.getBuffer(synthesized.length); - getChannel1Panel().receive(channel1Buffer.incrementUserCount()); - getChannel2Panel().receive(channel2Buffer.incrementUserCount()); - getSpectrumPanel().receive(synthesizedBuffer.incrementUserCount()); + channel1Buffer.incrementUserCount(); + getChannel1Panel().receive(channel1Buffer); + channel2Buffer.incrementUserCount(); + getChannel2Panel().receive(channel2Buffer); + synthesizedBuffer.incrementUserCount(); + getSpectrumPanel().receive(synthesizedBuffer); } } diff --git a/src/main/java/io/github/dsheirer/dsp/filter/channelizer/output/ChannelOutputProcessor.java b/src/main/java/io/github/dsheirer/dsp/filter/channelizer/output/ChannelOutputProcessor.java index 5f9f2954..60cfc9f7 100644 --- a/src/main/java/io/github/dsheirer/dsp/filter/channelizer/output/ChannelOutputProcessor.java +++ b/src/main/java/io/github/dsheirer/dsp/filter/channelizer/output/ChannelOutputProcessor.java @@ -22,7 +22,7 @@ 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.reusable.ReusableBufferAssembler; +import io.github.dsheirer.sample.buffer.ReusableBufferAssembler; import io.github.dsheirer.sample.real.IOverflowListener; import io.github.dsheirer.source.Source; import org.slf4j.Logger; diff --git a/src/main/java/io/github/dsheirer/dsp/filter/channelizer/output/IPolyphaseChannelOutputProcessor.java b/src/main/java/io/github/dsheirer/dsp/filter/channelizer/output/IPolyphaseChannelOutputProcessor.java index 2dc2216a..3726866e 100644 --- a/src/main/java/io/github/dsheirer/dsp/filter/channelizer/output/IPolyphaseChannelOutputProcessor.java +++ b/src/main/java/io/github/dsheirer/dsp/filter/channelizer/output/IPolyphaseChannelOutputProcessor.java @@ -19,7 +19,7 @@ package io.github.dsheirer.dsp.filter.channelizer.output; import io.github.dsheirer.dsp.filter.channelizer.PolyphaseChannelResultsBuffer; -import io.github.dsheirer.sample.complex.reusable.ReusableBufferAssembler; +import io.github.dsheirer.sample.buffer.ReusableBufferAssembler; import io.github.dsheirer.source.Source; import java.util.List; diff --git a/src/main/java/io/github/dsheirer/dsp/filter/channelizer/output/OneChannelOutputProcessor.java b/src/main/java/io/github/dsheirer/dsp/filter/channelizer/output/OneChannelOutputProcessor.java index b17970f9..1c5e534a 100644 --- a/src/main/java/io/github/dsheirer/dsp/filter/channelizer/output/OneChannelOutputProcessor.java +++ b/src/main/java/io/github/dsheirer/dsp/filter/channelizer/output/OneChannelOutputProcessor.java @@ -19,7 +19,7 @@ package io.github.dsheirer.dsp.filter.channelizer.output; import io.github.dsheirer.dsp.filter.channelizer.PolyphaseChannelResultsBuffer; -import io.github.dsheirer.sample.complex.reusable.ReusableBufferAssembler; +import io.github.dsheirer.sample.buffer.ReusableBufferAssembler; import org.slf4j.Logger; import org.slf4j.LoggerFactory; diff --git a/src/main/java/io/github/dsheirer/dsp/filter/channelizer/output/TwoChannelOutputProcessor.java b/src/main/java/io/github/dsheirer/dsp/filter/channelizer/output/TwoChannelOutputProcessor.java index dcc637a5..d51096c2 100644 --- a/src/main/java/io/github/dsheirer/dsp/filter/channelizer/output/TwoChannelOutputProcessor.java +++ b/src/main/java/io/github/dsheirer/dsp/filter/channelizer/output/TwoChannelOutputProcessor.java @@ -18,7 +18,7 @@ package io.github.dsheirer.dsp.filter.channelizer.output; import io.github.dsheirer.dsp.filter.channelizer.PolyphaseChannelResultsBuffer; import io.github.dsheirer.dsp.filter.channelizer.TwoChannelSynthesizerM2; import io.github.dsheirer.dsp.mixer.FS4DownConverter; -import io.github.dsheirer.sample.complex.reusable.ReusableBufferAssembler; +import io.github.dsheirer.sample.buffer.ReusableBufferAssembler; import org.slf4j.Logger; import org.slf4j.LoggerFactory; diff --git a/src/main/java/io/github/dsheirer/dsp/filter/fir/complex/ComplexFIRFilter.java b/src/main/java/io/github/dsheirer/dsp/filter/fir/complex/ComplexFIRFilter.java index 6c28556e..ece55ce4 100644 --- a/src/main/java/io/github/dsheirer/dsp/filter/fir/complex/ComplexFIRFilter.java +++ b/src/main/java/io/github/dsheirer/dsp/filter/fir/complex/ComplexFIRFilter.java @@ -17,13 +17,13 @@ package io.github.dsheirer.dsp.filter.fir.complex; import io.github.dsheirer.dsp.filter.fir.FIRFilter; import io.github.dsheirer.dsp.filter.fir.real.RealFIRFilter; +import io.github.dsheirer.sample.buffer.ReusableComplexBuffer; +import io.github.dsheirer.sample.buffer.ReusableComplexBufferQueue; import io.github.dsheirer.sample.complex.Complex; -import io.github.dsheirer.sample.complex.reusable.ReusableBufferQueue; -import io.github.dsheirer.sample.complex.reusable.ReusableComplexBuffer; public class ComplexFIRFilter extends FIRFilter { - private ReusableBufferQueue mReusableBufferQueue = new ReusableBufferQueue("Complex FIR Filter"); + private ReusableComplexBufferQueue mReusableComplexBufferQueue = new ReusableComplexBufferQueue("Complex FIR Filter"); private RealFIRFilter mIFilter; private RealFIRFilter mQFilter; @@ -84,7 +84,7 @@ public class ComplexFIRFilter extends FIRFilter */ public ReusableComplexBuffer filter(ReusableComplexBuffer originalBuffer) { - ReusableComplexBuffer filteredBuffer = mReusableBufferQueue.getBuffer(originalBuffer.getSamples().length); + ReusableComplexBuffer filteredBuffer = mReusableComplexBufferQueue.getBuffer(originalBuffer.getSamples().length); filteredBuffer.setTimestamp(originalBuffer.getTimestamp()); float[] samples = originalBuffer.getSamples(); @@ -107,6 +107,6 @@ public class ComplexFIRFilter extends FIRFilter { mIFilter.dispose(); mQFilter.dispose(); - mReusableBufferQueue.dispose(); + mReusableComplexBufferQueue.dispose(); } } diff --git a/src/main/java/io/github/dsheirer/dsp/filter/resample/AudioResampler.java b/src/main/java/io/github/dsheirer/dsp/filter/resample/AudioResampler.java deleted file mode 100644 index c67401b9..00000000 --- a/src/main/java/io/github/dsheirer/dsp/filter/resample/AudioResampler.java +++ /dev/null @@ -1,70 +0,0 @@ -/******************************************************************************* - * 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 - * - ******************************************************************************/ -package io.github.dsheirer.dsp.filter.resample; - -import com.laszlosystems.libresample4j.Resampler; -import io.github.dsheirer.module.Module; -import io.github.dsheirer.sample.Listener; -import io.github.dsheirer.source.ISourceEventListener; -import io.github.dsheirer.source.SourceEvent; - -public class AudioResampler extends Module implements ISourceEventListener -{ - private Resampler mResampler; - - public AudioResampler(double inputRate, double outputRate) - { - double resampleRate = outputRate / inputRate; - - mResampler = new Resampler(true, resampleRate, resampleRate); - } - - public float[] resample(float[] samples) - { - return null; - } - - @Override - public void reset() - { - - } - - @Override - public void start() - { - - } - - @Override - public void stop() - { - - } - - @Override - public void dispose() - { - - } - - @Override - public Listener getSourceEventListener() - { - //TODO: we should get the channel sample rate and setup the resampler to output 8 kHz audio - return null; - } -} diff --git a/src/main/java/io/github/dsheirer/dsp/filter/resample/RealResampler.java b/src/main/java/io/github/dsheirer/dsp/filter/resample/RealResampler.java new file mode 100644 index 00000000..ece20c3d --- /dev/null +++ b/src/main/java/io/github/dsheirer/dsp/filter/resample/RealResampler.java @@ -0,0 +1,166 @@ +/******************************************************************************* + * 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 + * + ******************************************************************************/ +package io.github.dsheirer.dsp.filter.resample; + +import com.laszlosystems.libresample4j.Resampler; +import com.laszlosystems.libresample4j.SampleBuffers; +import io.github.dsheirer.sample.Listener; +import io.github.dsheirer.sample.buffer.ReusableBuffer; +import io.github.dsheirer.sample.buffer.ReusableBufferQueue; +import io.github.dsheirer.sample.real.RealBuffer; +import org.slf4j.Logger; +import org.slf4j.LoggerFactory; + +import java.nio.ByteBuffer; +import java.nio.FloatBuffer; + +public class RealResampler implements Listener +{ + protected static final Logger mLog = LoggerFactory.getLogger(RealResampler.class); + + private Resampler mResampler; + private Listener mResampledListener; + private BufferManager mBufferManager = new BufferManager(); + private ReusableBufferQueue mReusableBufferQueue = new ReusableBufferQueue("RealResampler"); + private double mResampleFactor; + + /** + * Resampler for real sample buffers. + * @param inputRate + * @param outputRate + */ + public RealResampler(double inputRate, double outputRate) + { + mResampleFactor = outputRate / inputRate; + mResampler = new Resampler(true, mResampleFactor, mResampleFactor); + } + + /** + * Primary input method to the resampler + * @param realBuffer to resample + */ + @Override + public void receive(RealBuffer realBuffer) + { + mBufferManager.load(realBuffer); + mResampler.process(mResampleFactor, mBufferManager, false); + } + + /** + * Registers the listener to receive the resampled buffer output + * @param resampledBufferListener to receive buffers + */ + public void setListener(Listener resampledBufferListener) + { + mResampledListener = resampledBufferListener; + } + + /** + * Buffer manager for input and output buffers used during the resampling process. The manager will + * auto-resize the input and output buffers to meet the size of the incoming sample buffers. + */ + public class BufferManager implements SampleBuffers + { + private int mBufferLength = 1000; + private FloatBuffer mInputBuffer = ByteBuffer.allocate(mBufferLength).asFloatBuffer(); + private FloatBuffer mOutputBuffer = ByteBuffer.allocate(mBufferLength).asFloatBuffer(); + + public void resize(int bufferLength) + { + int floatBufferLength = bufferLength * 4; + + FloatBuffer inputBuffer = ByteBuffer.allocate(floatBufferLength).asFloatBuffer(); + inputBuffer.put(mInputBuffer); + mInputBuffer = inputBuffer; + + FloatBuffer outputBuffer = ByteBuffer.allocate(floatBufferLength).asFloatBuffer(); + outputBuffer.put(mOutputBuffer); + mOutputBuffer = outputBuffer; + + mBufferLength = bufferLength; + } + + /** + * Current length of the input and output buffers + */ + public int getBufferLength() + { + return mBufferLength; + } + + /** + * Queues the buffer sample for resampling + */ + public void load(RealBuffer realBuffer) + { + int remaining = mInputBuffer.remaining(); + + if(mInputBuffer.remaining() < realBuffer.getSamples().length) + { + resize(mInputBuffer.capacity() + (realBuffer.getSamples().length - mInputBuffer.remaining())); + } + + mInputBuffer.put(realBuffer.getSamples()); + } + + @Override + public int getInputBufferLength() + { + return mInputBuffer.position(); + } + + @Override + public int getOutputBufferLength() + { + return mOutputBuffer.remaining(); + } + + /** + * Provides input to the resampler + */ + @Override + public void produceInput(float[] samples, int offset, int length) + { + mInputBuffer.flip(); + mInputBuffer.get(samples, offset, length); + mInputBuffer.compact(); + } + + /** + * Receives output from the resampler and dispatches output buffers periodically. + */ + @Override + public void consumeOutput(float[] samples, int offset, int length) + { + mOutputBuffer.put(samples, offset, length); + + while(mOutputBuffer.position() > 200) + { + ReusableBuffer outputBuffer = mReusableBufferQueue.getBuffer(200); + + mOutputBuffer.flip(); + mOutputBuffer.get(outputBuffer.getSamples()); + mOutputBuffer.compact(); + + if(mResampledListener != null) + { + outputBuffer.incrementUserCount(); + mResampledListener.receive(outputBuffer); + } + } + } + } +} diff --git a/src/main/java/io/github/dsheirer/dsp/gain/ComplexFeedForwardGainControl.java b/src/main/java/io/github/dsheirer/dsp/gain/ComplexFeedForwardGainControl.java index 13c54eda..3cb99948 100644 --- a/src/main/java/io/github/dsheirer/dsp/gain/ComplexFeedForwardGainControl.java +++ b/src/main/java/io/github/dsheirer/dsp/gain/ComplexFeedForwardGainControl.java @@ -16,11 +16,11 @@ package io.github.dsheirer.dsp.gain; import io.github.dsheirer.buffer.FloatCircularBuffer; +import io.github.dsheirer.sample.buffer.ReusableComplexBuffer; +import io.github.dsheirer.sample.buffer.ReusableComplexBufferQueue; import io.github.dsheirer.sample.complex.Complex; import io.github.dsheirer.sample.complex.ComplexBuffer; import io.github.dsheirer.sample.complex.ComplexSampleListener; -import io.github.dsheirer.sample.complex.reusable.ReusableBufferQueue; -import io.github.dsheirer.sample.complex.reusable.ReusableComplexBuffer; public class ComplexFeedForwardGainControl implements ComplexSampleListener { @@ -29,7 +29,7 @@ public class ComplexFeedForwardGainControl implements ComplexSampleListener private ComplexSampleListener mListener; private FloatCircularBuffer mEnvelopeHistory; - private ReusableBufferQueue mReusableBufferQueue = new ReusableBufferQueue("ComplexFeedForwardGainControl"); + private ReusableComplexBufferQueue mReusableComplexBufferQueue = new ReusableComplexBufferQueue("ComplexFeedForwardGainControl"); private float mMaxEnvelope = 0.0f; private float mGain = 1.0f; @@ -157,7 +157,7 @@ public class ComplexFeedForwardGainControl implements ComplexSampleListener { float[] samples = buffer.getSamples(); - ReusableComplexBuffer filteredBuffer = mReusableBufferQueue.getBuffer(samples.length); + ReusableComplexBuffer filteredBuffer = mReusableComplexBufferQueue.getBuffer(samples.length); filteredBuffer.setTimestamp(buffer.getTimestamp()); float[] filtered = filteredBuffer.getSamples(); diff --git a/src/main/java/io/github/dsheirer/dsp/mixer/AbstractOscillator.java b/src/main/java/io/github/dsheirer/dsp/mixer/AbstractOscillator.java index 5be64115..3f7df3f8 100644 --- a/src/main/java/io/github/dsheirer/dsp/mixer/AbstractOscillator.java +++ b/src/main/java/io/github/dsheirer/dsp/mixer/AbstractOscillator.java @@ -15,8 +15,8 @@ ******************************************************************************/ package io.github.dsheirer.dsp.mixer; +import io.github.dsheirer.sample.buffer.ReusableComplexBuffer; import io.github.dsheirer.sample.complex.Complex; -import io.github.dsheirer.sample.complex.reusable.ReusableComplexBuffer; import org.slf4j.Logger; import org.slf4j.LoggerFactory; @@ -160,7 +160,7 @@ public abstract class AbstractOscillator implements IOscillator @Override public void generateComplex(ReusableComplexBuffer reusableComplexBuffer) { - int sampleCount = reusableComplexBuffer.getSampleLength(); + int sampleCount = reusableComplexBuffer.getSampleCount(); if(mSampleBuffer == null || mSampleBuffer.capacity() != sampleCount * 2) { diff --git a/src/main/java/io/github/dsheirer/dsp/mixer/IOscillator.java b/src/main/java/io/github/dsheirer/dsp/mixer/IOscillator.java index 1f843dda..18f522d5 100644 --- a/src/main/java/io/github/dsheirer/dsp/mixer/IOscillator.java +++ b/src/main/java/io/github/dsheirer/dsp/mixer/IOscillator.java @@ -15,7 +15,7 @@ ******************************************************************************/ package io.github.dsheirer.dsp.mixer; -import io.github.dsheirer.sample.complex.reusable.ReusableComplexBuffer; +import io.github.dsheirer.sample.buffer.ReusableComplexBuffer; /** * Oscillator interface. diff --git a/src/main/java/io/github/dsheirer/dsp/psk/PSKDemodulator.java b/src/main/java/io/github/dsheirer/dsp/psk/PSKDemodulator.java index c187a408..aebc59e6 100644 --- a/src/main/java/io/github/dsheirer/dsp/psk/PSKDemodulator.java +++ b/src/main/java/io/github/dsheirer/dsp/psk/PSKDemodulator.java @@ -17,9 +17,9 @@ package io.github.dsheirer.dsp.psk; import io.github.dsheirer.dsp.psk.pll.IPhaseLockedLoop; import io.github.dsheirer.sample.Listener; +import io.github.dsheirer.sample.buffer.ReusableComplexBuffer; import io.github.dsheirer.sample.complex.Complex; import io.github.dsheirer.sample.complex.ComplexSampleListener; -import io.github.dsheirer.sample.complex.reusable.ReusableComplexBuffer; public abstract class PSKDemodulator implements ComplexSampleListener { diff --git a/src/main/java/io/github/dsheirer/instrument/gui/viewer/PlaybackController.java b/src/main/java/io/github/dsheirer/instrument/gui/viewer/PlaybackController.java index 6462901f..ee74737e 100644 --- a/src/main/java/io/github/dsheirer/instrument/gui/viewer/PlaybackController.java +++ b/src/main/java/io/github/dsheirer/instrument/gui/viewer/PlaybackController.java @@ -17,8 +17,8 @@ package io.github.dsheirer.instrument.gui.viewer; import io.github.dsheirer.instrument.gui.viewer.decoder.DecoderPane; import io.github.dsheirer.sample.Listener; -import io.github.dsheirer.sample.complex.reusable.ReusableBufferBroadcaster; -import io.github.dsheirer.sample.complex.reusable.ReusableComplexBuffer; +import io.github.dsheirer.sample.buffer.ReusableBufferBroadcaster; +import io.github.dsheirer.sample.buffer.ReusableComplexBuffer; import io.github.dsheirer.source.IControllableFileSource; import io.github.dsheirer.source.IFrameLocationListener; import io.github.dsheirer.source.wave.ComplexWaveSource; diff --git a/src/main/java/io/github/dsheirer/instrument/gui/viewer/chart/ComplexPhaseLineChart.java b/src/main/java/io/github/dsheirer/instrument/gui/viewer/chart/ComplexPhaseLineChart.java index 3d7694a2..000f3a50 100644 --- a/src/main/java/io/github/dsheirer/instrument/gui/viewer/chart/ComplexPhaseLineChart.java +++ b/src/main/java/io/github/dsheirer/instrument/gui/viewer/chart/ComplexPhaseLineChart.java @@ -18,8 +18,8 @@ package io.github.dsheirer.instrument.gui.viewer.chart; import io.github.dsheirer.buffer.ComplexCircularBuffer; import io.github.dsheirer.dsp.gain.ComplexGain; import io.github.dsheirer.sample.Listener; +import io.github.dsheirer.sample.buffer.ReusableComplexBuffer; import io.github.dsheirer.sample.complex.Complex; -import io.github.dsheirer.sample.complex.reusable.ReusableComplexBuffer; import javafx.beans.property.IntegerProperty; import javafx.beans.property.SimpleIntegerProperty; import javafx.beans.value.ChangeListener; diff --git a/src/main/java/io/github/dsheirer/instrument/gui/viewer/chart/ComplexSampleLineChart.java b/src/main/java/io/github/dsheirer/instrument/gui/viewer/chart/ComplexSampleLineChart.java index bec81eef..d1b4b00c 100644 --- a/src/main/java/io/github/dsheirer/instrument/gui/viewer/chart/ComplexSampleLineChart.java +++ b/src/main/java/io/github/dsheirer/instrument/gui/viewer/chart/ComplexSampleLineChart.java @@ -18,8 +18,8 @@ package io.github.dsheirer.instrument.gui.viewer.chart; import io.github.dsheirer.buffer.ComplexCircularBuffer; import io.github.dsheirer.dsp.gain.ComplexGain; import io.github.dsheirer.sample.Listener; +import io.github.dsheirer.sample.buffer.ReusableComplexBuffer; import io.github.dsheirer.sample.complex.Complex; -import io.github.dsheirer.sample.complex.reusable.ReusableComplexBuffer; import javafx.beans.property.IntegerProperty; import javafx.beans.property.SimpleIntegerProperty; import javafx.beans.value.ChangeListener; diff --git a/src/main/java/io/github/dsheirer/instrument/gui/viewer/chart/FMDemodulatedLineChart.java b/src/main/java/io/github/dsheirer/instrument/gui/viewer/chart/FMDemodulatedLineChart.java index c209a5db..334db601 100644 --- a/src/main/java/io/github/dsheirer/instrument/gui/viewer/chart/FMDemodulatedLineChart.java +++ b/src/main/java/io/github/dsheirer/instrument/gui/viewer/chart/FMDemodulatedLineChart.java @@ -18,8 +18,8 @@ package io.github.dsheirer.instrument.gui.viewer.chart; import io.github.dsheirer.buffer.DoubleCircularBuffer; import io.github.dsheirer.dsp.fm.FMDemodulator; import io.github.dsheirer.sample.Listener; +import io.github.dsheirer.sample.buffer.ReusableComplexBuffer; import io.github.dsheirer.sample.complex.Complex; -import io.github.dsheirer.sample.complex.reusable.ReusableComplexBuffer; import javafx.collections.FXCollections; import javafx.collections.ObservableList; import javafx.scene.chart.LineChart; diff --git a/src/main/java/io/github/dsheirer/instrument/gui/viewer/decoder/DecoderPane.java b/src/main/java/io/github/dsheirer/instrument/gui/viewer/decoder/DecoderPane.java index daaa092d..82d3ae86 100644 --- a/src/main/java/io/github/dsheirer/instrument/gui/viewer/decoder/DecoderPane.java +++ b/src/main/java/io/github/dsheirer/instrument/gui/viewer/decoder/DecoderPane.java @@ -17,8 +17,8 @@ package io.github.dsheirer.instrument.gui.viewer.decoder; import io.github.dsheirer.module.decode.DecoderType; import io.github.dsheirer.sample.Listener; -import io.github.dsheirer.sample.complex.reusable.ReusableBufferBroadcaster; -import io.github.dsheirer.sample.complex.reusable.ReusableComplexBuffer; +import io.github.dsheirer.sample.buffer.ReusableBufferBroadcaster; +import io.github.dsheirer.sample.buffer.ReusableComplexBuffer; import javafx.scene.layout.VBox; import javafx.scene.text.Text; diff --git a/src/main/java/io/github/dsheirer/instrument/gui/viewer/decoder/P25Phase1C4FMPane.java b/src/main/java/io/github/dsheirer/instrument/gui/viewer/decoder/P25Phase1C4FMPane.java index 381e83be..56a3cff0 100644 --- a/src/main/java/io/github/dsheirer/instrument/gui/viewer/decoder/P25Phase1C4FMPane.java +++ b/src/main/java/io/github/dsheirer/instrument/gui/viewer/decoder/P25Phase1C4FMPane.java @@ -24,7 +24,7 @@ import io.github.dsheirer.message.Message; import io.github.dsheirer.module.decode.DecoderType; import io.github.dsheirer.module.decode.p25.P25DecoderC4FMInstrumented; import io.github.dsheirer.sample.Listener; -import io.github.dsheirer.sample.complex.reusable.ReusableBufferBroadcaster; +import io.github.dsheirer.sample.buffer.ReusableBufferBroadcaster; import javafx.scene.layout.HBox; import javafx.scene.layout.Priority; import org.slf4j.Logger; diff --git a/src/main/java/io/github/dsheirer/instrument/gui/viewer/decoder/P25Phase1LSMPane.java b/src/main/java/io/github/dsheirer/instrument/gui/viewer/decoder/P25Phase1LSMPane.java index b93fa58e..33b0f7e4 100644 --- a/src/main/java/io/github/dsheirer/instrument/gui/viewer/decoder/P25Phase1LSMPane.java +++ b/src/main/java/io/github/dsheirer/instrument/gui/viewer/decoder/P25Phase1LSMPane.java @@ -24,7 +24,7 @@ import io.github.dsheirer.message.Message; import io.github.dsheirer.module.decode.DecoderType; import io.github.dsheirer.module.decode.p25.P25DecoderLSMInstrumented; import io.github.dsheirer.sample.Listener; -import io.github.dsheirer.sample.complex.reusable.ReusableBufferBroadcaster; +import io.github.dsheirer.sample.buffer.ReusableBufferBroadcaster; import javafx.scene.layout.HBox; import javafx.scene.layout.Priority; import org.slf4j.Logger; diff --git a/src/main/java/io/github/dsheirer/module/ProcessingChain.java b/src/main/java/io/github/dsheirer/module/ProcessingChain.java index 060190ff..34b81cdb 100644 --- a/src/main/java/io/github/dsheirer/module/ProcessingChain.java +++ b/src/main/java/io/github/dsheirer/module/ProcessingChain.java @@ -49,8 +49,8 @@ import io.github.dsheirer.record.wave.ComplexBufferWaveRecorder; import io.github.dsheirer.record.wave.RealBufferWaveRecorder; import io.github.dsheirer.sample.Broadcaster; import io.github.dsheirer.sample.Listener; -import io.github.dsheirer.sample.complex.reusable.IReusableComplexBufferListener; -import io.github.dsheirer.sample.complex.reusable.ReusableBufferBroadcaster; +import io.github.dsheirer.sample.buffer.IReusableComplexBufferListener; +import io.github.dsheirer.sample.buffer.ReusableBufferBroadcaster; import io.github.dsheirer.sample.real.IFilteredRealBufferListener; import io.github.dsheirer.sample.real.IFilteredRealBufferProvider; import io.github.dsheirer.sample.real.IUnFilteredRealBufferListener; diff --git a/src/main/java/io/github/dsheirer/module/decode/DecoderFactory.java b/src/main/java/io/github/dsheirer/module/decode/DecoderFactory.java index 146e8f3a..c2020df7 100644 --- a/src/main/java/io/github/dsheirer/module/decode/DecoderFactory.java +++ b/src/main/java/io/github/dsheirer/module/decode/DecoderFactory.java @@ -20,7 +20,7 @@ package io.github.dsheirer.module.decode; import io.github.dsheirer.alias.AliasList; import io.github.dsheirer.alias.AliasModel; -import io.github.dsheirer.audio.AudioModule; +import io.github.dsheirer.audio.AudioModule2; import io.github.dsheirer.channel.metadata.Metadata; import io.github.dsheirer.channel.state.AlwaysUnsquelchedDecoderState; import io.github.dsheirer.channel.traffic.TrafficChannelManager; @@ -184,14 +184,14 @@ public class DecoderFactory modules.add(new AlwaysUnsquelchedDecoderState(DecoderType.AM, channel.getName())); modules.add(new AMDemodulatorModule()); modules.add(new DemodulatedAudioFilterModule(4000, 6000)); - modules.add(new AudioModule(metadata)); + modules.add(new AudioModule2(metadata)); break; case NBFM: modules.add(new NBFMDecoder(decodeConfig)); modules.add(new AlwaysUnsquelchedDecoderState(DecoderType.NBFM, channel.getName())); modules.add(new FMDemodulatorModule(NBFM_PASS_FREQUENCY, NBFM_STOP_FREQUENCY)); modules.add(new DemodulatedAudioFilterModule(4000, 6000)); - modules.add(new AudioModule(metadata)); + modules.add(new AudioModule2(metadata)); break; case LTR_STANDARD: MessageDirection direction = ((DecodeConfigLTRStandard) decodeConfig).getMessageDirection(); @@ -199,14 +199,14 @@ public class DecoderFactory modules.add(new LTRStandardDecoderState(aliasList)); modules.add(new FMDemodulatorModule(NBFM_PASS_FREQUENCY, NBFM_STOP_FREQUENCY)); modules.add(new DemodulatedAudioFilterModule(4000, 6000)); - modules.add(new AudioModule(metadata)); + modules.add(new AudioModule2(metadata)); break; case LTR_NET: modules.add(new LTRNetDecoder((DecodeConfigLTRNet) decodeConfig, aliasList)); modules.add(new LTRNetDecoderState(aliasList)); modules.add(new FMDemodulatorModule(NBFM_PASS_FREQUENCY, NBFM_STOP_FREQUENCY)); modules.add(new DemodulatedAudioFilterModule(4000, 6000)); - modules.add(new AudioModule(metadata)); + modules.add(new AudioModule2(metadata)); break; case MPT1327: DecodeConfigMPT1327 mptConfig = (DecodeConfigMPT1327) decodeConfig; @@ -229,14 +229,14 @@ public class DecoderFactory modules.add(new FMDemodulatorModule(NBFM_PASS_FREQUENCY, NBFM_STOP_FREQUENCY)); modules.add(new DemodulatedAudioFilterModule(P25_C4FM_DEMOD_FILTER, 1.0f)); - modules.add(new AudioModule(metadata)); + modules.add(new AudioModule2(metadata)); break; case PASSPORT: modules.add(new PassportDecoder(decodeConfig, aliasList)); modules.add(new PassportDecoderState(aliasList)); modules.add(new FMDemodulatorModule(NBFM_PASS_FREQUENCY, NBFM_STOP_FREQUENCY)); modules.add(new DemodulatedAudioFilterModule(4000, 6000)); - modules.add(new AudioModule(metadata)); + modules.add(new AudioModule2(metadata)); break; case P25_PHASE1: DecodeConfigP25Phase1 p25Config = (DecodeConfigP25Phase1) decodeConfig; diff --git a/src/main/java/io/github/dsheirer/module/decode/p25/P25Decoder.java b/src/main/java/io/github/dsheirer/module/decode/p25/P25Decoder.java index dfcf230b..f88348cf 100644 --- a/src/main/java/io/github/dsheirer/module/decode/p25/P25Decoder.java +++ b/src/main/java/io/github/dsheirer/module/decode/p25/P25Decoder.java @@ -19,8 +19,8 @@ import io.github.dsheirer.alias.AliasList; import io.github.dsheirer.module.decode.Decoder; import io.github.dsheirer.module.decode.DecoderType; import io.github.dsheirer.sample.Listener; -import io.github.dsheirer.sample.complex.reusable.IReusableComplexBufferListener; -import io.github.dsheirer.sample.complex.reusable.ReusableComplexBuffer; +import io.github.dsheirer.sample.buffer.IReusableComplexBufferListener; +import io.github.dsheirer.sample.buffer.ReusableComplexBuffer; import io.github.dsheirer.source.ISourceEventListener; import io.github.dsheirer.source.ISourceEventProvider; import io.github.dsheirer.source.SourceEvent; diff --git a/src/main/java/io/github/dsheirer/module/decode/p25/P25DecoderC4FM.java b/src/main/java/io/github/dsheirer/module/decode/p25/P25DecoderC4FM.java index f37f3aae..1155ec58 100644 --- a/src/main/java/io/github/dsheirer/module/decode/p25/P25DecoderC4FM.java +++ b/src/main/java/io/github/dsheirer/module/decode/p25/P25DecoderC4FM.java @@ -24,7 +24,7 @@ import io.github.dsheirer.dsp.gain.ComplexFeedForwardGainControl; import io.github.dsheirer.dsp.psk.DQPSKDecisionDirectedDemodulator; import io.github.dsheirer.dsp.psk.InterpolatingSampleBuffer; import io.github.dsheirer.dsp.psk.pll.CostasLoop; -import io.github.dsheirer.sample.complex.reusable.ReusableComplexBuffer; +import io.github.dsheirer.sample.buffer.ReusableComplexBuffer; import io.github.dsheirer.source.SourceEvent; import org.slf4j.Logger; import org.slf4j.LoggerFactory; diff --git a/src/main/java/io/github/dsheirer/module/decode/p25/P25DecoderC4FMInstrumented.java b/src/main/java/io/github/dsheirer/module/decode/p25/P25DecoderC4FMInstrumented.java index 46762450..c69a44e6 100644 --- a/src/main/java/io/github/dsheirer/module/decode/p25/P25DecoderC4FMInstrumented.java +++ b/src/main/java/io/github/dsheirer/module/decode/p25/P25DecoderC4FMInstrumented.java @@ -20,8 +20,8 @@ import io.github.dsheirer.dsp.psk.DQPSKDecisionDirectedDemodulatorInstrumented; import io.github.dsheirer.dsp.psk.InterpolatingSampleBufferInstrumented; import io.github.dsheirer.dsp.psk.SymbolDecisionData; import io.github.dsheirer.sample.Listener; +import io.github.dsheirer.sample.buffer.ReusableComplexBuffer; import io.github.dsheirer.sample.complex.Complex; -import io.github.dsheirer.sample.complex.reusable.ReusableComplexBuffer; public class P25DecoderC4FMInstrumented extends P25DecoderC4FM { diff --git a/src/main/java/io/github/dsheirer/module/decode/p25/P25DecoderLSM.java b/src/main/java/io/github/dsheirer/module/decode/p25/P25DecoderLSM.java index 1fd4c683..d9dea665 100644 --- a/src/main/java/io/github/dsheirer/module/decode/p25/P25DecoderLSM.java +++ b/src/main/java/io/github/dsheirer/module/decode/p25/P25DecoderLSM.java @@ -23,7 +23,7 @@ import io.github.dsheirer.dsp.gain.ComplexFeedForwardGainControl; import io.github.dsheirer.dsp.psk.DQPSKGardnerDemodulator; import io.github.dsheirer.dsp.psk.InterpolatingSampleBuffer; import io.github.dsheirer.dsp.psk.pll.CostasLoop; -import io.github.dsheirer.sample.complex.reusable.ReusableComplexBuffer; +import io.github.dsheirer.sample.buffer.ReusableComplexBuffer; import io.github.dsheirer.source.SourceEvent; import org.slf4j.Logger; import org.slf4j.LoggerFactory; diff --git a/src/main/java/io/github/dsheirer/module/decode/p25/P25DecoderLSMInstrumented.java b/src/main/java/io/github/dsheirer/module/decode/p25/P25DecoderLSMInstrumented.java index 41292d32..7997ae9f 100644 --- a/src/main/java/io/github/dsheirer/module/decode/p25/P25DecoderLSMInstrumented.java +++ b/src/main/java/io/github/dsheirer/module/decode/p25/P25DecoderLSMInstrumented.java @@ -20,8 +20,8 @@ import io.github.dsheirer.dsp.psk.DQPSKGardnerDemodulatorInstrumented; import io.github.dsheirer.dsp.psk.InterpolatingSampleBufferInstrumented; import io.github.dsheirer.dsp.psk.SymbolDecisionData; import io.github.dsheirer.sample.Listener; +import io.github.dsheirer.sample.buffer.ReusableComplexBuffer; import io.github.dsheirer.sample.complex.Complex; -import io.github.dsheirer.sample.complex.reusable.ReusableComplexBuffer; public class P25DecoderLSMInstrumented extends P25DecoderLSM { diff --git a/src/main/java/io/github/dsheirer/module/demodulate/am/AMDemodulatorModule.java b/src/main/java/io/github/dsheirer/module/demodulate/am/AMDemodulatorModule.java index e55b51cb..f155c890 100644 --- a/src/main/java/io/github/dsheirer/module/demodulate/am/AMDemodulatorModule.java +++ b/src/main/java/io/github/dsheirer/module/demodulate/am/AMDemodulatorModule.java @@ -23,9 +23,9 @@ import io.github.dsheirer.dsp.filter.fir.real.RealFIRFilter_RB_RB; import io.github.dsheirer.dsp.gain.AutomaticGainControl_RB; import io.github.dsheirer.module.Module; import io.github.dsheirer.sample.Listener; +import io.github.dsheirer.sample.buffer.IReusableComplexBufferListener; +import io.github.dsheirer.sample.buffer.ReusableComplexBuffer; import io.github.dsheirer.sample.complex.ComplexBuffer; -import io.github.dsheirer.sample.complex.reusable.IReusableComplexBufferListener; -import io.github.dsheirer.sample.complex.reusable.ReusableComplexBuffer; import io.github.dsheirer.sample.real.IFilteredRealBufferProvider; import io.github.dsheirer.sample.real.RealBuffer; diff --git a/src/main/java/io/github/dsheirer/module/demodulate/fm/FMDemodulatorModule.java b/src/main/java/io/github/dsheirer/module/demodulate/fm/FMDemodulatorModule.java index a976a3fa..b06864c8 100644 --- a/src/main/java/io/github/dsheirer/module/demodulate/fm/FMDemodulatorModule.java +++ b/src/main/java/io/github/dsheirer/module/demodulate/fm/FMDemodulatorModule.java @@ -23,8 +23,8 @@ import io.github.dsheirer.dsp.filter.fir.complex.ComplexFIRFilter_CB_CB; import io.github.dsheirer.dsp.fm.FMDemodulator_CB; import io.github.dsheirer.module.Module; import io.github.dsheirer.sample.Listener; -import io.github.dsheirer.sample.complex.reusable.IReusableComplexBufferListener; -import io.github.dsheirer.sample.complex.reusable.ReusableComplexBuffer; +import io.github.dsheirer.sample.buffer.IReusableComplexBufferListener; +import io.github.dsheirer.sample.buffer.ReusableComplexBuffer; import io.github.dsheirer.sample.real.IUnFilteredRealBufferProvider; import io.github.dsheirer.sample.real.RealBuffer; import io.github.dsheirer.source.ISourceEventListener; diff --git a/src/main/java/io/github/dsheirer/record/wave/ComplexBufferWaveRecorder.java b/src/main/java/io/github/dsheirer/record/wave/ComplexBufferWaveRecorder.java index 12b519a3..ff68d8fb 100644 --- a/src/main/java/io/github/dsheirer/record/wave/ComplexBufferWaveRecorder.java +++ b/src/main/java/io/github/dsheirer/record/wave/ComplexBufferWaveRecorder.java @@ -18,9 +18,9 @@ package io.github.dsheirer.record.wave; import io.github.dsheirer.module.Module; import io.github.dsheirer.sample.ConversionUtils; import io.github.dsheirer.sample.Listener; -import io.github.dsheirer.sample.complex.reusable.IReusableBufferDisposedListener; -import io.github.dsheirer.sample.complex.reusable.IReusableComplexBufferListener; -import io.github.dsheirer.sample.complex.reusable.ReusableComplexBuffer; +import io.github.dsheirer.sample.buffer.IReusableBufferDisposedListener; +import io.github.dsheirer.sample.buffer.IReusableComplexBufferListener; +import io.github.dsheirer.sample.buffer.ReusableComplexBuffer; import io.github.dsheirer.util.ThreadPool; import io.github.dsheirer.util.TimeStamp; import org.slf4j.Logger; @@ -207,14 +207,14 @@ public class ComplexBufferWaveRecorder extends Module implements IReusableComple { public PoisonPill() { - super(new IReusableBufferDisposedListener() + super(new IReusableBufferDisposedListener() { @Override public void disposed(ReusableComplexBuffer reusableComplexBuffer) { //no-op } - }, new float[2], System.currentTimeMillis()); + }, new float[2]); } } } diff --git a/src/main/java/io/github/dsheirer/sample/ConversionUtils.java b/src/main/java/io/github/dsheirer/sample/ConversionUtils.java index 7ad5c27c..9cc988ff 100644 --- a/src/main/java/io/github/dsheirer/sample/ConversionUtils.java +++ b/src/main/java/io/github/dsheirer/sample/ConversionUtils.java @@ -1,5 +1,6 @@ package io.github.dsheirer.sample; +import io.github.dsheirer.sample.buffer.ReusableBuffer; import io.github.dsheirer.sample.real.RealBuffer; import java.nio.ByteBuffer; @@ -11,61 +12,71 @@ import java.nio.ShortBuffer; */ public class ConversionUtils { - public static float[] convertFromSigned16BitSamples( byte[] bytes ) + public static float[] convertFromSigned16BitSamples(byte[] bytes) { - return convertFromSigned16BitSamples( ByteBuffer.wrap( bytes ) ); + return convertFromSigned16BitSamples(ByteBuffer.wrap(bytes)); } /** * Converts the byte buffer containing 16-bit samples into a float array */ - public static float[] convertFromSigned16BitSamples( ByteBuffer buffer ) + public static float[] convertFromSigned16BitSamples(ByteBuffer buffer) { - ShortBuffer byteBuffer = buffer.order( ByteOrder.LITTLE_ENDIAN ).asShortBuffer(); - - float[] samples = new float[ buffer.limit() / 2 ]; - - for( int x = 0; x < samples.length; x++ ) - { - samples[ x ] = (float)byteBuffer.get() / (float)Short.MAX_VALUE; - } + ShortBuffer byteBuffer = buffer.order(ByteOrder.LITTLE_ENDIAN).asShortBuffer(); - return samples; + float[] samples = new float[buffer.limit() / 2]; + + for(int x = 0; x < samples.length; x++) + { + samples[x] = (float)byteBuffer.get() / (float)Short.MAX_VALUE; + } + + return samples; } /** * Converts the float samples into a little-endian 16-bit sample byte buffer. + * * @param samples - float array of sample data * @return - little-endian 16-bit sample byte buffer */ - public static ByteBuffer convertToSigned16BitSamples( float[] samples ) + public static ByteBuffer convertToSigned16BitSamples(float[] samples) { - ByteBuffer converted = ByteBuffer.allocate( samples.length * 2 ); - converted.order( ByteOrder.LITTLE_ENDIAN ); + ByteBuffer converted = ByteBuffer.allocate(samples.length * 2); + converted.order(ByteOrder.LITTLE_ENDIAN); - for( float sample: samples ) - { - converted.putShort( (short)( sample * Short.MAX_VALUE ) ); - } - - return converted; + for(float sample : samples) + { + converted.putShort((short)(sample * Short.MAX_VALUE)); + } + + return converted; } - - /** - * Converts the float samples in a complex buffer to a little endian 16-bit - * buffer - */ - public static ByteBuffer convertToSigned16BitSamples( Buffer buffer ) - { - return convertToSigned16BitSamples( buffer.getSamples() ); - } - /** - * Converts the float samples in a complex buffer to a little endian 16-bit - * buffer - */ - public static ByteBuffer convertToSigned16BitSamples( RealBuffer buffer ) - { - return convertToSigned16BitSamples( buffer.getSamples() ); - } + /** + * Converts the float samples in a complex buffer to a little endian 16-bit + * buffer + */ + public static ByteBuffer convertToSigned16BitSamples(Buffer buffer) + { + return convertToSigned16BitSamples(buffer.getSamples()); + } + + /** + * Converts the float samples in a complex buffer to a little endian 16-bit + * buffer + */ + public static ByteBuffer convertToSigned16BitSamples(RealBuffer buffer) + { + return convertToSigned16BitSamples(buffer.getSamples()); + } + + /** + * Converts the float samples in a complex buffer to a little endian 16-bit + * buffer + */ + public static ByteBuffer convertToSigned16BitSamples(ReusableBuffer buffer) + { + return convertToSigned16BitSamples(buffer.getSamples()); + } } diff --git a/src/main/java/io/github/dsheirer/sample/adapter/ComplexShortAdapter.java b/src/main/java/io/github/dsheirer/sample/adapter/ComplexShortAdapter.java index 705a5448..6a3c80ac 100644 --- a/src/main/java/io/github/dsheirer/sample/adapter/ComplexShortAdapter.java +++ b/src/main/java/io/github/dsheirer/sample/adapter/ComplexShortAdapter.java @@ -15,7 +15,7 @@ ******************************************************************************/ package io.github.dsheirer.sample.adapter; -import io.github.dsheirer.sample.complex.reusable.ReusableComplexBuffer; +import io.github.dsheirer.sample.buffer.ReusableComplexBuffer; import java.nio.ByteBuffer; import java.nio.ByteOrder; diff --git a/src/main/java/io/github/dsheirer/sample/adapter/IComplexSampleAdapter.java b/src/main/java/io/github/dsheirer/sample/adapter/IComplexSampleAdapter.java index 471ee032..99d07eeb 100644 --- a/src/main/java/io/github/dsheirer/sample/adapter/IComplexSampleAdapter.java +++ b/src/main/java/io/github/dsheirer/sample/adapter/IComplexSampleAdapter.java @@ -15,7 +15,7 @@ ******************************************************************************/ package io.github.dsheirer.sample.adapter; -import io.github.dsheirer.sample.complex.reusable.ReusableComplexBuffer; +import io.github.dsheirer.sample.buffer.ReusableComplexBuffer; public interface IComplexSampleAdapter { diff --git a/src/main/java/io/github/dsheirer/sample/complex/reusable/ReusableBufferQueue.java b/src/main/java/io/github/dsheirer/sample/buffer/AbstractReusableBufferQueue.java similarity index 50% rename from src/main/java/io/github/dsheirer/sample/complex/reusable/ReusableBufferQueue.java rename to src/main/java/io/github/dsheirer/sample/buffer/AbstractReusableBufferQueue.java index 89f7e81b..0959b060 100644 --- a/src/main/java/io/github/dsheirer/sample/complex/reusable/ReusableBufferQueue.java +++ b/src/main/java/io/github/dsheirer/sample/buffer/AbstractReusableBufferQueue.java @@ -13,7 +13,7 @@ * If not, see * ******************************************************************************/ -package io.github.dsheirer.sample.complex.reusable; +package io.github.dsheirer.sample.buffer; import org.slf4j.Logger; import org.slf4j.LoggerFactory; @@ -21,21 +21,22 @@ import org.slf4j.LoggerFactory; import java.util.Queue; import java.util.concurrent.LinkedTransferQueue; -public class ReusableBufferQueue implements IReusableBufferDisposedListener +public abstract class AbstractReusableBufferQueue implements IReusableBufferDisposedListener { - private final static Logger mLog = LoggerFactory.getLogger(ReusableBufferQueue.class); - private Queue mReusableBufferQueue = new LinkedTransferQueue<>(); + private final static Logger mLog = LoggerFactory.getLogger(AbstractReusableBufferQueue.class); + + private Queue mReusableBufferQueue = new LinkedTransferQueue<>(); private int mBufferCount = 0; private String mDebugName; + private int mBufferUserCounter; - public ReusableBufferQueue(String debugName) + public AbstractReusableBufferQueue(String debugName) { mDebugName = debugName; } - public ReusableBufferQueue() + public AbstractReusableBufferQueue() { - } public void dispose() @@ -47,12 +48,17 @@ public class ReusableBufferQueue implements IReusableBufferDisposedListener * Implements buffer disposed listener interface. Disposed (ie user count = 0) buffers will automatically callback * to this method to indicate when they are disposed. * - * @param reusableComplexBuffer that has been disposed + * @param reusableBuffer that has been disposed */ @Override - public void disposed(ReusableComplexBuffer reusableComplexBuffer) + public void disposed(T reusableBuffer) { - mReusableBufferQueue.offer(reusableComplexBuffer); + mReusableBufferQueue.offer(reusableBuffer); + +// if(mDebugName != null && mDebugName.contentEquals("NativeBufferConverter")) +// { +// mLog.debug("Reclaimed: " + reusableBuffer.name()); +// } } /** @@ -62,18 +68,37 @@ public class ReusableBufferQueue implements IReusableBufferDisposedListener * @param size of the samples that will be loaded into the buffer * @return a reusable buffer */ - public ReusableComplexBuffer getBuffer(int size) + public T getBuffer(int size) { - ReusableComplexBuffer buffer = mReusableBufferQueue.poll(); + T buffer = mReusableBufferQueue.poll(); - //Create a new buffer if we currently don't have any buffers to reuse if(buffer == null) { - buffer = new ReusableComplexBuffer(this, new float[size]); + buffer = createBuffer(size); mBufferCount++; - mLog.debug("Buffer Created - Count:" + mBufferCount + (mDebugName != null ? " [" + mDebugName + "]" : "")); + buffer.setDebugName("Owner:" + mDebugName + " Number:" + mBufferUserCounter++ + " NEW BUFFER"); + +// if(mDebugName != null && mDebugName.contentEquals("NativeBufferConverter")) +// { +// mLog.debug("Buffer Created - Count:" + mBufferCount + (mDebugName != null ? " [" + mDebugName + "]" : "")); +// } + } + else + { + String previous = buffer.name(); + buffer.setDebugName("Owner:" + mDebugName + " Number:" + mBufferUserCounter++); + +// if(mDebugName != null && mDebugName.contentEquals("NativeBufferConverter")) +// { +// mLog.debug("Buffer Reused: " + previous + " New Name:" + buffer.name()); +// } } return buffer; } + + /** + * Create a new buffer of the specified size + */ + protected abstract T createBuffer(int size); } diff --git a/src/main/java/io/github/dsheirer/sample/complex/reusable/IReusableBufferDisposedListener.java b/src/main/java/io/github/dsheirer/sample/buffer/IReusableBufferDisposedListener.java similarity index 81% rename from src/main/java/io/github/dsheirer/sample/complex/reusable/IReusableBufferDisposedListener.java rename to src/main/java/io/github/dsheirer/sample/buffer/IReusableBufferDisposedListener.java index fd261e23..b3baaffb 100644 --- a/src/main/java/io/github/dsheirer/sample/complex/reusable/IReusableBufferDisposedListener.java +++ b/src/main/java/io/github/dsheirer/sample/buffer/IReusableBufferDisposedListener.java @@ -13,13 +13,13 @@ * If not, see * ******************************************************************************/ -package io.github.dsheirer.sample.complex.reusable; +package io.github.dsheirer.sample.buffer; -public interface IReusableBufferDisposedListener +public interface IReusableBufferDisposedListener { /** * Notifies the listener that the timestamped buffer has been disposed and can now be reused. - * @param reusableComplexBuffer that has been disposed + * @param t reusableBuffer that has been disposed */ - void disposed(ReusableComplexBuffer reusableComplexBuffer); + void disposed(T t); } diff --git a/src/main/java/io/github/dsheirer/sample/complex/reusable/IReusableComplexBufferProvider.java b/src/main/java/io/github/dsheirer/sample/buffer/IReusableBufferProvider.java similarity index 83% rename from src/main/java/io/github/dsheirer/sample/complex/reusable/IReusableComplexBufferProvider.java rename to src/main/java/io/github/dsheirer/sample/buffer/IReusableBufferProvider.java index 96ad66dd..afacda85 100644 --- a/src/main/java/io/github/dsheirer/sample/complex/reusable/IReusableComplexBufferProvider.java +++ b/src/main/java/io/github/dsheirer/sample/buffer/IReusableBufferProvider.java @@ -13,21 +13,21 @@ * If not, see * ******************************************************************************/ -package io.github.dsheirer.sample.complex.reusable; +package io.github.dsheirer.sample.buffer; import io.github.dsheirer.sample.Listener; -public interface IReusableComplexBufferProvider +public interface IReusableBufferProvider { /** * Adds the listener to receive complex buffer samples */ - void addBufferListener(Listener listener); + void addBufferListener(Listener listener); /** * Removes the listener from receiving complex buffer samples */ - void removeBufferListener(Listener listener); + void removeBufferListener(Listener listener); /** * Indicates if there are any complex buffer listeners registered diff --git a/src/main/java/io/github/dsheirer/sample/complex/reusable/IReusableComplexBufferListener.java b/src/main/java/io/github/dsheirer/sample/buffer/IReusableComplexBufferListener.java similarity index 84% rename from src/main/java/io/github/dsheirer/sample/complex/reusable/IReusableComplexBufferListener.java rename to src/main/java/io/github/dsheirer/sample/buffer/IReusableComplexBufferListener.java index 2f7a7306..47eada60 100644 --- a/src/main/java/io/github/dsheirer/sample/complex/reusable/IReusableComplexBufferListener.java +++ b/src/main/java/io/github/dsheirer/sample/buffer/IReusableComplexBufferListener.java @@ -13,11 +13,11 @@ * If not, see * ******************************************************************************/ -package io.github.dsheirer.sample.complex.reusable; +package io.github.dsheirer.sample.buffer; import io.github.dsheirer.sample.Listener; -public interface IReusableComplexBufferListener +public interface IReusableComplexBufferListener { - Listener getReusableComplexBufferListener(); + Listener getReusableComplexBufferListener(); } diff --git a/src/main/java/io/github/dsheirer/sample/complex/reusable/OverflowableReusableBufferTransferQueue.java b/src/main/java/io/github/dsheirer/sample/buffer/OverflowableReusableBufferTransferQueue.java similarity index 85% rename from src/main/java/io/github/dsheirer/sample/complex/reusable/OverflowableReusableBufferTransferQueue.java rename to src/main/java/io/github/dsheirer/sample/buffer/OverflowableReusableBufferTransferQueue.java index 05486981..914833bb 100644 --- a/src/main/java/io/github/dsheirer/sample/complex/reusable/OverflowableReusableBufferTransferQueue.java +++ b/src/main/java/io/github/dsheirer/sample/buffer/OverflowableReusableBufferTransferQueue.java @@ -13,11 +13,11 @@ * If not, see * ******************************************************************************/ -package io.github.dsheirer.sample.complex.reusable; +package io.github.dsheirer.sample.buffer; import io.github.dsheirer.sample.OverflowableTransferQueue; -public class OverflowableReusableBufferTransferQueue extends OverflowableTransferQueue +public class OverflowableReusableBufferTransferQueue extends OverflowableTransferQueue { /** * Concurrent transfer queue that couples a higher-throughput linked transfer queue with an atomic integer for @@ -38,12 +38,12 @@ public class OverflowableReusableBufferTransferQueue extends OverflowableTransfe * Overrides the overflow method to decrement the user count on any buffers that are being discarded when the queue * is in an overflow state. * - * @param reusableComplexBuffer that will be discarded + * @param t reusableBuffer that will be discarded */ @Override - protected void overflow(ReusableComplexBuffer reusableComplexBuffer) + protected void overflow(T t) { - reusableComplexBuffer.decrementUserCount(); + t.decrementUserCount(); } /** @@ -54,7 +54,7 @@ public class OverflowableReusableBufferTransferQueue extends OverflowableTransfe { synchronized(mQueue) { - ReusableComplexBuffer buffer = mQueue.poll(); + T buffer = mQueue.poll(); while(buffer != null) { diff --git a/src/main/java/io/github/dsheirer/sample/complex/reusable/ReusableComplexBuffer.java b/src/main/java/io/github/dsheirer/sample/buffer/ReusableBuffer.java similarity index 80% rename from src/main/java/io/github/dsheirer/sample/complex/reusable/ReusableComplexBuffer.java rename to src/main/java/io/github/dsheirer/sample/buffer/ReusableBuffer.java index 151852af..d9434004 100644 --- a/src/main/java/io/github/dsheirer/sample/complex/reusable/ReusableComplexBuffer.java +++ b/src/main/java/io/github/dsheirer/sample/buffer/ReusableBuffer.java @@ -13,19 +13,20 @@ * If not, see * ******************************************************************************/ -package io.github.dsheirer.sample.complex.reusable; +package io.github.dsheirer.sample.buffer; -import io.github.dsheirer.sample.complex.ComplexBuffer; import org.apache.commons.lang3.Validate; import java.nio.FloatBuffer; import java.util.concurrent.atomic.AtomicInteger; -public class ReusableComplexBuffer extends ComplexBuffer +public class ReusableBuffer { - private IReusableBufferDisposedListener mDisposalListener; + private float[] mSamples; + private IReusableBufferDisposedListener mBufferDisposedListener; private long mTimestamp; private AtomicInteger mUserCount = new AtomicInteger(); + private String mDebugName; /** * Creates a reusable, timestamped complex buffer using the specified time in milliseconds. @@ -34,32 +35,40 @@ public class ReusableComplexBuffer extends ComplexBuffer * buffer should not modify the buffer contents. Each component should also increment the user count before * sending this buffer to another component and should decrement the user count when finished using this buffer. * - * @param disposalListener to be notified when all consumers/users are finished using the buffer + * @param bufferDisposedListener to be notified when all consumers/users are finished using the buffer * @param samples of data * @param timestamp in millis for the buffer */ - public ReusableComplexBuffer(IReusableBufferDisposedListener disposalListener, float[] samples, long timestamp) + ReusableBuffer(IReusableBufferDisposedListener bufferDisposedListener, float[] samples, long timestamp) { - super(samples); + mSamples = samples; - Validate.notNull(disposalListener, "Timestamped Buffer Listener cannot be null"); - mDisposalListener = disposalListener; + Validate.notNull(bufferDisposedListener, "Reusable Buffer Listener cannot be null"); + mBufferDisposedListener = bufferDisposedListener; mTimestamp = timestamp; } - /** - * Constructs a timestamped complex buffer using the current system time in milliseconds. - * - * NOTE: reusability of this buffer requires strict user count tracking. Each component that receives this - * buffer should not modify the buffer contents. Each component should also increment the user count before - * sending this buffer to another component and should decrement the user count when finished using this buffer. - * - * @param disposalListener to be notified when all consumers are finished using the buffer - * @param samples of data - */ - public ReusableComplexBuffer(IReusableBufferDisposedListener disposalListener, float[] samples) + public void setDebugName(String debugName) { - this(disposalListener, samples, System.currentTimeMillis()); + mDebugName = debugName; + } + + public String name() + { + return (mDebugName != null ? mDebugName : "(null)") + " User Count:" + getUserCount(); + } + + public String toString() + { + return name(); + } + + /** + * Samples for this buffer + */ + public float[] getSamples() + { + return mSamples; } /** @@ -74,11 +83,26 @@ public class ReusableComplexBuffer extends ComplexBuffer } /** - * Number of complex samples contained in this buffer + * Number of samples contained in this buffer */ - public int getSampleLength() + public int getSampleCount() { - return getSamples().length / 2; + return getSamples().length; + } + + /** + * Constructs a timestamped complex buffer using the current system time in milliseconds. + * + * NOTE: reusability of this buffer requires strict user count tracking. Each component that receives this + * buffer should not modify the buffer contents. Each component should also increment the user count before + * sending this buffer to another component and should decrement the user count when finished using this buffer. + * + * @param bufferDisposedListener to be notified when all consumers are finished using the buffer + * @param samples of data + */ + public ReusableBuffer(IReusableBufferDisposedListener bufferDisposedListener, float[] samples) + { + this(bufferDisposedListener, samples, System.currentTimeMillis()); } /** @@ -173,21 +197,19 @@ public class ReusableComplexBuffer extends ComplexBuffer * * This method is thread-safe. */ - public ReusableComplexBuffer decrementUserCount() + public void decrementUserCount() { int currentCount = mUserCount.decrementAndGet(); if(currentCount == 0) { - mDisposalListener.disposed(this); + mBufferDisposedListener.disposed(this); } else if(currentCount < 0) { throw new IllegalStateException("User count is below zero. This indicates that this buffer's decrement" + " user count was invoked by more than the expected user count"); } - - return this; } /** @@ -199,10 +221,9 @@ public class ReusableComplexBuffer extends ComplexBuffer * * This method is thread-safe. */ - public ReusableComplexBuffer incrementUserCount() + public void incrementUserCount() { mUserCount.incrementAndGet(); - return this; } /** @@ -216,9 +237,16 @@ public class ReusableComplexBuffer extends ComplexBuffer * * @param additionalUserCount to add to the current user count */ - public ReusableComplexBuffer incrementUserCount(int additionalUserCount) + public void incrementUserCount(int additionalUserCount) { mUserCount.addAndGet(additionalUserCount); - return this; + } + + /** + * Number of users currently registered for this buffer + */ + public int getUserCount() + { + return mUserCount.get(); } } diff --git a/src/main/java/io/github/dsheirer/sample/complex/reusable/ReusableBufferAssembler.java b/src/main/java/io/github/dsheirer/sample/buffer/ReusableBufferAssembler.java similarity index 95% rename from src/main/java/io/github/dsheirer/sample/complex/reusable/ReusableBufferAssembler.java rename to src/main/java/io/github/dsheirer/sample/buffer/ReusableBufferAssembler.java index a58b62f3..d1f2847d 100644 --- a/src/main/java/io/github/dsheirer/sample/complex/reusable/ReusableBufferAssembler.java +++ b/src/main/java/io/github/dsheirer/sample/buffer/ReusableBufferAssembler.java @@ -13,7 +13,7 @@ * If not, see * ******************************************************************************/ -package io.github.dsheirer.sample.complex.reusable; +package io.github.dsheirer.sample.buffer; import io.github.dsheirer.dsp.filter.channelizer.SampleTimestampManager; import io.github.dsheirer.sample.Listener; @@ -27,7 +27,7 @@ import java.nio.FloatBuffer; */ public class ReusableBufferAssembler { - private ReusableBufferQueue mReusableBufferQueue = new ReusableBufferQueue(); + private ReusableComplexBufferQueue mReusableComplexBufferQueue = new ReusableComplexBufferQueue("ReusableBufferAssembler"); private SampleTimestampManager mTimestampManager; private FloatBuffer mBuffer; private int mBufferSize; @@ -115,7 +115,7 @@ public class ReusableBufferAssembler if(mListener != null && mBuffer.position() > 0) { mBuffer.rewind(); - ReusableComplexBuffer reusableComplexBuffer = mReusableBufferQueue.getBuffer(mBufferSize); + ReusableComplexBuffer reusableComplexBuffer = mReusableComplexBufferQueue.getBuffer(mBufferSize); reusableComplexBuffer.reloadFrom(mBuffer, mCurrentBufferTimestamp); reusableComplexBuffer.setUserCount(1); mListener.receive(reusableComplexBuffer); diff --git a/src/main/java/io/github/dsheirer/sample/complex/reusable/ReusableBufferBroadcaster.java b/src/main/java/io/github/dsheirer/sample/buffer/ReusableBufferBroadcaster.java similarity index 66% rename from src/main/java/io/github/dsheirer/sample/complex/reusable/ReusableBufferBroadcaster.java rename to src/main/java/io/github/dsheirer/sample/buffer/ReusableBufferBroadcaster.java index e2ac0f80..2df8493c 100644 --- a/src/main/java/io/github/dsheirer/sample/complex/reusable/ReusableBufferBroadcaster.java +++ b/src/main/java/io/github/dsheirer/sample/buffer/ReusableBufferBroadcaster.java @@ -13,7 +13,7 @@ * If not, see * ******************************************************************************/ -package io.github.dsheirer.sample.complex.reusable; +package io.github.dsheirer.sample.buffer; import io.github.dsheirer.sample.Broadcaster; import io.github.dsheirer.sample.Listener; @@ -36,29 +36,10 @@ public class ReusableBufferBroadcaster extends Broadcaster listener : mListeners) { - if(listenerCount > 0) - { - listener.receive(reusableComplexBuffer); - } - - listenerCount--; - } - - while(listenerCount > 0) - { - reusableComplexBuffer.decrementUserCount(); - listenerCount--; + reusableComplexBuffer.incrementUserCount(); + listener.receive(reusableComplexBuffer); } //Decrement user counter for this broadcaster diff --git a/src/main/java/io/github/dsheirer/sample/buffer/ReusableBufferQueue.java b/src/main/java/io/github/dsheirer/sample/buffer/ReusableBufferQueue.java new file mode 100644 index 00000000..85bb5efc --- /dev/null +++ b/src/main/java/io/github/dsheirer/sample/buffer/ReusableBufferQueue.java @@ -0,0 +1,39 @@ +/******************************************************************************* + * 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 + * + ******************************************************************************/ +package io.github.dsheirer.sample.buffer; + +import org.slf4j.Logger; +import org.slf4j.LoggerFactory; + +public class ReusableBufferQueue extends AbstractReusableBufferQueue +{ + private final static Logger mLog = LoggerFactory.getLogger(ReusableBufferQueue.class); + + public ReusableBufferQueue(String debugName) + { + super(debugName); + } + + public ReusableBufferQueue() + { + } + + @Override + protected ReusableBuffer createBuffer(int size) + { + return new ReusableBuffer(this, new float[size]); + } +} diff --git a/src/main/java/io/github/dsheirer/sample/buffer/ReusableComplexBuffer.java b/src/main/java/io/github/dsheirer/sample/buffer/ReusableComplexBuffer.java new file mode 100644 index 00000000..bc768d7c --- /dev/null +++ b/src/main/java/io/github/dsheirer/sample/buffer/ReusableComplexBuffer.java @@ -0,0 +1,59 @@ +/******************************************************************************* + * 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 + * + ******************************************************************************/ +package io.github.dsheirer.sample.buffer; + +public class ReusableComplexBuffer extends ReusableBuffer +{ + /** + * Creates a reusable, timestamped complex buffer using the specified time in milliseconds. + * + * NOTE: reusability of this buffer requires strict user count tracking. Each component that receives this + * buffer should not modify the buffer contents. Each component should also increment the user count before + * sending this buffer to another component and should decrement the user count when finished using this buffer. + * + * @param disposalListener to be notified when all consumers/users are finished using the buffer + * @param samples of data + * @param timestamp in millis for the buffer + */ + ReusableComplexBuffer(IReusableBufferDisposedListener disposalListener, float[] samples, long timestamp) + { + super(disposalListener, samples, timestamp); + } + + /** + * Constructs a timestamped complex buffer using the current system time in milliseconds. + * + * NOTE: reusability of this buffer requires strict user count tracking. Each component that receives this + * buffer should not modify the buffer contents. Each component should also increment the user count before + * sending this buffer to another component and should decrement the user count when finished using this buffer. + * + * @param disposalListener to be notified when all consumers are finished using the buffer + * @param samples of data + */ + protected ReusableComplexBuffer(IReusableBufferDisposedListener disposalListener, float[] samples) + { + this(disposalListener, samples, System.currentTimeMillis()); + } + + /** + * Number of complex samples contained in this buffer + */ + @Override + public int getSampleCount() + { + return getSamples().length / 2; + } +} diff --git a/src/main/java/io/github/dsheirer/sample/buffer/ReusableComplexBufferQueue.java b/src/main/java/io/github/dsheirer/sample/buffer/ReusableComplexBufferQueue.java new file mode 100644 index 00000000..0c52454e --- /dev/null +++ b/src/main/java/io/github/dsheirer/sample/buffer/ReusableComplexBufferQueue.java @@ -0,0 +1,39 @@ +/******************************************************************************* + * 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 + * + ******************************************************************************/ +package io.github.dsheirer.sample.buffer; + +import org.slf4j.Logger; +import org.slf4j.LoggerFactory; + +public class ReusableComplexBufferQueue extends AbstractReusableBufferQueue +{ + private final static Logger mLog = LoggerFactory.getLogger(ReusableComplexBufferQueue.class); + + public ReusableComplexBufferQueue(String debugName) + { + super(debugName); + } + + public ReusableComplexBufferQueue() + { + } + + @Override + protected ReusableComplexBuffer createBuffer(int size) + { + return new ReusableComplexBuffer(this, new float[size]); + } +} diff --git a/src/main/java/io/github/dsheirer/source/ComplexSource.java b/src/main/java/io/github/dsheirer/source/ComplexSource.java index a06be2dc..098641d5 100644 --- a/src/main/java/io/github/dsheirer/source/ComplexSource.java +++ b/src/main/java/io/github/dsheirer/source/ComplexSource.java @@ -17,7 +17,7 @@ package io.github.dsheirer.source; import io.github.dsheirer.sample.Provider; import io.github.dsheirer.sample.SampleType; -import io.github.dsheirer.sample.complex.reusable.ReusableComplexBuffer; +import io.github.dsheirer.sample.buffer.ReusableComplexBuffer; public abstract class ComplexSource extends Source implements Provider { diff --git a/src/main/java/io/github/dsheirer/source/mixer/ComplexMixer.java b/src/main/java/io/github/dsheirer/source/mixer/ComplexMixer.java index fc117def..9d83fdac 100644 --- a/src/main/java/io/github/dsheirer/source/mixer/ComplexMixer.java +++ b/src/main/java/io/github/dsheirer/source/mixer/ComplexMixer.java @@ -17,8 +17,8 @@ package io.github.dsheirer.source.mixer; import io.github.dsheirer.sample.Listener; import io.github.dsheirer.sample.adapter.IComplexSampleAdapter; -import io.github.dsheirer.sample.complex.reusable.ReusableBufferQueue; -import io.github.dsheirer.sample.complex.reusable.ReusableComplexBuffer; +import io.github.dsheirer.sample.buffer.ReusableComplexBuffer; +import io.github.dsheirer.sample.buffer.ReusableComplexBufferQueue; import io.github.dsheirer.source.SourceException; import org.slf4j.Logger; import org.slf4j.LoggerFactory; @@ -42,7 +42,7 @@ public class ComplexMixer private int mBytesPerFrame = 0; private IComplexSampleAdapter mSampleAdapter; private Listener mListener; - private ReusableBufferQueue mReusableBufferQueue = new ReusableBufferQueue(); + private ReusableComplexBufferQueue mReusableComplexBufferQueue = new ReusableComplexBufferQueue("ComplexMixer"); /** * Complex Mixer - constructs a reader on the mixer/sound card target data line using the specified audio @@ -173,7 +173,7 @@ public class ComplexMixer /* Dispatch samples to registered listeners */ if(mListener != null) { - ReusableComplexBuffer reusableComplexBuffer = mReusableBufferQueue.getBuffer(sampleCount); + ReusableComplexBuffer reusableComplexBuffer = mReusableComplexBufferQueue.getBuffer(sampleCount); //Convert samples and load into reusable buffer mSampleAdapter.convertAndLoad(buffer, reusableComplexBuffer); diff --git a/src/main/java/io/github/dsheirer/source/tuner/TunerController.java b/src/main/java/io/github/dsheirer/source/tuner/TunerController.java index ebd1c516..4d4b3377 100644 --- a/src/main/java/io/github/dsheirer/source/tuner/TunerController.java +++ b/src/main/java/io/github/dsheirer/source/tuner/TunerController.java @@ -18,9 +18,9 @@ package io.github.dsheirer.source.tuner; import io.github.dsheirer.sample.Listener; -import io.github.dsheirer.sample.complex.reusable.IReusableComplexBufferProvider; -import io.github.dsheirer.sample.complex.reusable.ReusableBufferBroadcaster; -import io.github.dsheirer.sample.complex.reusable.ReusableComplexBuffer; +import io.github.dsheirer.sample.buffer.IReusableBufferProvider; +import io.github.dsheirer.sample.buffer.ReusableBufferBroadcaster; +import io.github.dsheirer.sample.buffer.ReusableComplexBuffer; import io.github.dsheirer.source.ISourceEventListener; import io.github.dsheirer.source.ISourceEventProcessor; import io.github.dsheirer.source.SourceEvent; @@ -33,10 +33,10 @@ import org.slf4j.Logger; import org.slf4j.LoggerFactory; public abstract class TunerController implements Tunable, ISourceEventProcessor, ISourceEventListener, - IReusableComplexBufferProvider, Listener + IReusableBufferProvider, Listener { private final static Logger mLog = LoggerFactory.getLogger(TunerController.class); - protected ReusableBufferBroadcaster mSampleBroadcaster = new ReusableBufferBroadcaster(); + protected ReusableBufferBroadcaster mReusableBufferBroadcaster = new ReusableBufferBroadcaster(); protected FrequencyController mFrequencyController; private int mMiddleUnusableHalfBandwidth; private double mUsableBandwidthPercentage; @@ -263,7 +263,7 @@ public abstract class TunerController implements Tunable, ISourceEventProcessor, @Override public void addBufferListener(Listener listener) { - mSampleBroadcaster.addListener(listener); + mReusableBufferBroadcaster.addListener(listener); } /** @@ -272,7 +272,7 @@ public abstract class TunerController implements Tunable, ISourceEventProcessor, @Override public void removeBufferListener(Listener listener) { - mSampleBroadcaster.removeListener(listener); + mReusableBufferBroadcaster.removeListener(listener); } /** @@ -281,7 +281,7 @@ public abstract class TunerController implements Tunable, ISourceEventProcessor, @Override public boolean hasBufferListeners() { - return mSampleBroadcaster.hasListeners(); + return mReusableBufferBroadcaster.hasListeners(); } /** @@ -289,7 +289,7 @@ public abstract class TunerController implements Tunable, ISourceEventProcessor, */ protected void broadcast(ReusableComplexBuffer reusableComplexBuffer) { - mSampleBroadcaster.broadcast(reusableComplexBuffer.incrementUserCount()); + mReusableBufferBroadcaster.broadcast(reusableComplexBuffer); } /** diff --git a/src/main/java/io/github/dsheirer/source/tuner/channel/TunerChannelSource.java b/src/main/java/io/github/dsheirer/source/tuner/channel/TunerChannelSource.java index b195afac..914458ab 100644 --- a/src/main/java/io/github/dsheirer/source/tuner/channel/TunerChannelSource.java +++ b/src/main/java/io/github/dsheirer/source/tuner/channel/TunerChannelSource.java @@ -19,7 +19,7 @@ package io.github.dsheirer.source.tuner.channel; import io.github.dsheirer.sample.Listener; -import io.github.dsheirer.sample.complex.reusable.ReusableComplexBuffer; +import io.github.dsheirer.sample.buffer.ReusableComplexBuffer; import io.github.dsheirer.source.ComplexSource; import io.github.dsheirer.source.ISourceEventProcessor; import io.github.dsheirer.source.SourceEvent; diff --git a/src/main/java/io/github/dsheirer/source/tuner/fcd/FCDTunerController.java b/src/main/java/io/github/dsheirer/source/tuner/fcd/FCDTunerController.java index 9e77661e..002bcc63 100644 --- a/src/main/java/io/github/dsheirer/source/tuner/fcd/FCDTunerController.java +++ b/src/main/java/io/github/dsheirer/source/tuner/fcd/FCDTunerController.java @@ -20,7 +20,7 @@ package io.github.dsheirer.source.tuner.fcd; import io.github.dsheirer.sample.Listener; import io.github.dsheirer.sample.adapter.ComplexShortAdapter; -import io.github.dsheirer.sample.complex.reusable.ReusableComplexBuffer; +import io.github.dsheirer.sample.buffer.ReusableComplexBuffer; import io.github.dsheirer.source.SourceException; import io.github.dsheirer.source.mixer.ComplexMixer; import io.github.dsheirer.source.tuner.MixerTunerDataLine; @@ -88,7 +88,7 @@ public abstract class FCDTunerController extends TunerController } mComplexMixer = new ComplexMixer( mixerTDL.getTargetDataLine(), audioFormat, tunerName, - new ComplexShortAdapter(), mSampleBroadcaster); + new ComplexShortAdapter(), mReusableBufferBroadcaster); } /** diff --git a/src/main/java/io/github/dsheirer/source/tuner/manager/ChannelSourceManager.java b/src/main/java/io/github/dsheirer/source/tuner/manager/ChannelSourceManager.java index 9280ac4b..1b113313 100644 --- a/src/main/java/io/github/dsheirer/source/tuner/manager/ChannelSourceManager.java +++ b/src/main/java/io/github/dsheirer/source/tuner/manager/ChannelSourceManager.java @@ -54,7 +54,8 @@ public abstract class ChannelSourceManager implements ISourceEventProcessor */ public abstract TunerChannelSource getSource(TunerChannel tunerChannel); - public abstract TunerChannelSource getSpecialSource(int index); + //TODO: remove this testing method when finished with polyphase channel manager + public abstract TunerChannelSource getTestingSource(int index); /** * Adds a listener to receive source events diff --git a/src/main/java/io/github/dsheirer/source/tuner/manager/TunerChannelSourceManager.java b/src/main/java/io/github/dsheirer/source/tuner/manager/TunerChannelSourceManager.java index d3f2e7d8..0390fa40 100644 --- a/src/main/java/io/github/dsheirer/source/tuner/manager/TunerChannelSourceManager.java +++ b/src/main/java/io/github/dsheirer/source/tuner/manager/TunerChannelSourceManager.java @@ -50,10 +50,10 @@ public class TunerChannelSourceManager extends ChannelSourceManager } @Override - public TunerChannelSource getSpecialSource(int index) + public TunerChannelSource getTestingSource(int index) { //TODO: remove this debug method - return mPolyphaseChannelManager.getSpecialChannel(index); + return mPolyphaseChannelManager.getTestChannel(index); } /** diff --git a/src/main/java/io/github/dsheirer/source/tuner/rtl/RTL2832TunerController.java b/src/main/java/io/github/dsheirer/source/tuner/rtl/RTL2832TunerController.java index a6c8c138..95fe2445 100644 --- a/src/main/java/io/github/dsheirer/source/tuner/rtl/RTL2832TunerController.java +++ b/src/main/java/io/github/dsheirer/source/tuner/rtl/RTL2832TunerController.java @@ -22,7 +22,7 @@ package io.github.dsheirer.source.tuner.rtl; import io.github.dsheirer.sample.Listener; -import io.github.dsheirer.sample.complex.reusable.ReusableComplexBuffer; +import io.github.dsheirer.sample.buffer.ReusableComplexBuffer; import io.github.dsheirer.source.SourceException; import io.github.dsheirer.source.tuner.TunerManager; import io.github.dsheirer.source.tuner.TunerType; diff --git a/src/main/java/io/github/dsheirer/source/tuner/test/SampleGenerator.java b/src/main/java/io/github/dsheirer/source/tuner/test/SampleGenerator.java index 123fde8f..5c34cdb1 100644 --- a/src/main/java/io/github/dsheirer/source/tuner/test/SampleGenerator.java +++ b/src/main/java/io/github/dsheirer/source/tuner/test/SampleGenerator.java @@ -18,9 +18,9 @@ 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.Listener; -import io.github.dsheirer.sample.complex.reusable.ReusableBufferBroadcaster; -import io.github.dsheirer.sample.complex.reusable.ReusableBufferQueue; -import io.github.dsheirer.sample.complex.reusable.ReusableComplexBuffer; +import io.github.dsheirer.sample.buffer.ReusableBufferBroadcaster; +import io.github.dsheirer.sample.buffer.ReusableComplexBuffer; +import io.github.dsheirer.sample.buffer.ReusableComplexBufferQueue; import io.github.dsheirer.util.ThreadPool; import org.slf4j.Logger; import org.slf4j.LoggerFactory; @@ -185,18 +185,19 @@ public class SampleGenerator public class Generator implements Runnable { private int mTriggerInterval = 0; - private ReusableBufferQueue mReusableBufferQueue = new ReusableBufferQueue("** Sample Generator **"); + private ReusableComplexBufferQueue mReusableComplexBufferQueue = new ReusableComplexBufferQueue("SampleGenerator"); @Override public void run() { if(mComplexBufferBroadcaster.hasListeners()) { - ReusableComplexBuffer reusableComplexBuffer = mReusableBufferQueue.getBuffer(mSamplesPerInterval); + ReusableComplexBuffer reusableComplexBuffer = mReusableComplexBufferQueue.getBuffer(mSamplesPerInterval); mOscillator.generateComplex(reusableComplexBuffer); - mComplexBufferBroadcaster.broadcast(reusableComplexBuffer.incrementUserCount()); + reusableComplexBuffer.incrementUserCount(); + mComplexBufferBroadcaster.broadcast(reusableComplexBuffer); if(mSweepUpdateInterval != 0) { diff --git a/src/main/java/io/github/dsheirer/source/tuner/test/TestTunerController.java b/src/main/java/io/github/dsheirer/source/tuner/test/TestTunerController.java index 0638232b..fc2489f2 100644 --- a/src/main/java/io/github/dsheirer/source/tuner/test/TestTunerController.java +++ b/src/main/java/io/github/dsheirer/source/tuner/test/TestTunerController.java @@ -19,7 +19,7 @@ package io.github.dsheirer.source.tuner.test; import io.github.dsheirer.sample.Listener; -import io.github.dsheirer.sample.complex.reusable.ReusableComplexBuffer; +import io.github.dsheirer.sample.buffer.ReusableComplexBuffer; import io.github.dsheirer.source.SourceException; import io.github.dsheirer.source.tuner.TunerController; import io.github.dsheirer.source.tuner.configuration.TunerConfiguration; diff --git a/src/main/java/io/github/dsheirer/source/tuner/usb/USBTransferProcessor.java b/src/main/java/io/github/dsheirer/source/tuner/usb/USBTransferProcessor.java index 129f228e..55a09d1c 100644 --- a/src/main/java/io/github/dsheirer/source/tuner/usb/USBTransferProcessor.java +++ b/src/main/java/io/github/dsheirer/source/tuner/usb/USBTransferProcessor.java @@ -16,7 +16,7 @@ package io.github.dsheirer.source.tuner.usb; import io.github.dsheirer.sample.Listener; -import io.github.dsheirer.sample.complex.reusable.ReusableComplexBuffer; +import io.github.dsheirer.sample.buffer.ReusableComplexBuffer; import io.github.dsheirer.source.tuner.TunerManager; import io.github.dsheirer.source.tuner.usb.converter.NativeBufferConverter; import io.github.dsheirer.util.ThreadPool; @@ -394,9 +394,10 @@ public class USBTransferProcessor implements TransferCallback { ByteBuffer nativeBuffer = transfer.buffer(); - ReusableComplexBuffer sampleBuffer = mNativeBufferConverter.convert(nativeBuffer, transfer.actualLength()); + ReusableComplexBuffer reusableComplexBuffer = + mNativeBufferConverter.convert(nativeBuffer, transfer.actualLength()); - mComplexBufferListener.receive(sampleBuffer); + mComplexBufferListener.receive(reusableComplexBuffer); } transfer.buffer().rewind(); diff --git a/src/main/java/io/github/dsheirer/source/tuner/usb/USBTunerController.java b/src/main/java/io/github/dsheirer/source/tuner/usb/USBTunerController.java index db114e7d..185f901a 100644 --- a/src/main/java/io/github/dsheirer/source/tuner/usb/USBTunerController.java +++ b/src/main/java/io/github/dsheirer/source/tuner/usb/USBTunerController.java @@ -19,7 +19,7 @@ package io.github.dsheirer.source.tuner.usb; import io.github.dsheirer.sample.Listener; -import io.github.dsheirer.sample.complex.reusable.ReusableComplexBuffer; +import io.github.dsheirer.sample.buffer.ReusableComplexBuffer; import io.github.dsheirer.source.tuner.TunerController; public abstract class USBTunerController extends TunerController diff --git a/src/main/java/io/github/dsheirer/source/tuner/usb/converter/NativeBufferConverter.java b/src/main/java/io/github/dsheirer/source/tuner/usb/converter/NativeBufferConverter.java index 9d9dfcbf..cbe86064 100644 --- a/src/main/java/io/github/dsheirer/source/tuner/usb/converter/NativeBufferConverter.java +++ b/src/main/java/io/github/dsheirer/source/tuner/usb/converter/NativeBufferConverter.java @@ -15,15 +15,15 @@ ******************************************************************************/ package io.github.dsheirer.source.tuner.usb.converter; -import io.github.dsheirer.sample.complex.reusable.ReusableBufferQueue; -import io.github.dsheirer.sample.complex.reusable.ReusableComplexBuffer; +import io.github.dsheirer.sample.buffer.ReusableComplexBuffer; +import io.github.dsheirer.sample.buffer.ReusableComplexBufferQueue; import java.nio.ByteBuffer; import java.nio.FloatBuffer; public abstract class NativeBufferConverter { - private ReusableBufferQueue mReusableBufferQueue = new ReusableBufferQueue(); + private ReusableComplexBufferQueue mReusableComplexBufferQueue = new ReusableComplexBufferQueue("NativeBufferConverter"); /** * Converts native byte buffers into complex float samples and produces reusable complex sample buffers. Tracks @@ -45,10 +45,11 @@ public abstract class NativeBufferConverter { FloatBuffer floatBuffer = convertSamples(byteBuffer, length); - ReusableComplexBuffer reusableComplexBuffer = mReusableBufferQueue.getBuffer(floatBuffer.capacity()); + ReusableComplexBuffer reusableComplexBuffer = mReusableComplexBufferQueue.getBuffer(floatBuffer.capacity()); //The reusable buffer will rewind the float buffer to the beginning reusableComplexBuffer.reloadFrom(floatBuffer, System.currentTimeMillis()); + reusableComplexBuffer.incrementUserCount(); return reusableComplexBuffer; } diff --git a/src/main/java/io/github/dsheirer/source/wave/ComplexWaveSource.java b/src/main/java/io/github/dsheirer/source/wave/ComplexWaveSource.java index 256a53f3..219a5e1b 100644 --- a/src/main/java/io/github/dsheirer/source/wave/ComplexWaveSource.java +++ b/src/main/java/io/github/dsheirer/source/wave/ComplexWaveSource.java @@ -17,8 +17,8 @@ package io.github.dsheirer.source.wave; import io.github.dsheirer.sample.ConversionUtils; import io.github.dsheirer.sample.Listener; -import io.github.dsheirer.sample.complex.reusable.ReusableBufferQueue; -import io.github.dsheirer.sample.complex.reusable.ReusableComplexBuffer; +import io.github.dsheirer.sample.buffer.ReusableComplexBuffer; +import io.github.dsheirer.sample.buffer.ReusableComplexBufferQueue; import io.github.dsheirer.source.ComplexSource; import io.github.dsheirer.source.IControllableFileSource; import io.github.dsheirer.source.IFrameLocationListener; @@ -45,7 +45,7 @@ public class ComplexWaveSource extends ComplexSource implements IControllableFil private Listener mListener; private AudioInputStream mInputStream; private File mFile; - private ReusableBufferQueue mReusableBufferQueue = new ReusableBufferQueue("Complex Wave Source"); + private ReusableComplexBufferQueue mReusableComplexBufferQueue = new ReusableComplexBufferQueue("ComplexWaveSource"); public ComplexWaveSource(File file) throws IOException { @@ -216,7 +216,7 @@ public class ComplexWaveSource extends ComplexSource implements IControllableFil float[] samples = ConversionUtils.convertFromSigned16BitSamples(buffer); - ReusableComplexBuffer reusableBuffer = mReusableBufferQueue.getBuffer(samples.length); + ReusableComplexBuffer reusableBuffer = mReusableComplexBufferQueue.getBuffer(samples.length); reusableBuffer.reloadFrom(samples, System.currentTimeMillis()); reusableBuffer.incrementUserCount(); mListener.receive(reusableBuffer); diff --git a/src/main/java/io/github/dsheirer/spectrum/ChannelSpectrumPanel.java b/src/main/java/io/github/dsheirer/spectrum/ChannelSpectrumPanel.java index 0431ff26..4cc781df 100644 --- a/src/main/java/io/github/dsheirer/spectrum/ChannelSpectrumPanel.java +++ b/src/main/java/io/github/dsheirer/spectrum/ChannelSpectrumPanel.java @@ -26,8 +26,8 @@ import io.github.dsheirer.dsp.filter.smoothing.SmoothingFilter.SmoothingType; import io.github.dsheirer.module.ProcessingChain; import io.github.dsheirer.sample.Listener; import io.github.dsheirer.sample.SampleType; -import io.github.dsheirer.sample.complex.reusable.ReusableBufferQueue; -import io.github.dsheirer.sample.complex.reusable.ReusableComplexBuffer; +import io.github.dsheirer.sample.buffer.ReusableComplexBuffer; +import io.github.dsheirer.sample.buffer.ReusableComplexBufferQueue; import io.github.dsheirer.sample.real.RealBuffer; import io.github.dsheirer.settings.ColorSetting.ColorSettingName; import io.github.dsheirer.settings.ColorSettingMenuItem; @@ -69,435 +69,450 @@ public class ChannelSpectrumPanel extends JPanel implements ChannelEventListener private JLayeredPane mLayeredPane; private SpectrumPanel mSpectrumPanel; private ChannelOverlayPanel mOverlayPanel; - private ReusableBufferQueue mReusableBufferQueue = new ReusableBufferQueue(); + private ReusableComplexBufferQueue mReusableComplexBufferQueue = new ReusableComplexBufferQueue("ChannelSpectrumPanel"); private Channel mCurrentChannel; private int mSampleBufferSize = 2400; - private HalfBandFilter_RB_RB mDecimatingFilter = new HalfBandFilter_RB_RB( - Filters.FIR_HALF_BAND_31T_ONE_EIGHTH_FCO.getCoefficients(), 1.0f, true ); - + private HalfBandFilter_RB_RB mDecimatingFilter = new HalfBandFilter_RB_RB( + Filters.FIR_HALF_BAND_31T_ONE_EIGHTH_FCO.getCoefficients(), 1.0f, true); + private AtomicBoolean mEnabled = new AtomicBoolean(); - + private SettingsManager mSettingsManager; private ChannelProcessingManager mChannelProcessingManager; - public ChannelSpectrumPanel( SettingsManager settingsManager, - ChannelProcessingManager channelProcessingManager ) + public ChannelSpectrumPanel(SettingsManager settingsManager, + ChannelProcessingManager channelProcessingManager) { - mSettingsManager = settingsManager; - mChannelProcessingManager = channelProcessingManager; - - if( mSettingsManager != null ) - { - mSettingsManager.addListener( this ); - } - - mSpectrumPanel = new SpectrumPanel( mSettingsManager ); - mSpectrumPanel.setAveraging( 1 ); + mSettingsManager = settingsManager; + mChannelProcessingManager = channelProcessingManager; - mOverlayPanel = new ChannelOverlayPanel( mSettingsManager ); - mDFTProcessor.addConverter( mDFTConverter ); - mDFTConverter.addListener( mSpectrumPanel ); + if(mSettingsManager != null) + { + mSettingsManager.addListener(this); + } + + mSpectrumPanel = new SpectrumPanel(mSettingsManager); + mSpectrumPanel.setAveraging(1); + + mOverlayPanel = new ChannelOverlayPanel(mSettingsManager); + mDFTProcessor.addConverter(mDFTConverter); + mDFTConverter.addListener(mSpectrumPanel); /* Set the DFTProcessor to the decimated 24kHz sample rate */ mDFTProcessor.process(SourceEvent.sampleRateChange(24000.0)); initGui(); } - + public void dispose() { - setEnabled( false ); - - mDFTProcessor.dispose(); + setEnabled(false); - if( mSettingsManager != null ) - { - mSettingsManager.removeListener( this ); - } - - mSettingsManager = null; - mCurrentChannel = null; - mDFTProcessor = null; - mSpectrumPanel = null; + mDFTProcessor.dispose(); + + if(mSettingsManager != null) + { + mSettingsManager.removeListener(this); + } + + mSettingsManager = null; + mCurrentChannel = null; + mDFTProcessor = null; + mSpectrumPanel = null; } - - public void setFrameRate( int framesPerSecond ) + + public void setFrameRate(int framesPerSecond) { - mSampleBufferSize = (int)( 48000 / framesPerSecond ); - - mDFTProcessor.setFrameRate( framesPerSecond ); + mSampleBufferSize = (int)(48000 / framesPerSecond); + + mDFTProcessor.setFrameRate(framesPerSecond); } - + private void initGui() { - setLayout( new MigLayout( "insets 0 0 0 0 ", - "[grow,fill]", - "[grow,fill]") ); - - mLayeredPane = new JLayeredPane(); - mLayeredPane.addComponentListener( new ResizeListener() ); - - MouseEventProcessor mouser = new MouseEventProcessor(); - - mOverlayPanel.addMouseListener( mouser ); - mOverlayPanel.addMouseMotionListener( mouser ); - - mLayeredPane.add( mSpectrumPanel, new Integer( 0 ), 0 ); - mLayeredPane.add( mOverlayPanel, new Integer( 1 ), 0 ); + setLayout(new MigLayout("insets 0 0 0 0 ", + "[grow,fill]", + "[grow,fill]")); - add( mLayeredPane ); + mLayeredPane = new JLayeredPane(); + mLayeredPane.addComponentListener(new ResizeListener()); + + MouseEventProcessor mouser = new MouseEventProcessor(); + + mOverlayPanel.addMouseListener(mouser); + mOverlayPanel.addMouseMotionListener(mouser); + + mLayeredPane.add(mSpectrumPanel, new Integer(0), 0); + mLayeredPane.add(mOverlayPanel, new Integer(1), 0); + + add(mLayeredPane); } - - public void setEnabled( boolean enabled ) + + public void setEnabled(boolean enabled) { - if( enabled && mEnabled.compareAndSet( false, true ) ) - { - start(); - } - else if( !enabled && mEnabled.compareAndSet( true, false ) ) - { - stop(); - } + if(enabled && mEnabled.compareAndSet(false, true)) + { + start(); + } + else if(!enabled && mEnabled.compareAndSet(true, false)) + { + stop(); + } } @Override - @SuppressWarnings( "incomplete-switch" ) - public void channelChanged( ChannelEvent event ) + @SuppressWarnings("incomplete-switch") + public void channelChanged(ChannelEvent event) { - switch( event.getEvent() ) - { - case NOTIFICATION_SELECTION_CHANGE: - //ChannelSelectionManager ensures that only 1 channel can be - //selected and any previously selected channel will be first - //deselected before we get a new selection event - if( event.getChannel().isSelected() ) - { - if( mCurrentChannel != null ) - { - stop(); - mCurrentChannel = null; - } - - mCurrentChannel = event.getChannel(); - - if( mEnabled.get() ) - { - start(); - } - } - else - { - stop(); - mCurrentChannel = null; - } - break; - case NOTIFICATION_PROCESSING_STOP: - if( event.getChannel() == mCurrentChannel ) - { - if( mEnabled.get() ) - { - stop(); - } + switch(event.getEvent()) + { + case NOTIFICATION_SELECTION_CHANGE: + //ChannelSelectionManager ensures that only 1 channel can be + //selected and any previously selected channel will be first + //deselected before we get a new selection event + if(event.getChannel().isSelected()) + { + if(mCurrentChannel != null) + { + stop(); + mCurrentChannel = null; + } - mCurrentChannel = null; - } - break; - } - } - - private void start() - { - if( mEnabled.get() && mCurrentChannel != null && mCurrentChannel.isProcessing() ) - { - ProcessingChain processingChain = mChannelProcessingManager - .getProcessingChain( mCurrentChannel ); + mCurrentChannel = event.getChannel(); - if( processingChain != null ) - { - processingChain.addRealBufferListener( this ); - - mDFTProcessor.start(); - } - } - } - - private void stop() - { - if( mCurrentChannel != null && mCurrentChannel.isProcessing() ) - { - ProcessingChain processingChain = mChannelProcessingManager - .getProcessingChain( mCurrentChannel ); + if(mEnabled.get()) + { + start(); + } + } + else + { + stop(); + mCurrentChannel = null; + } + break; + case NOTIFICATION_PROCESSING_STOP: + if(event.getChannel() == mCurrentChannel) + { + if(mEnabled.get()) + { + stop(); + } - if( processingChain != null ) - { - processingChain.removeRealBufferListener( this ); - } - } - - mDFTProcessor.stop(); - - mSpectrumPanel.clearSpectrum(); - } - - @Override - public void settingChanged( Setting setting ) - { - if( mSpectrumPanel != null ) - { - mSpectrumPanel.settingChanged( setting ); - } - if( mOverlayPanel != null ) - { - mOverlayPanel.settingChanged( setting ); - } + mCurrentChannel = null; + } + break; + } } - @Override - public void settingDeleted( Setting setting ) + private void start() { - if( mSpectrumPanel != null ) - { - mSpectrumPanel.settingDeleted( setting ); - } + if(mEnabled.get() && mCurrentChannel != null && mCurrentChannel.isProcessing()) + { + ProcessingChain processingChain = mChannelProcessingManager + .getProcessingChain(mCurrentChannel); - if( mOverlayPanel != null ) - { - mOverlayPanel.settingDeleted( setting ); - } + if(processingChain != null) + { + processingChain.addRealBufferListener(this); + + mDFTProcessor.start(); + } + } } - @Override - public void receive( RealBuffer buffer ) + private void stop() { - RealBuffer decimated = mDecimatingFilter.filter( buffer ); + if(mCurrentChannel != null && mCurrentChannel.isProcessing()) + { + ProcessingChain processingChain = mChannelProcessingManager + .getProcessingChain(mCurrentChannel); + + if(processingChain != null) + { + processingChain.removeRealBufferListener(this); + } + } + + mDFTProcessor.stop(); + + mSpectrumPanel.clearSpectrum(); + } + + @Override + public void settingChanged(Setting setting) + { + if(mSpectrumPanel != null) + { + mSpectrumPanel.settingChanged(setting); + } + if(mOverlayPanel != null) + { + mOverlayPanel.settingChanged(setting); + } + } + + @Override + public void settingDeleted(Setting setting) + { + if(mSpectrumPanel != null) + { + mSpectrumPanel.settingDeleted(setting); + } + + if(mOverlayPanel != null) + { + mOverlayPanel.settingDeleted(setting); + } + } + + @Override + public void receive(RealBuffer buffer) + { + RealBuffer decimated = mDecimatingFilter.filter(buffer); //Hack: we're placing real samples in a complex buffer that the DFT //processor is expecting. - ReusableComplexBuffer reusableComplexBuffer = mReusableBufferQueue.getBuffer(decimated.getSamples().length); + ReusableComplexBuffer reusableComplexBuffer = mReusableComplexBufferQueue.getBuffer(decimated.getSamples().length); reusableComplexBuffer.reloadFrom(decimated.getSamples(), System.currentTimeMillis()); - mDFTProcessor.receive(reusableComplexBuffer.incrementUserCount()); + reusableComplexBuffer.incrementUserCount(); + mDFTProcessor.receive(reusableComplexBuffer); } - /** - * Monitors the sizing of the layered pane and resizes the spectrum and - * channel panels whenever the layered pane is resized - */ - public class ResizeListener implements ComponentListener - { - @Override - public void componentResized( ComponentEvent e ) + /** + * Monitors the sizing of the layered pane and resizes the spectrum and + * channel panels whenever the layered pane is resized + */ + public class ResizeListener implements ComponentListener + { + @Override + public void componentResized(ComponentEvent e) { - Component c = e.getComponent(); - - mSpectrumPanel.setBounds( 0, 0, c.getWidth(), c.getHeight() ); - mOverlayPanel.setBounds( 0, 0, c.getWidth(), c.getHeight() ); + Component c = e.getComponent(); + + mSpectrumPanel.setBounds(0, 0, c.getWidth(), c.getHeight()); + mOverlayPanel.setBounds(0, 0, c.getWidth(), c.getHeight()); } - @Override - public void componentHidden( ComponentEvent arg0 ) {} - @Override - public void componentMoved( ComponentEvent arg0 ) {} - @Override - public void componentShown( ComponentEvent arg0 ) {} - } - - /** - * Mouse event handler for the channel panel. - */ - public class MouseEventProcessor implements MouseMotionListener, MouseListener - { - @Override - public void mouseMoved( MouseEvent event ) + @Override + public void componentHidden(ComponentEvent arg0) { - update( event ); - } - @Override - public void mouseDragged( MouseEvent event ) - { - update( event ); - } - - private void update( MouseEvent event ) - { - if( event.getComponent() == mOverlayPanel ) - { - mOverlayPanel.setCursorLocation( event.getPoint() ); - } - } - - @Override - public void mouseEntered( MouseEvent e ) - { - if( e.getComponent() == mOverlayPanel ) - { - mOverlayPanel.setCursorVisible( true ); - } - } - - @Override - public void mouseExited( MouseEvent e ) - { - mOverlayPanel.setCursorVisible( false ); - } - - /** - * Displays the context menu. - */ - @Override - public void mouseClicked( MouseEvent event ) - { - if( SwingUtilities.isRightMouseButton( event ) ) - { - JPopupMenu contextMenu = new JPopupMenu(); - - /** - * Color Menus - */ - JMenu colorMenu = new JMenu( "Color" ); - - colorMenu.add( new ColorSettingMenuItem( mSettingsManager, - ColorSettingName.SPECTRUM_CURSOR ) ); - - colorMenu.add( new ColorSettingMenuItem( mSettingsManager, - ColorSettingName.SPECTRUM_LINE ) ); - - colorMenu.add( new ColorSettingMenuItem( mSettingsManager, - ColorSettingName.SPECTRUM_BACKGROUND ) ); - - colorMenu.add( new ColorSettingMenuItem( mSettingsManager, - ColorSettingName.SPECTRUM_GRADIENT_BOTTOM ) ); - - colorMenu.add( new ColorSettingMenuItem( mSettingsManager, - ColorSettingName.SPECTRUM_GRADIENT_TOP ) ); - - contextMenu.add( colorMenu ); - - /** - * Display items: fft and frame rate - */ - JMenu displayMenu = new JMenu( "Display" ); - contextMenu.add( displayMenu ); - - /** - * Averaging menu - */ - JMenu averagingMenu = new JMenu( "Averaging" ); - averagingMenu.add( - new AveragingItem( ChannelSpectrumPanel.this, 2 ) ); - displayMenu.add( averagingMenu ); - - /** - * FFT width - */ - JMenu fftWidthMenu = new JMenu( "FFT Width" ); - displayMenu.add( fftWidthMenu ); - - for( DFTSize width: DFTSize.values() ) - { - fftWidthMenu.add( new DFTSizeItem( mDFTProcessor, width ) ); - } - - /** - * DFT Processor Frame Rate - */ - JMenu frameRateMenu = new JMenu( "Frame Rate" ); - displayMenu.add( frameRateMenu ); - - frameRateMenu.add( new FrameRateItem( mDFTProcessor, 14 ) ); - frameRateMenu.add( new FrameRateItem( mDFTProcessor, 16 ) ); - frameRateMenu.add( new FrameRateItem( mDFTProcessor, 18 ) ); - frameRateMenu.add( new FrameRateItem( mDFTProcessor, 20 ) ); - frameRateMenu.add( new FrameRateItem( mDFTProcessor, 25 ) ); - frameRateMenu.add( new FrameRateItem( mDFTProcessor, 30 ) ); - frameRateMenu.add( new FrameRateItem( mDFTProcessor, 40 ) ); - frameRateMenu.add( new FrameRateItem( mDFTProcessor, 50 ) ); - - /** - * FFT Window Type - */ - JMenu fftWindowType = new JMenu( "Window Type" ); - displayMenu.add( fftWindowType ); - - for( WindowType type: WindowType.values() ) - { - fftWindowType.add( - new FFTWindowTypeItem( mDFTProcessor, type ) ); - } - - /** - * Smoothing menu - */ - JMenu smoothingMenu = new JMenu( "Smoothing" ); - - if( mSpectrumPanel.getSmoothingType() != SmoothingType.NONE ) - { - smoothingMenu.add( new SmoothingItem( ChannelSpectrumPanel.this, 5 ) ); - smoothingMenu.add( new JSeparator() ); - } - - smoothingMenu.add( new SmoothingTypeItem( ChannelSpectrumPanel.this, SmoothingType.GAUSSIAN ) ); - smoothingMenu.add( new SmoothingTypeItem( ChannelSpectrumPanel.this, SmoothingType.TRIANGLE ) ); - smoothingMenu.add( new SmoothingTypeItem( ChannelSpectrumPanel.this, SmoothingType.RECTANGLE ) ); - smoothingMenu.add( new SmoothingTypeItem( ChannelSpectrumPanel.this, SmoothingType.NONE ) ); - - displayMenu.add( smoothingMenu ); - - if( contextMenu != null ) - { - contextMenu.show( mOverlayPanel, - event.getX(), - event.getY() ); - } - } } - @Override - public void mousePressed( MouseEvent e ) {} - @Override - public void mouseReleased( MouseEvent e ) {} - } + @Override + public void componentMoved(ComponentEvent arg0) + { + } - @Override + @Override + public void componentShown(ComponentEvent arg0) + { + } + } + + /** + * Mouse event handler for the channel panel. + */ + public class MouseEventProcessor implements MouseMotionListener, MouseListener + { + @Override + public void mouseMoved(MouseEvent event) + { + update(event); + } + + @Override + public void mouseDragged(MouseEvent event) + { + update(event); + } + + private void update(MouseEvent event) + { + if(event.getComponent() == mOverlayPanel) + { + mOverlayPanel.setCursorLocation(event.getPoint()); + } + } + + @Override + public void mouseEntered(MouseEvent e) + { + if(e.getComponent() == mOverlayPanel) + { + mOverlayPanel.setCursorVisible(true); + } + } + + @Override + public void mouseExited(MouseEvent e) + { + mOverlayPanel.setCursorVisible(false); + } + + /** + * Displays the context menu. + */ + @Override + public void mouseClicked(MouseEvent event) + { + if(SwingUtilities.isRightMouseButton(event)) + { + JPopupMenu contextMenu = new JPopupMenu(); + + /** + * Color Menus + */ + JMenu colorMenu = new JMenu("Color"); + + colorMenu.add(new ColorSettingMenuItem(mSettingsManager, + ColorSettingName.SPECTRUM_CURSOR)); + + colorMenu.add(new ColorSettingMenuItem(mSettingsManager, + ColorSettingName.SPECTRUM_LINE)); + + colorMenu.add(new ColorSettingMenuItem(mSettingsManager, + ColorSettingName.SPECTRUM_BACKGROUND)); + + colorMenu.add(new ColorSettingMenuItem(mSettingsManager, + ColorSettingName.SPECTRUM_GRADIENT_BOTTOM)); + + colorMenu.add(new ColorSettingMenuItem(mSettingsManager, + ColorSettingName.SPECTRUM_GRADIENT_TOP)); + + contextMenu.add(colorMenu); + + /** + * Display items: fft and frame rate + */ + JMenu displayMenu = new JMenu("Display"); + contextMenu.add(displayMenu); + + /** + * Averaging menu + */ + JMenu averagingMenu = new JMenu("Averaging"); + averagingMenu.add( + new AveragingItem(ChannelSpectrumPanel.this, 2)); + displayMenu.add(averagingMenu); + + /** + * FFT width + */ + JMenu fftWidthMenu = new JMenu("FFT Width"); + displayMenu.add(fftWidthMenu); + + for(DFTSize width : DFTSize.values()) + { + fftWidthMenu.add(new DFTSizeItem(mDFTProcessor, width)); + } + + /** + * DFT Processor Frame Rate + */ + JMenu frameRateMenu = new JMenu("Frame Rate"); + displayMenu.add(frameRateMenu); + + frameRateMenu.add(new FrameRateItem(mDFTProcessor, 14)); + frameRateMenu.add(new FrameRateItem(mDFTProcessor, 16)); + frameRateMenu.add(new FrameRateItem(mDFTProcessor, 18)); + frameRateMenu.add(new FrameRateItem(mDFTProcessor, 20)); + frameRateMenu.add(new FrameRateItem(mDFTProcessor, 25)); + frameRateMenu.add(new FrameRateItem(mDFTProcessor, 30)); + frameRateMenu.add(new FrameRateItem(mDFTProcessor, 40)); + frameRateMenu.add(new FrameRateItem(mDFTProcessor, 50)); + + /** + * FFT Window Type + */ + JMenu fftWindowType = new JMenu("Window Type"); + displayMenu.add(fftWindowType); + + for(WindowType type : WindowType.values()) + { + fftWindowType.add( + new FFTWindowTypeItem(mDFTProcessor, type)); + } + + /** + * Smoothing menu + */ + JMenu smoothingMenu = new JMenu("Smoothing"); + + if(mSpectrumPanel.getSmoothingType() != SmoothingType.NONE) + { + smoothingMenu.add(new SmoothingItem(ChannelSpectrumPanel.this, 5)); + smoothingMenu.add(new JSeparator()); + } + + smoothingMenu.add(new SmoothingTypeItem(ChannelSpectrumPanel.this, SmoothingType.GAUSSIAN)); + smoothingMenu.add(new SmoothingTypeItem(ChannelSpectrumPanel.this, SmoothingType.TRIANGLE)); + smoothingMenu.add(new SmoothingTypeItem(ChannelSpectrumPanel.this, SmoothingType.RECTANGLE)); + smoothingMenu.add(new SmoothingTypeItem(ChannelSpectrumPanel.this, SmoothingType.NONE)); + + displayMenu.add(smoothingMenu); + + if(contextMenu != null) + { + contextMenu.show(mOverlayPanel, + event.getX(), + event.getY()); + } + } + } + + @Override + public void mousePressed(MouseEvent e) + { + } + + @Override + public void mouseReleased(MouseEvent e) + { + } + } + + @Override public int getAveraging() { - return mSpectrumPanel.getAveraging(); + return mSpectrumPanel.getAveraging(); } - @Override - public void setAveraging( int averaging ) + @Override + public void setAveraging(int averaging) { - mSpectrumPanel.setAveraging( averaging ); + mSpectrumPanel.setAveraging(averaging); } - public void setSampleSize( double sampleSize ) - { - mSpectrumPanel.setSampleSize( sampleSize ); - } + public void setSampleSize(double sampleSize) + { + mSpectrumPanel.setSampleSize(sampleSize); + } - @Override - public int getSmoothing() - { - return mSpectrumPanel.getSmoothing(); - } + @Override + public int getSmoothing() + { + return mSpectrumPanel.getSmoothing(); + } - @Override - public void setSmoothing( int smoothing ) - { - mSpectrumPanel.setSmoothing( smoothing ); - } + @Override + public void setSmoothing(int smoothing) + { + mSpectrumPanel.setSmoothing(smoothing); + } - @Override - public SmoothingType getSmoothingType() - { - return mSpectrumPanel.getSmoothingType(); - } + @Override + public SmoothingType getSmoothingType() + { + return mSpectrumPanel.getSmoothingType(); + } - @Override - public void setSmoothingType( SmoothingType type ) - { - mSpectrumPanel.setSmoothingType( type ); - } + @Override + public void setSmoothingType(SmoothingType type) + { + mSpectrumPanel.setSmoothingType(type); + } } diff --git a/src/main/java/io/github/dsheirer/spectrum/DFTProcessor.java b/src/main/java/io/github/dsheirer/spectrum/DFTProcessor.java index 172a278f..4a281381 100644 --- a/src/main/java/io/github/dsheirer/spectrum/DFTProcessor.java +++ b/src/main/java/io/github/dsheirer/spectrum/DFTProcessor.java @@ -20,7 +20,7 @@ import io.github.dsheirer.dsp.filter.Window.WindowType; import io.github.dsheirer.properties.SystemProperties; import io.github.dsheirer.sample.Listener; import io.github.dsheirer.sample.SampleType; -import io.github.dsheirer.sample.complex.reusable.ReusableComplexBuffer; +import io.github.dsheirer.sample.buffer.ReusableComplexBuffer; import io.github.dsheirer.sample.real.IOverflowListener; import io.github.dsheirer.source.ISourceEventProcessor; import io.github.dsheirer.source.SourceEvent; diff --git a/src/main/java/io/github/dsheirer/spectrum/OverflowableBufferStream.java b/src/main/java/io/github/dsheirer/spectrum/OverflowableBufferStream.java index 1525d8e4..af6077d2 100644 --- a/src/main/java/io/github/dsheirer/spectrum/OverflowableBufferStream.java +++ b/src/main/java/io/github/dsheirer/spectrum/OverflowableBufferStream.java @@ -17,7 +17,7 @@ package io.github.dsheirer.spectrum; import io.github.dsheirer.sample.OverflowableTransferQueue; -import io.github.dsheirer.sample.complex.reusable.ReusableComplexBuffer; +import io.github.dsheirer.sample.buffer.ReusableComplexBuffer; import org.slf4j.Logger; import org.slf4j.LoggerFactory; @@ -206,6 +206,13 @@ public class OverflowableBufferStream extends OverflowableTransferQueue Date: Mon, 26 Mar 2018 09:47:32 -0400 Subject: [PATCH 60/87] 39 [WIP] experiments to reduce processing time for high sample rate tuners (e.g airspy) --- .../ComplexPolyphaseChannelizerM2.java | 55 +++++++++++++++++-- .../channelizer/PolyphaseChannelManager.java | 2 + .../buffer/AbstractReusableBufferQueue.java | 5 +- 3 files changed, 52 insertions(+), 10 deletions(-) diff --git a/src/main/java/io/github/dsheirer/dsp/filter/channelizer/ComplexPolyphaseChannelizerM2.java b/src/main/java/io/github/dsheirer/dsp/filter/channelizer/ComplexPolyphaseChannelizerM2.java index 4ed168e1..476d937d 100644 --- a/src/main/java/io/github/dsheirer/dsp/filter/channelizer/ComplexPolyphaseChannelizerM2.java +++ b/src/main/java/io/github/dsheirer/dsp/filter/channelizer/ComplexPolyphaseChannelizerM2.java @@ -21,7 +21,9 @@ package io.github.dsheirer.dsp.filter.channelizer; import io.github.dsheirer.dsp.filter.FilterFactory; import io.github.dsheirer.dsp.filter.Window.WindowType; import io.github.dsheirer.dsp.filter.design.FilterDesignException; +import io.github.dsheirer.sample.Listener; import io.github.dsheirer.sample.buffer.ReusableComplexBuffer; +import io.github.dsheirer.sample.real.IOverflowListener; import org.jtransforms.fft.FloatFFT_1D; import org.slf4j.Logger; import org.slf4j.LoggerFactory; @@ -42,6 +44,7 @@ public class ComplexPolyphaseChannelizerM2 extends AbstractComplexPolyphaseChann private PolyphaseChannelResultsBuffer mChannelResultsBuffer; private int mChannelResultsBufferSize = 2500; private SampleTimestampManager mTimestampManager; + private IFFTProcessor mIFFTProcessor = new IFFTProcessor(300, 50); /** * Non-Maximally Decimated Polyphase Filter Bank (NMDPFB) channelizer that divides the input frequency band into @@ -95,6 +98,16 @@ public class ComplexPolyphaseChannelizerM2 extends AbstractComplexPolyphaseChann init(filterTaps); } + public void start() + { + mIFFTProcessor.start(); + } + + public void stop() + { + mIFFTProcessor.stop(); + } + /** * Calculates the multiple of two number of channels that can be channelized from the specified sample rate so that * each channel has a minimum bandwidth of the default channel bandwidth (12.5 kHz). @@ -144,6 +157,8 @@ public class ComplexPolyphaseChannelizerM2 extends AbstractComplexPolyphaseChann @Override public void receive(ReusableComplexBuffer reusableComplexBuffer) { + long start = System.nanoTime(); + //Use the buffer's reference timestamp to update our timestamp manager (for timestamping output buffers) mTimestampManager.setReferenceTimestamp(reusableComplexBuffer.getTimestamp()); @@ -172,6 +187,8 @@ public class ComplexPolyphaseChannelizerM2 extends AbstractComplexPolyphaseChann //Decrement the user count to let the originator know we're done with their buffer reusableComplexBuffer.decrementUserCount(); + + mLog.debug("Duration: " + (System.nanoTime() - start)); } /** @@ -216,7 +233,6 @@ public class ComplexPolyphaseChannelizerM2 extends AbstractComplexPolyphaseChann int half = getChannelCount() / 2; -// int outputOffset = getChannelCount() - half - 1; int outputOffset = getChannelCount() - 1; int outputIndex; int sampleIndex; @@ -234,7 +250,6 @@ public class ComplexPolyphaseChannelizerM2 extends AbstractComplexPolyphaseChann //The second half of the sample buffer was loaded using the previous column pointer - adjust for that now sampleIndexMap = mSampleIndexMap[(mColumnPointer == (mSamples.length - 1) ? 0 : mColumnPointer + 1)]; -// outputOffset = getChannelCount() + half - 1; outputOffset = getChannelCount() - 1; for(int channel = half; channel < getChannelCount(); channel++) @@ -248,10 +263,7 @@ public class ComplexPolyphaseChannelizerM2 extends AbstractComplexPolyphaseChann } } - //Rotate each of the channels to the correct phase using the IFFT - mFFT.complexInverse(processed, true); - - processChannelResults(processed); + mIFFTProcessor.receive(processed); } /** @@ -397,4 +409,35 @@ public class ComplexPolyphaseChannelizerM2 extends AbstractComplexPolyphaseChann } } } + + /** + * Processor to enqueue filtered channels, perform IFFT and dispatch the results + */ + public class IFFTProcessor extends ContinuousBufferProcessor + { + public IFFTProcessor(int maximumSize, int resetThreshold) + { + super(maximumSize, resetThreshold); + + setListener(new Listener() + { + @Override + public void receive(float[] floatsToProcess) + { + //Rotate each of the channels to the correct phase using the IFFT + mFFT.complexInverse(floatsToProcess, true); + processChannelResults(floatsToProcess); + } + }); + + setOverflowListener(new IOverflowListener() + { + @Override + public void sourceOverflow(boolean overflow) + { + mLog.debug("IFFTProcessor overflow changed - overflow:" + overflow); + } + }); + } + } } diff --git a/src/main/java/io/github/dsheirer/dsp/filter/channelizer/PolyphaseChannelManager.java b/src/main/java/io/github/dsheirer/dsp/filter/channelizer/PolyphaseChannelManager.java index d9118020..5c63dd78 100644 --- a/src/main/java/io/github/dsheirer/dsp/filter/channelizer/PolyphaseChannelManager.java +++ b/src/main/java/io/github/dsheirer/dsp/filter/channelizer/PolyphaseChannelManager.java @@ -232,6 +232,7 @@ public class PolyphaseChannelManager implements ISourceEventProcessor if(mPolyphaseChannelizer.getRegisteredChannelCount() == 1) { mReusableBufferProvider.addBufferListener(mBufferProcessor); + mPolyphaseChannelizer.start(); mBufferProcessor.start(); } } @@ -254,6 +255,7 @@ public class PolyphaseChannelManager implements ISourceEventProcessor { mReusableBufferProvider.removeBufferListener(mBufferProcessor); mBufferProcessor.stop(); + mPolyphaseChannelizer.stop(); } } diff --git a/src/main/java/io/github/dsheirer/sample/buffer/AbstractReusableBufferQueue.java b/src/main/java/io/github/dsheirer/sample/buffer/AbstractReusableBufferQueue.java index 0959b060..5e5f3bd6 100644 --- a/src/main/java/io/github/dsheirer/sample/buffer/AbstractReusableBufferQueue.java +++ b/src/main/java/io/github/dsheirer/sample/buffer/AbstractReusableBufferQueue.java @@ -78,10 +78,7 @@ public abstract class AbstractReusableBufferQueue impl mBufferCount++; buffer.setDebugName("Owner:" + mDebugName + " Number:" + mBufferUserCounter++ + " NEW BUFFER"); -// if(mDebugName != null && mDebugName.contentEquals("NativeBufferConverter")) -// { -// mLog.debug("Buffer Created - Count:" + mBufferCount + (mDebugName != null ? " [" + mDebugName + "]" : "")); -// } + mLog.debug("Buffer Created - Count:" + mBufferCount + (mDebugName != null ? " [" + mDebugName + "]" : "")); } else { From ff9395e4b9730f9f71452718880187c9e4a31052 Mon Sep 17 00:00:00 2001 From: Denny Date: Sat, 14 Apr 2018 07:23:33 -0400 Subject: [PATCH 61/87] 39 [WIP] working copy with new SIMD structures in place. --- .../AbstractComplexPolyphaseChannelizer.java | 10 + .../ComplexPolyphaseChannelizerM2.java | 470 +++++++++++++++++- .../channelizer/PolyphaseChannelManager.java | 2 +- 3 files changed, 470 insertions(+), 12 deletions(-) diff --git a/src/main/java/io/github/dsheirer/dsp/filter/channelizer/AbstractComplexPolyphaseChannelizer.java b/src/main/java/io/github/dsheirer/dsp/filter/channelizer/AbstractComplexPolyphaseChannelizer.java index 7de9ead9..e044b2af 100644 --- a/src/main/java/io/github/dsheirer/dsp/filter/channelizer/AbstractComplexPolyphaseChannelizer.java +++ b/src/main/java/io/github/dsheirer/dsp/filter/channelizer/AbstractComplexPolyphaseChannelizer.java @@ -37,6 +37,7 @@ public abstract class AbstractComplexPolyphaseChannelizer implements Listener mChannels = new CopyOnWriteArrayList<>(); private double mSampleRate; private int mChannelCount; + private int mSubChannelCount; private double mChannelSampleRate; /** @@ -47,6 +48,7 @@ public abstract class AbstractComplexPolyphaseChannelizer implements Listener mSampleToFilterMap = new HashMap<>(); + + private int[][] mTopBlockAccumulatorIndexMap; + private int[][] mMiddleBlockAccumulatorIndexMap; + private int[] mTopBlockMap; + private int[] mMiddleBlockMap; + private float[] mInlineSamples; + private float[] mInlineFilter; + private float[] mInlineInterimOutput; + private float[] mChannelAccumulator; + private int mSampleBufferPointer; + private int mTapsPerChannel; + private boolean mTopBlockIndicator = true; + + /** * Non-Maximally Decimated Polyphase Filter Bank (NMDPFB) channelizer that divides the input frequency band into * equal bandwidth channels that are each oversampled by 2x for output. @@ -154,8 +175,7 @@ public class ComplexPolyphaseChannelizerM2 extends AbstractComplexPolyphaseChann /** * Receives the complex sample buffer and processes the results through the channelizer. */ - @Override - public void receive(ReusableComplexBuffer reusableComplexBuffer) + public void receiveOld(ReusableComplexBuffer reusableComplexBuffer) { long start = System.nanoTime(); @@ -181,7 +201,8 @@ public class ComplexPolyphaseChannelizerM2 extends AbstractComplexPolyphaseChann if(mRowPointer % mBlockSize == 0) { - process(); + //if row pointer at max, then mRowPointer = 0 and mColumnPointer-- + processOld(); } } @@ -191,12 +212,266 @@ public class ComplexPolyphaseChannelizerM2 extends AbstractComplexPolyphaseChann mLog.debug("Duration: " + (System.nanoTime() - start)); } + /** + * Receives the complex sample buffer and processes the results through the channelizer. + */ + @Override + public void receive(ReusableComplexBuffer reusableComplexBuffer) + { + long start = System.nanoTime(); + long processing = 0; + + //Use the buffer's reference timestamp to update our timestamp manager (for timestamping output buffers) + mTimestampManager.setReferenceTimestamp(reusableComplexBuffer.getTimestamp()); + + float[] samples = reusableComplexBuffer.getSamples(); + + int samplesPointer = 0; + int samplesToCopy; + + while(samplesPointer < samples.length) + { + if(mSampleBufferPointer < mSamplesPerBlock) + { + samplesToCopy = mSamplesPerBlock - mSampleBufferPointer; + + if((samples.length - samplesPointer) < samplesToCopy) + { + samplesToCopy = (samples.length - samplesPointer); + } + + System.arraycopy(samples, samplesPointer, mInlineSamples, mSampleBufferPointer, samplesToCopy); + + mSampleBufferPointer += samplesToCopy; + samplesPointer += samplesToCopy; + } + + if(mSampleBufferPointer >= mSamplesPerBlock) + { + //Filter buffered samples and produce a single sample across each of the polyphase channels + processing += process(); + + //Right-shift the samples in the buffer over to make room for a new block of samples + //Note: since JDK 8, hotspot JIT compiler uses native processor intrinsics for efficiency + System.arraycopy(mInlineSamples, 0, mInlineSamples, mSamplesPerBlock, (mInlineSamples.length - mSamplesPerBlock)); + mSampleBufferPointer = 0; + } + } + + mLog.debug("Duration: " + (System.nanoTime() - start) + " Processing:" + processing); + //Decrement the user count to let the originator know we're done with their buffer + reusableComplexBuffer.decrementUserCount(); + + } + + /** + * Creates a base accumulator index map that is preloaded with the indexes that have to be summed to produce + * each subchannel filtered value. This index map structure is designed for processing of polyphase filter + * samples in a contiguous/single sample array where the filter taps are also arranged to align with this + * inline sample array buffer. This method preloads the index values, but the first dimension of the array + * must be rearranged to align with either top-block or middle-block processing so that each of the + * polyphase filter arm outputs is assigned to each sub-channel (I/Q) correctly. + * + * @param channelCount - number of channels + * @param tapsPerChannel - number of filter taps per channel + * @return output index to filter accumulator index mapping + */ + private static int[][] getBaseAccumulatorIndexMap(int channelCount, int tapsPerChannel) + { + int subChannelCount = channelCount * 2; + + int[][] accumulatorIndexMap = new int[subChannelCount][tapsPerChannel]; + + int pointer = 0; + + //Step 1: load the interim product output indexes in sequential order. + for(int column = 0; column < tapsPerChannel; column++) + { + for(int subChannel = 0; subChannel < subChannelCount; subChannel++) + { + accumulatorIndexMap[subChannel][column] = pointer++; + } + } + + return accumulatorIndexMap; + } + + /** + * Creates a top-block processing accumulator map that maps each interim filter and sample index product + * to the corresponding final output index for the array that will feed the IFFT. + * + * @param channelCount - number of channels + * @param tapsPerChannel - number of filter taps per channel + * @return output index to filter accumulator index mapping + */ + private static int[][] getTopBlockAccumulatorIndexMap(int channelCount, int tapsPerChannel) + { + int[][] baseIndexMap = getBaseAccumulatorIndexMap(channelCount, tapsPerChannel); + + //SubChannel count is the number if I/Q channels (ie channels x 2) + int subChannelCount = channelCount * 2; + + //Reorder the subchannel arrays to the structure needed for top-block processing + int blockSize = channelCount / 2; + + int[][] reorderedMap = new int[subChannelCount][tapsPerChannel]; + + for(int channel = 0; channel < blockSize; channel++) + { + int newIndex = 2 * channel; + int originalIndex = 2 * (blockSize - channel - 1); + int offset = 2 * blockSize; + + reorderedMap[newIndex] = baseIndexMap[originalIndex]; + reorderedMap[newIndex + 1] = baseIndexMap[originalIndex + 1]; + + reorderedMap[offset + newIndex] = baseIndexMap[offset + originalIndex]; + reorderedMap[offset + newIndex + 1] = baseIndexMap[offset + originalIndex + 1]; + } + + return reorderedMap; + } + + /** + * Creates a top-block processing accumulator map that maps each interim filter and sample index product + * to the corresponding final output index for the array that will feed the IFFT. + * + * @param channelCount - number of channels + * @return output index to filter accumulator index mapping + */ + private static int[] getTopBlockMap(int channelCount) + { + int[] newMap = new int[channelCount * 2]; + + //Reorder the subchannel arrays to the structure needed for top-block processing + int blockSize = channelCount / 2; + + for(int channel = 0; channel < blockSize; channel++) + { + int newIndex = 2 * channel; + int originalIndex = 2 * (blockSize - channel - 1); + int offset = 2 * blockSize; + + newMap[originalIndex] = newIndex; + newMap[originalIndex + 1] = newIndex + 1; + newMap[offset + originalIndex] = offset + newIndex; + newMap[offset + originalIndex + 1] = offset + newIndex + 1; + } + + return newMap; + } + + /** + * Creates a top-block processing accumulator map that maps each interim filter and sample index product + * to the corresponding final output index for the array that will feed the IFFT. + * + * @param channelCount - number of channels + * @return output index to filter accumulator index mapping + */ + private static int[] getMiddleBlockMap(int channelCount) + { + int[] newMap = new int[channelCount * 2]; + + //Reorder the subchannel arrays to the structure needed for top-block processing + int blockSize = channelCount / 2; + + for(int channel = 0; channel < blockSize; channel++) + { + int newIndex = 2 * channel; + int originalIndex = 2 * (blockSize - channel - 1); + int offset = 2 * blockSize; + + newMap[offset + originalIndex] = newIndex; + newMap[offset + originalIndex + 1] = newIndex + 1; + newMap[originalIndex] = offset + newIndex; + newMap[originalIndex + 1] = offset + newIndex + 1; + } + + return newMap; + } + /** + * Creates a middle-block processing accumulator map that maps each interim filter and sample index product + * to the corresponding final output index for the array that will feed the IFFT. + * + * @param channelCount - number of channels + * @param tapsPerChannel - number of filter taps per channel + * @return output index to filter accumulator index mapping + */ + private static int[][] getMiddleBlockAccumulatorIndexMap(int channelCount, int tapsPerChannel) + { + int[][] baseIndexMap = getBaseAccumulatorIndexMap(channelCount, tapsPerChannel); + + //SubChannel count is the number if I/Q channels (ie channels x 2) + int subChannelCount = channelCount * 2; + + //Reorder the subchannel arrays to the structure needed for middle-block processing, where the outputs + //from the top and bottom blocks are inverted when compared to top-block processing + int blockSize = channelCount / 2; + + int[][] reorderedMap = new int[subChannelCount][tapsPerChannel]; + + for(int channel = 0; channel < blockSize; channel++) + { + int newIndex = 2 * channel; + int originalIndex = 2 * (blockSize - channel - 1); + int offset = 2 * blockSize; + + reorderedMap[newIndex] = baseIndexMap[offset + originalIndex]; + reorderedMap[newIndex + 1] = baseIndexMap[offset + originalIndex + 1]; + + reorderedMap[offset + newIndex] = baseIndexMap[originalIndex]; + reorderedMap[offset + newIndex + 1] = baseIndexMap[originalIndex + 1]; + } + + return reorderedMap; + } + + /** + * Arranges the filter coefficients for processing the polyphase filter bank using a contiguous array. + * @param coefficients + * @param channelCount + * @return + */ + private static float[] getAlignedFilter(float[] coefficients, int channelCount, int tapsPerChannel) + { + float[] filter = new float[channelCount * tapsPerChannel * 2]; + int blockSize = channelCount; + + int coefficientPointer = 0; + int filterPointer = 0; + + while(coefficientPointer < coefficients.length) + { + filter[filterPointer++] = coefficients[coefficientPointer]; + filter[filterPointer++] = coefficients[coefficientPointer++]; + } + + //Swap each of the coefficients on block size boundaries + for(int x = 0; x < filter.length; x += blockSize) + { + for(int y = 0; y < blockSize / 2; y++) + { + int index1 = x + y; + int index2 = x + (blockSize - y - 1); + float temp = filter[index2]; + filter[index2] = filter[index1]; + filter[index1] = temp; + } + } + + return filter; + } + + /** * Processes the sample buffer for each new block of sample data that is loaded and distributes the results to any * registered channel listeners. */ - private void process() + private void processOld() { + mSampleToFilterMap.clear(); + + int sampleRowCount = getChannelCount() * 2; float[] processed = new float[sampleRowCount]; @@ -205,6 +480,7 @@ public class ComplexPolyphaseChannelizerM2 extends AbstractComplexPolyphaseChann //Top of block processing if(mRowPointer >= sampleRowCount) { + mLog.debug("Top Block"); int[] sampleIndexMap = mSampleIndexMap[mColumnPointer]; for(int channel = 0; channel < getChannelCount(); channel++) @@ -214,8 +490,8 @@ public class ComplexPolyphaseChannelizerM2 extends AbstractComplexPolyphaseChann float[] filter = mCoefficients[channel]; - processed[outputIndex] = filter((sampleIndex), mCoefficients[channel], sampleIndexMap); - processed[outputIndex + 1] = filter((sampleIndex + 1), mCoefficients[channel], sampleIndexMap); + processed[outputIndex] = filter(outputIndex, sampleIndex, mCoefficients[channel], sampleIndexMap); + processed[outputIndex + 1] = filter((outputIndex + 1), (sampleIndex + 1), mCoefficients[channel], sampleIndexMap); } mRowPointer = 0; @@ -229,6 +505,7 @@ public class ComplexPolyphaseChannelizerM2 extends AbstractComplexPolyphaseChann //Mid-point processing else { + mLog.debug("Middle Block"); int[] sampleIndexMap = mSampleIndexMap[mColumnPointer]; int half = getChannelCount() / 2; @@ -244,8 +521,8 @@ public class ComplexPolyphaseChannelizerM2 extends AbstractComplexPolyphaseChann sampleIndex = 2 * channel; filterIndex = channel + half; - processed[outputIndex] = filter(sampleIndex, mCoefficients[filterIndex], sampleIndexMap); - processed[outputIndex + 1] = filter(sampleIndex + 1, mCoefficients[filterIndex], sampleIndexMap); + processed[outputIndex] = filter(outputIndex, sampleIndex, mCoefficients[filterIndex], sampleIndexMap); + processed[outputIndex + 1] = filter((outputIndex + 1), (sampleIndex + 1), mCoefficients[filterIndex], sampleIndexMap); } //The second half of the sample buffer was loaded using the previous column pointer - adjust for that now @@ -258,14 +535,118 @@ public class ComplexPolyphaseChannelizerM2 extends AbstractComplexPolyphaseChann sampleIndex = 2 * channel; filterIndex = channel - half; - processed[outputIndex] = filter(sampleIndex, mCoefficients[filterIndex], sampleIndexMap); - processed[outputIndex + 1] = filter(sampleIndex + 1, mCoefficients[filterIndex], sampleIndexMap); + processed[outputIndex] = filter(outputIndex, sampleIndex, mCoefficients[filterIndex], sampleIndexMap); + processed[outputIndex + 1] = filter((outputIndex + 1), sampleIndex + 1, mCoefficients[filterIndex], sampleIndexMap); } } + mLog.debug("Processed:" + Arrays.toString(processed)); + mIFFTProcessor.receive(processed); } + private float[] getAccumulated() + { + return mChannelAccumulator; + } + + /** + * Processes the sample buffer for each new block of sample data that is loaded and distributes the results to any + * registered channel listeners. + */ + private long process() + { + //Calculate vector product of samples and filter coefficients using an iterative loop structure that the + //JIT compiler can easily recognize. + + //Note: since JDK 8, the hotspot JIT compiler can compile this loop to use vector-optimized native processor + //Single Instruction Multiple Data (SIMD) intrinsics, potentially making this loop iteration faster using + //32-bit float SIMD intrinsics (e.g. intel SSE/2/3/4(4x), AVX(8x), or AVX2(16x) instruction sets). + for(int x = 0; x < mInlineSamples.length; x++) + { + mInlineInterimOutput[x] = mInlineSamples[x] * mInlineFilter[x]; + } + + int offset = 0; + + Arrays.fill(mChannelAccumulator, 0.0f); + + long start = System.nanoTime(); + + //Accumulate the product results + for(int tap = 0; tap < mTapsPerChannel; tap++) + { + offset = tap * getSubChannelCount(); + + for(int channel = 0; channel < getSubChannelCount(); channel++) + { + mChannelAccumulator[channel] += mInlineInterimOutput[offset + channel]; + } + } + + float[] processed = new float[getSubChannelCount()]; + + //Rearrange the accumulated results + if(mTopBlockIndicator) + { + for(int x = 0; x < getSubChannelCount(); x++) + { + processed[x] = mChannelAccumulator[mTopBlockMap[x]]; + } + } + else + { + for(int x = 0; x < getSubChannelCount(); x++) + { + processed[x] = mChannelAccumulator[mMiddleBlockMap[x]]; + } + } + + long elapsed = System.nanoTime() - start; + + //Accumulate the filtered channel results from the interim product array according to top/middle block + + //Top of block processing +// if(mTopBlockIndicator) +// { +//// mLog.debug("Top Block"); +// +// //Accumulate the product of each filter coefficient and sample into each sub-channel +// for(int subChannel = 0; subChannel < getSubChannelCount(); subChannel++) +// { +// for(int interimOutputIndex: mTopBlockAccumulatorIndexMap[subChannel]) +// { +// processed[subChannel] += mInlineInterimOutput[interimOutputIndex]; +// } +// } +// } +// //Mid-point processing +// else +// { +//// mLog.debug("Middle Block"); +// +// //Accumulate the product of each filter coefficient and sample into each sub-channel +// for(int subChannel = 0; subChannel < getSubChannelCount(); subChannel++) +// { +// for(int interimOutputIndex: mMiddleBlockAccumulatorIndexMap[subChannel]) +// { +// processed[subChannel] += mInlineInterimOutput[interimOutputIndex]; +// } +// } +// } +// + + mTopBlockIndicator = !mTopBlockIndicator; + +// mLog.debug("Processed:" + Arrays.toString(processed)); +// mIFFTProcessor.receive(processed); + mFFT.complexInverse(processed, true); + + processChannelResults(processed); + + return elapsed; + } + /** * Buffers the channel results and dispatches the channel results buffer to the channel output processors when full. * @param channelResults @@ -325,15 +706,24 @@ public class ComplexPolyphaseChannelizerM2 extends AbstractComplexPolyphaseChann * @param sampleIndexMap to translate the column index offset based on the current sample column pointer * @return filtered sample for the row */ - private float filter(int sampleRow, float[] coefficients, int[] sampleIndexMap) + private float filter(int outputIndex, int sampleRow, float[] coefficients, int[] sampleIndexMap) { + StringBuilder sb = new StringBuilder(); + sb.append("Out[" + outputIndex + "] "); float accumulator = 0.0f; for(int column = 0; column < mSamples.length; column++) { + float sample = mSamples[sampleIndexMap[column]][sampleRow]; + float coefficient = coefficients[column]; + + mSampleToFilterMap.put((int)sample, (int)coefficient); + + sb.append("S").append((int)sample).append("*F").append((int)coefficient).append(" + "); accumulator += mSamples[sampleIndexMap[column]][sampleRow] * coefficients[column]; } + mLog.debug(sb.toString()); return accumulator; } @@ -389,6 +779,20 @@ public class ComplexPolyphaseChannelizerM2 extends AbstractComplexPolyphaseChann mCoefficients[rowIndex][columnIndex] = coefficients[coefficientIndex++]; } } + + mTapsPerChannel = (int)Math.ceil((double)coefficients.length / (double)getChannelCount()); + int channelCount = getChannelCount(); + int bufferLength = getSubChannelCount() * mTapsPerChannel; + + mSamplesPerBlock = getChannelCount(); //Same as subChannelCount / 2 + mTopBlockAccumulatorIndexMap = getTopBlockAccumulatorIndexMap(channelCount, mTapsPerChannel); + mMiddleBlockAccumulatorIndexMap = getMiddleBlockAccumulatorIndexMap(channelCount, mTapsPerChannel); + mTopBlockMap = getTopBlockMap(channelCount); + mMiddleBlockMap = getMiddleBlockMap(channelCount); + mInlineFilter = getAlignedFilter(coefficients, channelCount, mTapsPerChannel); + mInlineSamples = new float[bufferLength]; + mInlineInterimOutput = new float[bufferLength]; + mChannelAccumulator = new float[getSubChannelCount()]; } /** @@ -440,4 +844,48 @@ public class ComplexPolyphaseChannelizerM2 extends AbstractComplexPolyphaseChann }); } } + + public static void main(String[] args) + { + int channelCount = 4; + int tapsPerChannel = 8; + float[] taps = new float[channelCount * tapsPerChannel]; + + int sampleRate = 50000; + + for(int x = 0; x < taps.length; x++) + { + taps[x] = x; + } + + ComplexPolyphaseChannelizerM2 channelizer = new ComplexPolyphaseChannelizerM2(taps, sampleRate, channelCount); + + int[][] top = channelizer.getTopBlockAccumulatorIndexMap(channelCount, tapsPerChannel); + int[][] middle = channelizer.getMiddleBlockAccumulatorIndexMap(channelCount, tapsPerChannel); + + StringBuilder sb = new StringBuilder(); + sb.append("Middle:\n"); + + for(int x = 0; x < middle.length; x++) + { + sb.append(x).append(": ").append(Arrays.toString(middle[x])).append("\n"); + } + + mLog.debug(sb.toString()); + + mLog.debug(Arrays.toString(getMiddleBlockMap(channelCount))); + + ReusableComplexBufferQueue queue = new ReusableComplexBufferQueue(); + + int sampleCount = channelCount * 2 * tapsPerChannel; + ReusableComplexBuffer buffer = queue.getBuffer(sampleCount); + buffer.incrementUserCount(); + + for(int x = 0; x < sampleCount; x++) + { + buffer.getSamples()[x] = x; + } + + channelizer.receive(buffer); + } } diff --git a/src/main/java/io/github/dsheirer/dsp/filter/channelizer/PolyphaseChannelManager.java b/src/main/java/io/github/dsheirer/dsp/filter/channelizer/PolyphaseChannelManager.java index 5c63dd78..7de07312 100644 --- a/src/main/java/io/github/dsheirer/dsp/filter/channelizer/PolyphaseChannelManager.java +++ b/src/main/java/io/github/dsheirer/dsp/filter/channelizer/PolyphaseChannelManager.java @@ -49,7 +49,7 @@ public class PolyphaseChannelManager implements ISourceEventProcessor private final static Logger mLog = LoggerFactory.getLogger(PolyphaseChannelManager.class); private static final double MINIMUM_CHANNEL_BANDWIDTH = 12500.0; private static final double CHANNEL_OVERSAMPLING = 2.0; - private static final int POLYPHASE_FILTER_TAPS_PER_CHANNEL = 17; + private static final int POLYPHASE_FILTER_TAPS_PER_CHANNEL = 16; private Broadcaster mSourceEventBroadcaster = new Broadcaster<>(); private IReusableBufferProvider mReusableBufferProvider; From e971ad4582b6003d709e1a745b80b38224508a58 Mon Sep 17 00:00:00 2001 From: Denny Date: Sun, 15 Apr 2018 09:05:06 -0400 Subject: [PATCH 62/87] 39 [WIP] - restructured the polyphase filter process. Previous version was not computationally feasible with my 3rd Gen i7 processor running and airspy at 10 MHz. Restructured the polyphase channelizer to use vector iteration structures that the JDK 9 JIT compiler can translate into SIMD instructions. Separated the IFFT processing from the filter process so that they can be executed on separate threads and this allows the processing to occur real time. P25 C4FM decoding at 25 kHz channel sample rate using an airspy 10MHz sample stream seems to be working nicely with ~25% CPU usage and ~300 to 500 MB memory usage. Incremental (parallel) channel decoding should have a minimal increase in CPU loading since all of the 12.5 kHz channels are available in the polyphase channelizer and the only additional workload is the decoding of the 25 kHz sample stream. There is still an issue with stopping channels and the channelizer keeps processing, and occasionally when starting traffic channels something deadlocks and causes out of memory. --- .../AbstractComplexPolyphaseChannelizer.java | 19 +- .../ComplexPolyphaseChannelizerM2.java | 652 ++++-------------- .../ContinuousBufferProcessor.java | 23 +- .../channelizer/PolyphaseChannelManager.java | 28 +- .../channelizer/PolyphaseChannelSource.java | 3 +- .../output/ChannelOutputProcessor.java | 10 +- .../IPolyphaseChannelOutputProcessor.java | 4 +- .../output/OneChannelOutputProcessor.java | 8 +- .../output/TwoChannelOutputProcessor.java | 8 +- .../sample/OverflowableTransferQueue.java | 17 + .../sample/buffer/AbstractReusableBuffer.java | 151 ++++ .../buffer/AbstractReusableBufferQueue.java | 57 +- .../IReusableBufferDisposedListener.java | 2 +- .../sample/buffer/ReusableBuffer.java | 113 +-- .../sample/buffer/ReusableBufferQueue.java | 23 +- .../buffer/ReusableChannelResultsBuffer.java | 143 ++++ .../ReusableChannelResultsBufferQueue.java | 55 ++ .../buffer/ReusableComplexBufferQueue.java | 23 +- 18 files changed, 637 insertions(+), 702 deletions(-) create mode 100644 src/main/java/io/github/dsheirer/sample/buffer/AbstractReusableBuffer.java create mode 100644 src/main/java/io/github/dsheirer/sample/buffer/ReusableChannelResultsBuffer.java create mode 100644 src/main/java/io/github/dsheirer/sample/buffer/ReusableChannelResultsBufferQueue.java diff --git a/src/main/java/io/github/dsheirer/dsp/filter/channelizer/AbstractComplexPolyphaseChannelizer.java b/src/main/java/io/github/dsheirer/dsp/filter/channelizer/AbstractComplexPolyphaseChannelizer.java index e044b2af..8a7175bf 100644 --- a/src/main/java/io/github/dsheirer/dsp/filter/channelizer/AbstractComplexPolyphaseChannelizer.java +++ b/src/main/java/io/github/dsheirer/dsp/filter/channelizer/AbstractComplexPolyphaseChannelizer.java @@ -20,6 +20,8 @@ package io.github.dsheirer.dsp.filter.channelizer; import io.github.dsheirer.sample.Broadcaster; import io.github.dsheirer.sample.Listener; +import io.github.dsheirer.sample.buffer.ReusableChannelResultsBuffer; +import io.github.dsheirer.sample.buffer.ReusableChannelResultsBufferQueue; import io.github.dsheirer.sample.buffer.ReusableComplexBuffer; import io.github.dsheirer.source.ISourceEventListener; import io.github.dsheirer.source.SourceEvent; @@ -32,7 +34,7 @@ import java.util.concurrent.CopyOnWriteArrayList; public abstract class AbstractComplexPolyphaseChannelizer implements Listener, ISourceEventListener { private final static Logger mLog = LoggerFactory.getLogger(AbstractComplexPolyphaseChannelizer.class); - + private ReusableChannelResultsBufferQueue mBufferQueue = new ReusableChannelResultsBufferQueue(); private Broadcaster mSourceChangeBroadcaster = new Broadcaster(); private List mChannels = new CopyOnWriteArrayList<>(); private double mSampleRate; @@ -102,12 +104,15 @@ public abstract class AbstractComplexPolyphaseChannelizer implements Listener getSourceEventListener() { diff --git a/src/main/java/io/github/dsheirer/dsp/filter/channelizer/ComplexPolyphaseChannelizerM2.java b/src/main/java/io/github/dsheirer/dsp/filter/channelizer/ComplexPolyphaseChannelizerM2.java index 1293e153..a6be055a 100644 --- a/src/main/java/io/github/dsheirer/dsp/filter/channelizer/ComplexPolyphaseChannelizerM2.java +++ b/src/main/java/io/github/dsheirer/dsp/filter/channelizer/ComplexPolyphaseChannelizerM2.java @@ -22,6 +22,7 @@ import io.github.dsheirer.dsp.filter.FilterFactory; import io.github.dsheirer.dsp.filter.Window.WindowType; import io.github.dsheirer.dsp.filter.design.FilterDesignException; import io.github.dsheirer.sample.Listener; +import io.github.dsheirer.sample.buffer.ReusableChannelResultsBuffer; import io.github.dsheirer.sample.buffer.ReusableComplexBuffer; import io.github.dsheirer.sample.buffer.ReusableComplexBufferQueue; import io.github.dsheirer.sample.real.IOverflowListener; @@ -30,8 +31,7 @@ import org.slf4j.Logger; import org.slf4j.LoggerFactory; import java.util.Arrays; -import java.util.HashMap; -import java.util.Map; +import java.util.List; public class ComplexPolyphaseChannelizerM2 extends AbstractComplexPolyphaseChannelizer { @@ -39,46 +39,48 @@ public class ComplexPolyphaseChannelizerM2 extends AbstractComplexPolyphaseChann private static final int DEFAULT_MINIMUM_CHANNEL_BANDWIDTH = 12500; - private float[][] mSamples; - private float[][] mCoefficients; - private int[][] mSampleIndexMap; - private int mColumnPointer; - private int mRowPointer; - private int mBlockSize; - private int mSamplesPerBlock; + //Sized at 152 buffers a second where max = 5 seconds and reset = 2 seconds worth of buffers + private IFFTProcessor mIFFTProcessor = new IFFTProcessor((5 * 152), (2 * 152)); private FloatFFT_1D mFFT; - private PolyphaseChannelResultsBuffer mChannelResultsBuffer; - private int mChannelResultsBufferSize = 2500; - private SampleTimestampManager mTimestampManager; - private IFFTProcessor mIFFTProcessor = new IFFTProcessor(300, 50); - - private Map mSampleToFilterMap = new HashMap<>(); - - private int[][] mTopBlockAccumulatorIndexMap; - private int[][] mMiddleBlockAccumulatorIndexMap; - private int[] mTopBlockMap; - private int[] mMiddleBlockMap; private float[] mInlineSamples; private float[] mInlineFilter; private float[] mInlineInterimOutput; - private float[] mChannelAccumulator; - private int mSampleBufferPointer; - private int mTapsPerChannel; + private float[] mFilterAccumulator; private boolean mTopBlockIndicator = true; - + private int[] mTopBlockMap; + private int[] mMiddleBlockMap; + private int mSampleBufferPointer; + private int mSamplesPerBlock; + private int mTapsPerChannel; /** * Non-Maximally Decimated Polyphase Filter Bank (NMDPFB) channelizer that divides the input frequency band into * equal bandwidth channels that are each oversampled by 2x for output. * + * This polyphase channelizer is based off of the channelizer described by Fred Harris in Multirate Signal + * Processing for Communications Systems, p230-233. + * + * Samples are loaded into this filter one block at a time (1/2 channel count) and a filtered output is calculated + * to produce an overall 2x oversampled channel sample rate. Each sample block load is preceded by a serpentine + * shift of the existing sample blocks. We use the java System.arrayCopy() method which is able to leverage + * native processor intrinsics for efficiency. + * + * The prototype filter for the channelizer is rearranged to align with the structure of the sample buffer. + * + * Instead of using an array of channel filters as described in the Harris text, this filter and the sample buffer + * are arranged as a contiguous array to maximize Java's ability to leverage native processor Single Instruction + * Multiple Data (SIMD) intrinsics (since Java 8). The filter process is broken into three steps: + * -Multiply the inline array of samples and filter coefficients + * -Accumulate the results for each sub-channel + * -Rearrange the sub-channel results to correctly order the sub-channels + * * @param taps of a low-pass filter designed for the inbound sample rate with a cutoff frequency * equal to the channel bandwidth (sample rate / filters). If you need to synthesize (combine two or more - * filter outputs) a new bandwidth signal from the outputs of this filter, then the filter should be designed + * channel outputs) a new bandwidth signal from the outputs of this filter, then the filter should be designed * as a nyquist filter with -6 dB attenuation at the channel bandwidth cutoff frequency - * * @param sampleRate of the incoming sample stream - * @param channelCount - number of filters/channels to output. Since this filter bank oversamples each filter - * output, this number must be even (divisible by the 2x oversample rate). + * @param channelCount - number of filters/channels to output. Since this filter bank performs 2x oversampling for + * each channel output, this number must be even (divisible by 2). */ public ComplexPolyphaseChannelizerM2(float[] taps, int sampleRate, int channelCount) { @@ -89,8 +91,7 @@ public class ComplexPolyphaseChannelizerM2 extends AbstractComplexPolyphaseChann throw new IllegalArgumentException("Channel count must be an even multiple of the over-sample rate (2x)"); } - double oversampledChannelSampleRate = getChannelSampleRate() * 2.0; - mTimestampManager = new SampleTimestampManager(oversampledChannelSampleRate); + mTapsPerChannel = (int)Math.ceil((double)taps.length / (double)channelCount); init(taps); } @@ -99,22 +100,34 @@ public class ComplexPolyphaseChannelizerM2 extends AbstractComplexPolyphaseChann * Non-Maximally Decimated Polyphase Filter Bank (NMDPFB) channelizer that divides the input frequency band into * equal bandwidth channels that are each oversampled by 2x for output. * - * Note: this constructor uses a default minimum channel bandwidth of 12.5 kHz. The number of channels is calculate - * from the sample rate and this default minimum channel bandwidth. The resulting per-channel bandwidth will be - * between 12.5 and 25.0 kHz with the output per-channel sample rate of 2x the bandwidth. + * This polyphase channelizer is based off of the channelizer described by Fred Harris in Multirate Signal + * Processing for Communications Systems, p230-233. + * + * Samples are loaded into this filter one block at a time (1/2 channel count) and a filtered output is calculated + * to produce an overall 2x oversampled channel sample rate. Each sample block load is preceded by a serpentine + * shift of the existing sample blocks. We use the java System.arrayCopy() method which is able to leverage + * native processor intrinsics for efficiency. + * + * The prototype filter for the channelizer is rearranged to align with the structure of the sample buffer. + * + * Instead of using an array of channel filters as described in the Harris text, this filter and the sample buffer + * are arranged as a contiguous array to maximize Java's ability to leverage native processor Single Instruction + * Multiple Data (SIMD) intrinsics (since Java 8). The filter process is broken into three steps: + * -Multiply the inline array of samples and filter coefficients + * -Accumulate the results for each sub-channel + * -Rearrange the sub-channel results to correctly order the sub-channels * * @param sampleRate to be channelized. - * @param tapsPerFilter to use when designing the filter + * @param tapsPerChannel to use when designing the filter */ - public ComplexPolyphaseChannelizerM2(double sampleRate, int tapsPerFilter) throws FilterDesignException + public ComplexPolyphaseChannelizerM2(double sampleRate, int tapsPerChannel) throws FilterDesignException { super(sampleRate, getChannelCount(sampleRate)); - float[] filterTaps = FilterFactory.getSincM2Channelizer(getChannelSampleRate(), getChannelCount(), - tapsPerFilter, WindowType.BLACKMAN_HARRIS_7, true); + mTapsPerChannel = tapsPerChannel; - double oversampledChannelSampleRate = getChannelSampleRate() * 2.0; - mTimestampManager = new SampleTimestampManager(oversampledChannelSampleRate); + float[] filterTaps = FilterFactory.getSincM2Channelizer(getChannelSampleRate(), getChannelCount(), + mTapsPerChannel, WindowType.BLACKMAN_HARRIS_7, true); init(filterTaps); } @@ -161,7 +174,7 @@ public class ComplexPolyphaseChannelizerM2 extends AbstractComplexPolyphaseChann super.setSampleRate(sampleRate); float[] filterTaps = FilterFactory.getSincM2Channelizer(getChannelSampleRate(), getChannelCount(), - 15, WindowType.BLACKMAN_HARRIS_7, true); + mTapsPerChannel, WindowType.BLACKMAN_HARRIS_7, true); init(filterTaps); } @@ -172,46 +185,6 @@ public class ComplexPolyphaseChannelizerM2 extends AbstractComplexPolyphaseChann } } - /** - * Receives the complex sample buffer and processes the results through the channelizer. - */ - public void receiveOld(ReusableComplexBuffer reusableComplexBuffer) - { - long start = System.nanoTime(); - - //Use the buffer's reference timestamp to update our timestamp manager (for timestamping output buffers) - mTimestampManager.setReferenceTimestamp(reusableComplexBuffer.getTimestamp()); - - float[] samples = reusableComplexBuffer.getSamples(); - - int samplesPointer = 0; - - while(samplesPointer < samples.length) - { - int lengthToCopy = mBlockSize - (mRowPointer % mBlockSize); - - if((samplesPointer + lengthToCopy) > samples.length) - { - lengthToCopy = samples.length - samplesPointer; - } - - System.arraycopy(samples, samplesPointer, mSamples[mColumnPointer], mRowPointer, lengthToCopy); - samplesPointer += lengthToCopy; - mRowPointer += lengthToCopy; - - if(mRowPointer % mBlockSize == 0) - { - //if row pointer at max, then mRowPointer = 0 and mColumnPointer-- - processOld(); - } - } - - //Decrement the user count to let the originator know we're done with their buffer - reusableComplexBuffer.decrementUserCount(); - - mLog.debug("Duration: " + (System.nanoTime() - start)); - } - /** * Receives the complex sample buffer and processes the results through the channelizer. */ @@ -221,8 +194,8 @@ public class ComplexPolyphaseChannelizerM2 extends AbstractComplexPolyphaseChann long start = System.nanoTime(); long processing = 0; - //Use the buffer's reference timestamp to update our timestamp manager (for timestamping output buffers) - mTimestampManager.setReferenceTimestamp(reusableComplexBuffer.getTimestamp()); + ReusableChannelResultsBuffer channelResultsBuffer = getChannelResultsBuffer(); + channelResultsBuffer.setTimestamp(reusableComplexBuffer.getTimestamp()); float[] samples = reusableComplexBuffer.getSamples(); @@ -249,7 +222,7 @@ public class ComplexPolyphaseChannelizerM2 extends AbstractComplexPolyphaseChann if(mSampleBufferPointer >= mSamplesPerBlock) { //Filter buffered samples and produce a single sample across each of the polyphase channels - processing += process(); + processing += process(channelResultsBuffer); //Right-shift the samples in the buffer over to make room for a new block of samples //Note: since JDK 8, hotspot JIT compiler uses native processor intrinsics for efficiency @@ -258,78 +231,14 @@ public class ComplexPolyphaseChannelizerM2 extends AbstractComplexPolyphaseChann } } - mLog.debug("Duration: " + (System.nanoTime() - start) + " Processing:" + processing); + channelResultsBuffer.incrementUserCount(); + + //Enqueue the channel results buffer for IFFT processing and distribution on a different thread + mIFFTProcessor.receive(channelResultsBuffer); + +// mLog.debug("Duration: " + (System.nanoTime() - start) + " Processing:" + processing); //Decrement the user count to let the originator know we're done with their buffer reusableComplexBuffer.decrementUserCount(); - - } - - /** - * Creates a base accumulator index map that is preloaded with the indexes that have to be summed to produce - * each subchannel filtered value. This index map structure is designed for processing of polyphase filter - * samples in a contiguous/single sample array where the filter taps are also arranged to align with this - * inline sample array buffer. This method preloads the index values, but the first dimension of the array - * must be rearranged to align with either top-block or middle-block processing so that each of the - * polyphase filter arm outputs is assigned to each sub-channel (I/Q) correctly. - * - * @param channelCount - number of channels - * @param tapsPerChannel - number of filter taps per channel - * @return output index to filter accumulator index mapping - */ - private static int[][] getBaseAccumulatorIndexMap(int channelCount, int tapsPerChannel) - { - int subChannelCount = channelCount * 2; - - int[][] accumulatorIndexMap = new int[subChannelCount][tapsPerChannel]; - - int pointer = 0; - - //Step 1: load the interim product output indexes in sequential order. - for(int column = 0; column < tapsPerChannel; column++) - { - for(int subChannel = 0; subChannel < subChannelCount; subChannel++) - { - accumulatorIndexMap[subChannel][column] = pointer++; - } - } - - return accumulatorIndexMap; - } - - /** - * Creates a top-block processing accumulator map that maps each interim filter and sample index product - * to the corresponding final output index for the array that will feed the IFFT. - * - * @param channelCount - number of channels - * @param tapsPerChannel - number of filter taps per channel - * @return output index to filter accumulator index mapping - */ - private static int[][] getTopBlockAccumulatorIndexMap(int channelCount, int tapsPerChannel) - { - int[][] baseIndexMap = getBaseAccumulatorIndexMap(channelCount, tapsPerChannel); - - //SubChannel count is the number if I/Q channels (ie channels x 2) - int subChannelCount = channelCount * 2; - - //Reorder the subchannel arrays to the structure needed for top-block processing - int blockSize = channelCount / 2; - - int[][] reorderedMap = new int[subChannelCount][tapsPerChannel]; - - for(int channel = 0; channel < blockSize; channel++) - { - int newIndex = 2 * channel; - int originalIndex = 2 * (blockSize - channel - 1); - int offset = 2 * blockSize; - - reorderedMap[newIndex] = baseIndexMap[originalIndex]; - reorderedMap[newIndex + 1] = baseIndexMap[originalIndex + 1]; - - reorderedMap[offset + newIndex] = baseIndexMap[offset + originalIndex]; - reorderedMap[offset + newIndex + 1] = baseIndexMap[offset + originalIndex + 1]; - } - - return reorderedMap; } /** @@ -389,48 +298,12 @@ public class ComplexPolyphaseChannelizerM2 extends AbstractComplexPolyphaseChann return newMap; } - /** - * Creates a middle-block processing accumulator map that maps each interim filter and sample index product - * to the corresponding final output index for the array that will feed the IFFT. - * - * @param channelCount - number of channels - * @param tapsPerChannel - number of filter taps per channel - * @return output index to filter accumulator index mapping - */ - private static int[][] getMiddleBlockAccumulatorIndexMap(int channelCount, int tapsPerChannel) - { - int[][] baseIndexMap = getBaseAccumulatorIndexMap(channelCount, tapsPerChannel); - - //SubChannel count is the number if I/Q channels (ie channels x 2) - int subChannelCount = channelCount * 2; - - //Reorder the subchannel arrays to the structure needed for middle-block processing, where the outputs - //from the top and bottom blocks are inverted when compared to top-block processing - int blockSize = channelCount / 2; - - int[][] reorderedMap = new int[subChannelCount][tapsPerChannel]; - - for(int channel = 0; channel < blockSize; channel++) - { - int newIndex = 2 * channel; - int originalIndex = 2 * (blockSize - channel - 1); - int offset = 2 * blockSize; - - reorderedMap[newIndex] = baseIndexMap[offset + originalIndex]; - reorderedMap[newIndex + 1] = baseIndexMap[offset + originalIndex + 1]; - - reorderedMap[offset + newIndex] = baseIndexMap[originalIndex]; - reorderedMap[offset + newIndex + 1] = baseIndexMap[originalIndex + 1]; - } - - return reorderedMap; - } /** - * Arranges the filter coefficients for processing the polyphase filter bank using a contiguous array. - * @param coefficients - * @param channelCount - * @return + * Rearranges the filter coefficients to align with a contiguous sample buffer for processing efficiency. + * @param coefficients of the polyphase filter + * @param channelCount number of channels where each channel is an I/Q pair + * @return filter rearranged for inline sample buffer processing */ private static float[] getAlignedFilter(float[] coefficients, int channelCount, int tapsPerChannel) { @@ -467,370 +340,158 @@ public class ComplexPolyphaseChannelizerM2 extends AbstractComplexPolyphaseChann * Processes the sample buffer for each new block of sample data that is loaded and distributes the results to any * registered channel listeners. */ - private void processOld() + private long process(ReusableChannelResultsBuffer channelResultsBuffer) { - mSampleToFilterMap.clear(); - - - int sampleRowCount = getChannelCount() * 2; - - float[] processed = new float[sampleRowCount]; - - - //Top of block processing - if(mRowPointer >= sampleRowCount) - { - mLog.debug("Top Block"); - int[] sampleIndexMap = mSampleIndexMap[mColumnPointer]; - - for(int channel = 0; channel < getChannelCount(); channel++) - { - int outputIndex = 2 * (getChannelCount() - channel - 1); - int sampleIndex = 2 * channel; - - float[] filter = mCoefficients[channel]; - - processed[outputIndex] = filter(outputIndex, sampleIndex, mCoefficients[channel], sampleIndexMap); - processed[outputIndex + 1] = filter((outputIndex + 1), (sampleIndex + 1), mCoefficients[channel], sampleIndexMap); - } - - mRowPointer = 0; - mColumnPointer--; - - if(mColumnPointer < 0) - { - mColumnPointer = mSamples.length - 1; - } - } - //Mid-point processing - else - { - mLog.debug("Middle Block"); - int[] sampleIndexMap = mSampleIndexMap[mColumnPointer]; - - int half = getChannelCount() / 2; - - int outputOffset = getChannelCount() - 1; - int outputIndex; - int sampleIndex; - int filterIndex; - - for(int channel = 0; channel < half; channel++) - { - outputIndex = 2 * (outputOffset - channel); - sampleIndex = 2 * channel; - filterIndex = channel + half; - - processed[outputIndex] = filter(outputIndex, sampleIndex, mCoefficients[filterIndex], sampleIndexMap); - processed[outputIndex + 1] = filter((outputIndex + 1), (sampleIndex + 1), mCoefficients[filterIndex], sampleIndexMap); - } - - //The second half of the sample buffer was loaded using the previous column pointer - adjust for that now - sampleIndexMap = mSampleIndexMap[(mColumnPointer == (mSamples.length - 1) ? 0 : mColumnPointer + 1)]; - outputOffset = getChannelCount() - 1; - - for(int channel = half; channel < getChannelCount(); channel++) - { - outputIndex = 2 * (outputOffset - channel); - sampleIndex = 2 * channel; - filterIndex = channel - half; - - processed[outputIndex] = filter(outputIndex, sampleIndex, mCoefficients[filterIndex], sampleIndexMap); - processed[outputIndex + 1] = filter((outputIndex + 1), sampleIndex + 1, mCoefficients[filterIndex], sampleIndexMap); - } - } - - mLog.debug("Processed:" + Arrays.toString(processed)); - - mIFFTProcessor.receive(processed); - } - - private float[] getAccumulated() - { - return mChannelAccumulator; - } - - /** - * Processes the sample buffer for each new block of sample data that is loaded and distributes the results to any - * registered channel listeners. - */ - private long process() - { - //Calculate vector product of samples and filter coefficients using an iterative loop structure that the - //JIT compiler can easily recognize. - - //Note: since JDK 8, the hotspot JIT compiler can compile this loop to use vector-optimized native processor - //Single Instruction Multiple Data (SIMD) intrinsics, potentially making this loop iteration faster using - //32-bit float SIMD intrinsics (e.g. intel SSE/2/3/4(4x), AVX(8x), or AVX2(16x) instruction sets). + //Multiply each of the samples by the corresponding filter tap for(int x = 0; x < mInlineSamples.length; x++) { mInlineInterimOutput[x] = mInlineSamples[x] * mInlineFilter[x]; } - int offset = 0; - - Arrays.fill(mChannelAccumulator, 0.0f); - long start = System.nanoTime(); - //Accumulate the product results + Arrays.fill(mFilterAccumulator, 0.0f); + + int tapOffset = 0; + + //Accumulate the sample/filter product results into each of the I/Q sub-channels for(int tap = 0; tap < mTapsPerChannel; tap++) { - offset = tap * getSubChannelCount(); + tapOffset = tap * getSubChannelCount(); for(int channel = 0; channel < getSubChannelCount(); channel++) { - mChannelAccumulator[channel] += mInlineInterimOutput[offset + channel]; + mFilterAccumulator[channel] += mInlineInterimOutput[tapOffset + channel]; } } - float[] processed = new float[getSubChannelCount()]; + float[] processed = channelResultsBuffer.getEmptyBuffer(getSubChannelCount()); - //Rearrange the accumulated results if(mTopBlockIndicator) { for(int x = 0; x < getSubChannelCount(); x++) { - processed[x] = mChannelAccumulator[mTopBlockMap[x]]; + processed[x] = mFilterAccumulator[mTopBlockMap[x]]; } } else { for(int x = 0; x < getSubChannelCount(); x++) { - processed[x] = mChannelAccumulator[mMiddleBlockMap[x]]; + processed[x] = mFilterAccumulator[mMiddleBlockMap[x]]; } } - long elapsed = System.nanoTime() - start; - - //Accumulate the filtered channel results from the interim product array according to top/middle block - - //Top of block processing -// if(mTopBlockIndicator) -// { -//// mLog.debug("Top Block"); -// -// //Accumulate the product of each filter coefficient and sample into each sub-channel -// for(int subChannel = 0; subChannel < getSubChannelCount(); subChannel++) -// { -// for(int interimOutputIndex: mTopBlockAccumulatorIndexMap[subChannel]) -// { -// processed[subChannel] += mInlineInterimOutput[interimOutputIndex]; -// } -// } -// } -// //Mid-point processing -// else -// { -//// mLog.debug("Middle Block"); -// -// //Accumulate the product of each filter coefficient and sample into each sub-channel -// for(int subChannel = 0; subChannel < getSubChannelCount(); subChannel++) -// { -// for(int interimOutputIndex: mMiddleBlockAccumulatorIndexMap[subChannel]) -// { -// processed[subChannel] += mInlineInterimOutput[interimOutputIndex]; -// } -// } -// } -// + channelResultsBuffer.addChannelResults(processed); mTopBlockIndicator = !mTopBlockIndicator; -// mLog.debug("Processed:" + Arrays.toString(processed)); -// mIFFTProcessor.receive(processed); - mFFT.complexInverse(processed, true); - - processChannelResults(processed); + long elapsed = System.nanoTime() - start; return elapsed; } - /** - * Buffers the channel results and dispatches the channel results buffer to the channel output processors when full. - * @param channelResults - */ - private void processChannelResults(float[] channelResults) - { - if(mChannelResultsBuffer == null) - { - mChannelResultsBuffer = new PolyphaseChannelResultsBuffer(mTimestampManager.getCurrentTimestamp(), - mChannelResultsBufferSize); - } +// /** +// * Buffers the channel results and dispatches the channel results buffer to the channel output processors when full. +// * @param buffer containing filtered and IFFT'd channelResults with a corresponding timestamp +// */ +// private void processChannelResults(ReusableChannelResultsBuffer buffer) +// { +// if(mChannelResultsBuffer == null) +// { +// mChannelResultsBuffer = new PolyphaseChannelResultsBuffer(buffer.getTimestamp(), +// mChannelResultsBufferSize); +// } +// +// try +// { +// mChannelResultsBuffer.add(buffer.getSamplesCopy()); +// } +// catch(IllegalArgumentException iae) +// { +// //If the buffer is full (unlikely) or the channel results array length has changed (possible), flush the +// //current buffer, create a new one, and store the current results +// flushChannelResultsBuffer(); +// +// mChannelResultsBuffer = new PolyphaseChannelResultsBuffer(buffer.getTimestamp(), mChannelResultsBufferSize); +// +// mChannelResultsBuffer.add(buffer.getSamplesCopy()); +// } +// +// if(mChannelResultsBuffer.isFull()) +// { +// flushChannelResultsBuffer(); +// } +// +// buffer.decrementUserCount(); +// } - try - { - mChannelResultsBuffer.add(channelResults); - } - catch(IllegalArgumentException iae) - { - //If the buffer is full (unlikely) or the channel results array length has changed (possible), flush the - //current buffer, create a new one, and store the current results - flushChannelResultsBuffer(); - - mChannelResultsBuffer = new PolyphaseChannelResultsBuffer(mTimestampManager.getCurrentTimestamp(), - mChannelResultsBufferSize); - - mChannelResultsBuffer.add(channelResults); - } - - if(mChannelResultsBuffer.isFull()) - { - flushChannelResultsBuffer(); - } - - //Each channel results array is equivalent to one sample from our timestamp manager's perspective - mTimestampManager.increment(); - } +// /** +// * Dispatches the non-empty channel results buffer to the channel output processors and nullifies the reference to +// * the buffer so that a new buffer can be created upon receiving the next channel results. +// */ +// private void flushChannelResultsBuffer() +// { +// if(mChannelResultsBuffer != null && !mChannelResultsBuffer.isEmpty()) +// { +// dispatch(mChannelResultsBuffer); +// } +// +// mChannelResultsBuffer = null; +// } /** - * Dispatches the non-empty channel results buffer to the channel output processors and nullifies the reference to - * the buffer so that a new buffer can be created upon receiving the next channel results. - */ - private void flushChannelResultsBuffer() - { - if(mChannelResultsBuffer != null && !mChannelResultsBuffer.isEmpty()) - { - dispatch(mChannelResultsBuffer); - } - - mChannelResultsBuffer = null; - } - - /** - * Calculates the filtered output for polyphase filter arm. - * - * @param sampleRow index pointing to a row in the mSamples array to filter - * @param coefficients to use in filtering the samples - * @param sampleIndexMap to translate the column index offset based on the current sample column pointer - * @return filtered sample for the row - */ - private float filter(int outputIndex, int sampleRow, float[] coefficients, int[] sampleIndexMap) - { - StringBuilder sb = new StringBuilder(); - sb.append("Out[" + outputIndex + "] "); - float accumulator = 0.0f; - - for(int column = 0; column < mSamples.length; column++) - { - float sample = mSamples[sampleIndexMap[column]][sampleRow]; - float coefficient = coefficients[column]; - - mSampleToFilterMap.put((int)sample, (int)coefficient); - - sb.append("S").append((int)sample).append("*F").append((int)coefficient).append(" + "); - accumulator += mSamples[sampleIndexMap[column]][sampleRow] * coefficients[column]; - } - - mLog.debug(sb.toString()); - return accumulator; - } - - - /** - * Initializes the channelizer structures. Distributes the prototype filter coefficients to each of the polyphase - * filters, sets up the sample buffer array and pointers and initializes the FFT. - * - * These structures resemble those described by Fred Harris in Multirate Signal Processing for Communications - * Systems, p230-233. However, instead of swapping the input sample buffers and swapping the output sample array - * on every other processing block, we simply swap the filter coefficient banks and leave the input sample buffer - * and output array indexes aligned. However, the entire structure (sample buffer, coefficients and output array) - * is inverted (row zero index is M-1 and M-1 is zero index) to facilitate easy loading of data into the input - * sample buffer array using Java's efficient System.arrayCopy() utility. - * - * Note: the filter coefficients array is organized (row,column) for ease of accessing each filter arm row during - * convolution. The samples array is organized (column,row) for ease/efficiency in loading each row of data. Also, - * the input sample array implements a circular buffer where the mCoefficientsMap provides a lookup to each sample - * index based on the current value of the mColumnPointer. + * Initializes the channelizer filter structures. * * @param coefficients of the prototype filter for this channelizer */ private void init(float[] coefficients) { - int columnCount = (int)Math.ceil((double)coefficients.length / (double)getChannelCount()); - int filterRowCount = getChannelCount(); - int sampleRowCount = getChannelCount() * 2; - - //Sample and filter coefficient arrays are specifically setup as column/row and row/column - mSamples = new float[columnCount][sampleRowCount]; - mCoefficients = new float[filterRowCount][columnCount]; - - mColumnPointer = columnCount - 1; - mBlockSize = getChannelCount(); - mRowPointer = mBlockSize; - //Setup the FFT mFFT = new FloatFFT_1D(getChannelCount()); - //Setup the polyphase filters - - //Setup the index map for the sample array circular buffer - initIndexMap(columnCount); - - int coefficientIndex = 0; - - //Load filter columns left-2-right (samples are loaded in reverse column order, right-2-left, for easy convolution) - for(int columnIndex = 0; columnIndex < columnCount; columnIndex++) - { - //Load filter rows bottom to top, opposite of sample row loading and sample output ordering - for(int rowIndex = filterRowCount - 1; rowIndex >= 0; rowIndex--) - { - mCoefficients[rowIndex][columnIndex] = coefficients[coefficientIndex++]; - } - } - - mTapsPerChannel = (int)Math.ceil((double)coefficients.length / (double)getChannelCount()); int channelCount = getChannelCount(); int bufferLength = getSubChannelCount() * mTapsPerChannel; mSamplesPerBlock = getChannelCount(); //Same as subChannelCount / 2 - mTopBlockAccumulatorIndexMap = getTopBlockAccumulatorIndexMap(channelCount, mTapsPerChannel); - mMiddleBlockAccumulatorIndexMap = getMiddleBlockAccumulatorIndexMap(channelCount, mTapsPerChannel); mTopBlockMap = getTopBlockMap(channelCount); mMiddleBlockMap = getMiddleBlockMap(channelCount); mInlineFilter = getAlignedFilter(coefficients, channelCount, mTapsPerChannel); mInlineSamples = new float[bufferLength]; mInlineInterimOutput = new float[bufferLength]; - mChannelAccumulator = new float[getSubChannelCount()]; + mFilterAccumulator = new float[getSubChannelCount()]; } /** - * Generates a circular buffer index map to support lookup of the translated index based on the current column - * pointer and the desired sample index. + * Separate threaded processor to receive and enqueue filtered channel results buffers, perform IFFT on each array + * as required to align the phase of each polyphase channel, and then dispatch the results to any registered + * sample consumer channels. */ - private void initIndexMap(int size) - { - mSampleIndexMap = new int[size][size]; - - for(int row = 0; row < size; row++) - { - for(int column = 0;column < size;column++) - { - int offset = row + column; - - mSampleIndexMap[row][column] = ((offset < size) ? offset : offset - size); - } - } - } - - /** - * Processor to enqueue filtered channels, perform IFFT and dispatch the results - */ - public class IFFTProcessor extends ContinuousBufferProcessor + public class IFFTProcessor extends ContinuousBufferProcessor { public IFFTProcessor(int maximumSize, int resetThreshold) { super(maximumSize, resetThreshold); - setListener(new Listener() + //We create a listener interface to receive the buffers from the scheduled thread pool + //dispatcher thread that is part of this continuous buffer processor. We perform an IFFT on each + //channel results array contained in each results buffer and then dispatch the buffer + //so that it can be distributed to each channel listener. + setListener(new Listener>() { @Override - public void receive(float[] floatsToProcess) + public void receive(List buffers) { - //Rotate each of the channels to the correct phase using the IFFT - mFFT.complexInverse(floatsToProcess, true); - processChannelResults(floatsToProcess); + for(ReusableChannelResultsBuffer buffer: buffers) + { + for(float[] channelResults: buffer.getChannelResults()) + { + //Rotate each of the channels to the correct phase using the IFFT + mFFT.complexInverse(channelResults, true); + } + + dispatch(buffer); + } } }); @@ -860,21 +521,6 @@ public class ComplexPolyphaseChannelizerM2 extends AbstractComplexPolyphaseChann ComplexPolyphaseChannelizerM2 channelizer = new ComplexPolyphaseChannelizerM2(taps, sampleRate, channelCount); - int[][] top = channelizer.getTopBlockAccumulatorIndexMap(channelCount, tapsPerChannel); - int[][] middle = channelizer.getMiddleBlockAccumulatorIndexMap(channelCount, tapsPerChannel); - - StringBuilder sb = new StringBuilder(); - sb.append("Middle:\n"); - - for(int x = 0; x < middle.length; x++) - { - sb.append(x).append(": ").append(Arrays.toString(middle[x])).append("\n"); - } - - mLog.debug(sb.toString()); - - mLog.debug(Arrays.toString(getMiddleBlockMap(channelCount))); - ReusableComplexBufferQueue queue = new ReusableComplexBufferQueue(); int sampleCount = channelCount * 2 * tapsPerChannel; diff --git a/src/main/java/io/github/dsheirer/dsp/filter/channelizer/ContinuousBufferProcessor.java b/src/main/java/io/github/dsheirer/dsp/filter/channelizer/ContinuousBufferProcessor.java index 8c6ec27c..9c3a1791 100644 --- a/src/main/java/io/github/dsheirer/dsp/filter/channelizer/ContinuousBufferProcessor.java +++ b/src/main/java/io/github/dsheirer/dsp/filter/channelizer/ContinuousBufferProcessor.java @@ -33,7 +33,7 @@ public class ContinuousBufferProcessor implements Listener private final static Logger mLog = LoggerFactory.getLogger(ContinuousBufferProcessor.class); private OverflowableTransferQueue mQueue; - private Listener mListener; + private Listener> mListener; private ScheduledFuture mScheduledFuture; private AtomicBoolean mRunning = new AtomicBoolean(); @@ -89,7 +89,7 @@ public class ContinuousBufferProcessor implements Listener * Sets or changes the listener to receive buffers from this processor. * @param listener to receive buffers */ - public void setListener(Listener listener) + public void setListener(Listener> listener) { mListener = listener; } @@ -104,7 +104,6 @@ public class ContinuousBufferProcessor implements Listener @Override public void receive(E e) { -// mLog.debug(" Queuing: " + e.toString()); mQueue.offer(e); } @@ -157,28 +156,18 @@ public class ContinuousBufferProcessor implements Listener */ class Processor implements Runnable { - private List mBuffers = new ArrayList<>(); - @Override public void run() { + List buffers = new ArrayList<>(); + try { - mQueue.drainTo(mBuffers, 10); + mQueue.drainTo(buffers); if(mListener != null) { - while(!mBuffers.isEmpty()) - { - for(E buffer: mBuffers) - { -// mLog.debug("Dispatching: " + buffer.toString() + " Count:" + mBuffers.size()); - mListener.receive(buffer); - } - - mBuffers.clear(); - mQueue.drainTo(mBuffers, 10); - } + mListener.receive(buffers); } } catch(Throwable throwable) diff --git a/src/main/java/io/github/dsheirer/dsp/filter/channelizer/PolyphaseChannelManager.java b/src/main/java/io/github/dsheirer/dsp/filter/channelizer/PolyphaseChannelManager.java index 7de07312..dd11711f 100644 --- a/src/main/java/io/github/dsheirer/dsp/filter/channelizer/PolyphaseChannelManager.java +++ b/src/main/java/io/github/dsheirer/dsp/filter/channelizer/PolyphaseChannelManager.java @@ -546,7 +546,7 @@ public class PolyphaseChannelManager implements ISourceEventProcessor * that they can be processed on the buffer processor calling thread, avoiding unnecessary locks on the channelizer * and/or the channel sources and output processors. */ - public class BufferSourceEventMonitor implements Listener + public class BufferSourceEventMonitor implements Listener> { private Queue mQueuedSourceEvents = new ConcurrentLinkedQueue<>(); @@ -561,11 +561,11 @@ public class PolyphaseChannelManager implements ISourceEventProcessor } @Override - public void receive(ReusableComplexBuffer reusableComplexBuffer) + public void receive(List reusableComplexBuffers) { try { - //Process any queued source events before processing the buffer + //Process any queued source events before processing the buffers SourceEvent queuedSourceEvent = mQueuedSourceEvents.poll(); while(queuedSourceEvent != null) @@ -584,20 +584,26 @@ public class PolyphaseChannelManager implements ISourceEventProcessor queuedSourceEvent = mQueuedSourceEvents.poll(); } - if(mPolyphaseChannelizer != null) + for(ReusableComplexBuffer reusableComplexBuffer: reusableComplexBuffers) { - //User count management is handled by the channelizer - mPolyphaseChannelizer.receive(reusableComplexBuffer); - } - else - { - reusableComplexBuffer.decrementUserCount(); + if(mPolyphaseChannelizer != null) + { + //User count management is handled by the channelizer + mPolyphaseChannelizer.receive(reusableComplexBuffer); + } + else + { + reusableComplexBuffer.decrementUserCount(); + } } } catch(Throwable throwable) { mLog.error("Error", throwable); - reusableComplexBuffer.decrementUserCount(); + for(ReusableComplexBuffer buffer: reusableComplexBuffers) + { + buffer.decrementUserCount(); + } } } } diff --git a/src/main/java/io/github/dsheirer/dsp/filter/channelizer/PolyphaseChannelSource.java b/src/main/java/io/github/dsheirer/dsp/filter/channelizer/PolyphaseChannelSource.java index b6a6a99f..29a04521 100644 --- a/src/main/java/io/github/dsheirer/dsp/filter/channelizer/PolyphaseChannelSource.java +++ b/src/main/java/io/github/dsheirer/dsp/filter/channelizer/PolyphaseChannelSource.java @@ -18,6 +18,7 @@ package io.github.dsheirer.dsp.filter.channelizer; import io.github.dsheirer.dsp.filter.channelizer.output.IPolyphaseChannelOutputProcessor; import io.github.dsheirer.sample.Listener; import io.github.dsheirer.sample.buffer.ReusableBufferAssembler; +import io.github.dsheirer.sample.buffer.ReusableChannelResultsBuffer; import io.github.dsheirer.sample.buffer.ReusableComplexBuffer; import io.github.dsheirer.source.SourceEvent; import io.github.dsheirer.source.tuner.channel.TunerChannel; @@ -127,7 +128,7 @@ public class PolyphaseChannelSource extends TunerChannelSource * * @param channelResultsBuffer containing an array of polyphase channelizer outputs. */ - public void receiveChannelResults(PolyphaseChannelResultsBuffer channelResultsBuffer) + public void receiveChannelResults(ReusableChannelResultsBuffer channelResultsBuffer) { mReusableBufferAssembler.updateTimestamp(channelResultsBuffer.getTimestamp()); mPolyphaseChannelOutputProcessor.receiveChannelResults(channelResultsBuffer); diff --git a/src/main/java/io/github/dsheirer/dsp/filter/channelizer/output/ChannelOutputProcessor.java b/src/main/java/io/github/dsheirer/dsp/filter/channelizer/output/ChannelOutputProcessor.java index 60cfc9f7..69e7ec6f 100644 --- a/src/main/java/io/github/dsheirer/dsp/filter/channelizer/output/ChannelOutputProcessor.java +++ b/src/main/java/io/github/dsheirer/dsp/filter/channelizer/output/ChannelOutputProcessor.java @@ -18,11 +18,11 @@ ******************************************************************************/ 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.buffer.ReusableBufferAssembler; +import io.github.dsheirer.sample.buffer.ReusableChannelResultsBuffer; import io.github.dsheirer.sample.real.IOverflowListener; import io.github.dsheirer.source.Source; import org.slf4j.Logger; @@ -35,8 +35,8 @@ public abstract class ChannelOutputProcessor implements IPolyphaseChannelOutputP { private final static Logger mLog = LoggerFactory.getLogger(ChannelOutputProcessor.class); - private OverflowableTransferQueue mChannelResultsQueue; - private List mChannelResultsToProcess = new ArrayList<>(); + private OverflowableTransferQueue mChannelResultsQueue; + private List mChannelResultsToProcess = new ArrayList<>(); private int mMaxResultsToProcess; private int mInputChannelCount; @@ -82,7 +82,7 @@ public abstract class ChannelOutputProcessor implements IPolyphaseChannelOutputP } @Override - public void receiveChannelResults(PolyphaseChannelResultsBuffer channelResults) + public void receiveChannelResults(ReusableChannelResultsBuffer channelResults) { mChannelResultsQueue.offer(channelResults); } @@ -116,7 +116,7 @@ public abstract class ChannelOutputProcessor implements IPolyphaseChannelOutputP * @param channelResults to process * @param reusableBufferAssembler to receive the processed channelizer results */ - public abstract void process(List channelResults, + public abstract void process(List channelResults, ReusableBufferAssembler reusableBufferAssembler); diff --git a/src/main/java/io/github/dsheirer/dsp/filter/channelizer/output/IPolyphaseChannelOutputProcessor.java b/src/main/java/io/github/dsheirer/dsp/filter/channelizer/output/IPolyphaseChannelOutputProcessor.java index 3726866e..c9856f25 100644 --- a/src/main/java/io/github/dsheirer/dsp/filter/channelizer/output/IPolyphaseChannelOutputProcessor.java +++ b/src/main/java/io/github/dsheirer/dsp/filter/channelizer/output/IPolyphaseChannelOutputProcessor.java @@ -18,8 +18,8 @@ ******************************************************************************/ package io.github.dsheirer.dsp.filter.channelizer.output; -import io.github.dsheirer.dsp.filter.channelizer.PolyphaseChannelResultsBuffer; import io.github.dsheirer.sample.buffer.ReusableBufferAssembler; +import io.github.dsheirer.sample.buffer.ReusableChannelResultsBuffer; import io.github.dsheirer.source.Source; import java.util.List; @@ -30,7 +30,7 @@ public interface IPolyphaseChannelOutputProcessor * Receive and enqueue output results from the polyphase analysis channelizer * @param channelResultsBuffer to enqueue */ - void receiveChannelResults(PolyphaseChannelResultsBuffer channelResultsBuffer); + void receiveChannelResults(ReusableChannelResultsBuffer channelResultsBuffer); /** * Process the channel output channel results queue and deliver the output to the listener diff --git a/src/main/java/io/github/dsheirer/dsp/filter/channelizer/output/OneChannelOutputProcessor.java b/src/main/java/io/github/dsheirer/dsp/filter/channelizer/output/OneChannelOutputProcessor.java index 1c5e534a..9684efa6 100644 --- a/src/main/java/io/github/dsheirer/dsp/filter/channelizer/output/OneChannelOutputProcessor.java +++ b/src/main/java/io/github/dsheirer/dsp/filter/channelizer/output/OneChannelOutputProcessor.java @@ -18,8 +18,8 @@ ******************************************************************************/ package io.github.dsheirer.dsp.filter.channelizer.output; -import io.github.dsheirer.dsp.filter.channelizer.PolyphaseChannelResultsBuffer; import io.github.dsheirer.sample.buffer.ReusableBufferAssembler; +import io.github.dsheirer.sample.buffer.ReusableChannelResultsBuffer; import org.slf4j.Logger; import org.slf4j.LoggerFactory; @@ -76,10 +76,10 @@ public class OneChannelOutputProcessor extends ChannelOutputProcessor * @param reusableBufferAssembler to receive the extracted, frequency-translated channel results */ @Override - public void process(List channelResults, + public void process(List channelResults, ReusableBufferAssembler reusableBufferAssembler) { - for(PolyphaseChannelResultsBuffer channelResultsBuffer: channelResults) + for(ReusableChannelResultsBuffer channelResultsBuffer: channelResults) { try { @@ -96,6 +96,8 @@ public class OneChannelOutputProcessor extends ChannelOutputProcessor { mLog.error("Error extracting channel samples from polyphase channel results buffer"); } + + channelResultsBuffer.decrementUserCount(); } } } diff --git a/src/main/java/io/github/dsheirer/dsp/filter/channelizer/output/TwoChannelOutputProcessor.java b/src/main/java/io/github/dsheirer/dsp/filter/channelizer/output/TwoChannelOutputProcessor.java index d51096c2..44f9f8ea 100644 --- a/src/main/java/io/github/dsheirer/dsp/filter/channelizer/output/TwoChannelOutputProcessor.java +++ b/src/main/java/io/github/dsheirer/dsp/filter/channelizer/output/TwoChannelOutputProcessor.java @@ -15,10 +15,10 @@ ******************************************************************************/ package io.github.dsheirer.dsp.filter.channelizer.output; -import io.github.dsheirer.dsp.filter.channelizer.PolyphaseChannelResultsBuffer; import io.github.dsheirer.dsp.filter.channelizer.TwoChannelSynthesizerM2; import io.github.dsheirer.dsp.mixer.FS4DownConverter; import io.github.dsheirer.sample.buffer.ReusableBufferAssembler; +import io.github.dsheirer.sample.buffer.ReusableChannelResultsBuffer; import org.slf4j.Logger; import org.slf4j.LoggerFactory; @@ -95,10 +95,10 @@ public class TwoChannelOutputProcessor extends ChannelOutputProcessor * @param reusableBufferAssembler to receive the extracted, frequency-translated channel results */ @Override - public void process(List channelResultsBuffers, + public void process(List channelResultsBuffers, ReusableBufferAssembler reusableBufferAssembler) { - for(PolyphaseChannelResultsBuffer buffer: channelResultsBuffers) + for(ReusableChannelResultsBuffer buffer: channelResultsBuffers) { float[] channel1 = buffer.getChannel(mChannelOffset1); float[] channel2 = buffer.getChannel(mChannelOffset2); @@ -113,6 +113,8 @@ public class TwoChannelOutputProcessor extends ChannelOutputProcessor getFrequencyCorrectionMixer().mixComplex(synthesized); reusableBufferAssembler.receive(synthesized); + + buffer.decrementUserCount(); } } } diff --git a/src/main/java/io/github/dsheirer/sample/OverflowableTransferQueue.java b/src/main/java/io/github/dsheirer/sample/OverflowableTransferQueue.java index c2b5a6d9..d3ad1541 100644 --- a/src/main/java/io/github/dsheirer/sample/OverflowableTransferQueue.java +++ b/src/main/java/io/github/dsheirer/sample/OverflowableTransferQueue.java @@ -115,6 +115,23 @@ public class OverflowableTransferQueue return drainCount; } + /** + * Retrieves elements from the queue into the collection up to the maximum number of elements specified + */ + public int drainTo(Collection collection) + { + int drainCount = mQueue.drainTo(collection); + + int size = mCounter.addAndGet(-drainCount); + + if(mOverflow.get() && size <= mResetThreshold) + { + setOverflow(false); + } + + return drainCount; + } + /** * Sets a listener to receive overflow state change events. */ diff --git a/src/main/java/io/github/dsheirer/sample/buffer/AbstractReusableBuffer.java b/src/main/java/io/github/dsheirer/sample/buffer/AbstractReusableBuffer.java new file mode 100644 index 00000000..d374e76a --- /dev/null +++ b/src/main/java/io/github/dsheirer/sample/buffer/AbstractReusableBuffer.java @@ -0,0 +1,151 @@ +/******************************************************************************* + * 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 + * + ******************************************************************************/ +package io.github.dsheirer.sample.buffer; + +import java.util.concurrent.atomic.AtomicInteger; + +public abstract class AbstractReusableBuffer +{ + protected IReusableBufferDisposedListener mBufferDisposedListener; + protected long mTimestamp; + protected AtomicInteger mUserCount = new AtomicInteger(); + private String mDebugName; + + public AbstractReusableBuffer(IReusableBufferDisposedListener bufferDisposedListener, long timestamp) + { + mBufferDisposedListener = bufferDisposedListener; + mTimestamp = timestamp; + } + + public AbstractReusableBuffer(IReusableBufferDisposedListener bufferDisposedListener) + { + mBufferDisposedListener = bufferDisposedListener; + } + + public void setDebugName(String debugName) + { + mDebugName = debugName; + } + + public String name() + { + return (mDebugName != null ? mDebugName : "(null)") + " User Count:" + getUserCount(); + } + + public String toString() + { + return name(); + } + + /** + * Reference timestamp for this complex buffer. Timestamp is relative to the first sample in the buffer. + * @return timestamp as milliseconds since epoch + */ + public long getTimestamp() + { + return mTimestamp; + } + + public void setTimestamp(long timestamp) + { + mTimestamp = timestamp; + } + + /** + * Sets the number of users that will receive this buffer. Each user is expected to invoke the + * decrementUserCount() method to indicate that they are finished using this buffer so that this buffer can be + * reused once the user count is zero. + * + * @param userCount that will receive this reusable buffer + */ + public void setUserCount(int userCount) + { + mUserCount.set(userCount); + } + + /** + * Decrements the user count for this buffer to indicate that the user/receiver of this buffer is finished using + * the buffer. + * + * User count is established prior to sending this buffer to each listener and each listener invokes this method to + * indicate that they have finished processing the buffer so that when the user count reaches zero, this buffer + * can be reused. + * + * This method is thread-safe. + */ + public void decrementUserCount() + { + int currentCount = mUserCount.decrementAndGet(); + + if(currentCount == 0) + { + recycle(); + mBufferDisposedListener.disposed(this); + } + else if(currentCount < 0) + { + throw new IllegalStateException("User count is below zero. This indicates that this buffer's decrement" + + " user count was invoked by more than the expected user count"); + } + } + + /** + * Invoked just prior to notifying the owner that this buffer is ready for recycle. This method + * is intended for sub-class implementations to perform any recycle cleanup actions. + */ + protected void recycle() + { + //No-op in this abstract class + } + + /** + * Increments the user count to indicate that this buffer will be sent to another user. + * + * Users/receivers of this buffer that wish to forward the buffer to additional users should increment the expected + * user count prior to invoking the decrementUserCount() to indicate that the original receiver of the buffer is + * finished to avoid the user count reaching zero and the buffer being flagged as ready for reuse. + * + * This method is thread-safe. + */ + public void incrementUserCount() + { + mUserCount.incrementAndGet(); + } + + /** + * Increments the user count by the specified count to indicate that this buffer will be sent to additional users. + * + * Users/receivers of this buffer that wish to forward the buffer to additional users should increment the expected + * user count prior to invoking the decrementUserCount() to indicate that the original receiver of the buffer is + * finished to avoid the user count reaching zero and the buffer being flagged as ready for reuse. + * + * This method is thread-safe. + * + * @param additionalUserCount to add to the current user count + */ + public void incrementUserCount(int additionalUserCount) + { + mUserCount.addAndGet(additionalUserCount); + } + + /** + * Number of users currently registered for this buffer + */ + public int getUserCount() + { + return mUserCount.get(); + } +} diff --git a/src/main/java/io/github/dsheirer/sample/buffer/AbstractReusableBufferQueue.java b/src/main/java/io/github/dsheirer/sample/buffer/AbstractReusableBufferQueue.java index 5e5f3bd6..18b618e0 100644 --- a/src/main/java/io/github/dsheirer/sample/buffer/AbstractReusableBufferQueue.java +++ b/src/main/java/io/github/dsheirer/sample/buffer/AbstractReusableBufferQueue.java @@ -21,14 +21,14 @@ import org.slf4j.LoggerFactory; import java.util.Queue; import java.util.concurrent.LinkedTransferQueue; -public abstract class AbstractReusableBufferQueue implements IReusableBufferDisposedListener +public abstract class AbstractReusableBufferQueue + implements IReusableBufferDisposedListener { private final static Logger mLog = LoggerFactory.getLogger(AbstractReusableBufferQueue.class); private Queue mReusableBufferQueue = new LinkedTransferQueue<>(); private int mBufferCount = 0; private String mDebugName; - private int mBufferUserCounter; public AbstractReusableBufferQueue(String debugName) { @@ -54,48 +54,31 @@ public abstract class AbstractReusableBufferQueue impl public void disposed(T reusableBuffer) { mReusableBufferQueue.offer(reusableBuffer); - -// if(mDebugName != null && mDebugName.contentEquals("NativeBufferConverter")) -// { -// mLog.debug("Reclaimed: " + reusableBuffer.name()); -// } } /** - * Returns a reusable buffer from an internal recycling queue, or creates a new buffer if there are currently no - * buffers available for reuse. - * - * @param size of the samples that will be loaded into the buffer - * @return a reusable buffer + * Get a recycled buffer from the queue */ - public T getBuffer(int size) + protected T getRecycledBuffer() { - T buffer = mReusableBufferQueue.poll(); - - if(buffer == null) - { - buffer = createBuffer(size); - mBufferCount++; - buffer.setDebugName("Owner:" + mDebugName + " Number:" + mBufferUserCounter++ + " NEW BUFFER"); - - mLog.debug("Buffer Created - Count:" + mBufferCount + (mDebugName != null ? " [" + mDebugName + "]" : "")); - } - else - { - String previous = buffer.name(); - buffer.setDebugName("Owner:" + mDebugName + " Number:" + mBufferUserCounter++); - -// if(mDebugName != null && mDebugName.contentEquals("NativeBufferConverter")) -// { -// mLog.debug("Buffer Reused: " + previous + " New Name:" + buffer.name()); -// } - } - - return buffer; + return mReusableBufferQueue.poll(); } /** - * Create a new buffer of the specified size + * Increments the count of buffers managed by this queue. Note: this method is NOT thread safe */ - protected abstract T createBuffer(int size); + protected void incrementBufferCount() + { + mBufferCount++; + } + + protected int getBufferCount() + { + return mBufferCount; + } + + protected String getDebugName() + { + return mDebugName; + } } diff --git a/src/main/java/io/github/dsheirer/sample/buffer/IReusableBufferDisposedListener.java b/src/main/java/io/github/dsheirer/sample/buffer/IReusableBufferDisposedListener.java index b3baaffb..67cbf293 100644 --- a/src/main/java/io/github/dsheirer/sample/buffer/IReusableBufferDisposedListener.java +++ b/src/main/java/io/github/dsheirer/sample/buffer/IReusableBufferDisposedListener.java @@ -15,7 +15,7 @@ ******************************************************************************/ package io.github.dsheirer.sample.buffer; -public interface IReusableBufferDisposedListener +public interface IReusableBufferDisposedListener { /** * Notifies the listener that the timestamped buffer has been disposed and can now be reused. diff --git a/src/main/java/io/github/dsheirer/sample/buffer/ReusableBuffer.java b/src/main/java/io/github/dsheirer/sample/buffer/ReusableBuffer.java index d9434004..de1dd829 100644 --- a/src/main/java/io/github/dsheirer/sample/buffer/ReusableBuffer.java +++ b/src/main/java/io/github/dsheirer/sample/buffer/ReusableBuffer.java @@ -18,15 +18,10 @@ package io.github.dsheirer.sample.buffer; import org.apache.commons.lang3.Validate; import java.nio.FloatBuffer; -import java.util.concurrent.atomic.AtomicInteger; -public class ReusableBuffer +public class ReusableBuffer extends AbstractReusableBuffer { private float[] mSamples; - private IReusableBufferDisposedListener mBufferDisposedListener; - private long mTimestamp; - private AtomicInteger mUserCount = new AtomicInteger(); - private String mDebugName; /** * Creates a reusable, timestamped complex buffer using the specified time in milliseconds. @@ -41,26 +36,10 @@ public class ReusableBuffer */ ReusableBuffer(IReusableBufferDisposedListener bufferDisposedListener, float[] samples, long timestamp) { + super(bufferDisposedListener, timestamp); mSamples = samples; Validate.notNull(bufferDisposedListener, "Reusable Buffer Listener cannot be null"); - mBufferDisposedListener = bufferDisposedListener; - mTimestamp = timestamp; - } - - public void setDebugName(String debugName) - { - mDebugName = debugName; - } - - public String name() - { - return (mDebugName != null ? mDebugName : "(null)") + " User Count:" + getUserCount(); - } - - public String toString() - { - return name(); } /** @@ -105,20 +84,6 @@ public class ReusableBuffer this(bufferDisposedListener, samples, System.currentTimeMillis()); } - /** - * Reference timestamp for this complex buffer. Timestamp is relative to the first sample in the buffer. - * @return timestamp as milliseconds since epoch - */ - public long getTimestamp() - { - return mTimestamp; - } - - public void setTimestamp(long timestamp) - { - mTimestamp = timestamp; - } - /** * Reloads this buffer by copying the sample array into this buffer. Internal buffer length will be automatically * resized to fit the new sample array length. @@ -175,78 +140,4 @@ public class ReusableBuffer mTimestamp = timestamp; } - /** - * Sets the number of users that will receive this buffer. Each user is expected to invoke the - * decrementUserCount() method to indicate that they are finished using this buffer so that this buffer can be - * reused once the user count is zero. - * - * @param userCount that will receive this reusable buffer - */ - public void setUserCount(int userCount) - { - mUserCount.set(userCount); - } - - /** - * Decrements the user count for this buffer to indicate that the user/receiver of this buffer is finished using - * the buffer. - * - * User count is established prior to sending this buffer to each listener and each listener invokes this method to - * indicate that they have finished processing the buffer so that when the user count reaches zero, this buffer - * can be reused. - * - * This method is thread-safe. - */ - public void decrementUserCount() - { - int currentCount = mUserCount.decrementAndGet(); - - if(currentCount == 0) - { - mBufferDisposedListener.disposed(this); - } - else if(currentCount < 0) - { - throw new IllegalStateException("User count is below zero. This indicates that this buffer's decrement" + - " user count was invoked by more than the expected user count"); - } - } - - /** - * Increments the user count to indicate that this buffer will be sent to another user. - * - * Users/receivers of this buffer that wish to forward the buffer to additional users should increment the expected - * user count prior to invoking the decrementUserCount() to indicate that the original receiver of the buffer is - * finished to avoid the user count reaching zero and the buffer being flagged as ready for reuse. - * - * This method is thread-safe. - */ - public void incrementUserCount() - { - mUserCount.incrementAndGet(); - } - - /** - * Increments the user count by the specified count to indicate that this buffer will be sent to additional users. - * - * Users/receivers of this buffer that wish to forward the buffer to additional users should increment the expected - * user count prior to invoking the decrementUserCount() to indicate that the original receiver of the buffer is - * finished to avoid the user count reaching zero and the buffer being flagged as ready for reuse. - * - * This method is thread-safe. - * - * @param additionalUserCount to add to the current user count - */ - public void incrementUserCount(int additionalUserCount) - { - mUserCount.addAndGet(additionalUserCount); - } - - /** - * Number of users currently registered for this buffer - */ - public int getUserCount() - { - return mUserCount.get(); - } } diff --git a/src/main/java/io/github/dsheirer/sample/buffer/ReusableBufferQueue.java b/src/main/java/io/github/dsheirer/sample/buffer/ReusableBufferQueue.java index 85bb5efc..292e1033 100644 --- a/src/main/java/io/github/dsheirer/sample/buffer/ReusableBufferQueue.java +++ b/src/main/java/io/github/dsheirer/sample/buffer/ReusableBufferQueue.java @@ -31,9 +31,26 @@ public class ReusableBufferQueue extends AbstractReusableBufferQueue + * + ******************************************************************************/ +package io.github.dsheirer.sample.buffer; + +import java.util.LinkedList; +import java.util.List; + +public class ReusableChannelResultsBuffer extends AbstractReusableBuffer +{ + private LinkedList mEmptyBuffers = new LinkedList<>(); + private LinkedList mFilledBuffers = new LinkedList<>(); + private Integer mLength; + + /** + * Reusable buffer for storing polyphase channelizer results arrays. This buffer is NOT thread safe and is + * designed to be used by only one consumer. The consumer must invoke the recycle() method to indicate that this + * buffer can be reused. + * + * @param disposedListener to receive notification when this buffer is ready for recycle + */ + ReusableChannelResultsBuffer(IReusableBufferDisposedListener disposedListener) + { + super(disposedListener); + } + + /** + * Recycles this buffer for reuse. + */ + protected void recycle() + { + mEmptyBuffers.addAll(mFilledBuffers); + mFilledBuffers.clear(); + mLength = null; + } + + /** + * Gets an empty channel results array correctly sized to the requested length. This method should be used to + * obtain all empty channel results buffers to fill with information and add back to this buffer so that the results + * arrays can be reused. + * + * @param length of the requested array + * @return a float array of the requested length that may contain stale data from previous use. + */ + public float[] getEmptyBuffer(int length) + { + float[] emptyBuffer = mEmptyBuffers.poll(); + + if(emptyBuffer == null || emptyBuffer.length != length) + { + emptyBuffer = new float[length]; + } + + return emptyBuffer; + } + + /** + * Adds the channel results to this buffer. Note: you should only use channel results arrays obtained via the + * getEmptyBuffer() method and filled with data and then added back to this buffer via this method, so that array + * reuse can be accurately controlled. + * + * @param channelResults to add back to this buffer + */ + public void addChannelResults(float[] channelResults) + { + if(mLength == null) + { + mLength = channelResults.length; + } + else if(mLength != channelResults.length) + { + throw new IllegalArgumentException("Channel results length must be the same for all added results"); + } + + mFilledBuffers.offer(channelResults); + } + + /** + * Channel results array contained in this buffer + */ + public List getChannelResults() + { + return mFilledBuffers; + } + + /** + * Extracts a single I/Q interleaved sample buffer from this buffer of channel results arrays + * + * @param iChannelIndex to extract for the inphase sample. The q index is assumed to be one greater than i. + * @return float[] containing interleaved I and Q samples + * @throws IllegalArgumentException if this buffer doesn't contain any samples or if the requested channel is + * not within the channel range of the contained channel results. + */ + public float[] getChannel(int iChannelIndex) + { + if(mFilledBuffers.isEmpty()) + { + throw new IllegalArgumentException("Buffer is empty - cannot extract channel [" + iChannelIndex + "]"); + } + + if(!isValidChannelIndex(iChannelIndex)) + { + throw new IllegalArgumentException("Channel [" + iChannelIndex + "] is not valid for the contained channel " + + "results -- max channel is " + mLength); + } + + float[] samples = new float[mFilledBuffers.size() * 2]; + int pointer = 0; + + int qChannelIndex = iChannelIndex + 1; + + for(float[] channelResults: mFilledBuffers) + { + samples[pointer++] = channelResults[iChannelIndex]; + samples[pointer++] = channelResults[qChannelIndex]; + } + + return samples; + } + + /** + * Indicates if the channel argument is valid for the contained channel results. + * + * @param channelIndex number (0 to N) to check + * @return false if the buffer is empty or if the channel is greater than the contained channel results + */ + private boolean isValidChannelIndex(int channelIndex) + { + return (mLength != null) && (channelIndex <= mLength); + } +} diff --git a/src/main/java/io/github/dsheirer/sample/buffer/ReusableChannelResultsBufferQueue.java b/src/main/java/io/github/dsheirer/sample/buffer/ReusableChannelResultsBufferQueue.java new file mode 100644 index 00000000..9d6f7c16 --- /dev/null +++ b/src/main/java/io/github/dsheirer/sample/buffer/ReusableChannelResultsBufferQueue.java @@ -0,0 +1,55 @@ +/******************************************************************************* + * 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 + * + ******************************************************************************/ +package io.github.dsheirer.sample.buffer; + +import org.slf4j.Logger; +import org.slf4j.LoggerFactory; + +public class ReusableChannelResultsBufferQueue extends AbstractReusableBufferQueue +{ + private final static Logger mLog = LoggerFactory.getLogger(ReusableChannelResultsBufferQueue.class); + + public ReusableChannelResultsBufferQueue(String debugName) + { + super(debugName); + } + + public ReusableChannelResultsBufferQueue() + { + } + + /** + * Returns a reusable buffer from an internal recycling queue, or creates a new buffer if there are currently no + * buffers available for reuse. + * + * @return a reusable buffer + */ + public ReusableChannelResultsBuffer getBuffer() + { + ReusableChannelResultsBuffer buffer = getRecycledBuffer(); + + if(buffer == null) + { + buffer = new ReusableChannelResultsBuffer(this); + incrementBufferCount(); + + buffer.setDebugName("Owner:" + getDebugName()); + mLog.debug("Buffer Created - Count:" + getBufferCount() + (getDebugName() != null ? " [" + getDebugName() + "]" : "")); + } + + return buffer; + } +} diff --git a/src/main/java/io/github/dsheirer/sample/buffer/ReusableComplexBufferQueue.java b/src/main/java/io/github/dsheirer/sample/buffer/ReusableComplexBufferQueue.java index 0c52454e..4f0bcc88 100644 --- a/src/main/java/io/github/dsheirer/sample/buffer/ReusableComplexBufferQueue.java +++ b/src/main/java/io/github/dsheirer/sample/buffer/ReusableComplexBufferQueue.java @@ -31,9 +31,26 @@ public class ReusableComplexBufferQueue extends AbstractReusableBufferQueue Date: Sun, 22 Apr 2018 04:44:35 -0400 Subject: [PATCH 63/87] [WIP]-39 updates to resolve issue with stopping channel streams. --- .../filter/channelizer/ChannelCalculator.java | 2 +- .../ComplexPolyphaseChannelizerM2.java | 23 ++++++++ .../ContinuousBufferProcessor.java | 7 ++- .../channelizer/PolyphaseChannelManager.java | 39 +++++++++++-- .../channelizer/PolyphaseChannelSource.java | 47 ++++++++++------ .../output/ChannelOutputProcessor.java | 14 ++++- .../IPolyphaseChannelOutputProcessor.java | 3 + .../dsheirer/module/ProcessingChain.java | 2 - .../sample/OverflowableTransferQueue.java | 7 +++ .../sample/buffer/AbstractReusableBuffer.java | 55 ++++++++++++++----- .../buffer/AbstractReusableBufferQueue.java | 27 ++++++++- .../buffer/ReusableBufferAssembler.java | 4 +- .../buffer/ReusableChannelResultsBuffer.java | 6 +- .../buffer/ReusableComplexBufferQueue.java | 7 +++ .../tuner/channel/TunerChannelSource.java | 27 ++++----- 15 files changed, 206 insertions(+), 64 deletions(-) diff --git a/src/main/java/io/github/dsheirer/dsp/filter/channelizer/ChannelCalculator.java b/src/main/java/io/github/dsheirer/dsp/filter/channelizer/ChannelCalculator.java index a33b2f2b..2fb541f0 100644 --- a/src/main/java/io/github/dsheirer/dsp/filter/channelizer/ChannelCalculator.java +++ b/src/main/java/io/github/dsheirer/dsp/filter/channelizer/ChannelCalculator.java @@ -33,7 +33,7 @@ public class ChannelCalculator * from an index where the frequency may exist either at the overlapping boundary of two indexes, or in the middle * of the wrap-around (N/2) index. */ - private enum IndexBoundaryPolicy + public enum IndexBoundaryPolicy { ADJUST_POSITIVE, ADJUST_NEGATIVE diff --git a/src/main/java/io/github/dsheirer/dsp/filter/channelizer/ComplexPolyphaseChannelizerM2.java b/src/main/java/io/github/dsheirer/dsp/filter/channelizer/ComplexPolyphaseChannelizerM2.java index a6be055a..d727d72c 100644 --- a/src/main/java/io/github/dsheirer/dsp/filter/channelizer/ComplexPolyphaseChannelizerM2.java +++ b/src/main/java/io/github/dsheirer/dsp/filter/channelizer/ComplexPolyphaseChannelizerM2.java @@ -31,6 +31,7 @@ import org.slf4j.Logger; import org.slf4j.LoggerFactory; import java.util.Arrays; +import java.util.Collection; import java.util.List; public class ComplexPolyphaseChannelizerM2 extends AbstractComplexPolyphaseChannelizer @@ -132,11 +133,17 @@ public class ComplexPolyphaseChannelizerM2 extends AbstractComplexPolyphaseChann init(filterTaps); } + /** + * Starts sample processing + */ public void start() { mIFFTProcessor.start(); } + /** + * Stops sample processing. + */ public void stop() { mIFFTProcessor.stop(); @@ -504,6 +511,22 @@ public class ComplexPolyphaseChannelizerM2 extends AbstractComplexPolyphaseChann } }); } + + /** + * Clears any buffers from the dispatch/processing queue. Overrides the parent method so that we can set + * the user count to 0 to allow the buffer to be reclaimed. + */ + protected Collection clearQueue() + { + Collection buffersToDispose = super.clearQueue(); + + for(ReusableChannelResultsBuffer buffer: buffersToDispose) + { + buffer.clearUserCount(); + } + + return buffersToDispose; + } } public static void main(String[] args) diff --git a/src/main/java/io/github/dsheirer/dsp/filter/channelizer/ContinuousBufferProcessor.java b/src/main/java/io/github/dsheirer/dsp/filter/channelizer/ContinuousBufferProcessor.java index 9c3a1791..1df13430 100644 --- a/src/main/java/io/github/dsheirer/dsp/filter/channelizer/ContinuousBufferProcessor.java +++ b/src/main/java/io/github/dsheirer/dsp/filter/channelizer/ContinuousBufferProcessor.java @@ -23,6 +23,7 @@ import org.slf4j.Logger; import org.slf4j.LoggerFactory; import java.util.ArrayList; +import java.util.Collection; import java.util.List; import java.util.concurrent.ScheduledFuture; import java.util.concurrent.TimeUnit; @@ -138,9 +139,11 @@ public class ContinuousBufferProcessor implements Listener /** * Clears any buffers from the queue */ - protected void clearQueue() + protected Collection clearQueue() { - mQueue.clear(); + Collection clearedElements = new ArrayList<>(); + mQueue.drainTo(clearedElements); + return clearedElements; } /** diff --git a/src/main/java/io/github/dsheirer/dsp/filter/channelizer/PolyphaseChannelManager.java b/src/main/java/io/github/dsheirer/dsp/filter/channelizer/PolyphaseChannelManager.java index dd11711f..660be28d 100644 --- a/src/main/java/io/github/dsheirer/dsp/filter/channelizer/PolyphaseChannelManager.java +++ b/src/main/java/io/github/dsheirer/dsp/filter/channelizer/PolyphaseChannelManager.java @@ -26,6 +26,7 @@ import io.github.dsheirer.sample.Listener; import io.github.dsheirer.sample.buffer.IReusableBufferProvider; import io.github.dsheirer.sample.buffer.ReusableComplexBuffer; import io.github.dsheirer.source.ISourceEventProcessor; +import io.github.dsheirer.source.Source; import io.github.dsheirer.source.SourceEvent; import io.github.dsheirer.source.SourceException; import io.github.dsheirer.source.tuner.TunerController; @@ -134,21 +135,34 @@ public class PolyphaseChannelManager implements ISourceEventProcessor */ public TunerChannelSource getChannel(TunerChannel tunerChannel) { + mLog.debug("Getting channel: " + tunerChannel.toString()); PolyphaseChannelSource channelSource = null; try { List polyphaseIndexes = mChannelCalculator.getChannelIndexes(tunerChannel); + StringBuilder sb = new StringBuilder(); + sb.append("Channel Indexes - "); + for(int index: polyphaseIndexes) + { + sb.append(index).append(":").append((long)mChannelCalculator.getIndexCenterFrequency(index, + ChannelCalculator.IndexBoundaryPolicy.ADJUST_POSITIVE)).append(" "); + } + mLog.debug(sb.toString()); + IPolyphaseChannelOutputProcessor outputProcessor = getOutputProcessor(polyphaseIndexes); + mLog.debug("Channel Ouput Processor: " + outputProcessor.getClass()); + if(outputProcessor != null) { + long centerFrequency = mChannelCalculator.getCenterFrequencyForIndexes(polyphaseIndexes); + channelSource = new PolyphaseChannelSource(tunerChannel, outputProcessor, mChannelSourceEventListener, - mChannelCalculator.getChannelSampleRate()); + mChannelCalculator.getChannelSampleRate(), centerFrequency); mChannelSources.add(channelSource); - mSourceEventBroadcaster.broadcast(SourceEvent.channelCountChange(getTunerChannelCount())); } } catch(IllegalArgumentException iae) @@ -176,8 +190,10 @@ public class PolyphaseChannelManager implements ISourceEventProcessor if(outputProcessor != null) { + long centerFrequency = mChannelCalculator.getCenterFrequencyForIndexes(polyphaseIndexes); + channelSource = new PolyphaseChannelSource(tunerChannel, outputProcessor, mChannelSourceEventListener, - mChannelCalculator.getChannelSampleRate()); + mChannelCalculator.getChannelSampleRate(), centerFrequency); } } catch(IllegalArgumentException iae) @@ -227,8 +243,12 @@ public class PolyphaseChannelManager implements ISourceEventProcessor { synchronized(mBufferProcessor) { - mPolyphaseChannelizer.addChannel(channelSource); + //Note: the polyphase channel source has already been added to the mChannelSources in getChannel() method + mPolyphaseChannelizer.addChannel(channelSource); + mSourceEventBroadcaster.broadcast(SourceEvent.channelCountChange(getTunerChannelCount())); + + //If this is the first channel, register to start the sample buffers flowing if(mPolyphaseChannelizer.getRegisteredChannelCount() == 1) { mReusableBufferProvider.addBufferListener(mBufferProcessor); @@ -249,8 +269,10 @@ public class PolyphaseChannelManager implements ISourceEventProcessor synchronized(mBufferProcessor) { mChannelSources.remove(channelSource); + mPolyphaseChannelizer.removeChannel(channelSource); mSourceEventBroadcaster.broadcast(SourceEvent.channelCountChange(getTunerChannelCount())); + //If this is the last/only channel, deregister to stop the sample buffers if(mPolyphaseChannelizer.getRegisteredChannelCount() == 0) { mReusableBufferProvider.removeBufferListener(mBufferProcessor); @@ -532,9 +554,18 @@ public class PolyphaseChannelManager implements ISourceEventProcessor (sourceEvent.hasSource() ? sourceEvent.getSource().getClass() : "null source")); } break; + case REQUEST_SOURCE_DISPOSE: + Source source = sourceEvent.getSource(); + + if(source instanceof PolyphaseChannelSource) + { + source.dispose(); + } + break; default: mLog.error("Received unrecognized source event from polyphase channel source [" + sourceEvent.getEvent() + "]"); + break; } } } diff --git a/src/main/java/io/github/dsheirer/dsp/filter/channelizer/PolyphaseChannelSource.java b/src/main/java/io/github/dsheirer/dsp/filter/channelizer/PolyphaseChannelSource.java index 29a04521..19a09b36 100644 --- a/src/main/java/io/github/dsheirer/dsp/filter/channelizer/PolyphaseChannelSource.java +++ b/src/main/java/io/github/dsheirer/dsp/filter/channelizer/PolyphaseChannelSource.java @@ -33,8 +33,8 @@ public class PolyphaseChannelSource extends TunerChannelSource private ReusableBufferAssembler mReusableBufferAssembler; private IPolyphaseChannelOutputProcessor mPolyphaseChannelOutputProcessor; private double mChannelSampleRate; - private long mCenterFrequency; - private long mFrequencyCorrection; + private long mIndexCenterFrequency; + private long mChannelFrequencyCorrection; /** * Polyphase channelizer tuner channel source implementation. Adapts the channel array output samples from the @@ -45,14 +45,17 @@ public class PolyphaseChannelSource extends TunerChannelSource * @param outputProcessor - to process polyphase channelizer channel results into a channel stream * @param producerSourceEventListener to receive source event requests (e.g. start/stop sample stream) * @param channelSampleRate for the downstream sample output + * @param centerFrequency of the incoming polyphase channel(s) */ public PolyphaseChannelSource(TunerChannel tunerChannel, IPolyphaseChannelOutputProcessor outputProcessor, - Listener producerSourceEventListener, double channelSampleRate) + Listener producerSourceEventListener, double channelSampleRate, + long centerFrequency) { super(producerSourceEventListener, tunerChannel); mPolyphaseChannelOutputProcessor = outputProcessor; mChannelSampleRate = channelSampleRate; mReusableBufferAssembler = new ReusableBufferAssembler(2500, mChannelSampleRate); + setFrequency(centerFrequency); } /** @@ -114,10 +117,10 @@ public class PolyphaseChannelSource extends TunerChannelSource //Finally, setup the frequency offset for the output processor and reset the frequency correction value //to allow consumers to adjust and calculate a new frequency correction value, if needed. - mCenterFrequency = frequency; - mFrequencyCorrection = 0; + mIndexCenterFrequency = frequency; + mChannelFrequencyCorrection = 0; mPolyphaseChannelOutputProcessor.setFrequencyOffset(getFrequencyOffset()); - broadcastConsumerSourceEvent(SourceEvent.frequencyCorrectionChange(mFrequencyCorrection)); + broadcastConsumerSourceEvent(SourceEvent.frequencyCorrectionChange(mChannelFrequencyCorrection)); } } @@ -158,13 +161,22 @@ public class PolyphaseChannelSource extends TunerChannelSource @Override public void dispose() { - mLog.info("Polyphase channel source received request to dispose"); + if(mReusableBufferAssembler != null) + { + mReusableBufferAssembler.dispose(); + } + + if(mPolyphaseChannelOutputProcessor != null) + { + mPolyphaseChannelOutputProcessor.dispose(); + mPolyphaseChannelOutputProcessor = null; + } } @Override - public long getFrequencyCorrection() + public long getChannelFrequencyCorrection() { - return mFrequencyCorrection; + return mChannelFrequencyCorrection; } /** @@ -173,26 +185,26 @@ public class PolyphaseChannelSource extends TunerChannelSource * * @param value to apply for frequency correction in hertz */ - protected void setFrequencyCorrection(long value) + protected void setChannelFrequencyCorrection(long value) { - mFrequencyCorrection = value; + mChannelFrequencyCorrection = value; updateFrequencyOffset(); - broadcastConsumerSourceEvent(SourceEvent.frequencyCorrectionChange(mFrequencyCorrection)); + broadcastConsumerSourceEvent(SourceEvent.frequencyCorrectionChange(mChannelFrequencyCorrection)); } /** - * Sets the center frequency for the incoming sample stream channel results and resets frequency correction to zero. + * Sets the center frequency for this channel.frequency for the incoming sample stream channel results and resets frequency correction to zero. * @param frequency in hertz */ @Override protected void setFrequency(long frequency) { - mLog.debug("Center Frequency set to: " + frequency + " for channel " + getTunerChannel().toString()); - mCenterFrequency = frequency; + mLog.debug("Index Center Frequency set to: " + frequency + " for channel " + getTunerChannel().toString()); + mIndexCenterFrequency = frequency; //Set frequency correction to zero to trigger an update to the mixer and allow downstream monitors to //recalculate the frequency error correction again - setFrequencyCorrection(0); + setChannelFrequencyCorrection(0); } @Override @@ -210,7 +222,7 @@ public class PolyphaseChannelSource extends TunerChannelSource */ private long getFrequencyOffset() { - return getTunerChannel().getFrequency() - mCenterFrequency + mFrequencyCorrection; + return getTunerChannel().getFrequency() - mIndexCenterFrequency + mChannelFrequencyCorrection; } /** @@ -222,6 +234,7 @@ public class PolyphaseChannelSource extends TunerChannelSource { synchronized(mPolyphaseChannelOutputProcessor) { + mLog.debug("Setting output processor frequency offset to: " + getFrequencyOffset()); mPolyphaseChannelOutputProcessor.setFrequencyOffset(getFrequencyOffset()); } } diff --git a/src/main/java/io/github/dsheirer/dsp/filter/channelizer/output/ChannelOutputProcessor.java b/src/main/java/io/github/dsheirer/dsp/filter/channelizer/output/ChannelOutputProcessor.java index 69e7ec6f..e27d199a 100644 --- a/src/main/java/io/github/dsheirer/dsp/filter/channelizer/output/ChannelOutputProcessor.java +++ b/src/main/java/io/github/dsheirer/dsp/filter/channelizer/output/ChannelOutputProcessor.java @@ -40,7 +40,6 @@ public abstract class ChannelOutputProcessor implements IPolyphaseChannelOutputP private int mMaxResultsToProcess; private int mInputChannelCount; -//TODO: swap this out and use the LowPhaseNoiseOscillator private IOscillator mFrequencyCorrectionMixer; private boolean mFrequencyCorrectionEnabled; @@ -55,12 +54,23 @@ public abstract class ChannelOutputProcessor implements IPolyphaseChannelOutputP public ChannelOutputProcessor(int inputChannelCount, double sampleRate) { mInputChannelCount = inputChannelCount; + +//TODO: swap this out and use the LowPhaseNoiseOscillator mFrequencyCorrectionMixer = new Oscillator(0, sampleRate); mMaxResultsToProcess = (int)(sampleRate / 10) * 2; //process at 100 millis interval, twice the expected inflow rate mChannelResultsQueue = new OverflowableTransferQueue<>((int)(sampleRate * 3), (int)(sampleRate * 0.5)); } + public void dispose() + { + if(mChannelResultsQueue != null) + { + mChannelResultsQueue.dispose(); + mChannelResultsQueue = null; + } + } + /** * Sets the frequency offset to apply to the incoming samples to mix the desired signal to baseband. * @param frequencyOffset in hertz @@ -68,7 +78,7 @@ public abstract class ChannelOutputProcessor implements IPolyphaseChannelOutputP @Override public void setFrequencyOffset(long frequencyOffset) { - mLog.debug("Setting frequency offset to: " + frequencyOffset); + mLog.debug("Setting frequency offset to: " + frequencyOffset + " for class:" + this.getClass()); mFrequencyCorrectionMixer.setFrequency(frequencyOffset); mFrequencyCorrectionEnabled = (frequencyOffset != 0); } diff --git a/src/main/java/io/github/dsheirer/dsp/filter/channelizer/output/IPolyphaseChannelOutputProcessor.java b/src/main/java/io/github/dsheirer/dsp/filter/channelizer/output/IPolyphaseChannelOutputProcessor.java index c9856f25..95c45055 100644 --- a/src/main/java/io/github/dsheirer/dsp/filter/channelizer/output/IPolyphaseChannelOutputProcessor.java +++ b/src/main/java/io/github/dsheirer/dsp/filter/channelizer/output/IPolyphaseChannelOutputProcessor.java @@ -70,4 +70,7 @@ public interface IPolyphaseChannelOutputProcessor * @param source to receive overflow notifications */ void setSourceOverflowListener(Source source); + + + void dispose(); } diff --git a/src/main/java/io/github/dsheirer/module/ProcessingChain.java b/src/main/java/io/github/dsheirer/module/ProcessingChain.java index 34b81cdb..5beafede 100644 --- a/src/main/java/io/github/dsheirer/module/ProcessingChain.java +++ b/src/main/java/io/github/dsheirer/module/ProcessingChain.java @@ -641,8 +641,6 @@ public class ProcessingChain implements IChannelEventListener "chain"); } - /* Release the source */ - mSource.dispose(); mSource = null; } } diff --git a/src/main/java/io/github/dsheirer/sample/OverflowableTransferQueue.java b/src/main/java/io/github/dsheirer/sample/OverflowableTransferQueue.java index d3ad1541..c342ac3f 100644 --- a/src/main/java/io/github/dsheirer/sample/OverflowableTransferQueue.java +++ b/src/main/java/io/github/dsheirer/sample/OverflowableTransferQueue.java @@ -56,6 +56,13 @@ public class OverflowableTransferQueue mResetThreshold = resetThreshold; } + public void dispose() + { + clear(); + mOverflowListener = null; + mSourceOverflowListener = null; + } + /** * Adds the element to the queue if able to do so without exceeding maximum queue size. Otherwise, ignores * the element. diff --git a/src/main/java/io/github/dsheirer/sample/buffer/AbstractReusableBuffer.java b/src/main/java/io/github/dsheirer/sample/buffer/AbstractReusableBuffer.java index d374e76a..77fbaf2e 100644 --- a/src/main/java/io/github/dsheirer/sample/buffer/AbstractReusableBuffer.java +++ b/src/main/java/io/github/dsheirer/sample/buffer/AbstractReusableBuffer.java @@ -15,10 +15,15 @@ ******************************************************************************/ package io.github.dsheirer.sample.buffer; +import org.slf4j.Logger; +import org.slf4j.LoggerFactory; + import java.util.concurrent.atomic.AtomicInteger; public abstract class AbstractReusableBuffer { + private final static Logger mLog = LoggerFactory.getLogger(AbstractReusableBuffer.class); + protected IReusableBufferDisposedListener mBufferDisposedListener; protected long mTimestamp; protected AtomicInteger mUserCount = new AtomicInteger(); @@ -35,6 +40,11 @@ public abstract class AbstractReusableBuffer mBufferDisposedListener = bufferDisposedListener; } + public void dispose() + { + mBufferDisposedListener = null; + } + public void setDebugName(String debugName) { mDebugName = debugName; @@ -65,15 +75,12 @@ public abstract class AbstractReusableBuffer } /** - * Sets the number of users that will receive this buffer. Each user is expected to invoke the - * decrementUserCount() method to indicate that they are finished using this buffer so that this buffer can be - * reused once the user count is zero. - * - * @param userCount that will receive this reusable buffer + * Sets the user count to 0 and recycles this buffer */ - public void setUserCount(int userCount) + public void clearUserCount() { - mUserCount.set(userCount); + mUserCount.set(0); + recycle(); } /** @@ -88,14 +95,25 @@ public abstract class AbstractReusableBuffer */ public void decrementUserCount() { - int currentCount = mUserCount.decrementAndGet(); - - if(currentCount == 0) + if(mUserCount.decrementAndGet() <= 0) { recycle(); + } + } + + /** + * Sends this buffer back to the owning buffer queue for reuse + */ + private void recycle() + { + prepareForRecycle(); + + if(mBufferDisposedListener != null) + { mBufferDisposedListener.disposed(this); } - else if(currentCount < 0) + + if(mUserCount.get() < 0) { throw new IllegalStateException("User count is below zero. This indicates that this buffer's decrement" + " user count was invoked by more than the expected user count"); @@ -103,10 +121,10 @@ public abstract class AbstractReusableBuffer } /** - * Invoked just prior to notifying the owner that this buffer is ready for recycle. This method - * is intended for sub-class implementations to perform any recycle cleanup actions. + * Invoked just prior to notifying the owner that this buffer is ready for prepareForRecycle. This method + * is intended for sub-class implementations to perform any prepareForRecycle cleanup actions. */ - protected void recycle() + protected void prepareForRecycle() { //No-op in this abstract class } @@ -138,7 +156,14 @@ public abstract class AbstractReusableBuffer */ public void incrementUserCount(int additionalUserCount) { - mUserCount.addAndGet(additionalUserCount); + if(additionalUserCount >= 0) + { + mUserCount.addAndGet(additionalUserCount); + } + else + { + mLog.debug("Request to add negative user count [" + additionalUserCount + "] ignored"); + } } /** diff --git a/src/main/java/io/github/dsheirer/sample/buffer/AbstractReusableBufferQueue.java b/src/main/java/io/github/dsheirer/sample/buffer/AbstractReusableBufferQueue.java index 18b618e0..e777b1eb 100644 --- a/src/main/java/io/github/dsheirer/sample/buffer/AbstractReusableBufferQueue.java +++ b/src/main/java/io/github/dsheirer/sample/buffer/AbstractReusableBufferQueue.java @@ -30,18 +30,37 @@ public abstract class AbstractReusableBufferQueue mProducerSourceEventListener; private Listener mConsumerSourceEventListener; - private BufferProcessor mBufferProcessor = new BufferProcessor(); + private ScheduledIntervalProcessor mScheduledIntervalProcessor = new ScheduledIntervalProcessor(); /** * Tuner Channel Source is a Digital Drop Channel (DDC) abstract class that defines the minimum functionality @@ -78,12 +78,12 @@ public abstract class TunerChannelSource extends ComplexSource implements ISourc * Sets the frequency correction for the outbound sample stream to the consumer * @param correction in hertz */ - protected abstract void setFrequencyCorrection(long correction); + protected abstract void setChannelFrequencyCorrection(long correction); /** * Frequency correction value currently being applied to the outbound sample stream */ - public abstract long getFrequencyCorrection(); + public abstract long getChannelFrequencyCorrection(); /** * Sets the listener to receive the complex buffer sample output from this channel @@ -125,7 +125,7 @@ public abstract class TunerChannelSource extends ComplexSource implements ISourc mProducerSourceEventListener.receive(SourceEvent.startSampleStreamRequest(this)); } - mBufferProcessor.start(); + mScheduledIntervalProcessor.start(); } /** @@ -140,7 +140,7 @@ public abstract class TunerChannelSource extends ComplexSource implements ISourc mProducerSourceEventListener.receive(SourceEvent.sourceDisposeRequest(this)); } - mBufferProcessor.stop(); + mScheduledIntervalProcessor.stop(); } @Override @@ -172,22 +172,19 @@ public abstract class TunerChannelSource extends ComplexSource implements ISourc { switch(sourceEvent.getEvent()) { - //Notification events from the producer case NOTIFICATION_FREQUENCY_CHANGE: - mLog.debug("Processing Source Event - setting frequency to " + sourceEvent.getValue().longValue()); - setFrequency(sourceEvent.getValue().longValue()); + case NOTIFICATION_FREQUENCY_AND_SAMPLE_RATE_LOCKED: + case NOTIFICATION_FREQUENCY_AND_SAMPLE_RATE_UNLOCKED: + case NOTIFICATION_FREQUENCY_CORRECTION_CHANGE: + case NOTIFICATION_STOP_SAMPLE_STREAM: + //no-op break; case NOTIFICATION_SAMPLE_RATE_CHANGE: setSampleRate(sourceEvent.getValue().doubleValue()); break; - case NOTIFICATION_FREQUENCY_AND_SAMPLE_RATE_LOCKED: - case NOTIFICATION_FREQUENCY_AND_SAMPLE_RATE_UNLOCKED: - case NOTIFICATION_FREQUENCY_CORRECTION_CHANGE: - //no-op - break; //Request events from the consumer case REQUEST_CHANNEL_FREQUENCY_CORRECTION_CHANGE: - setFrequencyCorrection(sourceEvent.getValue().longValue()); + setChannelFrequencyCorrection(sourceEvent.getValue().longValue()); break; default: mLog.error("Ignoring unrecognized source event: " + sourceEvent.getEvent() + " from [" + @@ -255,7 +252,7 @@ public abstract class TunerChannelSource extends ComplexSource implements ISourc * sends a heartbeat to the registered consumer and then commands the sub-class implementation to process any * queued buffers and distribute complex buffer sample(s) to the registered consumer. */ - public class BufferProcessor implements Runnable + public class ScheduledIntervalProcessor implements Runnable { private ScheduledFuture mScheduledFuture; private boolean mStopped = false; From 488b2e02ced82dc0153b886aaa0bfcb7461c8d25 Mon Sep 17 00:00:00 2001 From: Denny Date: Sun, 29 Apr 2018 06:19:39 -0400 Subject: [PATCH 64/87] [WIP] Work continues on resolving issues with the 2-channel synthesizer that is currently up/down converting the signal of interest, but the synthesizer is not correctly combining the original two channels. Some experimentation with the tap count for the channelizer indicates that we can cut the tap count in half and still have decent decoding on the P25 decoder when paired to an aligned channelized output. The 2-channel synthesized output decodes partially, but needs more work. Resolved issue with channel gain in the channelizer that was making the sample values so small that they were registering as zero when converted from float to 16-bit int value in the complex wave recorder. --- .../dsheirer/dsp/filter/FilterFactory.java | 36 +- .../channelizer/PolyphaseChannelManager.java | 7 +- .../channelizer/PolyphaseChannelSource.java | 6 +- .../channelizer/TwoChannelSynthesizerM2.java | 2 +- .../output/TwoChannelOutputProcessor.java | 47 +- .../dsp/filter/design/FilterViewer.java | 40 +- .../filter/fir/complex/ComplexFIRFilter.java | 24 +- .../dsp/filter/fir/real/RealFIRFilter.java | 19 +- .../dsheirer/dsp/mixer/FS4DownConverter.java | 8 +- .../channelizer/ChannelizerViewer.java | 7 +- .../gui/channelizer/ChannelizerViewer2.java | 467 ++++++++++++++++++ .../channelizer/SynthesizerViewer.java | 3 +- .../dsheirer/module/ProcessingChain.java | 20 +- .../dsheirer/record/RecorderManager.java | 3 +- .../wave/ComplexBufferWaveRecorder.java | 191 ++++--- .../dsheirer/record/wave/WaveWriter.java | 1 - .../github/dsheirer/source/SourceEvent.java | 39 +- .../tuner/channel/TunerChannelSource.java | 1 - .../tuner/test/TestTunerController.java | 2 +- 19 files changed, 764 insertions(+), 159 deletions(-) rename src/main/java/io/github/dsheirer/{dsp/filter => gui}/channelizer/ChannelizerViewer.java (98%) create mode 100644 src/main/java/io/github/dsheirer/gui/channelizer/ChannelizerViewer2.java rename src/main/java/io/github/dsheirer/{dsp/filter => gui}/channelizer/SynthesizerViewer.java (99%) diff --git a/src/main/java/io/github/dsheirer/dsp/filter/FilterFactory.java b/src/main/java/io/github/dsheirer/dsp/filter/FilterFactory.java index a0447635..1438104f 100644 --- a/src/main/java/io/github/dsheirer/dsp/filter/FilterFactory.java +++ b/src/main/java/io/github/dsheirer/dsp/filter/FilterFactory.java @@ -121,6 +121,32 @@ public class FilterFactory return coefficients; } + /** + * Normalizes all filter coefficients to achieve an overall objective gain, by ensuring + * that the sum of the absolute value of all coefficients adds up to the objective. + * + * @param coefficients + * @return + */ + public static float[] normalize(float[] coefficients, float objective) + { + float accumulator = 0; + + for(int x = 0; x < coefficients.length; x++) + { + accumulator += Math.abs(coefficients[x]); + } + + accumulator /= objective; + + for(int x = 0; x < coefficients.length; x++) + { + coefficients[x] = coefficients[x] / accumulator; + } + + return coefficients; + } + /** * Constructs an array of unity (1) response values representing the * desired (pre-windowing) frequency response, used by the sync function @@ -740,8 +766,11 @@ public class FilterFactory public static float[] getSincM2Channelizer(double channelBandwidth, int channels, int tapsPerChannel, Window.WindowType windowType, boolean logResults) throws FilterDesignException { - return getSincFilter(channelBandwidth * channels, channelBandwidth, + float[] filter = getSincFilter(channelBandwidth * channels, channelBandwidth, channels, tapsPerChannel, windowType,logResults); + + //Increase the overall gain of the filter to an objective of 1.0 times the number of channels + return normalize(filter, 1.0f * (float)channels); } /** @@ -759,8 +788,11 @@ public class FilterFactory public static float[] getSincM2Synthesizer(double channelBandwidth, int channels, int tapsPerChannel, Window.WindowType windowType, boolean logResults) throws FilterDesignException { - return getSincFilter(channelBandwidth * channels, channelBandwidth, channels, + float[] filter = getSincFilter(channelBandwidth * channels, channelBandwidth, channels, tapsPerChannel, windowType,logResults); + + //Decrease the overall gain of the filter to an objective of 1.0 divided by the number of channels + return normalize(filter, 1.0f);// / (float)channels); } /** diff --git a/src/main/java/io/github/dsheirer/dsp/filter/channelizer/PolyphaseChannelManager.java b/src/main/java/io/github/dsheirer/dsp/filter/channelizer/PolyphaseChannelManager.java index 660be28d..2e3af921 100644 --- a/src/main/java/io/github/dsheirer/dsp/filter/channelizer/PolyphaseChannelManager.java +++ b/src/main/java/io/github/dsheirer/dsp/filter/channelizer/PolyphaseChannelManager.java @@ -50,7 +50,7 @@ public class PolyphaseChannelManager implements ISourceEventProcessor private final static Logger mLog = LoggerFactory.getLogger(PolyphaseChannelManager.class); private static final double MINIMUM_CHANNEL_BANDWIDTH = 12500.0; private static final double CHANNEL_OVERSAMPLING = 2.0; - private static final int POLYPHASE_FILTER_TAPS_PER_CHANNEL = 16; + private static final int POLYPHASE_FILTER_TAPS_PER_CHANNEL = 8; private Broadcaster mSourceEventBroadcaster = new Broadcaster<>(); private IReusableBufferProvider mReusableBufferProvider; @@ -370,7 +370,7 @@ public class PolyphaseChannelManager implements ISourceEventProcessor //Broadcast the new channel sample rate double updatedChannelSampleRate = mChannelCalculator.getChannelSampleRate(); - updateOutputProcessors(SourceEvent.sampleRateChange(updatedChannelSampleRate)); + updateOutputProcessors(SourceEvent.sampleRateChange(updatedChannelSampleRate, "Channelizer Sample Rate Update")); } else { @@ -514,7 +514,7 @@ public class PolyphaseChannelManager implements ISourceEventProcessor if(taps == null) { taps = FilterFactory.getSincM2Synthesizer(mChannelCalculator.getChannelBandwidth(), channels, - POLYPHASE_FILTER_TAPS_PER_CHANNEL, Window.WindowType.BLACKMAN_HARRIS_7, true); + POLYPHASE_FILTER_TAPS_PER_CHANNEL + 20, Window.WindowType.BLACKMAN_HARRIS_7, true); mOutputProcessorFilters.put(channels, taps); } @@ -530,6 +530,7 @@ public class PolyphaseChannelManager implements ISourceEventProcessor @Override public void receive(SourceEvent sourceEvent) { + mLog.debug("****** Got a source event: " + sourceEvent.toString()); switch(sourceEvent.getEvent()) { case REQUEST_START_SAMPLE_STREAM: diff --git a/src/main/java/io/github/dsheirer/dsp/filter/channelizer/PolyphaseChannelSource.java b/src/main/java/io/github/dsheirer/dsp/filter/channelizer/PolyphaseChannelSource.java index 19a09b36..4e41ffbe 100644 --- a/src/main/java/io/github/dsheirer/dsp/filter/channelizer/PolyphaseChannelSource.java +++ b/src/main/java/io/github/dsheirer/dsp/filter/channelizer/PolyphaseChannelSource.java @@ -199,7 +199,6 @@ public class PolyphaseChannelSource extends TunerChannelSource @Override protected void setFrequency(long frequency) { - mLog.debug("Index Center Frequency set to: " + frequency + " for channel " + getTunerChannel().toString()); mIndexCenterFrequency = frequency; //Set frequency correction to zero to trigger an update to the mixer and allow downstream monitors to @@ -218,11 +217,12 @@ public class PolyphaseChannelSource extends TunerChannelSource } /** - * Calculates the frequency offset required to mix the incoming signal to the desired output baseband + * Calculates the frequency offset required to mix the incoming signal to center the desired frequency + * within the channel */ private long getFrequencyOffset() { - return getTunerChannel().getFrequency() - mIndexCenterFrequency + mChannelFrequencyCorrection; + return mIndexCenterFrequency - getTunerChannel().getFrequency() + mChannelFrequencyCorrection; } /** diff --git a/src/main/java/io/github/dsheirer/dsp/filter/channelizer/TwoChannelSynthesizerM2.java b/src/main/java/io/github/dsheirer/dsp/filter/channelizer/TwoChannelSynthesizerM2.java index 093f682b..d0967a3e 100644 --- a/src/main/java/io/github/dsheirer/dsp/filter/channelizer/TwoChannelSynthesizerM2.java +++ b/src/main/java/io/github/dsheirer/dsp/filter/channelizer/TwoChannelSynthesizerM2.java @@ -148,7 +148,7 @@ public class TwoChannelSynthesizerM2 try { - float[] taps = FilterFactory.getSincM2Channelizer(12500.0, 2, 19, Window.WindowType.BLACKMAN_HARRIS_7, true); + float[] taps = FilterFactory.getSincM2Synthesizer(12500.0, 2, 19, Window.WindowType.BLACKMAN_HARRIS_7, true); TwoChannelSynthesizerM2 synthesizer = new TwoChannelSynthesizerM2(taps); diff --git a/src/main/java/io/github/dsheirer/dsp/filter/channelizer/output/TwoChannelOutputProcessor.java b/src/main/java/io/github/dsheirer/dsp/filter/channelizer/output/TwoChannelOutputProcessor.java index 44f9f8ea..f7c3a635 100644 --- a/src/main/java/io/github/dsheirer/dsp/filter/channelizer/output/TwoChannelOutputProcessor.java +++ b/src/main/java/io/github/dsheirer/dsp/filter/channelizer/output/TwoChannelOutputProcessor.java @@ -16,6 +16,10 @@ package io.github.dsheirer.dsp.filter.channelizer.output; import io.github.dsheirer.dsp.filter.channelizer.TwoChannelSynthesizerM2; +import io.github.dsheirer.dsp.filter.design.FilterDesignException; +import io.github.dsheirer.dsp.filter.fir.FIRFilterSpecification; +import io.github.dsheirer.dsp.filter.fir.complex.ComplexFIRFilter; +import io.github.dsheirer.dsp.filter.fir.remez.RemezFIRFilterDesigner; import io.github.dsheirer.dsp.mixer.FS4DownConverter; import io.github.dsheirer.sample.buffer.ReusableBufferAssembler; import io.github.dsheirer.sample.buffer.ReusableChannelResultsBuffer; @@ -26,10 +30,43 @@ import java.util.List; public class TwoChannelOutputProcessor extends ChannelOutputProcessor { + private final static Logger mLog = LoggerFactory.getLogger(TwoChannelOutputProcessor.class); + + + private static float[] mFilterCoefficients; + + static + { + try + { + FIRFilterSpecification specification = FIRFilterSpecification.lowPassBuilder() + .sampleRate(25000) + .gridDensity(16) + .oddLength(true) + .passBandCutoff(6000) + .passBandAmplitude(1.0) + .passBandRipple(0.01) + .stopBandStart(6500) + .stopBandAmplitude(0.0) + .stopBandRipple(0.005) //Approximately 94 dB attenuation + .build(); + + RemezFIRFilterDesigner designer = new RemezFIRFilterDesigner(specification); + + if(designer.isValid()) + { + mFilterCoefficients = designer.getImpulseResponse(); + } + } + catch(FilterDesignException fde) + { + mLog.error("Error designing low pass filter for two channel output processor", fde); + } + } + private TwoChannelSynthesizerM2 mSynthesizer; private FS4DownConverter mFS4DownConverter = new FS4DownConverter(); - - private final static Logger mLog = LoggerFactory.getLogger(TwoChannelOutputProcessor.class); + private ComplexFIRFilter mLowPassFilter; private int mChannelOffset1; private int mChannelOffset2; @@ -45,9 +82,11 @@ public class TwoChannelOutputProcessor extends ChannelOutputProcessor { //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); + super(2, sampleRate); setPolyphaseChannelIndices(channelIndexes); setSynthesisFilter(filter); + + mLowPassFilter = new ComplexFIRFilter(mFilterCoefficients, 1.0f); } /** @@ -112,6 +151,8 @@ public class TwoChannelOutputProcessor extends ChannelOutputProcessor //Apply offset and frequency correction to center the signal of interest within the synthesized channel getFrequencyCorrectionMixer().mixComplex(synthesized); + mLowPassFilter.filter(synthesized); + reusableBufferAssembler.receive(synthesized); buffer.decrementUserCount(); diff --git a/src/main/java/io/github/dsheirer/dsp/filter/design/FilterViewer.java b/src/main/java/io/github/dsheirer/dsp/filter/design/FilterViewer.java index 22354565..9fce71d0 100644 --- a/src/main/java/io/github/dsheirer/dsp/filter/design/FilterViewer.java +++ b/src/main/java/io/github/dsheirer/dsp/filter/design/FilterViewer.java @@ -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; @@ -48,7 +49,7 @@ public class FilterViewer extends Application .passBandRipple(0.01) .stopBandStart(6500) .stopBandAmplitude(0.0) - .stopBandRipple(0.028) //Approximately 60 dB attenuation + .stopBandRipple(0.005) //Approximately 94 dB attenuation .build(); // FIRFilterSpecification specification = FIRFilterSpecification.highPassBuilder() @@ -73,29 +74,32 @@ public class FilterViewer extends Application float[] taps = null; -// try -// { -// taps = FilterFactory.getSincM2Channelizer(12500.0, 2, 19, Window.WindowType.BLACKMAN_HARRIS_7, true); -// } -// catch(FilterDesignException fde) -// { -// mLog.error("Error"); -// } - try { - RemezFIRFilterDesigner designer = new RemezFIRFilterDesigner(specification); - - if(designer.isValid()) - { - taps = designer.getImpulseResponse(); - } +// taps = FilterFactory.getSincM2Synthesizer(12500.0, 2, 19, Window.WindowType.BLACKMAN_HARRIS_7, true); + taps = FilterFactory.getSincFilter(25000.0, 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; } } diff --git a/src/main/java/io/github/dsheirer/dsp/filter/fir/complex/ComplexFIRFilter.java b/src/main/java/io/github/dsheirer/dsp/filter/fir/complex/ComplexFIRFilter.java index ece55ce4..c4e3a909 100644 --- a/src/main/java/io/github/dsheirer/dsp/filter/fir/complex/ComplexFIRFilter.java +++ b/src/main/java/io/github/dsheirer/dsp/filter/fir/complex/ComplexFIRFilter.java @@ -26,6 +26,7 @@ public class ComplexFIRFilter extends FIRFilter private ReusableComplexBufferQueue mReusableComplexBufferQueue = new ReusableComplexBufferQueue("Complex FIR Filter"); private RealFIRFilter mIFilter; private RealFIRFilter mQFilter; + private float[] mTemporaryBuffer; /** * Complex FIR Filter for processing complex sample pairs. Wraps two real @@ -50,11 +51,21 @@ public class ComplexFIRFilter extends FIRFilter return mIFilter.filter(sample); } + public float currentInphaseValue() + { + return mIFilter.currentValue(); + } + public float filterQuadrature(float sample) { return mQFilter.filter(sample); } + public float currentQuadratureValue() + { + return mQFilter.currentValue(); + } + public Complex filter(Complex sample) { float i = filterInphase(sample.inphase()); @@ -65,15 +76,20 @@ public class ComplexFIRFilter extends FIRFilter public float[] filter(float[] samples) { - float[] filteredSamples = new float[samples.length]; + if(mTemporaryBuffer == null || mTemporaryBuffer.length != samples.length) + { + mTemporaryBuffer = new float[samples.length]; + } + + System.arraycopy(samples, 0, mTemporaryBuffer, 0, samples.length); for(int x = 0; x < samples.length; x += 2) { - filteredSamples[x] = filterInphase(samples[x]); - filteredSamples[x + 1] = filterQuadrature(samples[x + 1]); + samples[x] = filterInphase(mTemporaryBuffer[x]); + samples[x + 1] = filterQuadrature(mTemporaryBuffer[x + 1]); } - return filteredSamples; + return samples; } /** diff --git a/src/main/java/io/github/dsheirer/dsp/filter/fir/real/RealFIRFilter.java b/src/main/java/io/github/dsheirer/dsp/filter/fir/real/RealFIRFilter.java index ec019bdc..1666de80 100644 --- a/src/main/java/io/github/dsheirer/dsp/filter/fir/real/RealFIRFilter.java +++ b/src/main/java/io/github/dsheirer/dsp/filter/fir/real/RealFIRFilter.java @@ -26,6 +26,7 @@ public class RealFIRFilter extends FIRFilter private float[] mCoefficients; private float mGain; + private float mAccumulator; /** * Float sample FIR filter base class. @@ -84,12 +85,11 @@ public class RealFIRFilter extends FIRFilter { mBuffer[mBufferPointer] = sample; - float accumulator = 0.0f; + mAccumulator = 0.0f; for(int x = 0; x < mBufferSize; x++) { - accumulator += mCoefficients[x] * - mBuffer[mIndexMap[mBufferPointer][x]]; + mAccumulator += mCoefficients[x] * mBuffer[mIndexMap[mBufferPointer][x]]; } mBufferPointer--; @@ -100,7 +100,18 @@ public class RealFIRFilter extends FIRFilter } /* Apply gain and return the filtered value */ - return accumulator * mGain; + mAccumulator *= mGain; + + return mAccumulator; + } + + /** + * Current output value for the filter after the filter() method has been invoked. + * @return + */ + public float currentValue() + { + return mAccumulator; } /** diff --git a/src/main/java/io/github/dsheirer/dsp/mixer/FS4DownConverter.java b/src/main/java/io/github/dsheirer/dsp/mixer/FS4DownConverter.java index 5c70593e..d1645698 100644 --- a/src/main/java/io/github/dsheirer/dsp/mixer/FS4DownConverter.java +++ b/src/main/java/io/github/dsheirer/dsp/mixer/FS4DownConverter.java @@ -15,17 +15,13 @@ ******************************************************************************/ package io.github.dsheirer.dsp.mixer; -import org.slf4j.Logger; -import org.slf4j.LoggerFactory; - public class FS4DownConverter { - private final static Logger mLog = LoggerFactory.getLogger(FS4DownConverter.class); - private int mPointer = 0; /** - * Performs complex down-sampling by one-fourth (-FS/4) of the sample rate using simple value assignments. + * Performs complex frequency conversion at one-fourth (-FS/4) of the sample rate using simple value + * assignments. * * See: Digital Signal Processing 3e, Lyons, p.674-675 */ diff --git a/src/main/java/io/github/dsheirer/dsp/filter/channelizer/ChannelizerViewer.java b/src/main/java/io/github/dsheirer/gui/channelizer/ChannelizerViewer.java similarity index 98% rename from src/main/java/io/github/dsheirer/dsp/filter/channelizer/ChannelizerViewer.java rename to src/main/java/io/github/dsheirer/gui/channelizer/ChannelizerViewer.java index c6d6c25f..0537bf77 100644 --- a/src/main/java/io/github/dsheirer/dsp/filter/channelizer/ChannelizerViewer.java +++ b/src/main/java/io/github/dsheirer/gui/channelizer/ChannelizerViewer.java @@ -1,7 +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 @@ -12,9 +11,9 @@ * * You should have received a copy of the GNU General Public License along with this program. * If not, see - * ********************************************************************************************************************* + * ******************************************************************************/ -package io.github.dsheirer.dsp.filter.channelizer; +package io.github.dsheirer.gui.channelizer; import io.github.dsheirer.sample.Listener; import io.github.dsheirer.sample.SampleType; diff --git a/src/main/java/io/github/dsheirer/gui/channelizer/ChannelizerViewer2.java b/src/main/java/io/github/dsheirer/gui/channelizer/ChannelizerViewer2.java new file mode 100644 index 00000000..47dfa93f --- /dev/null +++ b/src/main/java/io/github/dsheirer/gui/channelizer/ChannelizerViewer2.java @@ -0,0 +1,467 @@ +/******************************************************************************* + * 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 + * + ******************************************************************************/ +package io.github.dsheirer.gui.channelizer; + +import io.github.dsheirer.sample.Listener; +import io.github.dsheirer.sample.SampleType; +import io.github.dsheirer.sample.buffer.ReusableComplexBuffer; +import io.github.dsheirer.settings.SettingsManager; +import io.github.dsheirer.source.ISourceEventProcessor; +import io.github.dsheirer.source.SourceEvent; +import io.github.dsheirer.source.SourceException; +import io.github.dsheirer.source.tuner.Tuner; +import io.github.dsheirer.source.tuner.channel.TunerChannel; +import io.github.dsheirer.source.tuner.channel.TunerChannelSource; +import io.github.dsheirer.source.tuner.configuration.TunerConfigurationModel; +import io.github.dsheirer.source.tuner.test.TestTuner; +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 net.miginfocom.swing.MigLayout; +import org.slf4j.Logger; +import org.slf4j.LoggerFactory; + +import javax.swing.JButton; +import javax.swing.JFrame; +import javax.swing.JLabel; +import javax.swing.JOptionPane; +import javax.swing.JPanel; +import javax.swing.JSpinner; +import javax.swing.JToggleButton; +import javax.swing.SpinnerNumberModel; +import javax.swing.event.ChangeEvent; +import javax.swing.event.ChangeListener; +import java.awt.Dimension; +import java.awt.EventQueue; +import java.awt.event.ActionEvent; +import java.awt.event.ActionListener; +import java.util.ArrayList; +import java.util.Arrays; +import java.util.List; + +public class ChannelizerViewer2 extends JFrame +{ + private final static Logger mLog = LoggerFactory.getLogger(ChannelizerViewer2.class); + + private static final int CHANNEL_BANDWIDTH = 12500; + + private SettingsManager mSettingsManager = new SettingsManager(new TunerConfigurationModel()); + private JPanel mPrimaryPanel; + private JPanel mControlPanel; + private JButton mTopFrameAddChannelButton; + private JButton mBottomFrameAddChannelButton; + private JLabel mToneFrequencyLabel; + private JLabel mCenterFrequencyLabel; + private PrimarySpectrumPanel mPrimarySpectrumPanel; + private ChannelArrayPanel mTopChannelArrayPanel; + private ChannelArrayPanel mBottomChannelArrayPanel; + private DFTSize mMainPanelDFTSize = DFTSize.FFT32768; + private DFTSize mChannelPanelDFTSize = DFTSize.FFT04096; + private TestTuner mTestTuner = new TestTuner(); + + /** + * GUI Test utility for researching polyphase channelizers. + */ + public ChannelizerViewer2() + { + init(); + } + + private void init() + { + setTitle("Polyphase Channelizer Viewer"); + setSize(1200, 700); + 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]")); + mPrimaryPanel.add(getSpectrumPanel(), "wrap"); + mPrimaryPanel.add(getControlPanel(), "wrap"); + mPrimaryPanel.add(getTopChannelArrayPanel(), "wrap"); + mPrimaryPanel.add(getBottomChannelArrayPanel()); + } + + return mPrimaryPanel; + } + + private PrimarySpectrumPanel getSpectrumPanel() + { + if(mPrimarySpectrumPanel == null) + { + mPrimarySpectrumPanel = new PrimarySpectrumPanel(mSettingsManager, + mTestTuner.getTunerController().getSampleRate()); + mPrimarySpectrumPanel.setPreferredSize(new Dimension(1200, 200)); + mPrimarySpectrumPanel.setDFTSize(mMainPanelDFTSize); + mTestTuner.getTunerController().addBufferListener(mPrimarySpectrumPanel); + } + + return mPrimarySpectrumPanel; + } + + private JPanel getControlPanel() + { + if(mControlPanel == null) + { + mControlPanel = new JPanel(); + mControlPanel.setLayout(new MigLayout("insets 0 0 0 0", "", "")); + + mControlPanel.add(new JLabel("Tone:"), "align left"); + long minimumFrequency = -(long)mTestTuner.getTunerController().getSampleRate() / 2 + 1; + long maximumFrequency = (long)mTestTuner.getTunerController().getSampleRate() / 2 - 1; + long toneFrequency = 0; + + SpinnerNumberModel model = new SpinnerNumberModel(toneFrequency, minimumFrequency, maximumFrequency, + 100); + + model.addChangeListener(new ChangeListener() + { + @Override + public void stateChanged(ChangeEvent e) + { + long toneFrequency = model.getNumber().longValue(); + mTestTuner.getTunerController().setToneFrequency(toneFrequency); + mToneFrequencyLabel.setText(String.valueOf(getToneFrequency())); + mCenterFrequencyLabel.setText(String.valueOf(getCenterFrequency())); + } + }); + + JSpinner spinner = new JSpinner(model); + + mControlPanel.add(spinner); + mControlPanel.add(new JLabel("Hz")); + + mControlPanel.add(new JLabel("Tone Frequency:"), "push,align right"); + mToneFrequencyLabel = new JLabel(String.valueOf(getToneFrequency())); + mControlPanel.add(mToneFrequencyLabel, "push,align left"); + + mControlPanel.add(new JLabel("Center Frequency:"), "push,align right"); + mCenterFrequencyLabel = new JLabel(String.valueOf(getCenterFrequency())); + mControlPanel.add(mCenterFrequencyLabel, "push,align left"); + + mControlPanel.add(getBottomFrameAddChannelButton(), "push,align right"); + mControlPanel.add(getTopFrameAddChannelButton(), "push,align right"); + } + + return mControlPanel; + } + + private JButton getTopFrameAddChannelButton() + { + if(mTopFrameAddChannelButton == null) + { + mTopFrameAddChannelButton = new JButton("Top - Add Channel"); + + mTopFrameAddChannelButton.addActionListener(new ActionListener() + { + @Override + public void actionPerformed(ActionEvent e) + { + String value = JOptionPane.showInputDialog(ChannelizerViewer2.this, "Frequency?"); + + if(value != null && !value.isEmpty()) + { + try + { + int frequency = Integer.parseInt(value); + + TunerChannel tunerChannel = new TunerChannel(frequency, CHANNEL_BANDWIDTH); + + getTopChannelArrayPanel().addChannel(tunerChannel); + } + catch(Exception exception) + { + mLog.error("Can't parse frequency from value: " + value); + } + } + } + }); + } + + return mTopFrameAddChannelButton; + } + + private JButton getBottomFrameAddChannelButton() + { + if(mBottomFrameAddChannelButton == null) + { + mBottomFrameAddChannelButton = new JButton("Bottom - Add Channel"); + + mBottomFrameAddChannelButton.addActionListener(new ActionListener() + { + @Override + public void actionPerformed(ActionEvent e) + { + String value = JOptionPane.showInputDialog(ChannelizerViewer2.this, "Frequency?"); + + if(value != null && !value.isEmpty()) + { + try + { + int frequency = Integer.parseInt(value); + + TunerChannel tunerChannel = new TunerChannel(frequency, CHANNEL_BANDWIDTH); + + getBottomChannelArrayPanel().addChannel(tunerChannel); + } + catch(Exception exception) + { + mLog.error("Can't parse frequency from value: " + value); + } + } + } + }); + } + + return mBottomFrameAddChannelButton; + } + + private long getToneFrequency() + { + return mTestTuner.getTunerController().getFrequency() + mTestTuner.getTunerController().getToneFrequency(); + } + + private long getCenterFrequency() + { + return mTestTuner.getTunerController().getFrequency(); + } + + private ChannelArrayPanel getTopChannelArrayPanel() + { + if(mTopChannelArrayPanel == null) + { + mTopChannelArrayPanel = new ChannelArrayPanel(); + } + + return mTopChannelArrayPanel; + } + + private ChannelArrayPanel getBottomChannelArrayPanel() + { + if(mBottomChannelArrayPanel == null) + { + mBottomChannelArrayPanel = new ChannelArrayPanel(); + } + + return mBottomChannelArrayPanel; + } + + public class ChannelArrayPanel extends JPanel + { + public ChannelArrayPanel() + { + init(); + } + + private void init() + { + setLayout(new MigLayout("insets 0 0 0 0", "fill", "fill")); + } + + public void addChannel(TunerChannel tunerChannel) + { + ChannelPanel channelPanel = new ChannelPanel(mSettingsManager, CHANNEL_BANDWIDTH * 2, + tunerChannel.getFrequency(), CHANNEL_BANDWIDTH); + channelPanel.setDFTSize(mChannelPanelDFTSize); + + add(channelPanel, "grow,push"); + + validate(); + repaint(); + } + } + + /** + * Returns a list of tuner channels that will fit within the tuner's bandwidth, minus a half channel each at the + * lower and upper ends of the spectrum. + * + * @param tuner to create channels for + * @return list of contiguous channels filling the tuner bandwidth + */ + public static List getTunerChannels(Tuner tuner) + { + List tunerChannels = new ArrayList<>(); + + long baseFrequency = tuner.getTunerController().getFrequency(); + baseFrequency -= tuner.getTunerController().getSampleRate() / 2; + baseFrequency += (CHANNEL_BANDWIDTH / 2); + + int channelCount = (int)(tuner.getTunerController().getSampleRate() / CHANNEL_BANDWIDTH) - 1; + + for(int x = 0; x < channelCount; x++) + { + long frequency = baseFrequency + (x * CHANNEL_BANDWIDTH); + TunerChannel tunerChannel = new TunerChannel(frequency, CHANNEL_BANDWIDTH); + tunerChannels.add(tunerChannel); + } + + return tunerChannels; + } + + public class PrimarySpectrumPanel extends JPanel implements Listener, ISourceEventProcessor + { + private DFTProcessor mDFTProcessor = new DFTProcessor(SampleType.COMPLEX); + private ComplexDecibelConverter mComplexDecibelConverter = new ComplexDecibelConverter(); + private SpectrumPanel mSpectrumPanel; + + public PrimarySpectrumPanel(SettingsManager settingsManager, double sampleRate) + { + setLayout(new MigLayout("insets 0 0 0 0", "[grow,fill]", "[grow,fill]")); + mSpectrumPanel = new SpectrumPanel(settingsManager); + mSpectrumPanel.setSampleSize(16); + add(mSpectrumPanel); + + mDFTProcessor.addConverter(mComplexDecibelConverter); + mDFTProcessor.process(SourceEvent.sampleRateChange(sampleRate)); + mComplexDecibelConverter.addListener(mSpectrumPanel); + } + + public void setDFTSize(DFTSize dftSize) + { + mDFTProcessor.setDFTSize(dftSize); + } + + @Override + public void receive(ReusableComplexBuffer reusableComplexBuffer) + { + mDFTProcessor.receive(reusableComplexBuffer); + } + + @Override + public void process(SourceEvent event) throws SourceException + { + mLog.debug("Source Event! Add handler support for this to channelizer viewer"); + } + } + + public class ChannelPanel extends JPanel implements Listener, ISourceEventProcessor + { + private TunerChannelSource mSource; + private DFTProcessor mDFTProcessor = new DFTProcessor(SampleType.COMPLEX); + private ComplexDecibelConverter mComplexDecibelConverter = new ComplexDecibelConverter(); + private SpectrumPanel mSpectrumPanel; + private JToggleButton mLoggingButton; + private boolean mLoggingEnabled; + + public ChannelPanel(SettingsManager settingsManager, double sampleRate, long frequency, int bandwidth) + { + setLayout(new MigLayout("insets 0 0 0 0", "[grow,fill][grow,fill][grow,fill]", "[grow,fill][]")); + mSpectrumPanel = new SpectrumPanel(settingsManager); + mSpectrumPanel.setSampleSize(16); + add(mSpectrumPanel, "span"); + + mDFTProcessor.addConverter(mComplexDecibelConverter); + mDFTProcessor.process(SourceEvent.sampleRateChange(sampleRate)); + mComplexDecibelConverter.addListener(mSpectrumPanel); + + TunerChannel tunerChannel = new TunerChannel(frequency, bandwidth); + mSource = mTestTuner.getChannelSourceManager().getSource(tunerChannel); + + if(mSource != null) + { + mSource.setListener(new Listener() + { + @Override + public void receive(ReusableComplexBuffer complexBuffer) + { + if(mLoggingEnabled) + { + mLog.debug("Samples:" + Arrays.toString(complexBuffer.getSamples())); + } + + complexBuffer.incrementUserCount(); + mDFTProcessor.receive(complexBuffer); + complexBuffer.decrementUserCount(); + } + }); + + mSource.start(); + } + else + { + mLog.error("Couldn't get a source from the tuner for frequency: " + frequency); + } + + if(mSource != null) + { + int half = (int)(sampleRate / 2.0f); + add(new JLabel("Min:" + (frequency - half)), "align left"); + add(new JLabel("Center:" + frequency)); + add(new JLabel("Max:" + (frequency + half)), "align right"); + } + else + { + add(new JLabel("NO SRC:" + frequency)); + } + + mLoggingButton = new JToggleButton("Logging"); + mLoggingButton.addActionListener(new ActionListener() + { + @Override + public void actionPerformed(ActionEvent e) + { + mLoggingEnabled = mLoggingButton.isSelected(); + } + }); + } + + public TunerChannelSource getSource() + { + return mSource; + } + + public void setDFTSize(DFTSize dftSize) + { + mDFTProcessor.setDFTSize(dftSize); + } + + @Override + public void receive(ReusableComplexBuffer reusableComplexBuffer) + { + reusableComplexBuffer.incrementUserCount(); + mDFTProcessor.receive(reusableComplexBuffer); + reusableComplexBuffer.decrementUserCount(); + } + + @Override + public void process(SourceEvent event) throws SourceException + { + mLog.debug("Source Event! Add handler support for this to channelizer viewer"); + } + } + + public static void main(String[] args) + { + final ChannelizerViewer2 frame = new ChannelizerViewer2(); + + EventQueue.invokeLater(new Runnable() + { + @Override + public void run() + { + frame.setVisible(true); + } + }); + } +} \ No newline at end of file diff --git a/src/main/java/io/github/dsheirer/dsp/filter/channelizer/SynthesizerViewer.java b/src/main/java/io/github/dsheirer/gui/channelizer/SynthesizerViewer.java similarity index 99% rename from src/main/java/io/github/dsheirer/dsp/filter/channelizer/SynthesizerViewer.java rename to src/main/java/io/github/dsheirer/gui/channelizer/SynthesizerViewer.java index ff8ae0d9..22b50402 100644 --- a/src/main/java/io/github/dsheirer/dsp/filter/channelizer/SynthesizerViewer.java +++ b/src/main/java/io/github/dsheirer/gui/channelizer/SynthesizerViewer.java @@ -13,10 +13,11 @@ * If not, see * ******************************************************************************/ -package io.github.dsheirer.dsp.filter.channelizer; +package io.github.dsheirer.gui.channelizer; import io.github.dsheirer.dsp.filter.FilterFactory; import io.github.dsheirer.dsp.filter.Window; +import io.github.dsheirer.dsp.filter.channelizer.TwoChannelSynthesizerM2; import io.github.dsheirer.dsp.filter.design.FilterDesignException; import io.github.dsheirer.dsp.mixer.FS4DownConverter; import io.github.dsheirer.dsp.mixer.IOscillator; diff --git a/src/main/java/io/github/dsheirer/module/ProcessingChain.java b/src/main/java/io/github/dsheirer/module/ProcessingChain.java index 5beafede..25197f1b 100644 --- a/src/main/java/io/github/dsheirer/module/ProcessingChain.java +++ b/src/main/java/io/github/dsheirer/module/ProcessingChain.java @@ -564,12 +564,18 @@ public class ProcessingChain implements IChannelEventListener { if(mSource != null) { - /* Reset each of the modules */ + //Reset each of the modules for(Module module : mModules) { module.reset(); } + //Broadcast the source sample rate so that each of the modules can self-configure + mSourceEventBroadcaster.broadcast(SourceEvent.sampleRateChange(mSource.getSampleRate(), "Processing Chain Startup")); + + //Broadcast the source center frequencysample rate so that each of the modules can self-configure + mSourceEventBroadcaster.broadcast(SourceEvent.frequencyChange(mSource.getFrequency(), "Processing Chain Startup")); + //Setup the channel state to monitor source overflow conditions mSource.setOverflowListener(mChannelState); @@ -616,12 +622,6 @@ public class ProcessingChain implements IChannelEventListener { if(mRunning.compareAndSet(true, false)) { - /* Stop each of the modules */ - for(Module module : mModules) - { - module.stop(); - } - if(mSource != null) { removeModule(mSource); @@ -642,6 +642,12 @@ public class ProcessingChain implements IChannelEventListener } mSource = null; + } + + /* Stop each of the remaining modules */ + for(Module module : mModules) + { + module.stop(); } } } diff --git a/src/main/java/io/github/dsheirer/record/RecorderManager.java b/src/main/java/io/github/dsheirer/record/RecorderManager.java index 60dacef2..1d6e37c0 100644 --- a/src/main/java/io/github/dsheirer/record/RecorderManager.java +++ b/src/main/java/io/github/dsheirer/record/RecorderManager.java @@ -41,8 +41,7 @@ public class RecorderManager implements Listener { private static final Logger mLog = LoggerFactory.getLogger(RecorderManager.class); - public static final int AUDIO_SAMPLE_RATE = 8000; - public static final int BASEBAND_SAMPLE_RATE = 48000; + public static final float BASEBAND_SAMPLE_RATE = 25000.0f; //Default sample rate - source can override public static final long IDLE_RECORDER_REMOVAL_THRESHOLD = 6000; //6 seconds private Map mRecorders = new HashMap<>(); diff --git a/src/main/java/io/github/dsheirer/record/wave/ComplexBufferWaveRecorder.java b/src/main/java/io/github/dsheirer/record/wave/ComplexBufferWaveRecorder.java index ff68d8fb..977f2588 100644 --- a/src/main/java/io/github/dsheirer/record/wave/ComplexBufferWaveRecorder.java +++ b/src/main/java/io/github/dsheirer/record/wave/ComplexBufferWaveRecorder.java @@ -15,12 +15,14 @@ ******************************************************************************/ package io.github.dsheirer.record.wave; +import io.github.dsheirer.dsp.filter.channelizer.ContinuousReusableBufferProcessor; import io.github.dsheirer.module.Module; import io.github.dsheirer.sample.ConversionUtils; import io.github.dsheirer.sample.Listener; -import io.github.dsheirer.sample.buffer.IReusableBufferDisposedListener; import io.github.dsheirer.sample.buffer.IReusableComplexBufferListener; import io.github.dsheirer.sample.buffer.ReusableComplexBuffer; +import io.github.dsheirer.source.ISourceEventListener; +import io.github.dsheirer.source.SourceEvent; import io.github.dsheirer.util.ThreadPool; import io.github.dsheirer.util.TimeStamp; import org.slf4j.Logger; @@ -30,33 +32,45 @@ import javax.sound.sampled.AudioFormat; import java.io.IOException; import java.nio.file.Path; import java.nio.file.Paths; -import java.util.concurrent.LinkedBlockingQueue; -import java.util.concurrent.ScheduledFuture; +import java.util.List; import java.util.concurrent.TimeUnit; import java.util.concurrent.atomic.AtomicBoolean; /** * WAVE audio recorder module for recording complex (I&Q) samples to a wave file */ -public class ComplexBufferWaveRecorder extends Module implements IReusableComplexBufferListener, Listener +public class ComplexBufferWaveRecorder extends Module implements IReusableComplexBufferListener, + Listener, ISourceEventListener { private final static Logger mLog = LoggerFactory.getLogger(ComplexBufferWaveRecorder.class); - private WaveWriter mWriter; + private ContinuousReusableBufferProcessor mBufferProcessor = + new ContinuousReusableBufferProcessor<>(500, 50); + + private AtomicBoolean mRunning = new AtomicBoolean(); + private ReusableBufferWaveWriter mWriter; private String mFilePrefix; private Path mFile; private AudioFormat mAudioFormat; - private BufferProcessor mBufferProcessor; - private ScheduledFuture mProcessorHandle; - private LinkedBlockingQueue mBuffers = new LinkedBlockingQueue<>(500); - - private AtomicBoolean mRunning = new AtomicBoolean(); - - public ComplexBufferWaveRecorder(int sampleRate, String filePrefix) + public ComplexBufferWaveRecorder(float sampleRate, String filePrefix) { - mAudioFormat = new AudioFormat(sampleRate, 16, 2, true, false); mFilePrefix = filePrefix; + setSampleRate(sampleRate); + } + + public void setSampleRate(float sampleRate) + { + if(mAudioFormat == null || mAudioFormat.getSampleRate() != sampleRate) + { + mAudioFormat = new AudioFormat(sampleRate, 16, 2, true, false); + + if(mRunning.get()) + { + stop(); + start(); + } + } } public Path getFile() @@ -68,11 +82,6 @@ public class ComplexBufferWaveRecorder extends Module implements IReusableComple { if(mRunning.compareAndSet(false, true)) { - if(mBufferProcessor == null) - { - mBufferProcessor = new BufferProcessor(); - } - try { StringBuilder sb = new StringBuilder(); @@ -83,11 +92,10 @@ public class ComplexBufferWaveRecorder extends Module implements IReusableComple mFile = Paths.get(sb.toString()); - mWriter = new WaveWriter(mAudioFormat, mFile); + mWriter = new ReusableBufferWaveWriter(mAudioFormat, mFile); - /* Schedule the processor to run every 500 milliseconds */ - mProcessorHandle = ThreadPool.SCHEDULED.scheduleAtFixedRate( - mBufferProcessor, 0, 500, TimeUnit.MILLISECONDS); + mBufferProcessor.setListener(mWriter); + mBufferProcessor.start(); } catch(IOException io) { @@ -100,25 +108,40 @@ public class ComplexBufferWaveRecorder extends Module implements IReusableComple { if(mRunning.compareAndSet(true, false)) { - receive(new PoisonPill()); + if(mBufferProcessor != null) + { + mBufferProcessor.stop(); + mBufferProcessor.setListener(null); + } + + if(mWriter != null) + { + //Thread this operation so that it doesn't tie up the calling thread. The wave writer + //close method will also rename the file and this can sometimes take a few seconds. + ThreadPool.SCHEDULED.schedule(new Runnable() + { + @Override + public void run() + { + try + { + mWriter.close(); + } + catch(IOException ioe) + { + mLog.error("Error closing baseband I/Q recorder", ioe); + } + } + }, 0, TimeUnit.MILLISECONDS); + } } } @Override public void receive(ReusableComplexBuffer buffer) { - if(mRunning.get()) - { - boolean success = mBuffers.offer(buffer); - - if(!success) - { - mLog.error("recorder buffer overflow - purging [" + - mFile.toFile().getAbsolutePath() + "]"); - - mBuffers.clear(); - } - } + //Queue the buffer with the buffer processor so that recording occurs on the buffer processor thread + mBufferProcessor.receive(buffer); } @Override @@ -138,83 +161,57 @@ public class ComplexBufferWaveRecorder extends Module implements IReusableComple { } - public class BufferProcessor implements Runnable + @Override + public Listener getSourceEventListener() { - public void run() + return new Listener() { - try + @Override + public void receive(SourceEvent sourceEvent) { - ReusableComplexBuffer reusableComplexBuffer = mBuffers.poll(); - - while(reusableComplexBuffer != null) + switch(sourceEvent.getEvent()) { - if(reusableComplexBuffer instanceof PoisonPill) - { - //Clear the buffer queue - reusableComplexBuffer = mBuffers.poll(); - - while(reusableComplexBuffer != null) - { - reusableComplexBuffer.decrementUserCount(); - reusableComplexBuffer = mBuffers.poll(); - } - - if(mWriter != null) - { - try - { - mWriter.close(); - mWriter = null; - } - catch(IOException io) - { - mLog.error("Error stopping complex wave recorder [" + getFile() + "]", io); - } - } - - mFile = null; - - if(mProcessorHandle != null) - { - mProcessorHandle.cancel(true); - } - - mProcessorHandle = null; - } - else - { - mWriter.writeData(ConversionUtils.convertToSigned16BitSamples(reusableComplexBuffer)); - reusableComplexBuffer.decrementUserCount(); - reusableComplexBuffer = mBuffers.poll(); - } + case NOTIFICATION_SAMPLE_RATE_CHANGE: + setSampleRate(sourceEvent.getValue().floatValue()); + break; } } - catch(IOException ioe) - { - /* Stop this module if/when we get an IO exception */ - mBuffers.clear(); - stop(); - - mLog.error("IOException while trying to write to the wave writer", ioe); - } - } + }; } /** - * This is used as a sentinel value to signal the buffer processor to end + * Wave writer implementation for reusable complex buffers delivered from buffer processor */ - public class PoisonPill extends ReusableComplexBuffer + public class ReusableBufferWaveWriter extends WaveWriter implements Listener> { - public PoisonPill() + public ReusableBufferWaveWriter(AudioFormat format, Path file) throws IOException { - super(new IReusableBufferDisposedListener() + super(format, file); + } + + @Override + public void receive(List reusableComplexBuffers) + { + boolean error = false; + + for(ReusableComplexBuffer reusableComplexBuffer: reusableComplexBuffers) { - @Override - public void disposed(ReusableComplexBuffer reusableComplexBuffer) + if(!error) { - //no-op + try + { + mWriter.writeData(ConversionUtils.convertToSigned16BitSamples(reusableComplexBuffer)); + } + catch(IOException ioe) + { + mLog.error("IOException while writing I/Q buffers to wave recorder - stopping recorder", ioe); + error = true; + stop(); + } } - }, new float[2]); + + reusableComplexBuffer.decrementUserCount(); + } } } } diff --git a/src/main/java/io/github/dsheirer/record/wave/WaveWriter.java b/src/main/java/io/github/dsheirer/record/wave/WaveWriter.java index 15c9abaa..9a7dd5f3 100644 --- a/src/main/java/io/github/dsheirer/record/wave/WaveWriter.java +++ b/src/main/java/io/github/dsheirer/record/wave/WaveWriter.java @@ -133,7 +133,6 @@ public class WaveWriter implements AutoCloseable { mFileChannel.force(true); mFileChannel.close(); - mFileChannel = null; rename(); } diff --git a/src/main/java/io/github/dsheirer/source/SourceEvent.java b/src/main/java/io/github/dsheirer/source/SourceEvent.java index c8b6996e..405fbb85 100644 --- a/src/main/java/io/github/dsheirer/source/SourceEvent.java +++ b/src/main/java/io/github/dsheirer/source/SourceEvent.java @@ -49,6 +49,17 @@ public class SourceEvent private Event mEvent; private Number mValue; private Source mSource; + private String mEventDescription; + + /** + * Private constructor. Use the static constructor methods to create an event. + */ + private SourceEvent(Event event, Number value, String eventDescription) + { + mEvent = event; + mValue = value; + mEventDescription = eventDescription; + } /** * Private constructor. Use the static constructor methods to create an event. @@ -147,6 +158,17 @@ public class SourceEvent return new SourceEvent(Event.NOTIFICATION_FREQUENCY_CHANGE, frequency); } + /** + * Creates a new frequency change event + * + * @param frequency in hertz + * @param eventDescription to add a tag to the event + */ + public static SourceEvent frequencyChange(long frequency, String eventDescription) + { + return new SourceEvent(Event.NOTIFICATION_FREQUENCY_CHANGE, frequency, eventDescription); + } + /** * Creates a new frequency correction change event * @@ -177,6 +199,17 @@ public class SourceEvent return new SourceEvent(Event.NOTIFICATION_SAMPLE_RATE_CHANGE, sampleRate); } + /** + * Creates a new sample rate change event + * + * @param sampleRate in hertz + * @param eventDescription to include with the event + */ + public static SourceEvent sampleRateChange(double sampleRate, String eventDescription) + { + return new SourceEvent(Event.NOTIFICATION_SAMPLE_RATE_CHANGE, sampleRate, eventDescription); + } + /** * Creates a new channel sample rate change event * @@ -250,8 +283,12 @@ public class SourceEvent return new SourceEvent(Event.REQUEST_SOURCE_DISPOSE, source); } + @Override public String toString() { - return "SOURCE EVENT:" + mEvent + " VALUE:" + (mValue != null ? mValue : "(empty)"); + return "SOURCE EVENT:" + mEvent + + " VALUE:" + (mValue != null ? mValue : "(empty)") + + " SOURCE:" + (mSource != null ? mSource.getClass() : "(null)") + + " DESCRIPTION:" + (mEventDescription != null ? mEventDescription : ""); } } diff --git a/src/main/java/io/github/dsheirer/source/tuner/channel/TunerChannelSource.java b/src/main/java/io/github/dsheirer/source/tuner/channel/TunerChannelSource.java index 76c6590b..d012be06 100644 --- a/src/main/java/io/github/dsheirer/source/tuner/channel/TunerChannelSource.java +++ b/src/main/java/io/github/dsheirer/source/tuner/channel/TunerChannelSource.java @@ -118,7 +118,6 @@ public abstract class TunerChannelSource extends ComplexSource implements ISourc { //Broadcast current frequency and sample rate so consumer can configure correctly broadcastConsumerSourceEvent(SourceEvent.frequencyChange(getFrequency())); - broadcastConsumerSourceEvent(SourceEvent.sampleRateChange(getSampleRate())); if(mProducerSourceEventListener != null) { diff --git a/src/main/java/io/github/dsheirer/source/tuner/test/TestTunerController.java b/src/main/java/io/github/dsheirer/source/tuner/test/TestTunerController.java index fc2489f2..8d9a809c 100644 --- a/src/main/java/io/github/dsheirer/source/tuner/test/TestTunerController.java +++ b/src/main/java/io/github/dsheirer/source/tuner/test/TestTunerController.java @@ -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 = 100000; + public static final int SAMPLE_RATE = 10000000; public static final int DC_NOISE_BANDWIDTH = 0; public static final double USABLE_BANDWIDTH_PERCENTAGE = 1.00; From cbc228d7984a4be474704097037cf402941ec84f Mon Sep 17 00:00:00 2001 From: Denny Date: Sun, 6 May 2018 07:37:24 -0400 Subject: [PATCH 65/87] [WIP] The 2 channel synthesizer appears to be working after restructuring. Check-point commit. I'm going to optimize the code so that it can leverage SIMD instructions. --- .../channelizer/TwoChannelSynthesizerM2.java | 70 ++++++++++++------- .../dsp/filter/design/FilterViewer.java | 4 +- .../gui/channelizer/SynthesizerViewer.java | 10 +-- 3 files changed, 51 insertions(+), 33 deletions(-) diff --git a/src/main/java/io/github/dsheirer/dsp/filter/channelizer/TwoChannelSynthesizerM2.java b/src/main/java/io/github/dsheirer/dsp/filter/channelizer/TwoChannelSynthesizerM2.java index d0967a3e..49955846 100644 --- a/src/main/java/io/github/dsheirer/dsp/filter/channelizer/TwoChannelSynthesizerM2.java +++ b/src/main/java/io/github/dsheirer/dsp/filter/channelizer/TwoChannelSynthesizerM2.java @@ -15,11 +15,8 @@ ******************************************************************************/ 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; @@ -32,11 +29,15 @@ public class TwoChannelSynthesizerM2 { private final static Logger mLog = LoggerFactory.getLogger(TwoChannelSynthesizerM2.class); - private ComplexFIRFilter mFilter1; - private ComplexFIRFilter mFilter2; - + private float[] mData1i; + private float[] mData1q; + private float[] mData2i; + private float[] mData2q; + private float[] mFilter1; + private float[] mFilter2; + private float mAccumulator; private FloatFFT_1D mFFT = new FloatFFT_1D(2); - private boolean mFlag = false; + private boolean mFlag = true; /** * Polyphase synthesizer for mixing two M2 oversampled channels into a composite channel using @@ -63,24 +64,26 @@ public class TwoChannelSynthesizerM2 { int length = (filter.length / 2) + (filter.length % 2); - float[] filter1 = new float[length]; - float[] filter2 = new float[length]; + mFilter1 = new float[length]; + mFilter2 = new float[length]; + + mData1i = new float[length]; + mData1q = new float[length]; + mData2i = new float[length]; + mData2q = 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]; + mFilter1[index] = filter[x]; if(x + 1 < filter.length) { - filter2[index] = filter[x + 1]; + mFilter2[index] = filter[x + 1]; } } - - mFilter1 = new ComplexFIRFilter(filter1, 1.0f); - mFilter2 = new ComplexFIRFilter(filter2, 1.0f); } /** @@ -98,6 +101,7 @@ public class TwoChannelSynthesizerM2 throw new IllegalArgumentException("Channel 1 and 2 array length must be equal"); } + //TODO: make this a reusable buffer float[] output = new float[channel1.length]; float[] buffer = new float[4]; @@ -111,15 +115,29 @@ public class TwoChannelSynthesizerM2 mFFT.complexInverse(buffer, true); + System.arraycopy(mData1i, 0, mData1i, 1, mData1i.length - 1); + System.arraycopy(mData1q, 0, mData1q, 1, mData1q.length - 1); + System.arraycopy(mData2i, 0, mData2i, 1, mData2i.length - 1); + System.arraycopy(mData2q, 0, mData2q, 1, mData2q.length - 1); + + mData1i[0] = buffer[0]; + mData1q[0] = buffer[1]; + mData2i[0] = buffer[2]; + mData2q[0] = buffer[3]; + if(mFlag) { - output[x] = mFilter1.filterInphase(buffer[0]) + mFilter2.filterInphase(buffer[2]); - output[x + 1] = mFilter1.filterQuadrature(buffer[1]) + mFilter2.filterQuadrature(buffer[3]); +// output[x] = filter(mFilter1, mData1i); +// output[x + 1] = filter(mFilter1, mData1q); + output[x] = filter(mFilter1, mData1i) + filter(mFilter2, mData2i); + output[x + 1] = filter(mFilter1, mData1q) + filter(mFilter2, mData2q); } else { - output[x] = mFilter1.filterInphase(buffer[2]) + mFilter2.filterInphase(buffer[0]); - output[x + 1] = mFilter1.filterQuadrature(buffer[3]) + mFilter2.filterQuadrature(buffer[1]); +// output[x] = filter(mFilter1, mData2i); +// output[x + 1] = filter(mFilter1, mData2q); + output[x] = filter(mFilter1, mData2i) + filter(mFilter2, mData1i); + output[x + 1] = filter(mFilter1, mData2q) + filter(mFilter2, mData1q); } mFlag = !mFlag; @@ -128,27 +146,27 @@ public class TwoChannelSynthesizerM2 return output; } - private static float filter(float[] taps, RealCircularBuffer dataBlock) + private float filter(float[] coefficients, float[] data) { - float accumulator = 0; + mAccumulator = 0.0f; - for(int x = 0; x < taps.length; x++) + for(int x = 0; x < coefficients.length; x++) { - accumulator += (taps[x] * dataBlock.get(x)); + mAccumulator += coefficients[x] * data[x]; } - return accumulator; + return mAccumulator; } public static void main(String[] args) { mLog.debug("Starting"); - int sampleCount = 10; + int sampleCount = 30; try { - float[] taps = FilterFactory.getSincM2Synthesizer(12500.0, 2, 19, Window.WindowType.BLACKMAN_HARRIS_7, true); + float[] taps = FilterFactory.getSincM2Synthesizer(12500.0, 2, 8, Window.WindowType.BLACKMAN_HARRIS_7, true); TwoChannelSynthesizerM2 synthesizer = new TwoChannelSynthesizerM2(taps); @@ -162,7 +180,7 @@ public class TwoChannelSynthesizerM2 mLog.debug("1:" + Arrays.toString(channel1)); mLog.debug("X:" + Arrays.toString(synthesized)); } - catch(FilterDesignException fde) + catch(Exception fde) { mLog.error("Filter design error", fde); } diff --git a/src/main/java/io/github/dsheirer/dsp/filter/design/FilterViewer.java b/src/main/java/io/github/dsheirer/dsp/filter/design/FilterViewer.java index 9fce71d0..dc8aec31 100644 --- a/src/main/java/io/github/dsheirer/dsp/filter/design/FilterViewer.java +++ b/src/main/java/io/github/dsheirer/dsp/filter/design/FilterViewer.java @@ -76,8 +76,8 @@ public class FilterViewer extends Application try { -// taps = FilterFactory.getSincM2Synthesizer(12500.0, 2, 19, Window.WindowType.BLACKMAN_HARRIS_7, true); - taps = FilterFactory.getSincFilter(25000.0, 12500.0, 2, 19, Window.WindowType.BLACKMAN_HARRIS_7, true); + taps = FilterFactory.getSincM2Synthesizer(12500.0, 2, 19, Window.WindowType.BLACKMAN_HARRIS_7, true); +// taps = FilterFactory.getSincFilter(25000.0, 12500.0, 2, 19, Window.WindowType.BLACKMAN_HARRIS_7, true); } catch(FilterDesignException fde) { diff --git a/src/main/java/io/github/dsheirer/gui/channelizer/SynthesizerViewer.java b/src/main/java/io/github/dsheirer/gui/channelizer/SynthesizerViewer.java index 22b50402..2997a6ec 100644 --- a/src/main/java/io/github/dsheirer/gui/channelizer/SynthesizerViewer.java +++ b/src/main/java/io/github/dsheirer/gui/channelizer/SynthesizerViewer.java @@ -66,7 +66,7 @@ public class SynthesizerViewer extends JFrame private ChannelPanel mChannel2Panel; private ChannelControlPanel mChannel1ControlPanel; private ChannelControlPanel mChannel2ControlPanel; - private DFTSize mMainPanelDFTSize = DFTSize.FFT32768; + private DFTSize mMainPanelDFTSize = DFTSize.FFT08192; private DFTSize mChannelPanelDFTSize = DFTSize.FFT08192; /** @@ -152,7 +152,7 @@ public class SynthesizerViewer extends JFrame { setLayout(new MigLayout("insets 0 0 0 0", "[grow,fill]", "[grow,fill]")); mSpectrumPanel = new SpectrumPanel(settingsManager); - mSpectrumPanel.setSampleSize(22); + mSpectrumPanel.setSampleSize(16); add(mSpectrumPanel); mDFTProcessor.addConverter(mComplexDecibelConverter); @@ -193,7 +193,7 @@ public class SynthesizerViewer extends JFrame { setLayout(new MigLayout("insets 0 0 0 0", "[grow,fill]", "[grow,fill][]")); mSpectrumPanel = new SpectrumPanel(settingsManager); - mSpectrumPanel.setSampleSize(22); + mSpectrumPanel.setSampleSize(16); add(mSpectrumPanel, "wrap"); add(channelControlPanel); @@ -282,7 +282,7 @@ public class SynthesizerViewer extends JFrame { try { - float[] taps = FilterFactory.getSincM2Synthesizer(12500.0, 2, 19, + float[] taps = FilterFactory.getSincM2Synthesizer(12500.0, 2, 12, Window.WindowType.BLACKMAN_HARRIS_7, true); mSynthesizer = new TwoChannelSynthesizerM2(taps); } @@ -302,8 +302,8 @@ public class SynthesizerViewer extends JFrame getChannel2ControlPanel().getOscillator().generateComplex(channel2Buffer); float[] synthesized = mSynthesizer.process(channel1Buffer.getSamples(), channel2Buffer.getSamples()); - mFS4DownConverter.mixComplex(synthesized); ReusableComplexBuffer synthesizedBuffer = mReusableComplexBufferQueue.getBuffer(synthesized.length); + System.arraycopy(synthesized, 0, synthesizedBuffer.getSamples(), 0, synthesized.length); channel1Buffer.incrementUserCount(); getChannel1Panel().receive(channel1Buffer); From 341e740e772ead3012ee301a27b4b789bad6a088 Mon Sep 17 00:00:00 2001 From: Denny Date: Thu, 10 May 2018 08:08:20 -0400 Subject: [PATCH 66/87] [WIP] updated the 2 channel synthesizer to use a more SIMD friendly filter structure. Updated the channel results array buffer to produce reusable buffers. This has dropped the overall memory usage considerably when profiling. Resolved issue in the processing chain where polyphase channel sources were not being stopped and were stacking up in memory. --- .../dsheirer/dsp/filter/FilterFactory.java | 2 +- .../AbstractComplexPolyphaseChannelizer.java | 2 +- .../channelizer/PolyphaseChannelManager.java | 9 +- .../channelizer/TwoChannelSynthesizerM2.java | 243 ++++++++++++------ .../output/ChannelOutputProcessor.java | 6 +- .../output/OneChannelOutputProcessor.java | 7 +- .../output/TwoChannelOutputProcessor.java | 15 +- .../gui/channelizer/SynthesizerViewer.java | 14 +- .../dsheirer/module/ProcessingChain.java | 2 + ...erflowableReusableBufferTransferQueue.java | 2 +- .../sample/buffer/ReusableBuffer.java | 23 +- .../buffer/ReusableBufferAssembler.java | 10 + .../sample/buffer/ReusableBufferQueue.java | 2 + .../buffer/ReusableChannelResultsBuffer.java | 33 ++- .../buffer/ReusableComplexBufferQueue.java | 2 + 15 files changed, 257 insertions(+), 115 deletions(-) diff --git a/src/main/java/io/github/dsheirer/dsp/filter/FilterFactory.java b/src/main/java/io/github/dsheirer/dsp/filter/FilterFactory.java index 1438104f..7c044567 100644 --- a/src/main/java/io/github/dsheirer/dsp/filter/FilterFactory.java +++ b/src/main/java/io/github/dsheirer/dsp/filter/FilterFactory.java @@ -792,7 +792,7 @@ public class FilterFactory tapsPerChannel, windowType,logResults); //Decrease the overall gain of the filter to an objective of 1.0 divided by the number of channels - return normalize(filter, 1.0f);// / (float)channels); + return normalize(filter, 1.0f); } /** diff --git a/src/main/java/io/github/dsheirer/dsp/filter/channelizer/AbstractComplexPolyphaseChannelizer.java b/src/main/java/io/github/dsheirer/dsp/filter/channelizer/AbstractComplexPolyphaseChannelizer.java index 8a7175bf..102f145b 100644 --- a/src/main/java/io/github/dsheirer/dsp/filter/channelizer/AbstractComplexPolyphaseChannelizer.java +++ b/src/main/java/io/github/dsheirer/dsp/filter/channelizer/AbstractComplexPolyphaseChannelizer.java @@ -34,7 +34,7 @@ import java.util.concurrent.CopyOnWriteArrayList; public abstract class AbstractComplexPolyphaseChannelizer implements Listener, ISourceEventListener { private final static Logger mLog = LoggerFactory.getLogger(AbstractComplexPolyphaseChannelizer.class); - private ReusableChannelResultsBufferQueue mBufferQueue = new ReusableChannelResultsBufferQueue(); + private ReusableChannelResultsBufferQueue mBufferQueue = new ReusableChannelResultsBufferQueue("AbstractComplexPolyphaseChannelizer"); private Broadcaster mSourceChangeBroadcaster = new Broadcaster(); private List mChannels = new CopyOnWriteArrayList<>(); private double mSampleRate; diff --git a/src/main/java/io/github/dsheirer/dsp/filter/channelizer/PolyphaseChannelManager.java b/src/main/java/io/github/dsheirer/dsp/filter/channelizer/PolyphaseChannelManager.java index 2e3af921..4045673b 100644 --- a/src/main/java/io/github/dsheirer/dsp/filter/channelizer/PolyphaseChannelManager.java +++ b/src/main/java/io/github/dsheirer/dsp/filter/channelizer/PolyphaseChannelManager.java @@ -50,7 +50,8 @@ public class PolyphaseChannelManager implements ISourceEventProcessor private final static Logger mLog = LoggerFactory.getLogger(PolyphaseChannelManager.class); private static final double MINIMUM_CHANNEL_BANDWIDTH = 12500.0; private static final double CHANNEL_OVERSAMPLING = 2.0; - private static final int POLYPHASE_FILTER_TAPS_PER_CHANNEL = 8; + private static final int POLYPHASE_CHANNELIZER_TAPS_PER_CHANNEL = 8; + private static final int POLYPHASE_SYNTHESIZER_TAPS_PER_CHANNEL = 20; private Broadcaster mSourceEventBroadcaster = new Broadcaster<>(); private IReusableBufferProvider mReusableBufferProvider; @@ -266,6 +267,7 @@ public class PolyphaseChannelManager implements ISourceEventProcessor */ private void stopChannelSource(PolyphaseChannelSource channelSource) { + mLog.debug("Stopping channel source ... count:" + mChannelSources.size()); synchronized(mBufferProcessor) { mChannelSources.remove(channelSource); @@ -290,6 +292,7 @@ public class PolyphaseChannelManager implements ISourceEventProcessor { //Do nothing } + mLog.debug("Stopped channel source ... count:" + mChannelSources.size()); } /** @@ -349,7 +352,7 @@ public class PolyphaseChannelManager implements ISourceEventProcessor if(mPolyphaseChannelizer == null) { mPolyphaseChannelizer = new ComplexPolyphaseChannelizerM2(sampleRate, - POLYPHASE_FILTER_TAPS_PER_CHANNEL); + POLYPHASE_CHANNELIZER_TAPS_PER_CHANNEL); } else { @@ -514,7 +517,7 @@ public class PolyphaseChannelManager implements ISourceEventProcessor if(taps == null) { taps = FilterFactory.getSincM2Synthesizer(mChannelCalculator.getChannelBandwidth(), channels, - POLYPHASE_FILTER_TAPS_PER_CHANNEL + 20, Window.WindowType.BLACKMAN_HARRIS_7, true); + POLYPHASE_SYNTHESIZER_TAPS_PER_CHANNEL, Window.WindowType.BLACKMAN_HARRIS_7, true); mOutputProcessorFilters.put(channels, taps); } diff --git a/src/main/java/io/github/dsheirer/dsp/filter/channelizer/TwoChannelSynthesizerM2.java b/src/main/java/io/github/dsheirer/dsp/filter/channelizer/TwoChannelSynthesizerM2.java index 49955846..02a142cc 100644 --- a/src/main/java/io/github/dsheirer/dsp/filter/channelizer/TwoChannelSynthesizerM2.java +++ b/src/main/java/io/github/dsheirer/dsp/filter/channelizer/TwoChannelSynthesizerM2.java @@ -15,27 +15,47 @@ ******************************************************************************/ 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.mixer.IOscillator; -import io.github.dsheirer.dsp.mixer.LowPhaseNoiseOscillator; +import io.github.dsheirer.sample.buffer.ReusableComplexBuffer; +import io.github.dsheirer.sample.buffer.ReusableComplexBufferQueue; import org.jtransforms.fft.FloatFFT_1D; import org.slf4j.Logger; import org.slf4j.LoggerFactory; import java.util.Arrays; +/** + * Implements a two-channel polyphase filer synthesizer. This class is intended to be used with an M2 polyphase + * channelizer in order to recover a signal that overlaps the boundary between two channels. This synthesizer will + * rejoin the two M2 neighboring channels, mix the signal of interest to baseband, and filter the the output stream to + * half of the sample rate, producing an overall M2 output sample rate. + * + * The synthesizer concept is modeled on the structure described by Fred Harris et al, in 'Interleaving Different + * Bandwidth Narrowband Channels in Perfect Reconstruction Cascade Polyphase Filter Banks for Efficient Flexible + * Variable Bandwidth Filters in Wideband Digital Transceivers'. + * + * The data buffers and sub-filter kernels bear no resemblance to the structures defined in the paper. The data and + * filter structures in this class are organized to take advantage of Java's ability to leverage SIMD processor + * intrinsics for optimal efficiency in processing. The paper specifies splitting the filter kernel into N polyphase + * partitions and creating N data buffers. This class organizes the data buffer as a contiguous sample + * array and creates two copies of the filter kernel, one arranged for normal filtering and the second arranged for + * phase-shifted filtering. Instead of calculating the dot-product for each sub-filter, we calculate the product of the + * full data array against the filter and then accumulate each sub-filter. This allows java to use SIMD for the array + * product and then normal processing for the accumulation. Since this is a two-channel processor and the results of + * each filter accumulation are added, we use a single accumulator across both filters. + * + */ public class TwoChannelSynthesizerM2 { private final static Logger mLog = LoggerFactory.getLogger(TwoChannelSynthesizerM2.class); - private float[] mData1i; - private float[] mData1q; - private float[] mData2i; - private float[] mData2q; - private float[] mFilter1; - private float[] mFilter2; - private float mAccumulator; + private ReusableComplexBufferQueue mReusableComplexBufferQueue = new ReusableComplexBufferQueue("Two Channel Synthesizer M2"); + private float[] mDataBuffer; + private float[] mNormalFilter; + private float[] mPhaseShiftedFilter; + private float[] mFilterProduct; + private float[] mIFFTBuffer = new float[4]; + private float mIAccumulator; + private float mQAccumulator; private FloatFFT_1D mFFT = new FloatFFT_1D(2); private boolean mFlag = true; @@ -62,100 +82,152 @@ public class TwoChannelSynthesizerM2 */ private void init(float[] filter) { - int length = (filter.length / 2) + (filter.length % 2); + int tapsPerChannel = (int)Math.ceil(filter.length / 2); - mFilter1 = new float[length]; - mFilter2 = new float[length]; - - mData1i = new float[length]; - mData1q = new float[length]; - mData2i = new float[length]; - mData2q = 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; - - mFilter1[index] = filter[x]; - - if(x + 1 < filter.length) - { - mFilter2[index] = filter[x + 1]; - } - } + mNormalFilter = getNormalFilter(filter, tapsPerChannel); + mPhaseShiftedFilter = getPhaseShiftedFilter(filter, tapsPerChannel); + mDataBuffer = new float[mNormalFilter.length]; + mFilterProduct = new float[mNormalFilter.length]; } /** * 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 + * @param channelBuffer1 input channel + * @param channelBuffer2 input channel * @return synthesized channel results of the same length as both channel 1/2 input arrays. */ - public float[] process(float[] channel1, float[] channel2) + public ReusableComplexBuffer process(ReusableComplexBuffer channelBuffer1, ReusableComplexBuffer channelBuffer2) { - if(channel1.length != channel2.length) + if(channelBuffer1.getSamples().length != channelBuffer1.getSamples().length) { throw new IllegalArgumentException("Channel 1 and 2 array length must be equal"); } - //TODO: make this a reusable buffer - float[] output = new float[channel1.length]; + float[] channel1 = channelBuffer1.getSamples(); + float[] channel2 = channelBuffer2.getSamples(); - float[] buffer = new float[4]; + ReusableComplexBuffer synthesizedComplexBuffer = mReusableComplexBufferQueue.getBuffer(channel1.length); + + float[] output = synthesizedComplexBuffer.getSamples(); 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 + mIFFTBuffer[0] = channel1[x]; //i + mIFFTBuffer[1] = channel1[x + 1]; //q + mIFFTBuffer[2] = channel2[x]; //i + mIFFTBuffer[3] = channel2[x + 1]; //q - mFFT.complexInverse(buffer, true); + mFFT.complexInverse(mIFFTBuffer, true); - System.arraycopy(mData1i, 0, mData1i, 1, mData1i.length - 1); - System.arraycopy(mData1q, 0, mData1q, 1, mData1q.length - 1); - System.arraycopy(mData2i, 0, mData2i, 1, mData2i.length - 1); - System.arraycopy(mData2q, 0, mData2q, 1, mData2q.length - 1); - - mData1i[0] = buffer[0]; - mData1q[0] = buffer[1]; - mData2i[0] = buffer[2]; - mData2q[0] = buffer[3]; + System.arraycopy(mDataBuffer, 0, mDataBuffer, 4, mDataBuffer.length - 4); + System.arraycopy(mIFFTBuffer, 0, mDataBuffer, 0, mIFFTBuffer.length); if(mFlag) { -// output[x] = filter(mFilter1, mData1i); -// output[x + 1] = filter(mFilter1, mData1q); - output[x] = filter(mFilter1, mData1i) + filter(mFilter2, mData2i); - output[x + 1] = filter(mFilter1, mData1q) + filter(mFilter2, mData2q); + for(int y = 0; y < mDataBuffer.length; y++) + { + mFilterProduct[y] = mDataBuffer[y] * mNormalFilter[y]; + } } else { -// output[x] = filter(mFilter1, mData2i); -// output[x + 1] = filter(mFilter1, mData2q); - output[x] = filter(mFilter1, mData2i) + filter(mFilter2, mData1i); - output[x + 1] = filter(mFilter1, mData2q) + filter(mFilter2, mData1q); + for(int y = 0; y < mDataBuffer.length; y++) + { + mFilterProduct[y] = mDataBuffer[y] * mPhaseShiftedFilter[y]; + } } + mIAccumulator = 0.0f; + mQAccumulator = 0.0f; + + for(int y = 0; y < mFilterProduct.length; y += 2) + { + mIAccumulator += mFilterProduct[y]; + mQAccumulator += mFilterProduct[y + 1]; + } + + output[x] = mIAccumulator; + output[x + 1] = mQAccumulator; + mFlag = !mFlag; } - return output; + channelBuffer1.decrementUserCount(); + channelBuffer2.decrementUserCount(); + + synthesizedComplexBuffer.incrementUserCount(); + + return synthesizedComplexBuffer; } - private float filter(float[] coefficients, float[] data) + private static float[] getNormalFilter(float[] coefficients, int tapsPerChannel) { - mAccumulator = 0.0f; + float[] filter = new float[4 * tapsPerChannel]; - for(int x = 0; x < coefficients.length; x++) + int idealLength = 2 * tapsPerChannel; + + int offset = idealLength - 2; + + int index; + + for(int x = 0; x < idealLength; x += 2) { - mAccumulator += coefficients[x] * data[x]; + index = offset - x; + + if(index < coefficients.length) + { + //Filter 1 + filter[2 * x + 0] = coefficients[index]; + filter[2 * x + 1] = coefficients[index]; + } + + index = offset - x + 1; + + if(index < coefficients.length) + { + //Filter 2 + filter[2 * x + 2] = coefficients[index]; + filter[2 * x + 3] = coefficients[index]; + } } - return mAccumulator; + return filter; + } + + private static float[] getPhaseShiftedFilter(float[] coefficients, int tapsPerChannel) + { + float[] filter = new float[4 * tapsPerChannel]; + + int idealLength = 2 * tapsPerChannel; + + int offset = idealLength - 2; + + int index; + + for(int x = 0; x < idealLength; x += 2) + { + index = offset - x + 1; + + if(index < coefficients.length) + { + //Filter 2 + filter[2 * x + 0] = coefficients[index]; + filter[2 * x + 1] = coefficients[index]; + } + + index = offset - x; + + if(index < coefficients.length) + { + //Filter 1 + filter[2 * x + 2] = coefficients[index]; + filter[2 * x + 3] = coefficients[index]; + } + } + + return filter; } public static void main(String[] args) @@ -166,19 +238,38 @@ public class TwoChannelSynthesizerM2 try { - float[] taps = FilterFactory.getSincM2Synthesizer(12500.0, 2, 8, Window.WindowType.BLACKMAN_HARRIS_7, true); +// float[] taps = FilterFactory.getSincM2Synthesizer(12500.0, 2, 20, Window.WindowType.BLACKMAN_HARRIS_7, true); - TwoChannelSynthesizerM2 synthesizer = new TwoChannelSynthesizerM2(taps); + float[] taps = new float[15]; + for(int x = 0; x < taps.length; x++) + { + taps[x] = x; + } - IOscillator oscillator = new LowPhaseNoiseOscillator(3000.0, 25000.0); + float[] normalFilter = getPhaseShiftedFilter(taps, 8); - float[] channel1 = oscillator.generateComplex(sampleCount); - float[] channel2 = new float[sampleCount * 2]; + mLog.debug("Filter: " + Arrays.toString(normalFilter)); - float[] synthesized = synthesizer.process(channel1, channel2); - - mLog.debug("1:" + Arrays.toString(channel1)); - mLog.debug("X:" + Arrays.toString(synthesized)); +// 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[] channel1 = new float[16]; +// float[] channel2 = new float[16]; +// for(int x = 1; x <= 16; x++) +// { +// channel1[x - 1] = x; +// channel2[x - 1] = x + 16; +// } +// +// +// float[] synthesized = synthesizer.process(channel1, channel2); +// +// mLog.debug("1:" + Arrays.toString(channel1)); +// mLog.debug("X:" + Arrays.toString(synthesized)); } catch(Exception fde) { diff --git a/src/main/java/io/github/dsheirer/dsp/filter/channelizer/output/ChannelOutputProcessor.java b/src/main/java/io/github/dsheirer/dsp/filter/channelizer/output/ChannelOutputProcessor.java index e27d199a..872700bb 100644 --- a/src/main/java/io/github/dsheirer/dsp/filter/channelizer/output/ChannelOutputProcessor.java +++ b/src/main/java/io/github/dsheirer/dsp/filter/channelizer/output/ChannelOutputProcessor.java @@ -20,7 +20,7 @@ package io.github.dsheirer.dsp.filter.channelizer.output; 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.buffer.OverflowableReusableBufferTransferQueue; import io.github.dsheirer.sample.buffer.ReusableBufferAssembler; import io.github.dsheirer.sample.buffer.ReusableChannelResultsBuffer; import io.github.dsheirer.sample.real.IOverflowListener; @@ -35,7 +35,7 @@ public abstract class ChannelOutputProcessor implements IPolyphaseChannelOutputP { private final static Logger mLog = LoggerFactory.getLogger(ChannelOutputProcessor.class); - private OverflowableTransferQueue mChannelResultsQueue; + private OverflowableReusableBufferTransferQueue mChannelResultsQueue; private List mChannelResultsToProcess = new ArrayList<>(); private int mMaxResultsToProcess; @@ -59,7 +59,7 @@ public abstract class ChannelOutputProcessor implements IPolyphaseChannelOutputP mFrequencyCorrectionMixer = new Oscillator(0, sampleRate); mMaxResultsToProcess = (int)(sampleRate / 10) * 2; //process at 100 millis interval, twice the expected inflow rate - mChannelResultsQueue = new OverflowableTransferQueue<>((int)(sampleRate * 3), (int)(sampleRate * 0.5)); + mChannelResultsQueue = new OverflowableReusableBufferTransferQueue<>((int)(sampleRate * 3), (int)(sampleRate * 0.5)); } public void dispose() diff --git a/src/main/java/io/github/dsheirer/dsp/filter/channelizer/output/OneChannelOutputProcessor.java b/src/main/java/io/github/dsheirer/dsp/filter/channelizer/output/OneChannelOutputProcessor.java index 9684efa6..d06f1e44 100644 --- a/src/main/java/io/github/dsheirer/dsp/filter/channelizer/output/OneChannelOutputProcessor.java +++ b/src/main/java/io/github/dsheirer/dsp/filter/channelizer/output/OneChannelOutputProcessor.java @@ -20,6 +20,7 @@ package io.github.dsheirer.dsp.filter.channelizer.output; import io.github.dsheirer.sample.buffer.ReusableBufferAssembler; import io.github.dsheirer.sample.buffer.ReusableChannelResultsBuffer; +import io.github.dsheirer.sample.buffer.ReusableComplexBuffer; import org.slf4j.Logger; import org.slf4j.LoggerFactory; @@ -83,14 +84,14 @@ public class OneChannelOutputProcessor extends ChannelOutputProcessor { try { - float[] samples = channelResultsBuffer.getChannel(mChannelOffset); + ReusableComplexBuffer channelBuffer = channelResultsBuffer.getChannel(mChannelOffset); if(hasFrequencyCorrection()) { - getFrequencyCorrectionMixer().mixComplex(samples); + getFrequencyCorrectionMixer().mixComplex(channelBuffer.getSamples()); } - reusableBufferAssembler.receive(samples); + reusableBufferAssembler.receive(channelBuffer); } catch(IllegalArgumentException iae) { diff --git a/src/main/java/io/github/dsheirer/dsp/filter/channelizer/output/TwoChannelOutputProcessor.java b/src/main/java/io/github/dsheirer/dsp/filter/channelizer/output/TwoChannelOutputProcessor.java index f7c3a635..42871dac 100644 --- a/src/main/java/io/github/dsheirer/dsp/filter/channelizer/output/TwoChannelOutputProcessor.java +++ b/src/main/java/io/github/dsheirer/dsp/filter/channelizer/output/TwoChannelOutputProcessor.java @@ -23,6 +23,7 @@ import io.github.dsheirer.dsp.filter.fir.remez.RemezFIRFilterDesigner; import io.github.dsheirer.dsp.mixer.FS4DownConverter; import io.github.dsheirer.sample.buffer.ReusableBufferAssembler; import io.github.dsheirer.sample.buffer.ReusableChannelResultsBuffer; +import io.github.dsheirer.sample.buffer.ReusableComplexBuffer; import org.slf4j.Logger; import org.slf4j.LoggerFactory; @@ -139,21 +140,21 @@ public class TwoChannelOutputProcessor extends ChannelOutputProcessor { for(ReusableChannelResultsBuffer buffer: channelResultsBuffers) { - float[] channel1 = buffer.getChannel(mChannelOffset1); - float[] channel2 = buffer.getChannel(mChannelOffset2); + ReusableComplexBuffer channel1 = buffer.getChannel(mChannelOffset1); + ReusableComplexBuffer channel2 = buffer.getChannel(mChannelOffset2); //Join the two channels using the synthesizer - float[] synthesized = mSynthesizer.process(channel1, channel2); + ReusableComplexBuffer synthesized = mSynthesizer.process(channel1, channel2); //The synthesized channels are centered at +FS/4 ... downconvert to center the spectrum - mFS4DownConverter.mixComplex(synthesized); + mFS4DownConverter.mixComplex(synthesized.getSamples()); //Apply offset and frequency correction to center the signal of interest within the synthesized channel - getFrequencyCorrectionMixer().mixComplex(synthesized); + getFrequencyCorrectionMixer().mixComplex(synthesized.getSamples()); - mLowPassFilter.filter(synthesized); + ReusableComplexBuffer lowPassFiltered = mLowPassFilter.filter(synthesized); - reusableBufferAssembler.receive(synthesized); + reusableBufferAssembler.receive(lowPassFiltered); buffer.decrementUserCount(); } diff --git a/src/main/java/io/github/dsheirer/gui/channelizer/SynthesizerViewer.java b/src/main/java/io/github/dsheirer/gui/channelizer/SynthesizerViewer.java index 2997a6ec..05997803 100644 --- a/src/main/java/io/github/dsheirer/gui/channelizer/SynthesizerViewer.java +++ b/src/main/java/io/github/dsheirer/gui/channelizer/SynthesizerViewer.java @@ -301,15 +301,17 @@ public class SynthesizerViewer extends JFrame ReusableComplexBuffer channel2Buffer = mReusableComplexBufferQueue.getBuffer(mSamplesPerCycle); getChannel2ControlPanel().getOscillator().generateComplex(channel2Buffer); - float[] synthesized = mSynthesizer.process(channel1Buffer.getSamples(), channel2Buffer.getSamples()); - ReusableComplexBuffer synthesizedBuffer = mReusableComplexBufferQueue.getBuffer(synthesized.length); - System.arraycopy(synthesized, 0, synthesizedBuffer.getSamples(), 0, synthesized.length); - channel1Buffer.incrementUserCount(); - getChannel1Panel().receive(channel1Buffer); + channel1Buffer.incrementUserCount(); channel2Buffer.incrementUserCount(); + channel2Buffer.incrementUserCount(); + + ReusableComplexBuffer synthesizedBuffer = mSynthesizer.process(channel1Buffer, channel2Buffer); + + getChannel1Panel().receive(channel1Buffer); + getChannel2Panel().receive(channel2Buffer); - synthesizedBuffer.incrementUserCount(); + getSpectrumPanel().receive(synthesizedBuffer); } } diff --git a/src/main/java/io/github/dsheirer/module/ProcessingChain.java b/src/main/java/io/github/dsheirer/module/ProcessingChain.java index 25197f1b..1a92cfc9 100644 --- a/src/main/java/io/github/dsheirer/module/ProcessingChain.java +++ b/src/main/java/io/github/dsheirer/module/ProcessingChain.java @@ -626,6 +626,8 @@ public class ProcessingChain implements IChannelEventListener { removeModule(mSource); + mSource.stop(); + mSource.setOverflowListener(null); switch(mSource.getSampleType()) diff --git a/src/main/java/io/github/dsheirer/sample/buffer/OverflowableReusableBufferTransferQueue.java b/src/main/java/io/github/dsheirer/sample/buffer/OverflowableReusableBufferTransferQueue.java index 914833bb..874f1a9b 100644 --- a/src/main/java/io/github/dsheirer/sample/buffer/OverflowableReusableBufferTransferQueue.java +++ b/src/main/java/io/github/dsheirer/sample/buffer/OverflowableReusableBufferTransferQueue.java @@ -17,7 +17,7 @@ package io.github.dsheirer.sample.buffer; import io.github.dsheirer.sample.OverflowableTransferQueue; -public class OverflowableReusableBufferTransferQueue extends OverflowableTransferQueue +public class OverflowableReusableBufferTransferQueue extends OverflowableTransferQueue { /** * Concurrent transfer queue that couples a higher-throughput linked transfer queue with an atomic integer for diff --git a/src/main/java/io/github/dsheirer/sample/buffer/ReusableBuffer.java b/src/main/java/io/github/dsheirer/sample/buffer/ReusableBuffer.java index de1dd829..5575d016 100644 --- a/src/main/java/io/github/dsheirer/sample/buffer/ReusableBuffer.java +++ b/src/main/java/io/github/dsheirer/sample/buffer/ReusableBuffer.java @@ -102,10 +102,7 @@ public class ReusableBuffer extends AbstractReusableBuffer mUserCount.set(0); - if(mSamples.length != samples.length) - { - mSamples = new float[samples.length]; - } + resize(samples.length); System.arraycopy(samples, 0, mSamples, 0, samples.length); @@ -129,15 +126,23 @@ public class ReusableBuffer extends AbstractReusableBuffer mUserCount.set(0); - if(mSamples.length != floatBuffer.capacity()) - { - mSamples = new float[floatBuffer.capacity()]; - } - + resize(floatBuffer.capacity()); floatBuffer.rewind(); floatBuffer.get(mSamples); mTimestamp = timestamp; } + /** + * Resizes the internal array to the size argument + * @param size for the internal array + */ + protected void resize(int size) + { + if(mSamples.length != size) + { + mSamples = new float[size]; + } + } + } diff --git a/src/main/java/io/github/dsheirer/sample/buffer/ReusableBufferAssembler.java b/src/main/java/io/github/dsheirer/sample/buffer/ReusableBufferAssembler.java index 73c43c38..111c33cb 100644 --- a/src/main/java/io/github/dsheirer/sample/buffer/ReusableBufferAssembler.java +++ b/src/main/java/io/github/dsheirer/sample/buffer/ReusableBufferAssembler.java @@ -107,6 +107,16 @@ public class ReusableBufferAssembler } } + /** + * Adds the samples to this assembler. As each buffer is assembled, it will be dispatched to the registered + * listener. + */ + public void receive(ReusableComplexBuffer samplesBuffer) + { + receive(samplesBuffer.getSamples()); + samplesBuffer.decrementUserCount(); + } + /** * Flushes the current buffer contents to the registered listener */ diff --git a/src/main/java/io/github/dsheirer/sample/buffer/ReusableBufferQueue.java b/src/main/java/io/github/dsheirer/sample/buffer/ReusableBufferQueue.java index 292e1033..bb96adea 100644 --- a/src/main/java/io/github/dsheirer/sample/buffer/ReusableBufferQueue.java +++ b/src/main/java/io/github/dsheirer/sample/buffer/ReusableBufferQueue.java @@ -51,6 +51,8 @@ public class ReusableBufferQueue extends AbstractReusableBufferQueue mEmptyBuffers = new LinkedList<>(); private LinkedList mFilledBuffers = new LinkedList<>(); private Integer mLength; @@ -103,7 +109,7 @@ public class ReusableChannelResultsBuffer extends AbstractReusableBuffer * @throws IllegalArgumentException if this buffer doesn't contain any samples or if the requested channel is * not within the channel range of the contained channel results. */ - public float[] getChannel(int iChannelIndex) + public ReusableComplexBuffer getChannel(int iChannelIndex) { if(mFilledBuffers.isEmpty()) { @@ -116,18 +122,35 @@ public class ReusableChannelResultsBuffer extends AbstractReusableBuffer "results -- max channel is " + mLength); } - float[] samples = new float[mFilledBuffers.size() * 2]; + ReusableComplexBuffer channelBuffer = mReusableComplexBufferQueue.getBuffer(mFilledBuffers.size() * 2); + + float[] samples = channelBuffer.getSamples(); + int pointer = 0; int qChannelIndex = iChannelIndex + 1; for(float[] channelResults: mFilledBuffers) { - samples[pointer++] = channelResults[iChannelIndex]; - samples[pointer++] = channelResults[qChannelIndex]; + try + { + samples[pointer] = channelResults[iChannelIndex]; + pointer++; + samples[pointer] = channelResults[qChannelIndex]; + pointer++; + } + catch(Exception e) + { + mLog.error("Error accessing channel results - iIndex:" + iChannelIndex + + " Results Size: " + mFilledBuffers.size() + + " Samples Length:" + samples.length + + " Pointer:" + pointer, e); + } } - return samples; + channelBuffer.incrementUserCount(); + + return channelBuffer; } /** diff --git a/src/main/java/io/github/dsheirer/sample/buffer/ReusableComplexBufferQueue.java b/src/main/java/io/github/dsheirer/sample/buffer/ReusableComplexBufferQueue.java index c8084129..c57b7139 100644 --- a/src/main/java/io/github/dsheirer/sample/buffer/ReusableComplexBufferQueue.java +++ b/src/main/java/io/github/dsheirer/sample/buffer/ReusableComplexBufferQueue.java @@ -58,6 +58,8 @@ public class ReusableComplexBufferQueue extends AbstractReusableBufferQueue Date: Sun, 20 May 2018 07:21:14 -0400 Subject: [PATCH 67/87] [WIP] The Two-Channel synthesizer is now fully-functional and the channelizer sub-system is functionally complete. Updated the channelizer and synthesizer filter design methods to use a Kaiser windowed Nyquist filter. Updated the synthesizer filter design to use a new cutoff frequency of 105% of the channel bandwidth to resolve a slight dip in the synthesized channel gain at the channel union region. Updated the 2 channel output processor to use a low-pass filter that is designed for the runtime channel sample rate versus hard-coded 12.5 khz. Testing and redesign continues ... --- .../dsheirer/dsp/filter/FilterFactory.java | 167 +++++++++----- .../io/github/dsheirer/dsp/filter/Window.java | 112 +++++++-- .../ComplexPolyphaseChannelizerM2.java | 145 +++--------- .../channelizer/PolyphaseChannelManager.java | 48 ++-- .../channelizer/TwoChannelSynthesizerM2.java | 214 ++++++------------ .../output/TwoChannelOutputProcessor.java | 92 +++++--- .../dsp/filter/design/FilterViewer.java | 3 +- .../filter/fir/complex/ComplexFIRFilter.java | 1 + .../filter/fir/complex/ComplexFIRFilter2.java | 139 ++++++++++++ .../fir/complex/ComplexFIRFilter_CB_CB.java | 1 + .../dsp/filter/fir/real/RealFIRFilter.java | 1 + .../dsp/filter/fir/real/RealFIRFilter2.java | 133 +++++++++++ .../filter/fir/real/RealFIRFilter_RB_RB.java | 1 + .../filter/fir/real/RealFIRFilter_R_R.java | 56 ----- .../gui/channelizer/ChannelizerViewer.java | 12 +- .../gui/channelizer/SynthesizerViewer.java | 4 +- .../module/decode/p25/P25DecoderC4FM.java | 6 +- .../module/decode/p25/P25DecoderLSM.java | 6 +- .../tuner/test/TestTunerController.java | 2 +- .../dsheirer/spectrum/DFTProcessor.java | 2 +- .../dsheirer/spectrum/SpectrumPanel.java | 12 +- 21 files changed, 698 insertions(+), 459 deletions(-) create mode 100644 src/main/java/io/github/dsheirer/dsp/filter/fir/complex/ComplexFIRFilter2.java create mode 100644 src/main/java/io/github/dsheirer/dsp/filter/fir/real/RealFIRFilter2.java delete mode 100644 src/main/java/io/github/dsheirer/dsp/filter/fir/real/RealFIRFilter_R_R.java diff --git a/src/main/java/io/github/dsheirer/dsp/filter/FilterFactory.java b/src/main/java/io/github/dsheirer/dsp/filter/FilterFactory.java index 7c044567..989372fb 100644 --- a/src/main/java/io/github/dsheirer/dsp/filter/FilterFactory.java +++ b/src/main/java/io/github/dsheirer/dsp/filter/FilterFactory.java @@ -31,6 +31,7 @@ import org.jtransforms.fft.FloatFFT_1D; import org.slf4j.Logger; import org.slf4j.LoggerFactory; +import java.util.Arrays; import java.util.Set; import java.util.TreeSet; @@ -41,6 +42,7 @@ public class FilterFactory { private final static Logger mLog = LoggerFactory.getLogger(FilterFactory.class); private static final double PERFECT_RECONSTRUCTION_GAIN_AT_BAND_EDGE = -6.020599842071533; //decibel(0.5, 0.0) + private static final double MARGIN_OF_ERROR = 0.0003; /** * Generates coefficients for a unity-gain, windowed low-pass filter @@ -462,7 +464,7 @@ public class FilterFactory { Set factors = new TreeSet(); - /* Brute force */ + /* Brute force */ for(int x = 1; x <= value; x++) { int remainder = (int)(value / x); @@ -526,7 +528,7 @@ public class FilterFactory int taps = getTapCount(outputSampleRate, passFrequency, passFrequency + 1500, attenuation); - /* Make tap count odd */ + /* Make tap count odd */ if(taps % 2 == 0) { taps++; @@ -594,7 +596,7 @@ public class FilterFactory * Ported to java from gnuradio/filter/firdes.cc * * For 40db attenuation, calculate the number of symbols based on the following formula: - * Symbols = -44 * alpha + 33 + * Symbols = -44 * alpha + 33 * * Polyphase Channelizer notes: * -Set samples Per Symbol at 2 (or more) * number of channels @@ -716,6 +718,7 @@ public class FilterFactory /** * Calculates the decibel magnitude of the real and imaginary complex sample + * * @param real (x-axis) value * @param imag (y-axis) value * @return magnitude in decibels @@ -753,26 +756,6 @@ 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 - { - float[] filter = getSincFilter(channelBandwidth * channels, channelBandwidth, - channels, tapsPerChannel, windowType,logResults); - - //Increase the overall gain of the filter to an objective of 1.0 times the number of channels - return normalize(filter, 1.0f * (float)channels); - } - /** * Polyphase M2 synthesizer sync filter. Designed for multiple M2 oversampled channel inputs and * an M*channel count output. @@ -780,19 +763,23 @@ public class FilterFactory * @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 + * @throws FilterDesignException */ - public static float[] getSincM2Synthesizer(double channelBandwidth, int channels, int tapsPerChannel, - Window.WindowType windowType, boolean logResults) throws FilterDesignException + public static float[] getSincM2Synthesizer(double channelBandwidth, int channels, int tapsPerChannel) + throws FilterDesignException { - float[] filter = getSincFilter(channelBandwidth * channels, channelBandwidth, channels, - tapsPerChannel, windowType,logResults); + int filterLength = (channels * tapsPerChannel) - 1; - //Decrease the overall gain of the filter to an objective of 1.0 divided by the number of channels - return normalize(filter, 1.0f); + //Design the prototype synthesizer with 105% of the channel bandwidth produced by the channelizer. + float[] taps = FilterFactory.getKaiserSinc(filterLength, channelBandwidth * 1.05, 80.0); + + //This is an odd length filter - increase the length by 1 by pre-padding a zero coefficient + float[] extendedTaps = new float[taps.length + 1]; + System.arraycopy(taps, 0, extendedTaps, 1, taps.length); + + //Adjust the overall gain of the filter to an objective of 1.0 divided by the number of channels + return normalize(extendedTaps, 1.0f); } /** @@ -820,18 +807,32 @@ public class FilterFactory * * @throws FilterDesignException if a filter cannot be designed with a band edge attenuation of -6.02 dB */ - public static float[] getSincFilter(double sampleRate, double channelBandwidth, int channels, - int tapsPerChannel, Window.WindowType windowType, boolean logResults) throws FilterDesignException + + /** + * 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 minimum. This may be increased to meet the band edge -6.02dB requirement + * @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, + boolean logResults) throws FilterDesignException { int currentTapsPerChannel = tapsPerChannel; int filterLength = (channels * currentTapsPerChannel) - 1; - double cutoffFrequency = channelBandwidth / sampleRate; - double bandEdge = cutoffFrequency; + double sampleRate = channelBandwidth * channels; + double bandEdge = channelBandwidth / sampleRate; + double cutoffFrequency = bandEdge / 2.0; double increment = cutoffFrequency * 0.1; //Get an initial filter and band edge frequency response using the channel bandwidth as a cutoff - float[] taps = FilterFactory.getSinc(cutoffFrequency, filterLength, windowType); + float[] taps = null; + + taps = FilterFactory.getKaiserSinc(filterLength, cutoffFrequency, 80.0); double response = FilterFactory.evaluate(taps, bandEdge); //Set cutoff adjustment threshold - we'll test cutoff frequencies to around 1 hertz resolution @@ -841,13 +842,13 @@ public class FilterFactory while(increment > incrementThreshold) { //If the current cutoff meets the objective, test if a higher cutoff also meets the objective - if(matchesObjective(response, PERFECT_RECONSTRUCTION_GAIN_AT_BAND_EDGE) && + if(matchesObjective(response, PERFECT_RECONSTRUCTION_GAIN_AT_BAND_EDGE, MARGIN_OF_ERROR) && (cutoffFrequency + increment <= bandEdge)) { - float[] higherCutoffTaps = FilterFactory.getSinc(cutoffFrequency + increment, filterLength, windowType); + float[] higherCutoffTaps = FilterFactory.getKaiserSinc(filterLength, cutoffFrequency + increment, 80.0); double higherCutoffResponse = FilterFactory.evaluate(higherCutoffTaps, bandEdge); - if(matchesObjective(higherCutoffResponse, PERFECT_RECONSTRUCTION_GAIN_AT_BAND_EDGE)) + if(matchesObjective(higherCutoffResponse, PERFECT_RECONSTRUCTION_GAIN_AT_BAND_EDGE, MARGIN_OF_ERROR)) { cutoffFrequency += increment; taps = higherCutoffTaps; @@ -859,7 +860,7 @@ public class FilterFactory } } //If the current cutoff meets the objective, decrease the increment to test for smaller resolution cutoffs - else if(matchesObjective(response, PERFECT_RECONSTRUCTION_GAIN_AT_BAND_EDGE)) + else if(matchesObjective(response, PERFECT_RECONSTRUCTION_GAIN_AT_BAND_EDGE, MARGIN_OF_ERROR)) { increment /= 2.0; } @@ -882,7 +883,7 @@ public class FilterFactory if(currentTapsPerChannel > (tapsPerChannel + 10)) { throw new FilterDesignException("Couldn't design filter with taps per channel count in the " + - "range of " + tapsPerChannel + " - " + (tapsPerChannel + 10)); + "range of " + tapsPerChannel + " - " + (tapsPerChannel + 10) + " Sample Rate:" + sampleRate + " Channels:" + channels); } filterLength = channels * currentTapsPerChannel - 1; @@ -890,13 +891,13 @@ public class FilterFactory increment = cutoffFrequency * 0.1; } - taps = FilterFactory.getSinc(cutoffFrequency, filterLength, windowType); + taps = FilterFactory.getKaiserSinc(filterLength, cutoffFrequency, 80.0); response = FilterFactory.evaluate(taps, bandEdge); } } //This will probably never happen, but ... - if(!matchesObjective(response, PERFECT_RECONSTRUCTION_GAIN_AT_BAND_EDGE)) + if(!matchesObjective(response, PERFECT_RECONSTRUCTION_GAIN_AT_BAND_EDGE, MARGIN_OF_ERROR)) { throw new FilterDesignException("Cannot design filter to specifications"); } @@ -912,20 +913,22 @@ public class FilterFactory mLog.debug("Input Sample Rate: " + sampleRate); mLog.debug("Channel Bandwidth: " + channelBandwidth); mLog.debug("Channels: " + channels); - mLog.debug("Window Type: " + windowType.name()); + mLog.debug("Window Type: " + Window.WindowType.KAISER.name()); mLog.debug("Taps Per Channel - Requested:" + tapsPerChannel + " Actual:" + ((double)extendedTaps.length / (double)channels)); mLog.debug("Filter Length: " + (extendedTaps.length)); mLog.debug("Requested Cutoff Frequency: " + (sampleRate * bandEdge)); mLog.debug("Actual Cutoff Frequency: " + (sampleRate * cutoffFrequency)); + mLog.debug("Attenuation at 0.0 OBJECTIVE: " + 0.0); mLog.debug("Attenuation at 1.0 OBJECTIVE: " + PERFECT_RECONSTRUCTION_GAIN_AT_BAND_EDGE); - mLog.debug("Attenuation at 1.00 Channels: " + evaluate(taps, bandEdge * 1.00)); - mLog.debug("Attenuation at 1.25 Channels: " + evaluate(taps, bandEdge * 1.25)); - mLog.debug("Attenuation at 1.50 Channels: " + evaluate(taps, bandEdge * 1.50)); - mLog.debug("Attenuation at 1.75 Channels: " + evaluate(taps, bandEdge * 1.75)); - mLog.debug("Attenuation at 2.00 Channels: " + evaluate(taps, bandEdge * 2.00)); + mLog.debug("Attenuation at 1.00 Channels: " + evaluate(taps, bandEdge * 1.00) + "\tFrequency: " + (sampleRate * bandEdge * 1.0)); + mLog.debug("Attenuation at 1.25 Channels: " + evaluate(taps, bandEdge * 1.25) + "\tFrequency: " + (sampleRate * bandEdge * 1.25)); + mLog.debug("Attenuation at 1.50 Channels: " + evaluate(taps, bandEdge * 1.50) + "\tFrequency: " + (sampleRate * bandEdge * 1.5)); + mLog.debug("Attenuation at 1.75 Channels: " + evaluate(taps, bandEdge * 1.75) + "\tFrequency: " + (sampleRate * bandEdge * 1.75)); + mLog.debug("Attenuation at 2.00 Channels: " + evaluate(taps, bandEdge * 2.00) + "\tFrequency: " + (sampleRate * bandEdge * 2.0)); } - return extendedTaps; + //Adjust the overall gain of the filter to an objective of 1.0 times the number of channels + return normalize(extendedTaps, 1.0f * (float)channels); } /** @@ -952,7 +955,46 @@ public class FilterFactory double scalor = 2.0 * cutoff; double piScalor = Math.PI * scalor; - coefficients[half] = (float)(1.0 * scalor); + coefficients[half] = (float)(1.0 * scalor * window[half]); + + for(int x = 1; x <= half; x++) + { + double a = piScalor * x; + double coefficient = scalor * Math.sin(a) / a; + + coefficient *= window[half + x]; + coefficients[half + x] = (float)coefficient; + coefficients[half - x] = (float)coefficient; + } + + return coefficients; + } + + /** + * Creates a Nyquist filter using a Kaiser Window. + * + * @param cutoff frequency (0.0 <> 0.5) + * @param length of the filter - must be odd-length + * @param attenuation desired for adjacent channels + * @return filter coefficients. + * @throws FilterDesignException if the requested length is not odd + */ + public static float[] getKaiserSinc(int length, double cutoff, double attenuation) throws FilterDesignException + { + if(length % 2 == 0) + { + throw new FilterDesignException("Sinc filters must be odd-length"); + } + + float[] coefficients = new float[length]; + int half = length / 2; + + double[] window = Window.getKaiser(length, attenuation); + + double scalor = 2.0 * cutoff; + double piScalor = Math.PI * scalor; + + coefficients[half] = (float)(1.0 * scalor * window[half]); for(int x = 1; x <= half; x++) { @@ -969,12 +1011,29 @@ public class FilterFactory /** * Compares two doubles for equals and avoids any rounding error that are present. + * * @param a value to compare * @param objective value to compare * @return true if they are equivalent within the margin of error */ - private static boolean matchesObjective(double a, double objective) + private static boolean matchesObjective(double a, double objective, double marginOfError) { - return Math.abs(a - objective) < 0.0000000001; + return Math.abs(a - objective) <= marginOfError; + } + + public static void main(String[] args) + { + float[] taps = null; + + try + { + taps = FilterFactory.getSincM2Channelizer(12500.0, 800, 9, true); + } + catch(FilterDesignException fde) + { + mLog.error("Error", fde); + } + + mLog.debug("Done: " + Arrays.toString(taps)); } } diff --git a/src/main/java/io/github/dsheirer/dsp/filter/Window.java b/src/main/java/io/github/dsheirer/dsp/filter/Window.java index ceb68afb..a97f1453 100644 --- a/src/main/java/io/github/dsheirer/dsp/filter/Window.java +++ b/src/main/java/io/github/dsheirer/dsp/filter/Window.java @@ -1,21 +1,39 @@ /******************************************************************************* - * 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 + * You should have received a copy of the GNU General Public License along with this program. + * If not, see * ******************************************************************************/ + +/* + * Kaiser Window design methods: getKaiser(), getKaiserBeta() and getBesselZerothOrder() with minor naming changes were + * adopted from: + * + * https://github.com/rjeschke/neetutils-base/blob/master/src/main/java/com/github/rjeschke/neetutils/audio/FIRUtils.java + * + * Copyright (C) 2012 René Jeschke + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ package io.github.dsheirer.dsp.filter; import org.slf4j.Logger; @@ -62,6 +80,8 @@ public class Window return getHamming(length); case HANN: return getHann(length); + case KAISER: + throw new IllegalArgumentException("Kaiser Window cannot be created via this method. Use the getKaiser() method instead."); case NUTALL: return getNutall(length); case NONE: @@ -337,8 +357,13 @@ public class Window return coefficients; } - - private static double getKaiserBeta(double attenuation) + /** + * Calculates the beta (roll-off factor) for a Kaiser window based on the specified attenuation. + * + * @param attenuation in decibels + * @return beta value in the range of 0.0 to 1.0 + */ + public static double getKaiserBeta(double attenuation) { if(attenuation > 50.0) { @@ -354,6 +379,57 @@ public class Window } } + /** + * Creates a Kaiser window. + * + * @param length of the window + * @param attenuation desired + * @return window + */ + public static double[] getKaiser(int length, double attenuation) + { + double[] coefficients = new double[length]; + + double beta = getKaiserBeta(attenuation); + + double betaBesselZerothOrder = getBesselZerothOrder(beta); + + double temp; + + for(int x = 0; x < coefficients.length; x++) + { + temp = beta * Math.sqrt(1.0 - Math.pow(2.0 * x / (length - 1) - 1.0, 2)); + coefficients[x] = getBesselZerothOrder(temp) / betaBesselZerothOrder; + } + + return coefficients; + } + + + /** + * Zeroth order modified Bessel function. + * + * @param x Value. + * @return Return value. + */ + public final static double getBesselZerothOrder(final double x) + { + double f = 1; + final double x2 = x * x * 0.25; + double xc = x2; + double v = 1 + x2; + for (int i = 2; i < 100; i++) + { + f *= i; + xc *= x2; + final double a = xc / (f * f); + v += a; + if (a < 1e-20) break; + } + + return v; + } + /** * Apply the window against an array of float-type samples */ @@ -421,6 +497,7 @@ public class Window FLAT_TOP("Flat Top"), HAMMING("Hamming"), HANN("Hann"), + KAISER("Kaiser"), NUTALL("Nutall"), NONE("None"); @@ -436,4 +513,11 @@ public class Window return mLabel; } } + + public static void main(String[] args) + { + double[] kaiser = getKaiser(16, 80.0); + + mLog.debug("Window:" + Arrays.toString(kaiser)); + } } diff --git a/src/main/java/io/github/dsheirer/dsp/filter/channelizer/ComplexPolyphaseChannelizerM2.java b/src/main/java/io/github/dsheirer/dsp/filter/channelizer/ComplexPolyphaseChannelizerM2.java index d727d72c..e2bf21b0 100644 --- a/src/main/java/io/github/dsheirer/dsp/filter/channelizer/ComplexPolyphaseChannelizerM2.java +++ b/src/main/java/io/github/dsheirer/dsp/filter/channelizer/ComplexPolyphaseChannelizerM2.java @@ -19,12 +19,10 @@ package io.github.dsheirer.dsp.filter.channelizer; import io.github.dsheirer.dsp.filter.FilterFactory; -import io.github.dsheirer.dsp.filter.Window.WindowType; import io.github.dsheirer.dsp.filter.design.FilterDesignException; import io.github.dsheirer.sample.Listener; import io.github.dsheirer.sample.buffer.ReusableChannelResultsBuffer; import io.github.dsheirer.sample.buffer.ReusableComplexBuffer; -import io.github.dsheirer.sample.buffer.ReusableComplexBufferQueue; import io.github.dsheirer.sample.real.IOverflowListener; import org.jtransforms.fft.FloatFFT_1D; import org.slf4j.Logger; @@ -34,6 +32,33 @@ import java.util.Arrays; import java.util.Collection; import java.util.List; +/** + * Non-Maximally Decimated Polyphase Filter Bank (NMDPFB) channelizer that divides the input baseband complex sample + * stream into equal bandwidth channels that are each oversampled by 2x for output. + * + * This polyphase channelizer is based off of the channelizer described by Fred Harris in Multirate Signal + * Processing for Communications Systems, p230-233. + * + * Samples are loaded into this filter one block at a time (1/2 channel count) and a filtered output is calculated + * to produce an overall 2x oversampled channel sample rate. Each sample block load is preceded by a serpentine + * shift of the existing sample blocks. We use the java System.arrayCopy() method which is able to leverage + * native processor intrinsics for efficiency. + * + * The prototype filter for the channelizer is rearranged to align with the structure of the sample buffer. + * + * Instead of using an array of channel filters as described in the Harris text, this filter and the sample buffer + * are arranged as a contiguous array to maximize Java's ability to leverage native processor Single Instruction + * Multiple Data (SIMD) intrinsics (since Java 8). The filter process is broken into four steps: + * + * -Multiply the inline array of samples and filter coefficients + * -Accumulate the results for each sub-channel + * -Rearrange the sub-channel results to correctly order the sub-channels + * -Perform IFFT + * + * Note: design the prototype filter as a Nyquist windowed filter with a -6.02 db attenuation at the channel edge + * frequency if you need Perfect Reconstruction where you'll later re-join two or more channels to form a wider + * bandwidth channel or to isolate a signal that located between two channels. + */ public class ComplexPolyphaseChannelizerM2 extends AbstractComplexPolyphaseChannelizer { private final static Logger mLog = LoggerFactory.getLogger(ComplexPolyphaseChannelizerM2.class); @@ -55,25 +80,7 @@ public class ComplexPolyphaseChannelizerM2 extends AbstractComplexPolyphaseChann private int mTapsPerChannel; /** - * Non-Maximally Decimated Polyphase Filter Bank (NMDPFB) channelizer that divides the input frequency band into - * equal bandwidth channels that are each oversampled by 2x for output. - * - * This polyphase channelizer is based off of the channelizer described by Fred Harris in Multirate Signal - * Processing for Communications Systems, p230-233. - * - * Samples are loaded into this filter one block at a time (1/2 channel count) and a filtered output is calculated - * to produce an overall 2x oversampled channel sample rate. Each sample block load is preceded by a serpentine - * shift of the existing sample blocks. We use the java System.arrayCopy() method which is able to leverage - * native processor intrinsics for efficiency. - * - * The prototype filter for the channelizer is rearranged to align with the structure of the sample buffer. - * - * Instead of using an array of channel filters as described in the Harris text, this filter and the sample buffer - * are arranged as a contiguous array to maximize Java's ability to leverage native processor Single Instruction - * Multiple Data (SIMD) intrinsics (since Java 8). The filter process is broken into three steps: - * -Multiply the inline array of samples and filter coefficients - * -Accumulate the results for each sub-channel - * -Rearrange the sub-channel results to correctly order the sub-channels + * Creates a NMDPFB channelizer instance. * * @param taps of a low-pass filter designed for the inbound sample rate with a cutoff frequency * equal to the channel bandwidth (sample rate / filters). If you need to synthesize (combine two or more @@ -98,25 +105,8 @@ public class ComplexPolyphaseChannelizerM2 extends AbstractComplexPolyphaseChann } /** - * Non-Maximally Decimated Polyphase Filter Bank (NMDPFB) channelizer that divides the input frequency band into - * equal bandwidth channels that are each oversampled by 2x for output. - * - * This polyphase channelizer is based off of the channelizer described by Fred Harris in Multirate Signal - * Processing for Communications Systems, p230-233. - * - * Samples are loaded into this filter one block at a time (1/2 channel count) and a filtered output is calculated - * to produce an overall 2x oversampled channel sample rate. Each sample block load is preceded by a serpentine - * shift of the existing sample blocks. We use the java System.arrayCopy() method which is able to leverage - * native processor intrinsics for efficiency. - * - * The prototype filter for the channelizer is rearranged to align with the structure of the sample buffer. - * - * Instead of using an array of channel filters as described in the Harris text, this filter and the sample buffer - * are arranged as a contiguous array to maximize Java's ability to leverage native processor Single Instruction - * Multiple Data (SIMD) intrinsics (since Java 8). The filter process is broken into three steps: - * -Multiply the inline array of samples and filter coefficients - * -Accumulate the results for each sub-channel - * -Rearrange the sub-channel results to correctly order the sub-channels + * Creates a NMDPFB channelizer instance and designs a Perfect Reconstruction prototype filter appropriate for + * the baseband sample rate and quantity of filter taps per polyphase sub-channel. * * @param sampleRate to be channelized. * @param tapsPerChannel to use when designing the filter @@ -128,7 +118,7 @@ public class ComplexPolyphaseChannelizerM2 extends AbstractComplexPolyphaseChann mTapsPerChannel = tapsPerChannel; float[] filterTaps = FilterFactory.getSincM2Channelizer(getChannelSampleRate(), getChannelCount(), - mTapsPerChannel, WindowType.BLACKMAN_HARRIS_7, true); + mTapsPerChannel, true); init(filterTaps); } @@ -181,7 +171,7 @@ public class ComplexPolyphaseChannelizerM2 extends AbstractComplexPolyphaseChann super.setSampleRate(sampleRate); float[] filterTaps = FilterFactory.getSincM2Channelizer(getChannelSampleRate(), getChannelCount(), - mTapsPerChannel, WindowType.BLACKMAN_HARRIS_7, true); + mTapsPerChannel, true); init(filterTaps); } @@ -243,7 +233,6 @@ public class ComplexPolyphaseChannelizerM2 extends AbstractComplexPolyphaseChann //Enqueue the channel results buffer for IFFT processing and distribution on a different thread mIFFTProcessor.receive(channelResultsBuffer); -// mLog.debug("Duration: " + (System.nanoTime() - start) + " Processing:" + processing); //Decrement the user count to let the originator know we're done with their buffer reusableComplexBuffer.decrementUserCount(); } @@ -320,6 +309,7 @@ public class ComplexPolyphaseChannelizerM2 extends AbstractComplexPolyphaseChann int coefficientPointer = 0; int filterPointer = 0; + //Create a new filter that duplicates each tap to produce an interleaved I/Q filter while(coefficientPointer < coefficients.length) { filter[filterPointer++] = coefficients[coefficientPointer]; @@ -398,55 +388,6 @@ public class ComplexPolyphaseChannelizerM2 extends AbstractComplexPolyphaseChann return elapsed; } -// /** -// * Buffers the channel results and dispatches the channel results buffer to the channel output processors when full. -// * @param buffer containing filtered and IFFT'd channelResults with a corresponding timestamp -// */ -// private void processChannelResults(ReusableChannelResultsBuffer buffer) -// { -// if(mChannelResultsBuffer == null) -// { -// mChannelResultsBuffer = new PolyphaseChannelResultsBuffer(buffer.getTimestamp(), -// mChannelResultsBufferSize); -// } -// -// try -// { -// mChannelResultsBuffer.add(buffer.getSamplesCopy()); -// } -// catch(IllegalArgumentException iae) -// { -// //If the buffer is full (unlikely) or the channel results array length has changed (possible), flush the -// //current buffer, create a new one, and store the current results -// flushChannelResultsBuffer(); -// -// mChannelResultsBuffer = new PolyphaseChannelResultsBuffer(buffer.getTimestamp(), mChannelResultsBufferSize); -// -// mChannelResultsBuffer.add(buffer.getSamplesCopy()); -// } -// -// if(mChannelResultsBuffer.isFull()) -// { -// flushChannelResultsBuffer(); -// } -// -// buffer.decrementUserCount(); -// } - -// /** -// * Dispatches the non-empty channel results buffer to the channel output processors and nullifies the reference to -// * the buffer so that a new buffer can be created upon receiving the next channel results. -// */ -// private void flushChannelResultsBuffer() -// { -// if(mChannelResultsBuffer != null && !mChannelResultsBuffer.isEmpty()) -// { -// dispatch(mChannelResultsBuffer); -// } -// -// mChannelResultsBuffer = null; -// } - /** * Initializes the channelizer filter structures. * @@ -454,12 +395,9 @@ public class ComplexPolyphaseChannelizerM2 extends AbstractComplexPolyphaseChann */ private void init(float[] coefficients) { - //Setup the FFT mFFT = new FloatFFT_1D(getChannelCount()); - int channelCount = getChannelCount(); int bufferLength = getSubChannelCount() * mTapsPerChannel; - mSamplesPerBlock = getChannelCount(); //Same as subChannelCount / 2 mTopBlockMap = getTopBlockMap(channelCount); mMiddleBlockMap = getMiddleBlockMap(channelCount); @@ -535,26 +473,13 @@ public class ComplexPolyphaseChannelizerM2 extends AbstractComplexPolyphaseChann int tapsPerChannel = 8; float[] taps = new float[channelCount * tapsPerChannel]; - int sampleRate = 50000; - for(int x = 0; x < taps.length; x++) { taps[x] = x; } - ComplexPolyphaseChannelizerM2 channelizer = new ComplexPolyphaseChannelizerM2(taps, sampleRate, channelCount); + float[] filter = getAlignedFilter(taps, channelCount, tapsPerChannel); - ReusableComplexBufferQueue queue = new ReusableComplexBufferQueue(); - - int sampleCount = channelCount * 2 * tapsPerChannel; - ReusableComplexBuffer buffer = queue.getBuffer(sampleCount); - buffer.incrementUserCount(); - - for(int x = 0; x < sampleCount; x++) - { - buffer.getSamples()[x] = x; - } - - channelizer.receive(buffer); + mLog.debug(Arrays.toString(filter)); } } diff --git a/src/main/java/io/github/dsheirer/dsp/filter/channelizer/PolyphaseChannelManager.java b/src/main/java/io/github/dsheirer/dsp/filter/channelizer/PolyphaseChannelManager.java index 4045673b..3a3a2750 100644 --- a/src/main/java/io/github/dsheirer/dsp/filter/channelizer/PolyphaseChannelManager.java +++ b/src/main/java/io/github/dsheirer/dsp/filter/channelizer/PolyphaseChannelManager.java @@ -16,7 +16,6 @@ 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; @@ -45,13 +44,29 @@ import java.util.TreeSet; import java.util.concurrent.ConcurrentLinkedQueue; import java.util.concurrent.CopyOnWriteArrayList; +/** + * Polyphase Channel Manager is a DDC channel manager and complex buffer queue/processor for a tuner. This class + * provides DDC polyphase channel sources and wraps a polyphase channelizer processing sample buffers produced by + * the tuner and distributing channelized sample buffers to each allocated DDC polyphase channel source. This + * class is responsible for monitoring the tuner for changes in center frequency and/or sample rate and updating + * active DDC polyphase channel sources accordingly. This class also monitors source event requests and + * notifications received from active DDC polyphase channel sources to adjust sample streams as required. + * + * Channel bandwidth and channel count are determined by the sample rate of the baseband buffer stream provider. This + * class is currently designed to provide channels each with a minimum usable bandwidth of 12.5 kHz and oversampled by + * 2.0 to a minimum of 25.0 kHz channel sample rate. If the baseband stream provider sample rate is not evenly + * divisible by 12.5 kHz channels for an even number of channels, the channel bandwidth will be increased. + * + * Note: add this channel manager as a source event listener to the complex buffer provider to ensure this manager + * adapts to changes in source frequency and sample rate. + */ public class PolyphaseChannelManager implements ISourceEventProcessor { private final static Logger mLog = LoggerFactory.getLogger(PolyphaseChannelManager.class); private static final double MINIMUM_CHANNEL_BANDWIDTH = 12500.0; private static final double CHANNEL_OVERSAMPLING = 2.0; - private static final int POLYPHASE_CHANNELIZER_TAPS_PER_CHANNEL = 8; - private static final int POLYPHASE_SYNTHESIZER_TAPS_PER_CHANNEL = 20; + private static final int POLYPHASE_CHANNELIZER_TAPS_PER_CHANNEL = 9; + private static final int POLYPHASE_SYNTHESIZER_TAPS_PER_CHANNEL = 9; private Broadcaster mSourceEventBroadcaster = new Broadcaster<>(); private IReusableBufferProvider mReusableBufferProvider; @@ -64,19 +79,12 @@ public class PolyphaseChannelManager implements ISourceEventProcessor private Map mOutputProcessorFilters = new HashMap<>(); /** - * Polyphase Channel Manager is a DDC channel manager and complex buffer queue/processor for a tuner. This class - * provides DDC polyphase channel sources and wraps a polyphase channelizer processing sample buffers produced by - * the tuner and distributing channelized sample buffers to each allocated DDC polyphase channel source. This - * class is responsible for monitoring the tuner for changes in center frequency and/or sample rate and updating - * active DDC polyphase channel sources accordingly. This class also monitors source event requests and - * notifications received from active DDC polyphase channel sources to adjust sample streams as required. + * Creates a polyphase channel manager instance. * - * Note: add this channel manager as a source event listener to the complex buffer provider to ensure this manager - * adapts to changes in source frequency and sample rate. - * - * @param reusableBufferProvider that supports register/deregister of this channel manager - * @param frequency of the provided complex buffer samples - * @param sampleRate of the provided complex buffer samples + * @param reusableBufferProvider (ie tuner) that supports register/deregister for reusable baseband sample buffer + * streams + * @param frequency of the baseband complex buffer sample stream (ie center frequency) + * @param sampleRate of the baseband complex buffer sample stream */ public PolyphaseChannelManager(IReusableBufferProvider reusableBufferProvider, long frequency, double sampleRate) @@ -106,6 +114,8 @@ public class PolyphaseChannelManager implements ISourceEventProcessor /** * Creates a polyphase channel manager for the tuner controller + * + * @param tunerController for a tuner that provides a baseband complex buffer stream. */ public PolyphaseChannelManager(TunerController tunerController) { @@ -187,12 +197,13 @@ public class PolyphaseChannelManager implements ISourceEventProcessor IPolyphaseChannelOutputProcessor outputProcessor = getOutputProcessor(polyphaseIndexes); - TunerChannel tunerChannel = new TunerChannel(100000000, 12500); if(outputProcessor != null) { long centerFrequency = mChannelCalculator.getCenterFrequencyForIndexes(polyphaseIndexes); + TunerChannel tunerChannel = new TunerChannel(centerFrequency, 12500); + channelSource = new PolyphaseChannelSource(tunerChannel, outputProcessor, mChannelSourceEventListener, mChannelCalculator.getChannelSampleRate(), centerFrequency); } @@ -267,7 +278,6 @@ public class PolyphaseChannelManager implements ISourceEventProcessor */ private void stopChannelSource(PolyphaseChannelSource channelSource) { - mLog.debug("Stopping channel source ... count:" + mChannelSources.size()); synchronized(mBufferProcessor) { mChannelSources.remove(channelSource); @@ -292,7 +302,6 @@ public class PolyphaseChannelManager implements ISourceEventProcessor { //Do nothing } - mLog.debug("Stopped channel source ... count:" + mChannelSources.size()); } /** @@ -517,7 +526,7 @@ public class PolyphaseChannelManager implements ISourceEventProcessor if(taps == null) { taps = FilterFactory.getSincM2Synthesizer(mChannelCalculator.getChannelBandwidth(), channels, - POLYPHASE_SYNTHESIZER_TAPS_PER_CHANNEL, Window.WindowType.BLACKMAN_HARRIS_7, true); + POLYPHASE_SYNTHESIZER_TAPS_PER_CHANNEL); mOutputProcessorFilters.put(channels, taps); } @@ -533,7 +542,6 @@ public class PolyphaseChannelManager implements ISourceEventProcessor @Override public void receive(SourceEvent sourceEvent) { - mLog.debug("****** Got a source event: " + sourceEvent.toString()); switch(sourceEvent.getEvent()) { case REQUEST_START_SAMPLE_STREAM: diff --git a/src/main/java/io/github/dsheirer/dsp/filter/channelizer/TwoChannelSynthesizerM2.java b/src/main/java/io/github/dsheirer/dsp/filter/channelizer/TwoChannelSynthesizerM2.java index 02a142cc..acc74239 100644 --- a/src/main/java/io/github/dsheirer/dsp/filter/channelizer/TwoChannelSynthesizerM2.java +++ b/src/main/java/io/github/dsheirer/dsp/filter/channelizer/TwoChannelSynthesizerM2.java @@ -21,27 +21,24 @@ import org.jtransforms.fft.FloatFFT_1D; import org.slf4j.Logger; import org.slf4j.LoggerFactory; -import java.util.Arrays; - /** * Implements a two-channel polyphase filer synthesizer. This class is intended to be used with an M2 polyphase * channelizer in order to recover a signal that overlaps the boundary between two channels. This synthesizer will - * rejoin the two M2 neighboring channels, mix the signal of interest to baseband, and filter the the output stream to + * rejoin the two M2 neighboring channels, mix the signal of interest to baseband, and filter the output stream to * half of the sample rate, producing an overall M2 output sample rate. * * The synthesizer concept is modeled on the structure described by Fred Harris et al, in 'Interleaving Different * Bandwidth Narrowband Channels in Perfect Reconstruction Cascade Polyphase Filter Banks for Efficient Flexible * Variable Bandwidth Filters in Wideband Digital Transceivers'. * - * The data buffers and sub-filter kernels bear no resemblance to the structures defined in the paper. The data and + * The data buffers and sub-filter kernels bear some resemblance to the structures defined in the paper. The data and * filter structures in this class are organized to take advantage of Java's ability to leverage SIMD processor * intrinsics for optimal efficiency in processing. The paper specifies splitting the filter kernel into N polyphase * partitions and creating N data buffers. This class organizes the data buffer as a contiguous sample - * array and creates two copies of the filter kernel, one arranged for normal filtering and the second arranged for - * phase-shifted filtering. Instead of calculating the dot-product for each sub-filter, we calculate the product of the - * full data array against the filter and then accumulate each sub-filter. This allows java to use SIMD for the array - * product and then normal processing for the accumulation. Since this is a two-channel processor and the results of - * each filter accumulation are added, we use a single accumulator across both filters. + * array and creates an I/Q interleaved filter kernel. Instead of calculating the dot-product for each sub-filter, + * we calculate the product of the full data array against the filter and then accumulate each sub-filter. This allows + * java to use SIMD for the array product and then normal processing for the accumulation. Since this is a two-channel + * processor and the results of each filter accumulation are added, we use a single accumulator across both filters. * */ public class TwoChannelSynthesizerM2 @@ -49,20 +46,19 @@ public class TwoChannelSynthesizerM2 private final static Logger mLog = LoggerFactory.getLogger(TwoChannelSynthesizerM2.class); private ReusableComplexBufferQueue mReusableComplexBufferQueue = new ReusableComplexBufferQueue("Two Channel Synthesizer M2"); - private float[] mDataBuffer; - private float[] mNormalFilter; - private float[] mPhaseShiftedFilter; - private float[] mFilterProduct; + private float[] mSerpentineDataBuffer; + private float[] mIQInterleavedFilter; + private float[] mFilterVectorProduct; private float[] mIFFTBuffer = new float[4]; private float mIAccumulator; private float mQAccumulator; private FloatFFT_1D mFFT = new FloatFFT_1D(2); - private boolean mFlag = true; + private boolean mTopBlockFlag = true; /** - * 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. + * 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) { @@ -70,24 +66,17 @@ public class TwoChannelSynthesizerM2 } /** - * 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. + * Initializes the synthesizer filter and data buffers for operation. * - * 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. + * @param filter to use for polyphase synthesis of two channels. */ private void init(float[] filter) { int tapsPerChannel = (int)Math.ceil(filter.length / 2); - mNormalFilter = getNormalFilter(filter, tapsPerChannel); - mPhaseShiftedFilter = getPhaseShiftedFilter(filter, tapsPerChannel); - mDataBuffer = new float[mNormalFilter.length]; - mFilterProduct = new float[mNormalFilter.length]; + mIQInterleavedFilter = getInterleavedFilter(filter, tapsPerChannel); + mSerpentineDataBuffer = new float[mIQInterleavedFilter.length]; + mFilterVectorProduct = new float[mIQInterleavedFilter.length]; } /** @@ -114,44 +103,54 @@ public class TwoChannelSynthesizerM2 for(int x = 0; x < channel1.length; x += 2) { + //Load samples from each channel into buffer for IFFT mIFFTBuffer[0] = channel1[x]; //i mIFFTBuffer[1] = channel1[x + 1]; //q mIFFTBuffer[2] = channel2[x]; //i mIFFTBuffer[3] = channel2[x + 1]; //q + //Perform Inverse FFT (IFFT) mFFT.complexInverse(mIFFTBuffer, true); - System.arraycopy(mDataBuffer, 0, mDataBuffer, 4, mDataBuffer.length - 4); - System.arraycopy(mIFFTBuffer, 0, mDataBuffer, 0, mIFFTBuffer.length); + //Perform serpentine shift of data blocks in the data buffer - make room for 4 new samples + System.arraycopy(mSerpentineDataBuffer, 0, mSerpentineDataBuffer, 4, mSerpentineDataBuffer.length - 4); - if(mFlag) + //Top Block - load samples into data buffer in normal order + if(mTopBlockFlag) { - for(int y = 0; y < mDataBuffer.length; y++) - { - mFilterProduct[y] = mDataBuffer[y] * mNormalFilter[y]; - } + System.arraycopy(mIFFTBuffer, 0, mSerpentineDataBuffer, 0, mIFFTBuffer.length); } + //Bottom Block - swap samples via data loading to account for phase shift else { - for(int y = 0; y < mDataBuffer.length; y++) - { - mFilterProduct[y] = mDataBuffer[y] * mPhaseShiftedFilter[y]; - } + System.arraycopy(mIFFTBuffer, 0, mSerpentineDataBuffer, 2, 2); + System.arraycopy(mIFFTBuffer, 2, mSerpentineDataBuffer, 0, 2); } + //Note: in order to use Java's ability to leverage SIMD intrinsics, we perform filtering in two steps + //(multiply then accumulate) since our filter and data are structured with I and Q vectors interleaved. + //This approach allows the Hotspot compiler to more easily recognize the scalor operations. + + //Multiply data samples by the I/Q interleaved filter to form the vector product + for(int y = 0; y < mSerpentineDataBuffer.length; y++) + { + mFilterVectorProduct[y] = mSerpentineDataBuffer[y] * mIQInterleavedFilter[y]; + } + + //Accumulate output I/Q samples from vector product mIAccumulator = 0.0f; mQAccumulator = 0.0f; - for(int y = 0; y < mFilterProduct.length; y += 2) + for(int y = 0; y < mFilterVectorProduct.length; y += 2) { - mIAccumulator += mFilterProduct[y]; - mQAccumulator += mFilterProduct[y + 1]; + mIAccumulator += mFilterVectorProduct[y]; + mQAccumulator += mFilterVectorProduct[y + 1]; } output[x] = mIAccumulator; output[x + 1] = mQAccumulator; - mFlag = !mFlag; + mTopBlockFlag = !mTopBlockFlag; } channelBuffer1.decrementUserCount(); @@ -162,120 +161,33 @@ public class TwoChannelSynthesizerM2 return synthesizedComplexBuffer; } - private static float[] getNormalFilter(float[] coefficients, int tapsPerChannel) + /** + * Creates an interleaved I/Q filter where each coefficient from the filter argument is duplicated and the returned + * filter is twice the length of the original filter. + * + * Note: the returned filter array is sized to: 2 * channel count * taps per channel, which may be slightly more + * than twice the length of the original filter. Any Added filter array elements will contain zero values. + * + * @param coefficients to create an interleaved filter + * @param tapsPerChannel count + * @return + */ + private static float[] getInterleavedFilter(float[] coefficients, int tapsPerChannel) { - float[] filter = new float[4 * tapsPerChannel]; + int channelCount = 2; - int idealLength = 2 * tapsPerChannel; + float[] filter = new float[channelCount * tapsPerChannel * 2]; - int offset = idealLength - 2; + int coefficientPointer = 0; + int filterPointer = 0; - int index; - - for(int x = 0; x < idealLength; x += 2) + //Create a new filter that duplicates each tap to produce an interleaved I/Q filter + while(coefficientPointer < coefficients.length) { - index = offset - x; - - if(index < coefficients.length) - { - //Filter 1 - filter[2 * x + 0] = coefficients[index]; - filter[2 * x + 1] = coefficients[index]; - } - - index = offset - x + 1; - - if(index < coefficients.length) - { - //Filter 2 - filter[2 * x + 2] = coefficients[index]; - filter[2 * x + 3] = coefficients[index]; - } + filter[filterPointer++] = coefficients[coefficientPointer]; + filter[filterPointer++] = coefficients[coefficientPointer++]; } return filter; } - - private static float[] getPhaseShiftedFilter(float[] coefficients, int tapsPerChannel) - { - float[] filter = new float[4 * tapsPerChannel]; - - int idealLength = 2 * tapsPerChannel; - - int offset = idealLength - 2; - - int index; - - for(int x = 0; x < idealLength; x += 2) - { - index = offset - x + 1; - - if(index < coefficients.length) - { - //Filter 2 - filter[2 * x + 0] = coefficients[index]; - filter[2 * x + 1] = coefficients[index]; - } - - index = offset - x; - - if(index < coefficients.length) - { - //Filter 1 - filter[2 * x + 2] = coefficients[index]; - filter[2 * x + 3] = coefficients[index]; - } - } - - return filter; - } - - public static void main(String[] args) - { - mLog.debug("Starting"); - - int sampleCount = 30; - - try - { -// float[] taps = FilterFactory.getSincM2Synthesizer(12500.0, 2, 20, Window.WindowType.BLACKMAN_HARRIS_7, true); - - float[] taps = new float[15]; - for(int x = 0; x < taps.length; x++) - { - taps[x] = x; - } - - float[] normalFilter = getPhaseShiftedFilter(taps, 8); - - mLog.debug("Filter: " + Arrays.toString(normalFilter)); - -// 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[] channel1 = new float[16]; -// float[] channel2 = new float[16]; -// for(int x = 1; x <= 16; x++) -// { -// channel1[x - 1] = x; -// channel2[x - 1] = x + 16; -// } -// -// -// float[] synthesized = synthesizer.process(channel1, channel2); -// -// mLog.debug("1:" + Arrays.toString(channel1)); -// mLog.debug("X:" + Arrays.toString(synthesized)); - } - catch(Exception fde) - { - mLog.error("Filter design error", fde); - } - - mLog.debug("Finished!!"); - } } diff --git a/src/main/java/io/github/dsheirer/dsp/filter/channelizer/output/TwoChannelOutputProcessor.java b/src/main/java/io/github/dsheirer/dsp/filter/channelizer/output/TwoChannelOutputProcessor.java index 42871dac..7852bd6d 100644 --- a/src/main/java/io/github/dsheirer/dsp/filter/channelizer/output/TwoChannelOutputProcessor.java +++ b/src/main/java/io/github/dsheirer/dsp/filter/channelizer/output/TwoChannelOutputProcessor.java @@ -18,7 +18,7 @@ package io.github.dsheirer.dsp.filter.channelizer.output; import io.github.dsheirer.dsp.filter.channelizer.TwoChannelSynthesizerM2; import io.github.dsheirer.dsp.filter.design.FilterDesignException; import io.github.dsheirer.dsp.filter.fir.FIRFilterSpecification; -import io.github.dsheirer.dsp.filter.fir.complex.ComplexFIRFilter; +import io.github.dsheirer.dsp.filter.fir.complex.ComplexFIRFilter2; import io.github.dsheirer.dsp.filter.fir.remez.RemezFIRFilterDesigner; import io.github.dsheirer.dsp.mixer.FS4DownConverter; import io.github.dsheirer.sample.buffer.ReusableBufferAssembler; @@ -27,47 +27,18 @@ import io.github.dsheirer.sample.buffer.ReusableComplexBuffer; import org.slf4j.Logger; import org.slf4j.LoggerFactory; +import java.util.HashMap; import java.util.List; +import java.util.Map; public class TwoChannelOutputProcessor extends ChannelOutputProcessor { private final static Logger mLog = LoggerFactory.getLogger(TwoChannelOutputProcessor.class); - - private static float[] mFilterCoefficients; - - static - { - try - { - FIRFilterSpecification specification = FIRFilterSpecification.lowPassBuilder() - .sampleRate(25000) - .gridDensity(16) - .oddLength(true) - .passBandCutoff(6000) - .passBandAmplitude(1.0) - .passBandRipple(0.01) - .stopBandStart(6500) - .stopBandAmplitude(0.0) - .stopBandRipple(0.005) //Approximately 94 dB attenuation - .build(); - - RemezFIRFilterDesigner designer = new RemezFIRFilterDesigner(specification); - - if(designer.isValid()) - { - mFilterCoefficients = designer.getImpulseResponse(); - } - } - catch(FilterDesignException fde) - { - mLog.error("Error designing low pass filter for two channel output processor", fde); - } - } - + private static Map sLowPassFilterKernelMap = new HashMap<>(); private TwoChannelSynthesizerM2 mSynthesizer; private FS4DownConverter mFS4DownConverter = new FS4DownConverter(); - private ComplexFIRFilter mLowPassFilter; + private ComplexFIRFilter2 mLowPassFilter; private int mChannelOffset1; private int mChannelOffset2; @@ -87,7 +58,23 @@ public class TwoChannelOutputProcessor extends ChannelOutputProcessor setPolyphaseChannelIndices(channelIndexes); setSynthesisFilter(filter); - mLowPassFilter = new ComplexFIRFilter(mFilterCoefficients, 1.0f); + float[] lowPassFilter = sLowPassFilterKernelMap.get((int)sampleRate); + + if(lowPassFilter == null) + { + try + { + lowPassFilter = getLowPassFilter(sampleRate); + sLowPassFilterKernelMap.put((int)sampleRate, lowPassFilter); + } + catch(FilterDesignException fde) + { + mLog.error("Couldn't design a low-pass filter for 2-channel synthesizer", fde); + throw new IllegalStateException("Cannot design low-pass filter for 2 channel synthesizer", fde); + } + } + + mLowPassFilter = new ComplexFIRFilter2(lowPassFilter, 1.0f); } /** @@ -159,4 +146,39 @@ public class TwoChannelOutputProcessor extends ChannelOutputProcessor buffer.decrementUserCount(); } } + + /** + * Creates a low-pass filter kernel for the specified sample rate. + * @param sampleRate for the channel + * @return filter + * @throws FilterDesignException if the filter cannot be designed + */ + private static float[] getLowPassFilter(double sampleRate) throws FilterDesignException + { + double cutoff = sampleRate / 4.0; + int passBandCutoff = (int)(cutoff * 0.92); + int stopBandStart = (int)(cutoff * 1.08); + + FIRFilterSpecification specification = FIRFilterSpecification.lowPassBuilder() + .sampleRate(sampleRate) + .gridDensity(16) + .oddLength(true) + .passBandCutoff((int)passBandCutoff) + .passBandAmplitude(1.0) + .passBandRipple(0.01) + .stopBandStart((int)stopBandStart) + .stopBandAmplitude(0.0) + .stopBandRipple(0.005) //Approximately 94 dB attenuation + .build(); + + RemezFIRFilterDesigner designer = new RemezFIRFilterDesigner(specification); + + if(designer.isValid()) + { + return designer.getImpulseResponse(); + } + + throw new FilterDesignException("Couldn't design low-pass filter for sample rate: " + sampleRate + + " Pass End:" + passBandCutoff + " Stop Begin:" + stopBandStart); + } } diff --git a/src/main/java/io/github/dsheirer/dsp/filter/design/FilterViewer.java b/src/main/java/io/github/dsheirer/dsp/filter/design/FilterViewer.java index dc8aec31..c4298aef 100644 --- a/src/main/java/io/github/dsheirer/dsp/filter/design/FilterViewer.java +++ b/src/main/java/io/github/dsheirer/dsp/filter/design/FilterViewer.java @@ -1,7 +1,6 @@ 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 javafx.application.Application; import javafx.scene.Scene; @@ -76,7 +75,7 @@ public class FilterViewer extends Application try { - taps = FilterFactory.getSincM2Synthesizer(12500.0, 2, 19, Window.WindowType.BLACKMAN_HARRIS_7, true); + taps = FilterFactory.getSincM2Synthesizer(12500.0, 2, 19); // taps = FilterFactory.getSincFilter(25000.0, 12500.0, 2, 19, Window.WindowType.BLACKMAN_HARRIS_7, true); } catch(FilterDesignException fde) diff --git a/src/main/java/io/github/dsheirer/dsp/filter/fir/complex/ComplexFIRFilter.java b/src/main/java/io/github/dsheirer/dsp/filter/fir/complex/ComplexFIRFilter.java index c4e3a909..a61f04f2 100644 --- a/src/main/java/io/github/dsheirer/dsp/filter/fir/complex/ComplexFIRFilter.java +++ b/src/main/java/io/github/dsheirer/dsp/filter/fir/complex/ComplexFIRFilter.java @@ -21,6 +21,7 @@ import io.github.dsheirer.sample.buffer.ReusableComplexBuffer; import io.github.dsheirer.sample.buffer.ReusableComplexBufferQueue; import io.github.dsheirer.sample.complex.Complex; +@Deprecated //Use ComplexFIRFilter2 instead public class ComplexFIRFilter extends FIRFilter { private ReusableComplexBufferQueue mReusableComplexBufferQueue = new ReusableComplexBufferQueue("Complex FIR Filter"); diff --git a/src/main/java/io/github/dsheirer/dsp/filter/fir/complex/ComplexFIRFilter2.java b/src/main/java/io/github/dsheirer/dsp/filter/fir/complex/ComplexFIRFilter2.java new file mode 100644 index 00000000..30dd818e --- /dev/null +++ b/src/main/java/io/github/dsheirer/dsp/filter/fir/complex/ComplexFIRFilter2.java @@ -0,0 +1,139 @@ +/******************************************************************************* + * 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 + * + ******************************************************************************/ +package io.github.dsheirer.dsp.filter.fir.complex; + +import io.github.dsheirer.dsp.filter.fir.FIRFilter; +import io.github.dsheirer.dsp.filter.fir.real.RealFIRFilter2; +import io.github.dsheirer.sample.buffer.ReusableComplexBuffer; +import io.github.dsheirer.sample.buffer.ReusableComplexBufferQueue; +import io.github.dsheirer.sample.complex.Complex; + +public class ComplexFIRFilter2 extends FIRFilter +{ + private ReusableComplexBufferQueue mReusableComplexBufferQueue = new ReusableComplexBufferQueue("Complex FIR Filter"); + private RealFIRFilter2 mIFilter; + private RealFIRFilter2 mQFilter; + + /** + * Complex FIR Filter for processing complex sample pairs that internally uses two RealFIRFilter + * instances. + * + * @param coefficients - filter taps + * @param gain - gain to apply to filtered outputs - use 1.0f for no gain + */ + public ComplexFIRFilter2(float[] coefficients, float gain) + { + mIFilter = new RealFIRFilter2(coefficients, gain); + mQFilter = new RealFIRFilter2(coefficients, gain); + } + + /** + * Complex FIR Filter for processing complex sample pairs that internally uses two RealFIRFilter + * instances. This constructor uses a default gain of 1.0f. + * + * @param coefficients - filter taps + */ + public ComplexFIRFilter2(float[] coefficients) + { + this(coefficients, 1.0f); + } + + /** + * Filters the inphase sample value. + * @param sample to filter + * @return filtered sample + */ + public float filterInphase(float sample) + { + return mIFilter.filter(sample); + } + + /** + * Current filtered inphase sample value after invoking the filterInphase() method. + */ + public float currentInphaseValue() + { + return mIFilter.currentValue(); + } + + /** + * Filters the quadrature sample value. + * @param sample to filter + * @return filtered sample + */ + public float filterQuadrature(float sample) + { + return mQFilter.filter(sample); + } + + /** + * Current filtered quadrature sample value after invoking the filterInphase() method. + */ + public float currentQuadratureValue() + { + return mQFilter.currentValue(); + } + + /** + * Filters the complex sample. + * @param sample to filter + * @return filtered sample + */ + public Complex filter(Complex sample) + { + float i = filterInphase(sample.inphase()); + float q = filterQuadrature(sample.quadrature()); + + return new Complex(i, q); + } + + /** + * Filters the complex samples from the reusable buffer and returns a new complex buffer with the filtered output + * + * Note: the original reusable buffer user count is decremented and a new reusable buffer is returned + * with the user count already incremented to one. + * + * @param originalBuffer with complex samples to filter + * @return new buffer containing filtered complex samples + */ + public ReusableComplexBuffer filter(ReusableComplexBuffer originalBuffer) + { + ReusableComplexBuffer filteredBuffer = mReusableComplexBufferQueue.getBuffer(originalBuffer.getSamples().length); + filteredBuffer.setTimestamp(originalBuffer.getTimestamp()); + + float[] samples = originalBuffer.getSamples(); + float[] filteredSamples = filteredBuffer.getSamples(); + + for(int x = 0; x < originalBuffer.getSamples().length; x += 2) + { + filteredSamples[x] = filterInphase(samples[x]); + filteredSamples[x + 1] = filterQuadrature(samples[x + 1]); + } + + originalBuffer.decrementUserCount(); + filteredBuffer.incrementUserCount(); + + return filteredBuffer; + } + + @Override + public void dispose() + { + mIFilter.dispose(); + mQFilter.dispose(); + mReusableComplexBufferQueue.dispose(); + } +} diff --git a/src/main/java/io/github/dsheirer/dsp/filter/fir/complex/ComplexFIRFilter_CB_CB.java b/src/main/java/io/github/dsheirer/dsp/filter/fir/complex/ComplexFIRFilter_CB_CB.java index 7e9ccc71..bb1e3c0a 100644 --- a/src/main/java/io/github/dsheirer/dsp/filter/fir/complex/ComplexFIRFilter_CB_CB.java +++ b/src/main/java/io/github/dsheirer/dsp/filter/fir/complex/ComplexFIRFilter_CB_CB.java @@ -3,6 +3,7 @@ package io.github.dsheirer.dsp.filter.fir.complex; import io.github.dsheirer.sample.Listener; import io.github.dsheirer.sample.complex.ComplexBuffer; +@Deprecated //Use ComplexFIRFilter2 instead public class ComplexFIRFilter_CB_CB extends ComplexFIRFilter implements Listener { private Listener mListener; diff --git a/src/main/java/io/github/dsheirer/dsp/filter/fir/real/RealFIRFilter.java b/src/main/java/io/github/dsheirer/dsp/filter/fir/real/RealFIRFilter.java index 1666de80..bc42fb6b 100644 --- a/src/main/java/io/github/dsheirer/dsp/filter/fir/real/RealFIRFilter.java +++ b/src/main/java/io/github/dsheirer/dsp/filter/fir/real/RealFIRFilter.java @@ -17,6 +17,7 @@ package io.github.dsheirer.dsp.filter.fir.real; import io.github.dsheirer.dsp.filter.fir.FIRFilter; +@Deprecated //Use RealFIRFilter2 instead public class RealFIRFilter extends FIRFilter { private float[] mBuffer; diff --git a/src/main/java/io/github/dsheirer/dsp/filter/fir/real/RealFIRFilter2.java b/src/main/java/io/github/dsheirer/dsp/filter/fir/real/RealFIRFilter2.java new file mode 100644 index 00000000..7122ea76 --- /dev/null +++ b/src/main/java/io/github/dsheirer/dsp/filter/fir/real/RealFIRFilter2.java @@ -0,0 +1,133 @@ +/******************************************************************************* + * 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 + * + ******************************************************************************/ +package io.github.dsheirer.dsp.filter.fir.real; + +import io.github.dsheirer.dsp.filter.fir.FIRFilter; +import io.github.dsheirer.sample.buffer.ReusableBuffer; +import io.github.dsheirer.sample.buffer.ReusableBufferQueue; + +/** + * Finite Impulse Response (FIR) filter for filtering individual float samples or float sample arrays. + * + * Note: filtering operations in this class are structured to leverage SIMD processor intrinsics when + * available to the Java runtime. + */ +public class RealFIRFilter2 extends FIRFilter +{ + private ReusableBufferQueue mReusableBufferQueue = new ReusableBufferQueue(); + + private float[] mData; + private float[] mCoefficients; + private float mGain; + private float mAccumulator; + + /** + * Float sample FIR filter base class. + * + * @param coefficients - filter coefficients in normal order. + * @param gain value to apply to the filtered output. Use 1.0f for no gain + */ + public RealFIRFilter2(float[] coefficients, float gain) + { + mGain = gain; + mCoefficients = coefficients; + mData = new float[coefficients.length]; + } + + /** + * Float sample FIR filter base class that uses a default gain value of 1.0f. + * + * @param coefficients - filter coefficients in normal order. + */ + public RealFIRFilter2(float[] coefficients) + { + this(coefficients, 1.0f); + } + + /** + * Disposes this filter to prepare for garbage collection. + */ + @Override + public void dispose() + { + mCoefficients = null; + mData = null; + } + + /** + * Filters the sample argument. Loads the sample into the internal data buffer and performs + * convolution between the sample buffer and the filter coefficients. + * + * @param sample to load + * @return filtered value + */ + public float filter(float sample) + { + //Use array copy to leverage SIMD intrinsics + System.arraycopy(mData, 0, mData, 1, mData.length - 1); + mData[0] = sample; + + mAccumulator = 0.0f; + + //Use vector dot product to leverage SIMD intrinsics + for(int x = 0; x < mCoefficients.length; x++) + { + mAccumulator += mData[x] * mCoefficients[x]; + } + + /* Apply gain and return the filtered value */ + mAccumulator *= mGain; + + return mAccumulator; + } + + /** + * Current filtered output value for the filter after the filter() method has been invoked. + */ + public float currentValue() + { + return mAccumulator; + } + + + /** + * Filters the samples contained in the unfilteredBuffer and returns a new reusable buffer with the + * filtered samples. + * + * Note: user count on the returned (new) buffer is set to one and the user count is decremented on + * the unfiltered buffer argument. + * + * @param unfilteredBuffer containing a sample array to be filtered + * @return a new reusable buffer with the filtered samples. + */ + public ReusableBuffer filter(ReusableBuffer unfilteredBuffer) + { + float[] unfilteredSamples = unfilteredBuffer.getSamples(); + + ReusableBuffer filteredBuffer = mReusableBufferQueue.getBuffer(unfilteredSamples.length); + float[] filteredSamples = filteredBuffer.getSamples(); + + for(int x = 0; x < unfilteredSamples.length; x++) + { + filteredSamples[x] = filter(unfilteredSamples[x]); + } + + filteredBuffer.incrementUserCount(); + unfilteredBuffer.decrementUserCount(); + + return filteredBuffer; + } +} \ No newline at end of file diff --git a/src/main/java/io/github/dsheirer/dsp/filter/fir/real/RealFIRFilter_RB_RB.java b/src/main/java/io/github/dsheirer/dsp/filter/fir/real/RealFIRFilter_RB_RB.java index 22197bbe..5f9a7669 100644 --- a/src/main/java/io/github/dsheirer/dsp/filter/fir/real/RealFIRFilter_RB_RB.java +++ b/src/main/java/io/github/dsheirer/dsp/filter/fir/real/RealFIRFilter_RB_RB.java @@ -3,6 +3,7 @@ package io.github.dsheirer.dsp.filter.fir.real; import io.github.dsheirer.sample.Listener; import io.github.dsheirer.sample.real.RealBuffer; +@Deprecated //Use RealFIRFilter2 version instead public class RealFIRFilter_RB_RB extends RealFIRFilter implements Listener { diff --git a/src/main/java/io/github/dsheirer/dsp/filter/fir/real/RealFIRFilter_R_R.java b/src/main/java/io/github/dsheirer/dsp/filter/fir/real/RealFIRFilter_R_R.java deleted file mode 100644 index f96feda4..00000000 --- a/src/main/java/io/github/dsheirer/dsp/filter/fir/real/RealFIRFilter_R_R.java +++ /dev/null @@ -1,56 +0,0 @@ -package io.github.dsheirer.dsp.filter.fir.real; - -import io.github.dsheirer.sample.real.RealSampleListener; - -public class RealFIRFilter_R_R extends RealFIRFilter implements RealSampleListener -{ - private RealSampleListener mListener; - - /** - * Float FIR filter with streaming float provider and listener interfaces. - * - * @param coefficients - * @param gain - */ - public RealFIRFilter_R_R( float[] coefficients, float gain ) - { - super( coefficients, gain ); - } - - @Override - public void dispose() - { - super.dispose(); - - mListener = null; - } - - @Override - public void receive( float sample ) - { - if( mListener != null ) - { - mListener.receive( filter( sample ) ); - } - } - - public float[] filter( float[] samples ) - { - for( int x = 0; x < samples.length; x++ ) - { - samples[ x ] = filter( samples[ x ] ); - } - - return samples; - } - - public void setListener( RealSampleListener listener ) - { - mListener = listener; - } - - public void removeListener() - { - mListener = null; - } -} diff --git a/src/main/java/io/github/dsheirer/gui/channelizer/ChannelizerViewer.java b/src/main/java/io/github/dsheirer/gui/channelizer/ChannelizerViewer.java index 0537bf77..8565848b 100644 --- a/src/main/java/io/github/dsheirer/gui/channelizer/ChannelizerViewer.java +++ b/src/main/java/io/github/dsheirer/gui/channelizer/ChannelizerViewer.java @@ -195,6 +195,8 @@ public class ChannelizerViewer extends JFrame public class ChannelArrayPanel extends JPanel { + private final Logger mLog = LoggerFactory.getLogger(ChannelArrayPanel.class); + public ChannelArrayPanel() { int bufferSize = CHANNEL_BANDWIDTH / CHANNEL_FFT_FRAME_RATE; @@ -261,6 +263,8 @@ public class ChannelizerViewer extends JFrame DiscreteChannelPanel channelPanel = new DiscreteChannelPanel(mSettingsManager, source, x); channelPanel.setDFTSize(mChannelPanelDFTSize); + mLog.debug("Testing Channel [" + x + "] is set to [" + source.getTunerChannel().getFrequency() + "]"); + if(x % mChannelsPerRow == mChannelsPerRow - 1) { add(channelPanel, "grow,push,wrap 2px"); @@ -310,7 +314,7 @@ public class ChannelizerViewer extends JFrame { setLayout(new MigLayout("insets 0 0 0 0", "[grow,fill]", "[grow,fill]")); mSpectrumPanel = new SpectrumPanel(settingsManager); - mSpectrumPanel.setSampleSize(16); + mSpectrumPanel.setSampleSize(28); add(mSpectrumPanel); mDFTProcessor.addConverter(mComplexDecibelConverter); @@ -349,7 +353,7 @@ public class ChannelizerViewer extends JFrame { setLayout(new MigLayout("insets 0 0 0 0", "[center,grow,fill][]", "[grow,fill][]")); mSpectrumPanel = new SpectrumPanel(settingsManager); - mSpectrumPanel.setSampleSize(16); + mSpectrumPanel.setSampleSize(32); add(mSpectrumPanel, "span"); mDFTProcessor.addConverter(mComplexDecibelConverter); @@ -433,6 +437,8 @@ public class ChannelizerViewer extends JFrame public class DiscreteChannelPanel extends JPanel implements Listener, ISourceEventProcessor { + private final Logger mLog = LoggerFactory.getLogger(DiscreteChannelPanel.class); + private TunerChannelSource mSource; private DFTProcessor mDFTProcessor = new DFTProcessor(SampleType.COMPLEX); private ComplexDecibelConverter mComplexDecibelConverter = new ComplexDecibelConverter(); @@ -445,7 +451,7 @@ public class ChannelizerViewer extends JFrame mSource = source; setLayout(new MigLayout("insets 0 0 0 0", "[center,grow,fill][]", "[grow,fill][]")); mSpectrumPanel = new SpectrumPanel(settingsManager); - mSpectrumPanel.setSampleSize(16); + mSpectrumPanel.setSampleSize(32); add(mSpectrumPanel, "span"); add(new JLabel("Index:" + index)); diff --git a/src/main/java/io/github/dsheirer/gui/channelizer/SynthesizerViewer.java b/src/main/java/io/github/dsheirer/gui/channelizer/SynthesizerViewer.java index 05997803..14e9aedf 100644 --- a/src/main/java/io/github/dsheirer/gui/channelizer/SynthesizerViewer.java +++ b/src/main/java/io/github/dsheirer/gui/channelizer/SynthesizerViewer.java @@ -16,7 +16,6 @@ package io.github.dsheirer.gui.channelizer; import io.github.dsheirer.dsp.filter.FilterFactory; -import io.github.dsheirer.dsp.filter.Window; import io.github.dsheirer.dsp.filter.channelizer.TwoChannelSynthesizerM2; import io.github.dsheirer.dsp.filter.design.FilterDesignException; import io.github.dsheirer.dsp.mixer.FS4DownConverter; @@ -282,8 +281,7 @@ public class SynthesizerViewer extends JFrame { try { - float[] taps = FilterFactory.getSincM2Synthesizer(12500.0, 2, 12, - Window.WindowType.BLACKMAN_HARRIS_7, true); + float[] taps = FilterFactory.getSincM2Synthesizer(12500.0, 2, 12); mSynthesizer = new TwoChannelSynthesizerM2(taps); } catch(FilterDesignException fde) diff --git a/src/main/java/io/github/dsheirer/module/decode/p25/P25DecoderC4FM.java b/src/main/java/io/github/dsheirer/module/decode/p25/P25DecoderC4FM.java index 1155ec58..14bc4d33 100644 --- a/src/main/java/io/github/dsheirer/module/decode/p25/P25DecoderC4FM.java +++ b/src/main/java/io/github/dsheirer/module/decode/p25/P25DecoderC4FM.java @@ -19,7 +19,7 @@ import io.github.dsheirer.alias.AliasList; import io.github.dsheirer.dsp.filter.FilterFactory; import io.github.dsheirer.dsp.filter.design.FilterDesignException; import io.github.dsheirer.dsp.filter.fir.FIRFilterSpecification; -import io.github.dsheirer.dsp.filter.fir.complex.ComplexFIRFilter; +import io.github.dsheirer.dsp.filter.fir.complex.ComplexFIRFilter2; import io.github.dsheirer.dsp.gain.ComplexFeedForwardGainControl; import io.github.dsheirer.dsp.psk.DQPSKDecisionDirectedDemodulator; import io.github.dsheirer.dsp.psk.InterpolatingSampleBuffer; @@ -42,7 +42,7 @@ public class P25DecoderC4FM extends P25Decoder protected P25MessageFramer mMessageFramer; private ComplexFeedForwardGainControl mAGC = new ComplexFeedForwardGainControl(32); private Map mBasebandFilters = new HashMap<>(); - private ComplexFIRFilter mBasebandFilter; + private ComplexFIRFilter2 mBasebandFilter; /** * P25 Phase 1 - standard C4FM modulation decoder. Uses Differential QPSK decoding with a Costas PLL and a @@ -60,7 +60,7 @@ public class P25DecoderC4FM extends P25Decoder { super.setSampleRate(sampleRate); - mBasebandFilter = new ComplexFIRFilter(getBasebandFilter(), 1.0f); + mBasebandFilter = new ComplexFIRFilter2(getBasebandFilter()); mCostasLoop = new CostasLoop(getSampleRate(), getSymbolRate()); mInterpolatingSampleBuffer = new InterpolatingSampleBuffer(getSamplesPerSymbol(), SAMPLE_COUNTER_GAIN); diff --git a/src/main/java/io/github/dsheirer/module/decode/p25/P25DecoderLSM.java b/src/main/java/io/github/dsheirer/module/decode/p25/P25DecoderLSM.java index d9dea665..064652cf 100644 --- a/src/main/java/io/github/dsheirer/module/decode/p25/P25DecoderLSM.java +++ b/src/main/java/io/github/dsheirer/module/decode/p25/P25DecoderLSM.java @@ -18,7 +18,7 @@ package io.github.dsheirer.module.decode.p25; import io.github.dsheirer.alias.AliasList; import io.github.dsheirer.dsp.filter.FilterFactory; import io.github.dsheirer.dsp.filter.Window.WindowType; -import io.github.dsheirer.dsp.filter.fir.complex.ComplexFIRFilter; +import io.github.dsheirer.dsp.filter.fir.complex.ComplexFIRFilter2; import io.github.dsheirer.dsp.gain.ComplexFeedForwardGainControl; import io.github.dsheirer.dsp.psk.DQPSKGardnerDemodulator; import io.github.dsheirer.dsp.psk.InterpolatingSampleBuffer; @@ -37,7 +37,7 @@ public class P25DecoderLSM extends P25Decoder protected static final float SAMPLE_COUNTER_GAIN = 0.5f; private Map mBasebandFilters = new HashMap<>(); - private ComplexFIRFilter mBasebandFilter; + private ComplexFIRFilter2 mBasebandFilter; private ComplexFeedForwardGainControl mAGC = new ComplexFeedForwardGainControl(32); protected DQPSKGardnerDemodulator mQPSKDemodulator; protected P25MessageFramer mMessageFramer; @@ -64,7 +64,7 @@ public class P25DecoderLSM extends P25Decoder { super.setSampleRate(sampleRate); - mBasebandFilter = new ComplexFIRFilter(getBasebandFilter(), 1.0f); + mBasebandFilter = new ComplexFIRFilter2(getBasebandFilter()); mCostasLoop = new CostasLoop(getSampleRate(), getSymbolRate()); mInterpolatingSampleBuffer = new InterpolatingSampleBuffer(getSamplesPerSymbol(), SAMPLE_COUNTER_GAIN); diff --git a/src/main/java/io/github/dsheirer/source/tuner/test/TestTunerController.java b/src/main/java/io/github/dsheirer/source/tuner/test/TestTunerController.java index 8d9a809c..37c6a1c0 100644 --- a/src/main/java/io/github/dsheirer/source/tuner/test/TestTunerController.java +++ b/src/main/java/io/github/dsheirer/source/tuner/test/TestTunerController.java @@ -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 = 10000000; + public static final int SAMPLE_RATE = 50000; public static final int DC_NOISE_BANDWIDTH = 0; public static final double USABLE_BANDWIDTH_PERCENTAGE = 1.00; diff --git a/src/main/java/io/github/dsheirer/spectrum/DFTProcessor.java b/src/main/java/io/github/dsheirer/spectrum/DFTProcessor.java index 4a281381..9c0f11c6 100644 --- a/src/main/java/io/github/dsheirer/spectrum/DFTProcessor.java +++ b/src/main/java/io/github/dsheirer/spectrum/DFTProcessor.java @@ -44,7 +44,7 @@ import java.util.concurrent.atomic.AtomicBoolean; public class DFTProcessor implements Listener, ISourceEventProcessor, IDFTWidthChangeProcessor { private static final Logger mLog = LoggerFactory.getLogger(DFTProcessor.class); - private static final int BUFFER_QUEUE_MAX_SIZE = 12; + private static final int BUFFER_QUEUE_MAX_SIZE = 20; private static final int BUFFER_QUEUE_OVERFLOW_RESET_THRESHOLD = 6; private static final String FRAME_RATE_PROPERTY = "spectral.display.frame.rate"; diff --git a/src/main/java/io/github/dsheirer/spectrum/SpectrumPanel.java b/src/main/java/io/github/dsheirer/spectrum/SpectrumPanel.java index 54738e6f..764e63ac 100644 --- a/src/main/java/io/github/dsheirer/spectrum/SpectrumPanel.java +++ b/src/main/java/io/github/dsheirer/spectrum/SpectrumPanel.java @@ -33,8 +33,14 @@ import org.apache.commons.lang3.Validate; import org.slf4j.Logger; import org.slf4j.LoggerFactory; -import javax.swing.*; -import java.awt.*; +import javax.swing.JPanel; +import java.awt.Color; +import java.awt.Dimension; +import java.awt.GradientPaint; +import java.awt.Graphics; +import java.awt.Graphics2D; +import java.awt.Rectangle; +import java.awt.RenderingHints; import java.awt.geom.GeneralPath; import java.awt.geom.Line2D; import java.util.Arrays; @@ -295,7 +301,7 @@ public class SpectrumPanel extends JPanel implements DFTResultsListener, Setting */ public void setSampleSize(double sampleSize) { - Validate.isTrue(2.0 <= sampleSize && sampleSize <= 32.0); + Validate.isTrue(2.0 <= sampleSize && sampleSize <= 64.0); mDBScale = (float)(20.0 * Math.log10(Math.pow(2.0, sampleSize - 1))); } From a898be265f600a5d6127cf74c21043a11176077e Mon Sep 17 00:00:00 2001 From: Denny Date: Sat, 2 Jun 2018 08:47:48 -0400 Subject: [PATCH 68/87] [WIP] Updates the LTR-Standard and LTR-Net decoders to use a new LTR decoder with a zero-crossing symbol timing error detector. The new decoder is optimized for 8 kHz FM demodulated audio produced by the AudioModule. --- .../io/github/dsheirer/audio/AudioModule.java | 143 ++++++------ .../github/dsheirer/audio/AudioModule2.java | 179 -------------- .../dsheirer/bits/ISyncDetectListener.java | 6 - .../github/dsheirer/bits/MessageFramer.java | 189 +++++++-------- .../dsheirer/bits/SoftSyncDetector.java | 2 + .../io/github/dsheirer/bits/SyncDetector.java | 2 + .../dsheirer/dsp/filter/ComplexFilter.java | 55 ----- .../channelizer/PolyphaseChannelManager.java | 12 +- .../dsp/filter/dc/BufferDCFilter.java | 47 +++- .../dsp/filter/dc/DCRemovalFilter.java | 108 ++++++--- .../dc/IIRSinglePoleDCRemovalFilter.java | 129 +++++++--- .../dc/IIRSinglePoleDCRemovalFilter_RB.java | 69 ------ .../dsp/filter/design/FilterViewer.java | 92 ++++---- .../dsp/filter/fir/real/RealFIRFilter2.java | 2 +- .../dsp/filter/resample/RealResampler.java | 51 ++-- .../github/dsheirer/dsp/fm/FMDemodulator.java | 72 +++++- .../dsheirer/dsp/fm/FMDemodulator_CB.java | 78 ------- .../dsheirer/dsp/fm/FMDiscriminator.java | 102 -------- .../dsheirer/dsp/fsk/ISyncStateListener.java | 24 ++ .../github/dsheirer/dsp/fsk/LTRDecoder.java | 221 ++++++++++++++++++ .../dsp/fsk/LTRDecoderInstrumented.java | 40 ++++ ...KDecoder.java => LTRFSKDecoderLegacy.java} | 4 +- .../github/dsheirer/dsp/fsk/SampleBuffer.java | 124 ++++++++++ .../dsp/fsk/SampleBufferInstrumented.java | 98 ++++++++ .../io/github/dsheirer/dsp/fsk/SyncState.java | 21 ++ .../dsp/fsk/SynchronizationMonitor.java | 149 ++++++++++++ .../dsp/fsk/ZeroCrossingErrorDetector.java | 124 ++++++++++ ...ZeroCrossingErrorDetectorInstrumented.java | 58 +++++ .../dsp/symbol/ISyncDetectListener.java | 21 ++ .../dsp/symbol/SyncDetectListener.java | 27 --- .../dsp/symbol/SyncDetectProvider.java | 2 +- .../gui/viewer/PlaybackController.java | 174 ++++++++++---- .../instrument/gui/viewer/ViewerDesktop.java | 8 +- .../gui/viewer/chart/RealSampleLineChart.java | 85 +++++++ .../gui/viewer/chart/SampleBufferChart.java | 171 ++++++++++++++ .../chart/ZeroCrossingErrorDetectorChart.java | 132 +++++++++++ .../viewer/decoder/AbstractDecoderPane.java | 30 +++ ...coderPane.java => ComplexDecoderPane.java} | 18 +- .../viewer/decoder/DecoderPaneFactory.java | 14 +- .../gui/viewer/decoder/LTRNetPane.java | 151 ++++++++++++ .../gui/viewer/decoder/P25Phase1C4FMPane.java | 2 +- .../gui/viewer/decoder/P25Phase1LSMPane.java | 2 +- .../gui/viewer/decoder/RealDecoderPane.java | 68 ++++++ .../instrument/tap/event/SyncDetectTap.java | 52 ++--- .../dsheirer/module/ProcessingChain.java | 33 ++- .../module/decode/DecoderFactory.java | 67 ++---- .../module/decode/ltrnet/LTRNetDecoder.java | 147 +++++------- .../ltrnet/LTRNetDecoderInstrumented.java | 44 ++++ .../ltrstandard/LTRStandardDecoder.java | 121 +++++----- .../module/decode/p25/P25MessageFramer.java | 2 +- .../decode/passport/PassportDecoder.java | 6 +- .../audio/DemodulatedAudioFilterModule.java | 1 + .../demodulate/fm/FMDemodulatorModule.java | 150 +++++++----- .../buffer/IReusableBufferListener.java | 23 ++ .../buffer/IReusableBufferProvider.java | 11 +- .../IReusableComplexBufferListener.java | 4 +- .../IReusableComplexBufferProvider.java | 36 +++ .../buffer/ReusableBufferBroadcaster.java | 12 +- .../sample/buffer/ReusableBufferQueue.java | 4 - .../io/github/dsheirer/source/RealSource.java | 4 +- .../source/mixer/RealMixerSource.java | 47 ++-- .../source/tuner/TunerController.java | 4 +- .../source/wave/ComplexWaveSource.java | 24 +- .../dsheirer/source/wave/RealWaveSource.java | 47 +++- 64 files changed, 2660 insertions(+), 1285 deletions(-) delete mode 100644 src/main/java/io/github/dsheirer/audio/AudioModule2.java delete mode 100644 src/main/java/io/github/dsheirer/bits/ISyncDetectListener.java delete mode 100644 src/main/java/io/github/dsheirer/dsp/filter/ComplexFilter.java delete mode 100644 src/main/java/io/github/dsheirer/dsp/filter/dc/IIRSinglePoleDCRemovalFilter_RB.java delete mode 100644 src/main/java/io/github/dsheirer/dsp/fm/FMDemodulator_CB.java delete mode 100644 src/main/java/io/github/dsheirer/dsp/fm/FMDiscriminator.java create mode 100644 src/main/java/io/github/dsheirer/dsp/fsk/ISyncStateListener.java create mode 100644 src/main/java/io/github/dsheirer/dsp/fsk/LTRDecoder.java create mode 100644 src/main/java/io/github/dsheirer/dsp/fsk/LTRDecoderInstrumented.java rename src/main/java/io/github/dsheirer/dsp/fsk/{LTRFSKDecoder.java => LTRFSKDecoderLegacy.java} (98%) create mode 100644 src/main/java/io/github/dsheirer/dsp/fsk/SampleBuffer.java create mode 100644 src/main/java/io/github/dsheirer/dsp/fsk/SampleBufferInstrumented.java create mode 100644 src/main/java/io/github/dsheirer/dsp/fsk/SyncState.java create mode 100644 src/main/java/io/github/dsheirer/dsp/fsk/SynchronizationMonitor.java create mode 100644 src/main/java/io/github/dsheirer/dsp/fsk/ZeroCrossingErrorDetector.java create mode 100644 src/main/java/io/github/dsheirer/dsp/fsk/ZeroCrossingErrorDetectorInstrumented.java create mode 100644 src/main/java/io/github/dsheirer/dsp/symbol/ISyncDetectListener.java delete mode 100644 src/main/java/io/github/dsheirer/dsp/symbol/SyncDetectListener.java create mode 100644 src/main/java/io/github/dsheirer/instrument/gui/viewer/chart/RealSampleLineChart.java create mode 100644 src/main/java/io/github/dsheirer/instrument/gui/viewer/chart/SampleBufferChart.java create mode 100644 src/main/java/io/github/dsheirer/instrument/gui/viewer/chart/ZeroCrossingErrorDetectorChart.java create mode 100644 src/main/java/io/github/dsheirer/instrument/gui/viewer/decoder/AbstractDecoderPane.java rename src/main/java/io/github/dsheirer/instrument/gui/viewer/decoder/{DecoderPane.java => ComplexDecoderPane.java} (87%) create mode 100644 src/main/java/io/github/dsheirer/instrument/gui/viewer/decoder/LTRNetPane.java create mode 100644 src/main/java/io/github/dsheirer/instrument/gui/viewer/decoder/RealDecoderPane.java create mode 100644 src/main/java/io/github/dsheirer/module/decode/ltrnet/LTRNetDecoderInstrumented.java create mode 100644 src/main/java/io/github/dsheirer/sample/buffer/IReusableBufferListener.java create mode 100644 src/main/java/io/github/dsheirer/sample/buffer/IReusableComplexBufferProvider.java diff --git a/src/main/java/io/github/dsheirer/audio/AudioModule.java b/src/main/java/io/github/dsheirer/audio/AudioModule.java index 81f3451c..5836d4bc 100644 --- a/src/main/java/io/github/dsheirer/audio/AudioModule.java +++ b/src/main/java/io/github/dsheirer/audio/AudioModule.java @@ -1,19 +1,16 @@ /******************************************************************************* - * 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 + * You should have received a copy of the GNU General Public License along with this program. + * If not, see * ******************************************************************************/ package io.github.dsheirer.audio; @@ -23,90 +20,76 @@ import io.github.dsheirer.audio.squelch.SquelchState; import io.github.dsheirer.channel.metadata.Metadata; import io.github.dsheirer.dsp.filter.design.FilterDesignException; import io.github.dsheirer.dsp.filter.fir.FIRFilterSpecification; +import io.github.dsheirer.dsp.filter.fir.real.RealFIRFilter2; import io.github.dsheirer.dsp.filter.fir.remez.RemezFIRFilterDesigner; -import io.github.dsheirer.dsp.filter.polyphase.PolyphaseFIRDecimatingFilter_RB; import io.github.dsheirer.module.Module; import io.github.dsheirer.sample.Listener; -import io.github.dsheirer.sample.real.IFilteredRealBufferListener; -import io.github.dsheirer.sample.real.RealBuffer; +import io.github.dsheirer.sample.buffer.IReusableBufferListener; +import io.github.dsheirer.sample.buffer.ReusableBuffer; import org.slf4j.Logger; import org.slf4j.LoggerFactory; /** - * Provides packaging of demodulated audio sample buffers into audio packets for - * broadcast to registered audio packet listeners. Includes audio packet - * metadata in constructed audio packets. + * Provides packaging of demodulated audio sample buffers into audio packets for broadcast to registered audio packet + * listeners. Includes audio packet metadata in constructed audio packets. * - * Incorporates audio squelch state listener to control if audio packets are - * broadcast or ignored. + * Incorporates audio squelch state listener to control if audio packets are broadcast or ignored. + * + * This class is designed to support 8 kHz sample rate demodulated audio. */ -public class AudioModule extends Module implements IAudioPacketProvider, IFilteredRealBufferListener, - ISquelchStateListener, Listener +public class AudioModule extends Module implements IAudioPacketProvider, IReusableBufferListener, + ISquelchStateListener, Listener { protected static final Logger mLog = LoggerFactory.getLogger(AudioModule.class); - private static float[] mDecimationCoefficients; + private static float[] sHighPassFilterCoefficients; static { - //Band-pass filter to both decimate (48kHz to 8kHz) and block 0 - 300 Hz (LTR signalling). - FIRFilterSpecification specification = FIRFilterSpecification.bandPassBuilder() - .sampleRate(48000) - .stopFrequency1(150) - .passFrequencyBegin(300) - .passFrequencyEnd(3500) - .stopFrequency2(4000) - .stopRipple(0.0004) - .passRipple(0.008) + FIRFilterSpecification specification = FIRFilterSpecification.highPassBuilder() + .sampleRate(8000) + .stopBandCutoff(200) + .stopBandAmplitude(0.0) + .stopBandRipple(0.025) + .passBandStart(300) + .passBandAmplitude(1.0) + .passBandRipple(0.01) .build(); - - RemezFIRFilterDesigner designer = new RemezFIRFilterDesigner(specification); - try { - if(!designer.isValid()) - { - throw new FilterDesignException("Couldn't design the audio decimation filter"); - } + RemezFIRFilterDesigner designer = new RemezFIRFilterDesigner(specification); - mDecimationCoefficients = designer.getImpulseResponse(); + if(designer.isValid()) + { + sHighPassFilterCoefficients = designer.getImpulseResponse(); + } } - catch(FilterDesignException e) + catch(FilterDesignException fde) { - mLog.debug("Error designing filter", e); + mLog.error("Filter design error", fde); } } + private RealFIRFilter2 mHighPassFilter = new RealFIRFilter2(sHighPassFilterCoefficients); private SquelchStateListener mSquelchStateListener = new SquelchStateListener(); private SquelchState mSquelchState = SquelchState.SQUELCH; private Listener mAudioPacketListener; - private PolyphaseFIRDecimatingFilter_RB mAudioDecimationFilter; private Metadata mMetadata; + /** + * Creates an Audio Module. + * @param metadata to use for audio packets produced by this audio module. + */ public AudioModule(Metadata metadata) { mMetadata = metadata; - - mAudioDecimationFilter = new PolyphaseFIRDecimatingFilter_RB(mDecimationCoefficients, 6, 2.0f); - mAudioDecimationFilter.setListener(new Listener() - { - @Override - public void receive(RealBuffer realBuffer) - { - if(mAudioPacketListener != null) - { - AudioPacket packet = new AudioPacket(realBuffer.getSamples(), mMetadata.copyOf()); - mAudioPacketListener.receive(packet); - } - } - }); } @Override public void dispose() { - mSquelchStateListener = null; mAudioPacketListener = null; + mSquelchStateListener = null; } @Override @@ -123,32 +106,13 @@ public class AudioModule extends Module implements IAudioPacketProvider, IFilter @Override public void stop() { - /* Issue an end audio packet in case a recorder is still rolling */ + /* Issue an end-audio packet in case a recorder is still rolling */ if(mAudioPacketListener != null) { mAudioPacketListener.receive(new AudioPacket(AudioPacket.Type.END, mMetadata.copyOf())); } } - /** - * Processes demodulated audio samples into audio packets with current audio - * metadata and sends to the registered listener - */ - @Override - public void receive(RealBuffer buffer) - { - if(mAudioPacketListener != null && mSquelchState == SquelchState.UNSQUELCH) - { - mAudioDecimationFilter.receive(buffer); - } - } - - @Override - public Listener getFilteredRealBufferListener() - { - return this; - } - @Override public void setAudioPacketListener(Listener listener) { @@ -167,6 +131,29 @@ public class AudioModule extends Module implements IAudioPacketProvider, IFilter return mSquelchStateListener; } + @Override + public void receive(ReusableBuffer reusableBuffer) + { + if(mAudioPacketListener != null && mSquelchState == SquelchState.UNSQUELCH) + { + ReusableBuffer highPassFiltered = mHighPassFilter.filter(reusableBuffer); + AudioPacket packet = new AudioPacket(highPassFiltered.getSamplesCopy(), mMetadata.copyOf()); + mAudioPacketListener.receive(packet); + highPassFiltered.decrementUserCount(); + } + else + { + reusableBuffer.decrementUserCount(); + } + } + + @Override + public Listener getReusableBufferListener() + { + //Redirect received reusable buffers to the receive(buffer) method + return this; + } + /** * Wrapper for squelch state listener */ diff --git a/src/main/java/io/github/dsheirer/audio/AudioModule2.java b/src/main/java/io/github/dsheirer/audio/AudioModule2.java deleted file mode 100644 index c2f498ac..00000000 --- a/src/main/java/io/github/dsheirer/audio/AudioModule2.java +++ /dev/null @@ -1,179 +0,0 @@ -/******************************************************************************* - * 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 - * - ******************************************************************************/ -package io.github.dsheirer.audio; - -import io.github.dsheirer.audio.squelch.ISquelchStateListener; -import io.github.dsheirer.audio.squelch.SquelchState; -import io.github.dsheirer.channel.metadata.Metadata; -import io.github.dsheirer.dsp.filter.resample.RealResampler; -import io.github.dsheirer.module.Module; -import io.github.dsheirer.sample.Listener; -import io.github.dsheirer.sample.buffer.ReusableBuffer; -import io.github.dsheirer.sample.real.IFilteredRealBufferListener; -import io.github.dsheirer.sample.real.RealBuffer; -import io.github.dsheirer.source.ISourceEventListener; -import io.github.dsheirer.source.SourceEvent; -import org.slf4j.Logger; -import org.slf4j.LoggerFactory; - -/** - * Provides packaging of demodulated audio sample buffers into audio packets for - * broadcast to registered audio packet listeners. Includes audio packet - * metadata in constructed audio packets. - * - * Incorporates audio squelch state listener to control if audio packets are - * broadcast or ignored. - */ -public class AudioModule2 extends Module implements IAudioPacketProvider, IFilteredRealBufferListener, - ISquelchStateListener, ISourceEventListener, Listener -{ - protected static final Logger mLog = LoggerFactory.getLogger(AudioModule2.class); - private static final double OUTPUT_SAMPLE_RATE = 8000.0; - private SquelchStateListener mSquelchStateListener = new SquelchStateListener(); - private SquelchState mSquelchState = SquelchState.SQUELCH; - private Listener mAudioPacketListener; - private Listener mResampledBufferListener; - private RealResampler mAudioResampler; - private Metadata mMetadata; - - public AudioModule2(Metadata metadata) - { - mMetadata = metadata; - - mResampledBufferListener = new Listener() - { - @Override - public void receive(ReusableBuffer reusableBuffer) - { - if(mAudioPacketListener != null) - { - AudioPacket packet = new AudioPacket(reusableBuffer.getSamplesCopy(), mMetadata.copyOf()); - reusableBuffer.decrementUserCount(); - mAudioPacketListener.receive(packet); - } - } - }; - - setSampleRate(25000.0); - } - - /** - * Sets the incoming sample rate - */ - private void setSampleRate(double sampleRate) - { - mAudioResampler = new RealResampler(sampleRate, OUTPUT_SAMPLE_RATE); - mAudioResampler.setListener(mResampledBufferListener); - } - - @Override - public void dispose() - { - mSquelchStateListener = null; - mAudioPacketListener = null; - } - - @Override - public void reset() - { - } - - @Override - public void start() - { - /* No start operations provided */ - } - - @Override - public void stop() - { - /* Issue an end audio packet in case a recorder is still rolling */ - if(mAudioPacketListener != null) - { - mAudioPacketListener.receive(new AudioPacket(AudioPacket.Type.END, mMetadata.copyOf())); - } - } - - /** - * Processes demodulated audio samples into audio packets with current audio - * metadata and sends to the registered listener - */ - @Override - public void receive(RealBuffer buffer) - { - if(mAudioResampler != null && mSquelchState == SquelchState.UNSQUELCH) - { - mAudioResampler.receive(buffer); - } - } - - @Override - public Listener getFilteredRealBufferListener() - { - return this; - } - - @Override - public void setAudioPacketListener(Listener listener) - { - mAudioPacketListener = listener; - } - - @Override - public void removeAudioPacketListener() - { - mAudioPacketListener = null; - } - - @Override - public Listener getSourceEventListener() - { - return new Listener() - { - @Override - public void receive(SourceEvent sourceEvent) - { - if(sourceEvent.getEvent() == SourceEvent.Event.NOTIFICATION_SAMPLE_RATE_CHANGE) - { - setSampleRate(sourceEvent.getValue().doubleValue()); - } - } - }; - } - - @Override - public Listener getSquelchStateListener() - { - return mSquelchStateListener; - } - - /** - * Wrapper for squelch state listener - */ - public class SquelchStateListener implements Listener - { - @Override - public void receive(SquelchState state) - { - if(state == SquelchState.SQUELCH && mAudioPacketListener != null) - { - mAudioPacketListener.receive(new AudioPacket(AudioPacket.Type.END, mMetadata.copyOf())); - } - - mSquelchState = state; - } - } -} diff --git a/src/main/java/io/github/dsheirer/bits/ISyncDetectListener.java b/src/main/java/io/github/dsheirer/bits/ISyncDetectListener.java deleted file mode 100644 index bbe93440..00000000 --- a/src/main/java/io/github/dsheirer/bits/ISyncDetectListener.java +++ /dev/null @@ -1,6 +0,0 @@ -package io.github.dsheirer.bits; - -public interface ISyncDetectListener -{ - public void syncDetected(); -} diff --git a/src/main/java/io/github/dsheirer/bits/MessageFramer.java b/src/main/java/io/github/dsheirer/bits/MessageFramer.java index 79596cae..591858c5 100644 --- a/src/main/java/io/github/dsheirer/bits/MessageFramer.java +++ b/src/main/java/io/github/dsheirer/bits/MessageFramer.java @@ -1,119 +1,110 @@ /******************************************************************************* - * SDR Trunk - * Copyright (C) 2014 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 + * 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 + * ******************************************************************************/ package io.github.dsheirer.bits; -import io.github.dsheirer.dsp.symbol.SyncDetectListener; +import io.github.dsheirer.dsp.symbol.ISyncDetectListener; import io.github.dsheirer.dsp.symbol.SyncDetectProvider; import io.github.dsheirer.sample.Broadcaster; import io.github.dsheirer.sample.Listener; import java.util.ArrayList; import java.util.Iterator; +import java.util.List; /** * MessageFramer - processes bitsets looking for a sync pattern within * the bits, and then extracts the message, including the sync * pattern, for a total bit length of messageLength. - * + * * Will extract multiple messages simultaneously, for each sync pattern that is * encountered within the bitset bit stream. */ -public class MessageFramer implements Listener, - SyncDetectProvider +public class MessageFramer implements Listener, SyncDetectProvider { - private boolean[] mSyncPattern; - private int mMessageLength; - private SyncDetectListener mSyncDetectListener; - private Broadcaster mBroadcaster = - new Broadcaster(); - private ArrayList mMessageAssemblers = - new ArrayList(); + private boolean[] mSyncPattern; + private int mMessageLength; + private ISyncDetectListener mSyncDetectListener; + private Broadcaster mBroadcaster = new Broadcaster(); + private List mMessageAssemblers = new ArrayList(); + private List mCompletedMessageAssemblers = new ArrayList(); + private SyncPatternMatcher mMatcher; - private ArrayList mCompletedMessageAssemblers = - new ArrayList(); - - private BinaryMessage mPreviousBuffer = null; - - private SyncPatternMatcher mMatcher; - - public MessageFramer( boolean[] syncPattern, int messageLength ) + public MessageFramer(boolean[] syncPattern, int messageLength) { mSyncPattern = syncPattern; - mMatcher = new SyncPatternMatcher( syncPattern ); + mMatcher = new SyncPatternMatcher(syncPattern); mMessageLength = messageLength; } - + public void reset() { - for( MessageAssembler assembler: mMessageAssemblers ) - { - assembler.dispose(); - } - + for(MessageAssembler assembler : mMessageAssemblers) + { + assembler.dispose(); + } + mMessageAssemblers.clear(); - } - + } + public void dispose() { - mBroadcaster.dispose(); - mCompletedMessageAssemblers.clear(); - mMessageAssemblers.clear(); + mBroadcaster.dispose(); + mCompletedMessageAssemblers.clear(); + mMessageAssemblers.clear(); } @Override - public void receive( Boolean bit ) + public void receive(Boolean bit) { - mMatcher.receive( bit ); - + mMatcher.receive(bit); + Iterator it = mMessageAssemblers.iterator(); - + MessageAssembler assembler; - - while( it.hasNext() ) + + while(it.hasNext()) { assembler = it.next(); /* Dispose and remove any completed assemblers */ - if( assembler.complete() ) + if(assembler.complete()) { - assembler.dispose(); - it.remove(); + assembler.dispose(); + it.remove(); } /* Otherwise, send them the bit */ else { - assembler.receive( bit ); + assembler.receive(bit); } } - + /* Check for sync match and add new message assembler */ - if( mMatcher.matches() ) - { - addMessageAssembler( new MessageAssembler( mMessageLength, mSyncPattern ) ); - + if(mMatcher.matches()) + { + addMessageAssembler(new MessageAssembler(mMessageLength, mSyncPattern)); + /* Notify any sync detect listener(s) */ - if( mSyncDetectListener != null ) + if(mSyncDetectListener != null) { - mSyncDetectListener.syncDetected(); + mSyncDetectListener.syncDetected(); } - } + } } - + /** * Causes all messages currently under assembly to be forcibly * sent (ie flushed) to all registered message listeners, and @@ -121,44 +112,44 @@ public class MessageFramer implements Listener, */ public void flush() { - for( MessageAssembler assembler: mMessageAssemblers ) + for(MessageAssembler assembler : mMessageAssemblers) { assembler.flush(); } } - - @Override - public void setSyncDetectListener( SyncDetectListener listener ) + + @Override + public void setSyncDetectListener(ISyncDetectListener listener) { - mSyncDetectListener = listener; + mSyncDetectListener = listener; } - + /** * Allow a message listener to register with this framer to receive * all framed messages */ - public void addMessageListener( Listener listener ) + public void addMessageListener(Listener listener) { - mBroadcaster.addListener( listener ); + mBroadcaster.addListener(listener); } - - public void removeMessageListener( Listener listener ) + + public void removeMessageListener(Listener listener) { - mBroadcaster.removeListener( listener ); + mBroadcaster.removeListener(listener); } /* * Adds a message assembler to receive bits from the bit stream */ - private void addMessageAssembler( MessageAssembler assembler ) + private void addMessageAssembler(MessageAssembler assembler) { - mMessageAssemblers.add( assembler ); + mMessageAssemblers.add(assembler); } - @SuppressWarnings( "unused" ) - private void removeMessageAssembler( MessageAssembler assembler ) + @SuppressWarnings("unused") + private void removeMessageAssembler(MessageAssembler assembler) { - mMessageAssemblers.remove( assembler ); + mMessageAssemblers.remove(assembler); } /** @@ -166,7 +157,7 @@ public class MessageFramer implements Listener, * identified sync pattern, and every bit thereafter. Once the accumulated * bits equal the message length, the message is sent and the assembler * flags itself as complete. - * + * * By design, multiple message assemblers can exist at the same time, each * assembling different, overlapping potential messages */ @@ -174,46 +165,46 @@ public class MessageFramer implements Listener, { BinaryMessage mMessage; boolean mComplete = false; - - MessageAssembler( int messageLength ) + + MessageAssembler(int messageLength) { - mMessage = new BinaryMessage( messageLength ); + mMessage = new BinaryMessage(messageLength); } - - MessageAssembler( int messageLength, boolean[] initialFill ) + + MessageAssembler(int messageLength, boolean[] initialFill) { - this( messageLength ); - + this(messageLength); + /* Pre-load the message with the sync pattern */ - for( int x = 0; x < initialFill.length; x++ ) + for(int x = 0; x < initialFill.length; x++) { - receive( initialFill[ x ] ); + receive(initialFill[x]); } } - + public void dispose() { - mMessage = null; + mMessage = null; } @Override /** * Receives one bit at a time, and assembles them into a message */ - public void receive( Boolean bit ) + public void receive(Boolean bit) { try { - mMessage.add( bit ); + mMessage.add(bit); } - catch( BitSetFullException e ) + catch(BitSetFullException e) { e.printStackTrace(); } - + /* Once our message is complete (ie full), send it to all registered * message listeners, and set complete flag so for auto-removal */ - if( mMessage.isFull() ) + if(mMessage.isFull()) { mComplete = true; flush(); @@ -226,7 +217,7 @@ public class MessageFramer implements Listener, */ public void flush() { - mBroadcaster.receive( mMessage ); + mBroadcaster.receive(mMessage); mComplete = true; } diff --git a/src/main/java/io/github/dsheirer/bits/SoftSyncDetector.java b/src/main/java/io/github/dsheirer/bits/SoftSyncDetector.java index 1a285f70..c2748f83 100644 --- a/src/main/java/io/github/dsheirer/bits/SoftSyncDetector.java +++ b/src/main/java/io/github/dsheirer/bits/SoftSyncDetector.java @@ -1,5 +1,7 @@ package io.github.dsheirer.bits; +import io.github.dsheirer.dsp.symbol.ISyncDetectListener; + public class SoftSyncDetector implements ISyncProcessor { private ISyncDetectListener mListener; diff --git a/src/main/java/io/github/dsheirer/bits/SyncDetector.java b/src/main/java/io/github/dsheirer/bits/SyncDetector.java index fd8e2091..23aa6be0 100644 --- a/src/main/java/io/github/dsheirer/bits/SyncDetector.java +++ b/src/main/java/io/github/dsheirer/bits/SyncDetector.java @@ -1,5 +1,7 @@ package io.github.dsheirer.bits; +import io.github.dsheirer.dsp.symbol.ISyncDetectListener; + public class SyncDetector implements ISyncProcessor { private long mPattern; diff --git a/src/main/java/io/github/dsheirer/dsp/filter/ComplexFilter.java b/src/main/java/io/github/dsheirer/dsp/filter/ComplexFilter.java deleted file mode 100644 index 59b9abb9..00000000 --- a/src/main/java/io/github/dsheirer/dsp/filter/ComplexFilter.java +++ /dev/null @@ -1,55 +0,0 @@ -/******************************************************************************* - * SDR Trunk - * Copyright (C) 2014 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 io.github.dsheirer.dsp.filter; - -import io.github.dsheirer.sample.Listener; -import io.github.dsheirer.sample.complex.Complex; - -@Deprecated -public abstract class ComplexFilter implements Listener -{ - private Listener mListener; - - public void setListener( Listener listener ) - { - mListener = listener; - } - - public void dispose() - { - mListener = null; - } - - public Listener getListener() - { - return mListener; - } - - public boolean hasListener() - { - return mListener != null; - } - - protected void send( Complex sample ) - { - if( mListener != null ) - { - mListener.receive( sample ); - } - } -} diff --git a/src/main/java/io/github/dsheirer/dsp/filter/channelizer/PolyphaseChannelManager.java b/src/main/java/io/github/dsheirer/dsp/filter/channelizer/PolyphaseChannelManager.java index 3a3a2750..c9cb1cac 100644 --- a/src/main/java/io/github/dsheirer/dsp/filter/channelizer/PolyphaseChannelManager.java +++ b/src/main/java/io/github/dsheirer/dsp/filter/channelizer/PolyphaseChannelManager.java @@ -22,7 +22,7 @@ import io.github.dsheirer.dsp.filter.channelizer.output.TwoChannelOutputProcesso import io.github.dsheirer.dsp.filter.design.FilterDesignException; import io.github.dsheirer.sample.Broadcaster; import io.github.dsheirer.sample.Listener; -import io.github.dsheirer.sample.buffer.IReusableBufferProvider; +import io.github.dsheirer.sample.buffer.IReusableComplexBufferProvider; import io.github.dsheirer.sample.buffer.ReusableComplexBuffer; import io.github.dsheirer.source.ISourceEventProcessor; import io.github.dsheirer.source.Source; @@ -69,7 +69,7 @@ public class PolyphaseChannelManager implements ISourceEventProcessor private static final int POLYPHASE_SYNTHESIZER_TAPS_PER_CHANNEL = 9; private Broadcaster mSourceEventBroadcaster = new Broadcaster<>(); - private IReusableBufferProvider mReusableBufferProvider; + private IReusableComplexBufferProvider mReusableBufferProvider; private List mChannelSources = new CopyOnWriteArrayList<>(); private ChannelCalculator mChannelCalculator; private ComplexPolyphaseChannelizerM2 mPolyphaseChannelizer; @@ -81,20 +81,20 @@ public class PolyphaseChannelManager implements ISourceEventProcessor /** * Creates a polyphase channel manager instance. * - * @param reusableBufferProvider (ie tuner) that supports register/deregister for reusable baseband sample buffer + * @param reusableComplexBufferProvider (ie tuner) that supports register/deregister for reusable baseband sample buffer * streams * @param frequency of the baseband complex buffer sample stream (ie center frequency) * @param sampleRate of the baseband complex buffer sample stream */ - public PolyphaseChannelManager(IReusableBufferProvider reusableBufferProvider, + public PolyphaseChannelManager(IReusableComplexBufferProvider reusableComplexBufferProvider, long frequency, double sampleRate) { - if(reusableBufferProvider == null) + if(reusableComplexBufferProvider == null) { throw new IllegalArgumentException("Complex buffer provider argument cannot be null"); } - mReusableBufferProvider = reusableBufferProvider; + mReusableBufferProvider = reusableComplexBufferProvider; int channelCount = (int)(sampleRate / MINIMUM_CHANNEL_BANDWIDTH); diff --git a/src/main/java/io/github/dsheirer/dsp/filter/dc/BufferDCFilter.java b/src/main/java/io/github/dsheirer/dsp/filter/dc/BufferDCFilter.java index bf229d4a..8236b6b1 100644 --- a/src/main/java/io/github/dsheirer/dsp/filter/dc/BufferDCFilter.java +++ b/src/main/java/io/github/dsheirer/dsp/filter/dc/BufferDCFilter.java @@ -15,26 +15,23 @@ ******************************************************************************/ package io.github.dsheirer.dsp.filter.dc; +import io.github.dsheirer.sample.buffer.ReusableBuffer; +import io.github.dsheirer.sample.buffer.ReusableBufferQueue; import org.slf4j.Logger; import org.slf4j.LoggerFactory; public class BufferDCFilter { private final static Logger mLog = LoggerFactory.getLogger(BufferDCFilter.class); + private ReusableBufferQueue mReusableBufferQueue = new ReusableBufferQueue("Buffer DC Filter"); + private double mAccumulator; /** * Calculates the DC offset present in the samples and subtracts the offset from all samples. */ - public static void filterReal(float[] samples) + public void filterReal(float[] samples) { - double accumulator = 0.0; - - for(int x = 0; x < samples.length; x++) - { - accumulator += samples[x]; - } - - float adjustment = (float)(accumulator / (double)samples.length); + float adjustment = getOffset(samples); for(int x = 0; x < samples.length; x++) { @@ -42,6 +39,18 @@ public class BufferDCFilter } } + private float getOffset(float[] samples) + { + mAccumulator = 0.0; + + for(int x = 0; x < samples.length; x++) + { + mAccumulator += samples[x]; + } + + return (float)(mAccumulator / (double)samples.length); + } + /** * Calculates the DC offset present in the samples and subtracts the offset from all samples. */ @@ -67,4 +76,24 @@ public class BufferDCFilter samples[x + 1] = samples[x + 1] - qAdjustment; } } + + public ReusableBuffer filter(ReusableBuffer unfilteredBuffer) + { + float[] unfilteredSamples = unfilteredBuffer.getSamples(); + + ReusableBuffer filteredBuffer = mReusableBufferQueue.getBuffer(unfilteredSamples.length); + float[] filteredSamples = filteredBuffer.getSamples(); + + float offset = getOffset(unfilteredSamples); + + for(int x = 0; x < unfilteredSamples.length; x++) + { + filteredSamples[x] = unfilteredSamples[x] - offset; + } + + unfilteredBuffer.decrementUserCount(); + filteredBuffer.incrementUserCount(); + + return filteredBuffer; + } } diff --git a/src/main/java/io/github/dsheirer/dsp/filter/dc/DCRemovalFilter.java b/src/main/java/io/github/dsheirer/dsp/filter/dc/DCRemovalFilter.java index d265b8e6..3f044177 100644 --- a/src/main/java/io/github/dsheirer/dsp/filter/dc/DCRemovalFilter.java +++ b/src/main/java/io/github/dsheirer/dsp/filter/dc/DCRemovalFilter.java @@ -1,41 +1,87 @@ /******************************************************************************* - * SDR Trunk - * Copyright (C) 2014,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 + * 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 + * ******************************************************************************/ package io.github.dsheirer.dsp.filter.dc; +import io.github.dsheirer.sample.buffer.ReusableBuffer; +import io.github.dsheirer.sample.buffer.ReusableBufferQueue; + +/** + * DC removal filter for removing any DC offset that may be present when a signal is not aligned within the + * demodulated channel. + */ public class DCRemovalFilter { - protected float mAverage; - protected float mRatio; + private ReusableBufferQueue mReusableBufferQueue = new ReusableBufferQueue("DC Removal Filter"); + protected float mAverage; + protected float mRatio; - public DCRemovalFilter( float ratio ) - { - mRatio = ratio; - } + /** + * Creates a DC removal filter. + * + * @param ratio + */ + public DCRemovalFilter(float ratio) + { + mRatio = ratio; + } - public void reset() - { - mAverage = 0.0f; - } + /** + * Resets the running average to zero. + */ + public void reset() + { + mAverage = 0.0f; + } - public float filter( float sample ) - { - float filtered = sample - mAverage; - mAverage += mRatio * filtered; - return filtered; - } + /** + * Filters the sample and returns a filtered (ie DC-removed) sample. + * @param sample to filter + * @return filtered sample + */ + public float filter(float sample) + { + float filtered = sample - mAverage; + mAverage += mRatio * filtered; + return filtered; + } + + /** + * Filters the buffer samples and returns a new reusable buffer with the filtered samples. + * + * Note: this method automatically manages the user accounting between the buffer argument and the new returned + * filtered sample buffer. + * + * @param unfilteredBuffer containing samples to filter + * @return a new reusable buffer containing the filtered samples with the user count set to 1. + */ + public ReusableBuffer filter(ReusableBuffer unfilteredBuffer) + { + ReusableBuffer filteredBuffer = mReusableBufferQueue.getBuffer(unfilteredBuffer.getSampleCount()); + + float[] unfilteredSamples = unfilteredBuffer.getSamples(); + float[] filteredSamples = filteredBuffer.getSamples(); + + for(int x = 0; x < unfilteredSamples.length; x++) + { + filteredSamples[x] = filter(unfilteredSamples[x]); + } + + unfilteredBuffer.decrementUserCount(); + filteredBuffer.incrementUserCount(); + + return filteredBuffer; + } } \ No newline at end of file diff --git a/src/main/java/io/github/dsheirer/dsp/filter/dc/IIRSinglePoleDCRemovalFilter.java b/src/main/java/io/github/dsheirer/dsp/filter/dc/IIRSinglePoleDCRemovalFilter.java index 83a59eae..202c83f0 100644 --- a/src/main/java/io/github/dsheirer/dsp/filter/dc/IIRSinglePoleDCRemovalFilter.java +++ b/src/main/java/io/github/dsheirer/dsp/filter/dc/IIRSinglePoleDCRemovalFilter.java @@ -1,45 +1,106 @@ +/******************************************************************************* + * 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 + * + ******************************************************************************/ package io.github.dsheirer.dsp.filter.dc; +import io.github.dsheirer.sample.buffer.ReusableBuffer; +import io.github.dsheirer.sample.buffer.ReusableBufferQueue; import io.github.dsheirer.sample.real.RealSampleListener; public class IIRSinglePoleDCRemovalFilter implements RealSampleListener { - private float mAlpha; - private float mPreviousInput = 0.0f; - private float mPreviousOutput = 0.0f; + private ReusableBufferQueue mReusableBufferQueue = new ReusableBufferQueue("IIR DC Filter"); + private float mAlpha; + private float mPreviousInput = 0.0f; + private float mPreviousOutput = 0.0f; + private float mCurrentOutput = 0.0f; - private RealSampleListener mListener; - - /** - * IIR single-pole DC removal filter, as described by J M de Freitas in - * 29Jan2007 paper at: - * - * http://www.mathworks.com/matlabcentral/fileexchange/downloads/72134/download - * - * @param alpha 0.0 - 1.0 float - the closer alpha is to unity, the closer - * the cutoff frequency is to DC. - */ - public IIRSinglePoleDCRemovalFilter( float alpha ) - { - mAlpha = alpha; - } - @Override - public void receive( float currentInput ) + private RealSampleListener mListener; + + /** + * IIR single-pole DC removal filter, as described by J M de Freitas in + * 29Jan2007 paper at: + * + * http://www.mathworks.com/matlabcentral/fileexchange/downloads/72134/download + * + * @param alpha 0.0 - 1.0 float - the closer alpha is to unity, the closer + * the cutoff frequency is to DC. + */ + public IIRSinglePoleDCRemovalFilter(float alpha) { - float currentOutput = ( currentInput - mPreviousInput ) + - ( mAlpha * mPreviousOutput ); - - if( mListener != null ) - { - mListener.receive( currentOutput ); - } - - mPreviousInput = currentInput; - mPreviousOutput = currentOutput; + mAlpha = alpha; } - public void setListener( RealSampleListener listener ) - { - mListener = listener; - } + /** + * Filters the sample + * @param sample to filter + * @return filtered output sample + */ + public float filter(float sample) + { + mCurrentOutput = (sample - mPreviousInput) + (mAlpha * mPreviousOutput); + + mPreviousInput = sample; + mPreviousOutput = mCurrentOutput; + + return mCurrentOutput; + } + + /** + * Filters the input buffer and returns a new filtered output buffer. + * + * Buffer user accounting is handled by this method where the input buffer user count is decremented and the + * returned output buffer has the user count incremented to one. + * + * @param inputBuffer to filter + * @return filtered samples in a new (ie reused) output buffer + */ + public ReusableBuffer filter(ReusableBuffer inputBuffer) + { + ReusableBuffer outputBuffer = mReusableBufferQueue.getBuffer(inputBuffer.getSampleCount()); + float[] inputSamples = inputBuffer.getSamples(); + float[] outputSamples = outputBuffer.getSamples(); + + for(int x = 0; x < inputSamples.length; x++) + { + outputSamples[x] = filter(inputSamples[x]); + } + + inputBuffer.decrementUserCount(); + outputBuffer.incrementUserCount(); + + return outputBuffer; + } + + /** + * Implements the RealSampleListener interface for single sample streams. + */ + @Override + public void receive(float sample) + { + if(mListener != null) + { + mListener.receive(filter(sample)); + } + } + + /** + * Assigns the listener to receive filtered samples processed by the RealSampleListener interface + */ + public void setListener(RealSampleListener listener) + { + mListener = listener; + } } diff --git a/src/main/java/io/github/dsheirer/dsp/filter/dc/IIRSinglePoleDCRemovalFilter_RB.java b/src/main/java/io/github/dsheirer/dsp/filter/dc/IIRSinglePoleDCRemovalFilter_RB.java deleted file mode 100644 index 974374c0..00000000 --- a/src/main/java/io/github/dsheirer/dsp/filter/dc/IIRSinglePoleDCRemovalFilter_RB.java +++ /dev/null @@ -1,69 +0,0 @@ -package io.github.dsheirer.dsp.filter.dc; - -import io.github.dsheirer.sample.Listener; -import io.github.dsheirer.sample.real.RealBuffer; - -public class IIRSinglePoleDCRemovalFilter_RB implements Listener -{ - private float mAlpha; - private float mPreviousInput = 0.0f; - private float mPreviousOutput = 0.0f; - private Listener mListener; - - /** - * IIR single-pole DC removal filter, as described by J M de Freitas in - * 29Jan2007 paper at: - * - * http://www.mathworks.com/matlabcentral/fileexchange/downloads/72134/download - * - * @param alpha 0.0 - 1.0 float - the closer alpha is to unity, the closer - * the cutoff frequency is to DC. - */ - public IIRSinglePoleDCRemovalFilter_RB( float alpha ) - { - mAlpha = alpha; - } - - @Override - public void receive( RealBuffer buffer ) - { - if( mListener != null ) - { - mListener.receive( filter( buffer ) ); - } - } - - public RealBuffer filter( RealBuffer buffer ) - { - float[] samples = buffer.getSamples(); - - for( int x = 0; x < samples.length; x++ ) - { - samples[ x ] = filter( samples[ x ] ); - } - - return buffer; - } - - public float filter( float sample ) - { - float currentOutput = ( sample - mPreviousInput ) + - ( mAlpha * mPreviousOutput ); - - mPreviousInput = sample; - mPreviousOutput = currentOutput; - - return currentOutput; - } - - public void reset() - { - mPreviousInput = 0.0f; - mPreviousOutput = 0.0f; - } - - public void setListener( Listener listener ) - { - mListener = listener; - } -} diff --git a/src/main/java/io/github/dsheirer/dsp/filter/design/FilterViewer.java b/src/main/java/io/github/dsheirer/dsp/filter/design/FilterViewer.java index c4298aef..821c432b 100644 --- a/src/main/java/io/github/dsheirer/dsp/filter/design/FilterViewer.java +++ b/src/main/java/io/github/dsheirer/dsp/filter/design/FilterViewer.java @@ -1,7 +1,7 @@ package io.github.dsheirer.dsp.filter.design; -import io.github.dsheirer.dsp.filter.FilterFactory; 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; @@ -39,64 +39,68 @@ public class FilterViewer extends Application */ private float[] getFilter() { - FIRFilterSpecification specification = FIRFilterSpecification.lowPassBuilder() - .sampleRate(25000) - .gridDensity(16) - .oddLength(true) - .passBandCutoff(6000) +// FIRFilterSpecification specification = FIRFilterSpecification.lowPassBuilder() +// .sampleRate(8000) +// .gridDensity(16) +// .oddLength(true) +// .passBandCutoff(300) +// .passBandAmplitude(1.0) +// .passBandRipple(0.01) +// .stopBandStart(500) +// .stopBandAmplitude(0.0) +// .stopBandRipple(0.03) //Approximately 60 dB attenuation +// .build(); +// + FIRFilterSpecification specification = FIRFilterSpecification.highPassBuilder() + .sampleRate(8000) + .stopBandCutoff(200) + .stopBandAmplitude(0.0) + .stopBandRipple(0.025) + .passBandStart(300) .passBandAmplitude(1.0) .passBandRipple(0.01) - .stopBandStart(6500) - .stopBandAmplitude(0.0) - .stopBandRipple(0.005) //Approximately 94 dB attenuation .build(); -// FIRFilterSpecification specification = FIRFilterSpecification.highPassBuilder() -// .sampleRate(24000) -// .stopBandCutoff(800) -// .stopBandAmplitude(0.0) -// .stopBandRipple(0.03) -// .passBandStart(1000) -// .passBandAmplitude(1.0) -// .passBandRipple(0.08) +// FIRFilterSpecification specification = FIRFilterSpecification.bandPassBuilder() +// .sampleRate(8000) +// .stopFrequency1(200) +// .passFrequencyBegin(300) +// .passFrequencyEnd(3800) +// .stopFrequency2(3900) +// .stopRipple(0.01) +// .passRipple(0.01) // .build(); -// FIRFilterSpecification specification = FIRFilterSpecification.bandPassBuilder() -// .sampleRate( 48000 ) -// .stopFrequency1( 200 ) -// .passFrequencyBegin( 400 ) -// .passFrequencyEnd( 3200 ) -// .stopFrequency2( 3400 ) -// .stopRipple( 0.0003 ) -// .passRipple( 0.008) -// .build(); - float[] taps = null; - try - { - taps = FilterFactory.getSincM2Synthesizer(12500.0, 2, 19); -// taps = FilterFactory.getSincFilter(25000.0, 12500.0, 2, 19, Window.WindowType.BLACKMAN_HARRIS_7, true); - } - catch(FilterDesignException fde) - { - mLog.error("Error"); - } - // try // { -// RemezFIRFilterDesigner designer = new RemezFIRFilterDesigner(specification); -// -// if(designer.isValid()) -// { -// taps = designer.getImpulseResponse(); -// } +// taps = FilterFactory.getSincM2Synthesizer(12500.0, 2, 19); +// taps = FilterFactory.getSincFilter(25000.0, 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); + } + + if(taps == null) + { + throw new IllegalStateException("Couldn't design filter"); + } return taps; diff --git a/src/main/java/io/github/dsheirer/dsp/filter/fir/real/RealFIRFilter2.java b/src/main/java/io/github/dsheirer/dsp/filter/fir/real/RealFIRFilter2.java index 7122ea76..7023debb 100644 --- a/src/main/java/io/github/dsheirer/dsp/filter/fir/real/RealFIRFilter2.java +++ b/src/main/java/io/github/dsheirer/dsp/filter/fir/real/RealFIRFilter2.java @@ -27,7 +27,7 @@ import io.github.dsheirer.sample.buffer.ReusableBufferQueue; */ public class RealFIRFilter2 extends FIRFilter { - private ReusableBufferQueue mReusableBufferQueue = new ReusableBufferQueue(); + private ReusableBufferQueue mReusableBufferQueue = new ReusableBufferQueue("RealFIRFilter2"); private float[] mData; private float[] mCoefficients; diff --git a/src/main/java/io/github/dsheirer/dsp/filter/resample/RealResampler.java b/src/main/java/io/github/dsheirer/dsp/filter/resample/RealResampler.java index ece20c3d..39238e8c 100644 --- a/src/main/java/io/github/dsheirer/dsp/filter/resample/RealResampler.java +++ b/src/main/java/io/github/dsheirer/dsp/filter/resample/RealResampler.java @@ -20,21 +20,20 @@ import com.laszlosystems.libresample4j.SampleBuffers; import io.github.dsheirer.sample.Listener; import io.github.dsheirer.sample.buffer.ReusableBuffer; import io.github.dsheirer.sample.buffer.ReusableBufferQueue; -import io.github.dsheirer.sample.real.RealBuffer; import org.slf4j.Logger; import org.slf4j.LoggerFactory; import java.nio.ByteBuffer; import java.nio.FloatBuffer; -public class RealResampler implements Listener +public class RealResampler { protected static final Logger mLog = LoggerFactory.getLogger(RealResampler.class); + private ReusableBufferQueue mReusableBufferQueue = new ReusableBufferQueue("RealResampler"); private Resampler mResampler; private Listener mResampledListener; private BufferManager mBufferManager = new BufferManager(); - private ReusableBufferQueue mReusableBufferQueue = new ReusableBufferQueue("RealResampler"); private double mResampleFactor; /** @@ -50,12 +49,11 @@ public class RealResampler implements Listener /** * Primary input method to the resampler - * @param realBuffer to resample + * @param reusableBuffer to resample */ - @Override - public void receive(RealBuffer realBuffer) + public void resample(ReusableBuffer reusableBuffer) { - mBufferManager.load(realBuffer); + mBufferManager.load(reusableBuffer); mResampler.process(mResampleFactor, mBufferManager, false); } @@ -74,25 +72,10 @@ public class RealResampler implements Listener */ public class BufferManager implements SampleBuffers { - private int mBufferLength = 1000; + private int mBufferLength = 8000; private FloatBuffer mInputBuffer = ByteBuffer.allocate(mBufferLength).asFloatBuffer(); private FloatBuffer mOutputBuffer = ByteBuffer.allocate(mBufferLength).asFloatBuffer(); - public void resize(int bufferLength) - { - int floatBufferLength = bufferLength * 4; - - FloatBuffer inputBuffer = ByteBuffer.allocate(floatBufferLength).asFloatBuffer(); - inputBuffer.put(mInputBuffer); - mInputBuffer = inputBuffer; - - FloatBuffer outputBuffer = ByteBuffer.allocate(floatBufferLength).asFloatBuffer(); - outputBuffer.put(mOutputBuffer); - mOutputBuffer = outputBuffer; - - mBufferLength = bufferLength; - } - /** * Current length of the input and output buffers */ @@ -104,16 +87,10 @@ public class RealResampler implements Listener /** * Queues the buffer sample for resampling */ - public void load(RealBuffer realBuffer) + public void load(ReusableBuffer reusableBuffer) { - int remaining = mInputBuffer.remaining(); - - if(mInputBuffer.remaining() < realBuffer.getSamples().length) - { - resize(mInputBuffer.capacity() + (realBuffer.getSamples().length - mInputBuffer.remaining())); - } - - mInputBuffer.put(realBuffer.getSamples()); + mInputBuffer.put(reusableBuffer.getSamples()); + reusableBuffer.decrementUserCount(); } @Override @@ -147,9 +124,10 @@ public class RealResampler implements Listener { mOutputBuffer.put(samples, offset, length); - while(mOutputBuffer.position() > 200) + while(mOutputBuffer.position() > 1000) { - ReusableBuffer outputBuffer = mReusableBufferQueue.getBuffer(200); + ReusableBuffer outputBuffer = mReusableBufferQueue.getBuffer(1000); + outputBuffer.incrementUserCount(); mOutputBuffer.flip(); mOutputBuffer.get(outputBuffer.getSamples()); @@ -157,9 +135,12 @@ public class RealResampler implements Listener if(mResampledListener != null) { - outputBuffer.incrementUserCount(); mResampledListener.receive(outputBuffer); } + else + { + outputBuffer.decrementUserCount(); + } } } } diff --git a/src/main/java/io/github/dsheirer/dsp/fm/FMDemodulator.java b/src/main/java/io/github/dsheirer/dsp/fm/FMDemodulator.java index 6a53a8ff..9a46e9dd 100644 --- a/src/main/java/io/github/dsheirer/dsp/fm/FMDemodulator.java +++ b/src/main/java/io/github/dsheirer/dsp/fm/FMDemodulator.java @@ -17,19 +17,47 @@ ******************************************************************************/ package io.github.dsheirer.dsp.fm; +import io.github.dsheirer.sample.buffer.ReusableBuffer; +import io.github.dsheirer.sample.buffer.ReusableBufferQueue; +import io.github.dsheirer.sample.buffer.ReusableComplexBuffer; import io.github.dsheirer.sample.complex.Complex; -public abstract class FMDemodulator +/** + * FM Demodulator for demodulating complex samples and producing demodulated floating point samples. + */ +public class FMDemodulator { + private ReusableBufferQueue mReusableBufferQueue = new ReusableBufferQueue("FMDemodulator"); private float mPreviousI = 0.0f; private float mPreviousQ = 0.0f; protected float mGain; + /** + * Creates an FM demodulator instance with a default gain of 1.0. + */ + public FMDemodulator() + { + this(1.0f); + } + + /** + * Creates an FM demodulator instance and applies the gain value to each demodulated output sample. + * @param gain to apply to demodulated samples. + */ public FMDemodulator(float gain) { mGain = gain; } + /** + * Demodulates the I/Q sample via complex sample multiplication by the complex conjugates of the most recently + * demodulated complex sample (ie previous sample). Each new sample overwrites the previously stored sample to + * allow future invocations of the method to simply use a new sample value. + * + * @param currentI of the sample + * @param currentQ of the sample + * @return demodulated sample + */ public float demodulate(float currentI, float currentQ) { /** @@ -65,6 +93,12 @@ public abstract class FMDemodulator return (float)(angle * mGain); } + /** + * Demodulates the complex samples and returns the demodulated value. + * @param previous + * @param current + * @return + */ public static double demodulate(Complex previous, Complex current) { double inphase = (current.inphase() * previous.inphase()) - (current.quadrature() * -previous.quadrature()); @@ -88,14 +122,48 @@ public abstract class FMDemodulator return angle; } - public abstract void dispose(); + /** + * Demodulates the complex baseband sample buffer and returns a demodulated reusable buffer with the user count + * set to 1. The complex baseband buffer's user count is decremented after demodulation. + * + * @param basebandSampleBuffer containing samples to demodulate + * @return demodulated sample buffer. + */ + public ReusableBuffer demodulate(ReusableComplexBuffer basebandSampleBuffer) + { + ReusableBuffer demodulatedBuffer = mReusableBufferQueue.getBuffer(basebandSampleBuffer.getSampleCount()); + float[] basebandSamples = basebandSampleBuffer.getSamples(); + float[] demodulatedSamples = demodulatedBuffer.getSamples(); + + for(int x = 0; x < basebandSamples.length; x += 2) + { + demodulatedSamples[x / 2] = demodulate(basebandSamples[x], basebandSamples[x + 1]); + } + + basebandSampleBuffer.decrementUserCount(); + demodulatedBuffer.incrementUserCount(); + + return demodulatedBuffer; + } + + public void dispose() + { + //no-op + } + + /** + * Resets this demodulator by zeroing the stored previous sample. + */ public void reset() { mPreviousI = 0.0f; mPreviousQ = 0.0f; } + /** + * Sets the gain to the specified level. + */ public void setGain(float gain) { mGain = gain; diff --git a/src/main/java/io/github/dsheirer/dsp/fm/FMDemodulator_CB.java b/src/main/java/io/github/dsheirer/dsp/fm/FMDemodulator_CB.java deleted file mode 100644 index b5cd4380..00000000 --- a/src/main/java/io/github/dsheirer/dsp/fm/FMDemodulator_CB.java +++ /dev/null @@ -1,78 +0,0 @@ -/******************************************************************************* - * SDR Trunk - * Copyright (C) 2014,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 - ******************************************************************************/ -package io.github.dsheirer.dsp.fm; - -import io.github.dsheirer.sample.Listener; -import io.github.dsheirer.sample.complex.ComplexBuffer; -import io.github.dsheirer.sample.real.RateCounter_RB; -import io.github.dsheirer.sample.real.RealBuffer; -import org.slf4j.Logger; -import org.slf4j.LoggerFactory; - -public class FMDemodulator_CB extends FMDemodulator implements Listener -{ - private final static Logger mLog = LoggerFactory.getLogger( FMDemodulator_CB.class ); - - private Listener mListener; - - private RateCounter_RB mRateCounter = new RateCounter_RB( "FM DEMOD OUTPUT" ); - - public FMDemodulator_CB( float gain ) - { - super( gain ); - } - - @Override - public void receive( ComplexBuffer buffer ) - { - if( mListener != null ) - { - float[] samples = buffer.getSamples(); - - int half = samples.length / 2; - - float[] demodulated = new float[ half ]; - - for( int x = 0; x < half; x++ ) - { - int index = x * 2; - - demodulated[ x ] = demodulate( samples[ index ], - samples[ index + 1 ] ); - } - - mListener.receive( new RealBuffer( demodulated ) ); - } - } - - public void setListener( Listener listener ) - { - mListener = listener; - } - - public void removeListener() - { - mListener = null; - } - - @Override - public void dispose() - { - mListener = null; - } -} diff --git a/src/main/java/io/github/dsheirer/dsp/fm/FMDiscriminator.java b/src/main/java/io/github/dsheirer/dsp/fm/FMDiscriminator.java deleted file mode 100644 index 5d157f87..00000000 --- a/src/main/java/io/github/dsheirer/dsp/fm/FMDiscriminator.java +++ /dev/null @@ -1,102 +0,0 @@ -/******************************************************************************* - * SDR Trunk - * Copyright (C) 2014,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 - ******************************************************************************/ -package io.github.dsheirer.dsp.fm; - -import io.github.dsheirer.sample.Listener; -import io.github.dsheirer.sample.complex.Complex; -import io.github.dsheirer.sample.real.RealSampleListener; - -@Deprecated -public class FMDiscriminator implements Listener -{ - private RealSampleListener mListener; - private Complex mPreviousSample = new Complex( 0.0f, 0.0f ); - private double mGain; - - /** - * Implements a polar discriminator with ArcTangent angle estimator to - * demodulate complex sampled frequency modulated signals. - * - * @param gain - gain to be applied to the demodulated output - can be - * dynamically applied to continuously adjust audio output - */ - public FMDiscriminator( double gain ) - { - mGain = gain; - } - - public void dispose() - { - mListener = null; - mPreviousSample = null; - } - - public synchronized void setGain( double gain ) - { - mGain = gain; - } - - @Override - public void receive( Complex currentSample ) - { - /** - * Multiply the current sample against the complex conjugate of the - * previous sample to derive the phase delta between the two samples - * - * Negating the previous sample quadrature produces the conjugate - */ - double i = ( currentSample.inphase() * mPreviousSample.inphase() ) - - ( currentSample.quadrature() * -mPreviousSample.quadrature() ); - double q = ( currentSample.quadrature() * mPreviousSample.inphase() ) + - ( currentSample.inphase() * -mPreviousSample.quadrature() ); - - double angle; - - //Check for divide by zero - if( i == 0 ) - { - angle = 0.0; - } - else - { - /** - * Use the arc-tangent of imaginary (q) divided by real (i) to - * get the phase angle (+/-) which was directly manipulated by the - * original message waveform during the modulation. This value now - * serves as the instantaneous amplitude of the demodulated signal - */ - double denominator = 1.0d / i; - angle = Math.atan( (double)q * denominator ); - } - - if( mListener != null ) - { - mListener.receive( (float)( angle * mGain ) ); - } - - /** - * Store the current sample to use during the next iteration - */ - mPreviousSample = currentSample; - } - - public void setListener( RealSampleListener listener ) - { - mListener = listener; - } -} diff --git a/src/main/java/io/github/dsheirer/dsp/fsk/ISyncStateListener.java b/src/main/java/io/github/dsheirer/dsp/fsk/ISyncStateListener.java new file mode 100644 index 00000000..336fa42e --- /dev/null +++ b/src/main/java/io/github/dsheirer/dsp/fsk/ISyncStateListener.java @@ -0,0 +1,24 @@ +/******************************************************************************* + * 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 + * + ******************************************************************************/ +package io.github.dsheirer.dsp.fsk; + +/** + * Interface to receive sync state change events. + */ +public interface ISyncStateListener +{ + void setSyncState(SyncState syncState); +} diff --git a/src/main/java/io/github/dsheirer/dsp/fsk/LTRDecoder.java b/src/main/java/io/github/dsheirer/dsp/fsk/LTRDecoder.java new file mode 100644 index 00000000..b4a48545 --- /dev/null +++ b/src/main/java/io/github/dsheirer/dsp/fsk/LTRDecoder.java @@ -0,0 +1,221 @@ +/******************************************************************************* + * 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 + * + ******************************************************************************/ +package io.github.dsheirer.dsp.fsk; + +import io.github.dsheirer.dsp.filter.dc.IIRSinglePoleDCRemovalFilter; +import io.github.dsheirer.dsp.filter.design.FilterDesignException; +import io.github.dsheirer.dsp.filter.fir.FIRFilterSpecification; +import io.github.dsheirer.dsp.filter.fir.real.RealFIRFilter2; +import io.github.dsheirer.dsp.filter.fir.remez.RemezFIRFilterDesigner; +import io.github.dsheirer.dsp.symbol.ISyncDetectListener; +import io.github.dsheirer.sample.Listener; +import io.github.dsheirer.sample.buffer.ReusableBuffer; +import org.slf4j.Logger; +import org.slf4j.LoggerFactory; + +/** + * Logic Trunked Radio (LTR) 300-baud FSK decoder is designed to operate against an 8 kHz FM demodulated audio stream. + * + * This decoder employs a zero-crossing timing error detector with adjustable symbol timing error gain. This decoder + * also employs a synchronization monitor that automatically adjusts symbol timing error gain according to the quantity + * of successful message sync pattern detections. This is optimized for LTR signals that periodically transmit idle + * bursts and transmit continuous message streams during voice transmissions. The synchronization monitor adjusts to an + * aggressive gain value when sync is lost and automatically reduces timing error gain as successive sync pattern + * detections occur. + * + * FM demodulated sample buffers should not be filtered prior to this decoder. An internal DC-removal filter coupled + * with a low-pass filter removes any tuning offset and/or audio components above 300 Hertz. The DC removal filter is + * adjusted for a slower offset removal in order to prevent overly distorting the FSK signalling component. This mild + * adjustment may not fully compensate for mis-tuned signals and may result in missed decodes of idle bursts. However, + * the DC filter will track a mis-tuned signal to baseband quickly upon a continuous transmission. + */ +public class LTRDecoder implements Listener, ISyncDetectListener, ISyncStateListener +{ + private final static Logger mLog = LoggerFactory.getLogger(LTRDecoder.class); + + public static final float SAMPLES_PER_SYMBOL = 8000.0f / 300.0f; + + protected static final float COARSE_TIMING_GAIN = 1.0f / 3.0f; + protected static final float MEDIUM_TIMING_GAIN = 1.0f / 4.0f; + protected static final float FINE_TIMING_GAIN = 1.0f / 5.0f; + + private static float[] sLowPassFilterCoefficients; + + static + { + FIRFilterSpecification specification = FIRFilterSpecification.lowPassBuilder() + .sampleRate(8000) + .gridDensity(16) + .oddLength(true) + .passBandCutoff(300) + .passBandAmplitude(1.0) + .passBandRipple(0.01) + .stopBandStart(500) + .stopBandAmplitude(0.0) + .stopBandRipple(0.03) //Approximately 60 dB attenuation + .build(); + + try + { + RemezFIRFilterDesigner designer = new RemezFIRFilterDesigner(specification); + + if(designer.isValid()) + { + sLowPassFilterCoefficients = designer.getImpulseResponse(); + } + } + catch(FilterDesignException fde) + { + mLog.error("Filter design error", fde); + } + } + + protected float mSymbolTimingGain = COARSE_TIMING_GAIN; + protected SampleBuffer mSampleBuffer; + protected ZeroCrossingErrorDetector mTimingErrorDetector = new ZeroCrossingErrorDetector(SAMPLES_PER_SYMBOL); + protected SynchronizationMonitor mSynchronizationMonitor; + private IIRSinglePoleDCRemovalFilter mDCFilter = new IIRSinglePoleDCRemovalFilter(0.99999f); + private RealFIRFilter2 mLowPassFilter = new RealFIRFilter2(sLowPassFilterCoefficients); + private Listener mSymbolDecisionListener; + + private boolean mSampleDecision; + + /** + * Implements a Logic Trunked Radio sub-audible 300 baud FSK signaling decoder + * + * @param messageLength in symbols + */ + public LTRDecoder(int messageLength) + { + this(messageLength, new SampleBuffer(SAMPLES_PER_SYMBOL, COARSE_TIMING_GAIN), + new ZeroCrossingErrorDetector(SAMPLES_PER_SYMBOL)); + } + + /** + * Implements a Logic Trunked Radio sub-audible 300 baud FSK signaling decoder. + * + * @param messageLength in symbols + * @param sampleBuffer to use for storing sample decisions + * @param detector for symbol timing errors + */ + public LTRDecoder(int messageLength, SampleBuffer sampleBuffer, ZeroCrossingErrorDetector detector) + { + mSynchronizationMonitor = new SynchronizationMonitor(messageLength); + mSynchronizationMonitor.setListener(this); + mTimingErrorDetector = detector; + mSampleBuffer = sampleBuffer; + mSampleBuffer.setTimingGain(mSymbolTimingGain); + } + + /** + * Implements the ISyncDetectedListener interface to be notified of message sync detection events. + * + * This allows the internal timing error detector to adjust symbol timing error gain levels according to the + * message synchronization state to quickly adjust to initial signal streams or to reduce gain levels once + * synchronization has been achieved. + */ + @Override + public void syncDetected() + { + mSynchronizationMonitor.syncDetected(); + } + + /** + * Processes the buffer samples by converting all samples to boolean values reflecting if the sample value is + * greater than zero (or not). Average symbol timing offset is calculated for the full buffer and the offset is + * adjusted and then each of the symbols are decoded using a simple majority decision. + * + * @param buffer containing 8.0 kHz unfiltered FM demodulated audio samples with sub-audible LTR signalling. + */ + @Override + public void receive(ReusableBuffer buffer) + { + ReusableBuffer dcFiltered = mDCFilter.filter(buffer); + ReusableBuffer lowPassFiltered = mLowPassFilter.filter(dcFiltered); + + for(float sample : lowPassFiltered.getSamples()) + { + mSampleDecision = sample > 0.0; + + mSampleBuffer.receive(mSampleDecision); + mTimingErrorDetector.receive(mSampleDecision); + + if(mSampleBuffer.hasSymbol()) + { + if(mSymbolDecisionListener != null) + { + mSymbolDecisionListener.receive(mSampleBuffer.getSymbol()); + } + + mSampleBuffer.resetAndAdjust(-mTimingErrorDetector.getError()); + + mSynchronizationMonitor.increment(); + } + } + + lowPassFiltered.decrementUserCount(); + } + + /** + * Registers a listener to receive decoded LTR symbols. + * + * @param listener to receive symbols. + */ + public void setListener(Listener listener) + { + mSymbolDecisionListener = listener; + } + + /** + * Removes the symbol listener from receiving decoded LTR symbols. + */ + public void removeListener() + { + mSymbolDecisionListener = null; + } + + + /** + * Implements the ISyncStateListener interface to receive synchronization state events and adjust the symbol timing + * error gain levels on the internal timing error detector. + * + * Gain levels are defined relative to a unit gain of 1.0 over a number of symbol periods. LTR uses an initial + * ramp-up symbol reversal pattern of 0101 which provides three zero crossing detection opportunities for the timing + * error detector, therefore we react to a COARSE gain state to average timing error over 3 symbols. + * + * @param syncState from an external synchronization state monitor + */ + @Override + public void setSyncState(SyncState syncState) + { + switch(syncState) + { + case FINE: + mSymbolTimingGain = FINE_TIMING_GAIN; + break; + case MEDIUM: + mSymbolTimingGain = MEDIUM_TIMING_GAIN; + break; + case COARSE: + mSymbolTimingGain = COARSE_TIMING_GAIN; + break; + default: + throw new IllegalArgumentException("Unrecognized sync state level: " + syncState.name()); + } + + mSampleBuffer.setTimingGain(mSymbolTimingGain); + } +} diff --git a/src/main/java/io/github/dsheirer/dsp/fsk/LTRDecoderInstrumented.java b/src/main/java/io/github/dsheirer/dsp/fsk/LTRDecoderInstrumented.java new file mode 100644 index 00000000..eeabaadf --- /dev/null +++ b/src/main/java/io/github/dsheirer/dsp/fsk/LTRDecoderInstrumented.java @@ -0,0 +1,40 @@ +/******************************************************************************* + * 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 + * + ******************************************************************************/ +package io.github.dsheirer.dsp.fsk; + +public class LTRDecoderInstrumented extends LTRDecoder +{ + /** + * Instrumented extension to LTRDecoder + * + * @param messageLength in symbols + */ + public LTRDecoderInstrumented(int messageLength) + { + super(messageLength, new SampleBufferInstrumented(SAMPLES_PER_SYMBOL, COARSE_TIMING_GAIN), + new ZeroCrossingErrorDetectorInstrumented(SAMPLES_PER_SYMBOL)); + } + + public SampleBufferInstrumented getSampleBuffer() + { + return (SampleBufferInstrumented)mSampleBuffer; + } + + public ZeroCrossingErrorDetectorInstrumented getErrorDetector() + { + return (ZeroCrossingErrorDetectorInstrumented)mTimingErrorDetector; + } +} diff --git a/src/main/java/io/github/dsheirer/dsp/fsk/LTRFSKDecoder.java b/src/main/java/io/github/dsheirer/dsp/fsk/LTRFSKDecoderLegacy.java similarity index 98% rename from src/main/java/io/github/dsheirer/dsp/fsk/LTRFSKDecoder.java rename to src/main/java/io/github/dsheirer/dsp/fsk/LTRFSKDecoderLegacy.java index 6e473ba6..34812d2e 100644 --- a/src/main/java/io/github/dsheirer/dsp/fsk/LTRFSKDecoder.java +++ b/src/main/java/io/github/dsheirer/dsp/fsk/LTRFSKDecoderLegacy.java @@ -40,7 +40,7 @@ import io.github.dsheirer.sample.real.RealBuffer; import java.util.ArrayList; import java.util.List; -public class LTRFSKDecoder implements Listener, Instrumentable +public class LTRFSKDecoderLegacy implements Listener, Instrumentable { private RealPrimeCICDecimate mCICDecimator; @@ -72,7 +72,7 @@ public class LTRFSKDecoder implements Listener, Instrumentable * Implements a Logic Trunked Radio sub-audible 300 baud FSK signaling * decoder. Expects a 48000 sample rate input. */ - public LTRFSKDecoder() + public LTRFSKDecoderLegacy() { mCICDecimator = new RealPrimeCICDecimate( 32, 1, 375, 60, WindowType.HAMMING, 2 ); diff --git a/src/main/java/io/github/dsheirer/dsp/fsk/SampleBuffer.java b/src/main/java/io/github/dsheirer/dsp/fsk/SampleBuffer.java new file mode 100644 index 00000000..5a264320 --- /dev/null +++ b/src/main/java/io/github/dsheirer/dsp/fsk/SampleBuffer.java @@ -0,0 +1,124 @@ +/******************************************************************************* + * 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 + * + ******************************************************************************/ +package io.github.dsheirer.dsp.fsk; + +import org.slf4j.Logger; +import org.slf4j.LoggerFactory; + +public class SampleBuffer +{ + private final static Logger mLog = LoggerFactory.getLogger(SampleBuffer.class); + + protected boolean[] mDelayLine; + protected int mDelayLinePointer = 0; + + protected int mTwiceSamplesPerSymbol; + private int mIntegerSamplesPerSymbol; + private int mHalfIntegerSamplesPerSymbol; + private int mPositiveSampleDecisionCount; + protected int mSymbolStart; + protected int mSymbolEnd; + protected float mSamplesPerSymbol; + protected float mMidSymbolSamplingPoint; + protected float mSymbolTimingGain; + + /** + * Buffer to store complex sample data and produce interpolated samples. + * @param samplesPerSymbol + */ + public SampleBuffer(float samplesPerSymbol, float symbolTimingGain) + { + mSamplesPerSymbol = samplesPerSymbol; + mSymbolTimingGain = symbolTimingGain; + + mIntegerSamplesPerSymbol = (int)(samplesPerSymbol + 0.5f); + mHalfIntegerSamplesPerSymbol = (int)(samplesPerSymbol / 2.0f + 0.5f); + mMidSymbolSamplingPoint = samplesPerSymbol + mHalfIntegerSamplesPerSymbol; + mTwiceSamplesPerSymbol = (int)Math.floor(2.0 * samplesPerSymbol); + mDelayLine = new boolean[2 * mTwiceSamplesPerSymbol]; + } + + /** + * Stores the sample in the sample buffer to support making symbol decisions. + * + * Note: use the hasSymbol() method to determine if sufficient samples have been collected to make a symbol + * determination. + * + * @param sample to store in the sample buffer + */ + public void receive(boolean sample) + { + mMidSymbolSamplingPoint--; + + mDelayLine[mDelayLinePointer] = sample; + mDelayLine[mDelayLinePointer + mTwiceSamplesPerSymbol] = sample; + + //Increment pointer and keep pointer in bounds + mDelayLinePointer++; + mDelayLinePointer = mDelayLinePointer % mTwiceSamplesPerSymbol; + } + + /** + * Adjusts samples per symbol and symbol timing counters and increments the sample counter to collect another + * symbol. + * + * @param symbolTimingError from a symbol timing error detector + */ + public void resetAndAdjust(float symbolTimingError) + { + mMidSymbolSamplingPoint += (mSamplesPerSymbol + (symbolTimingError * mSymbolTimingGain)); + } + + public void setTimingGain(float gain) + { + mSymbolTimingGain = gain; + } + + /** + * Indicates if this buffer has accumulated enough samples to represent a full symbol + */ + public boolean hasSymbol() + { + return mMidSymbolSamplingPoint < 1.0f; + } + + /** + * Calculates the symbol as a majority decision where the number of positive/true samples are greater than half + * of the samples per symbol. Symbol decision is based on the samples collected over the preceeding 0.5 to 1.5 + * symbol periods. + * + * @return a true symbol if the majority of the samples were true or positive, otherwise a false symbol. + */ + public boolean getSymbol() + { + mPositiveSampleDecisionCount = 0; + + //Symbol samples are 0.5 and 1.5 symbols ahead of the current buffer pointer, but 0.5 to 1.5 symbol period + //before the most recently stored sample decision. + mSymbolStart = mDelayLinePointer + mHalfIntegerSamplesPerSymbol; + mSymbolEnd = mSymbolStart + mIntegerSamplesPerSymbol; + + for(int x = mSymbolStart; x < mSymbolEnd; x++) + { + if(mDelayLine[x]) + { + mPositiveSampleDecisionCount++; + } + } + + return mPositiveSampleDecisionCount > mHalfIntegerSamplesPerSymbol; + } +} diff --git a/src/main/java/io/github/dsheirer/dsp/fsk/SampleBufferInstrumented.java b/src/main/java/io/github/dsheirer/dsp/fsk/SampleBufferInstrumented.java new file mode 100644 index 00000000..255a15e7 --- /dev/null +++ b/src/main/java/io/github/dsheirer/dsp/fsk/SampleBufferInstrumented.java @@ -0,0 +1,98 @@ +/******************************************************************************* + * 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 + * + ******************************************************************************/ +package io.github.dsheirer.dsp.fsk; + +public class SampleBufferInstrumented extends SampleBuffer +{ + public boolean mLastSymbol; + + /** + * Buffer to store complex sample data and produce interpolated samples. + * + * @param samplesPerSymbol + * @param symbolTimingGain + */ + public SampleBufferInstrumented(float samplesPerSymbol, float symbolTimingGain) + { + super(samplesPerSymbol, symbolTimingGain); + } + + /** + * Sample delay buffer containing boolean (ie positive or negative) samples. + */ + public boolean[] getDelayLine() + { + return mDelayLine; + } + + /** + * Current delay buffer pointer which points to where the next sample will be stored and/or the oldest sample in + * the buffer. + */ + public int getDelayLinePointer() + { + return mDelayLinePointer; + } + + public int getDelayLineSecondPointer() + { + return mDelayLinePointer + mTwiceSamplesPerSymbol; + } + + /** + * Current symbol timing gain value + * @return + */ + public float getSymbolTimingGain() + { + return mSymbolTimingGain; + } + + /** + * Current mid-symbol sampling point + */ + public float getMidSymbolSamplingPoint() + { + return mMidSymbolSamplingPoint; + } + + public float getSamplesPerSymbol() + { + return mSamplesPerSymbol; + } + + @Override + public boolean getSymbol() + { + mLastSymbol = super.getSymbol(); + return mLastSymbol; + } + + public boolean getLastSymbol() + { + return mLastSymbol; + } + + public int getSymbolStart() + { + return mSymbolStart; + } + + public int getSymbolEnd() + { + return mSymbolEnd; + } +} diff --git a/src/main/java/io/github/dsheirer/dsp/fsk/SyncState.java b/src/main/java/io/github/dsheirer/dsp/fsk/SyncState.java new file mode 100644 index 00000000..57a3fddd --- /dev/null +++ b/src/main/java/io/github/dsheirer/dsp/fsk/SyncState.java @@ -0,0 +1,21 @@ +/******************************************************************************* + * 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 + * + ******************************************************************************/ +package io.github.dsheirer.dsp.fsk; + +public enum SyncState +{ + COARSE, MEDIUM, FINE; +} diff --git a/src/main/java/io/github/dsheirer/dsp/fsk/SynchronizationMonitor.java b/src/main/java/io/github/dsheirer/dsp/fsk/SynchronizationMonitor.java new file mode 100644 index 00000000..1608a3d6 --- /dev/null +++ b/src/main/java/io/github/dsheirer/dsp/fsk/SynchronizationMonitor.java @@ -0,0 +1,149 @@ +/******************************************************************************* + * 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 + * + ******************************************************************************/ +package io.github.dsheirer.dsp.fsk; + +import io.github.dsheirer.dsp.symbol.ISyncDetectListener; +import org.slf4j.Logger; +import org.slf4j.LoggerFactory; + +/** + * Monitors a symbol stream and externally provided sync detection events to track the sync state as the number of + * message synchronization events relative the count of symbols that have been decoded to provide synchronization state + * events to a registered listener. + * + * This might be used to provide a control signal to a symbol timing error detector to adjust timing error gain levels. + * + * This class only works with messages of uniform symbol length. + * + * Synchronization States: + * + * -COARSE - default state. demoted from MEDIUM after one message period with no sync detection. + * + * -MEDIUM - promoted from COARSE when one sync detection occurs after two message periods, or demoted from FINE + * after two message periods with no sync detections. + * + * -FINE - promoted from MEDIUM after two or more consecutive sync detections within the preceding two or more + * message periods. + */ +public class SynchronizationMonitor implements ISyncDetectListener +{ + private final static Logger mLog = LoggerFactory.getLogger(SynchronizationMonitor.class); + + private ISyncStateListener mSyncStateListener; + private SyncState mSyncState = SyncState.COARSE; + private int mSyncCount; + private int mSymbolCount = 0; + private int mMessageLength; + + /** + * Constructs the synchronization monitor for the specified message symbol length. + * @param messageLength in symbols. + */ + public SynchronizationMonitor(int messageLength) + { + mMessageLength = messageLength; + } + + /** + * Resets to the default state of COARSE + */ + public void reset() + { + setSyncState(SyncState.COARSE); + mSymbolCount = 0; + } + + /** + * Updates the sync state and broadcasts any state changes to the registered sync state listener. + */ + private void setSyncState(SyncState syncState) + { + if(mSyncState != syncState) + { + mSyncState = syncState; + + if(mSyncStateListener != null) + { + mSyncStateListener.setSyncState(mSyncState); + } + } + } + + /** + * Updates the message synchronization state based on the current number of sync detections. + */ + private void updateSyncState() + { + if(mSyncCount < 0) + { + mSyncCount = 0; + + //The previous state was also zero, so there are no changes + return; + } + + switch(mSyncCount) + { + case 0: + setSyncState(SyncState.COARSE); + break; + case 1: + setSyncState(SyncState.MEDIUM); + break; + default: + setSyncState(SyncState.FINE); + } + + //Max is 3 which means state will drop to MEDIUM after 2 elapsed message periods and COARSE after 3 elapsed + // message periods with no sync detection. + if(mSyncCount > 3) + { + mSyncCount = 3; + } + } + + /** + * Implements the ISyncDetectionListener interface to be notified when an external entity detects a message + * sync word in the symbol stream. + */ + @Override + public void syncDetected() + { + mSymbolCount = 0; + mSyncCount++; + updateSyncState(); + } + + /** + * Increments the symbol count for each decoded symbol. + */ + public void increment() + { + mSymbolCount++; + + if(mSymbolCount > mMessageLength) + { + mSymbolCount = 0; + mSyncCount--; + updateSyncState(); + } + } + + public void setListener(ISyncStateListener listener) + { + mSyncStateListener = listener; + } +} diff --git a/src/main/java/io/github/dsheirer/dsp/fsk/ZeroCrossingErrorDetector.java b/src/main/java/io/github/dsheirer/dsp/fsk/ZeroCrossingErrorDetector.java new file mode 100644 index 00000000..c93a09a1 --- /dev/null +++ b/src/main/java/io/github/dsheirer/dsp/fsk/ZeroCrossingErrorDetector.java @@ -0,0 +1,124 @@ +/******************************************************************************* + * 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 + * + ******************************************************************************/ +package io.github.dsheirer.dsp.fsk; + +/** + * Zero crossing error detector. Provides symbol timing error correction values by detecting zero-crossings within + * each symbol period. Ideally there should be only 0 or 1 zero crossings within each symbol period. However, when + * there is a slight DC offset in the demodulated samples, there can be up to 2 zero crossings within a symbol + * period. + * + * This detector will only provide error corrections when there are 1 or 2 zero crossings detected. If the number + * of zero crossings is greater than 2, then it is assumed that there is no signal present and the returned timing + * error value is zero. This detector provides an error correction value relative to (approximately) the middle of + * a symbol period and therefore continuously attempts to align zero crossings to the middle of the period. + * + * Since this detector does NOT use interpolation, the error signal will contain minor levels of symbol timing + * jitter. Error signal values are reported as integrals of the sampling period. + */ +public class ZeroCrossingErrorDetector +{ + protected boolean[] mBuffer; + protected float mZeroCrossingIdeal; + protected float mZeroCrossingOne; + protected float mZeroCrossingTwo; + protected float mDetectedZeroCrossing; + private int mZeroCrossingCount; + + /** + * Constructs the detector for the specified samples per symbol. + * + * @param samplesPerSymbol + */ + public ZeroCrossingErrorDetector(float samplesPerSymbol) + { + mBuffer = new boolean[(int)Math.ceil(samplesPerSymbol)]; + mZeroCrossingIdeal = mBuffer.length - 1.5f - (samplesPerSymbol / 2.0f); + } + + /** + * Stores the sample in the buffer. Use the getError() method once enough samples have been processed to determine + * the error for a symbol period. + * + * @param sample + */ + public void receive(boolean sample) + { + //Shift all samples left and store new sample at the end + System.arraycopy(mBuffer, 1, mBuffer, 0, mBuffer.length - 1); + mBuffer[mBuffer.length - 1] = sample; + } + + /** + * Determines the symbol timing error for the samples processed thus far. This method should be invoked once per + * symbol period, triggered via an external samples per symbol tracking process. + * + * @return symbol timing error calculated as the distance between the approximate symbol center and the next closest + * zero crossing index. An error value of zero is returned if the number of zero crossings is less than one or + * greater than two. + */ + public float getError() + { + mZeroCrossingCount = 0; + + for(int x = 0; x < mBuffer.length - 1; x++) + { + if(mBuffer[x] ^ mBuffer[x + 1]) + { + if(mZeroCrossingCount == 0) + { + mZeroCrossingCount++; + mZeroCrossingOne = x + 0.5f; + } + else if(mZeroCrossingCount == 1) + { + mZeroCrossingCount++; + mZeroCrossingTwo = x + 0.5f; + } + else + { + //Max zero crossings exceeded - no error can be calculated - abort early + return 0.0f; + } + } + } + + if(mZeroCrossingCount == 1) + { + mDetectedZeroCrossing = mZeroCrossingOne; + + return mZeroCrossingIdeal - mZeroCrossingOne; + } + else if(mZeroCrossingCount == 2) + { + float errorDistanceOne = mZeroCrossingIdeal - mZeroCrossingOne; + float errorDistanceTwo = mZeroCrossingCount - mZeroCrossingTwo; + + if(Math.abs(errorDistanceOne) < Math.abs(errorDistanceTwo)) + { + mDetectedZeroCrossing = mZeroCrossingOne; + } + else + { + mDetectedZeroCrossing = mZeroCrossingTwo; + } + + return mZeroCrossingIdeal - mDetectedZeroCrossing; + } + + return 0.0f; + } +} diff --git a/src/main/java/io/github/dsheirer/dsp/fsk/ZeroCrossingErrorDetectorInstrumented.java b/src/main/java/io/github/dsheirer/dsp/fsk/ZeroCrossingErrorDetectorInstrumented.java new file mode 100644 index 00000000..2e90ab01 --- /dev/null +++ b/src/main/java/io/github/dsheirer/dsp/fsk/ZeroCrossingErrorDetectorInstrumented.java @@ -0,0 +1,58 @@ +/******************************************************************************* + * 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 + * + ******************************************************************************/ +package io.github.dsheirer.dsp.fsk; + +import javafx.beans.property.SimpleFloatProperty; + +public class ZeroCrossingErrorDetectorInstrumented extends ZeroCrossingErrorDetector +{ + public SimpleFloatProperty timingError = new SimpleFloatProperty(); + + /** + * Constructs the detector for the specified samples per symbol. + * + * @param samplesPerSymbol + */ + public ZeroCrossingErrorDetectorInstrumented(float samplesPerSymbol) + { + super(samplesPerSymbol); + } + + @Override + public float getError() + { + float error = super.getError(); + + timingError.setValue(error); + + return error; + } + + public float getZeroCrossingIdeal() + { + return mZeroCrossingIdeal; + } + + public float getDetectedZeroCrossing() + { + return mDetectedZeroCrossing; + } + + public boolean[] getBuffer() + { + return mBuffer; + } +} diff --git a/src/main/java/io/github/dsheirer/dsp/symbol/ISyncDetectListener.java b/src/main/java/io/github/dsheirer/dsp/symbol/ISyncDetectListener.java new file mode 100644 index 00000000..5a47798a --- /dev/null +++ b/src/main/java/io/github/dsheirer/dsp/symbol/ISyncDetectListener.java @@ -0,0 +1,21 @@ +/******************************************************************************* + * 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 + * + ******************************************************************************/ +package io.github.dsheirer.dsp.symbol; + +public interface ISyncDetectListener +{ + void syncDetected(); +} diff --git a/src/main/java/io/github/dsheirer/dsp/symbol/SyncDetectListener.java b/src/main/java/io/github/dsheirer/dsp/symbol/SyncDetectListener.java deleted file mode 100644 index 942c1635..00000000 --- a/src/main/java/io/github/dsheirer/dsp/symbol/SyncDetectListener.java +++ /dev/null @@ -1,27 +0,0 @@ -/******************************************************************************* - * SDR Trunk - * Copyright (C) 2014 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 io.github.dsheirer.dsp.symbol; - -/** - * Interface to allow an external process indicate that it has detected a - * sync pattern in the bit stream - */ -public interface SyncDetectListener -{ - public void syncDetected(); -} diff --git a/src/main/java/io/github/dsheirer/dsp/symbol/SyncDetectProvider.java b/src/main/java/io/github/dsheirer/dsp/symbol/SyncDetectProvider.java index 69dc7043..df0ff937 100644 --- a/src/main/java/io/github/dsheirer/dsp/symbol/SyncDetectProvider.java +++ b/src/main/java/io/github/dsheirer/dsp/symbol/SyncDetectProvider.java @@ -20,5 +20,5 @@ package io.github.dsheirer.dsp.symbol; public interface SyncDetectProvider { - public void setSyncDetectListener( SyncDetectListener listener ); + public void setSyncDetectListener( ISyncDetectListener listener ); } diff --git a/src/main/java/io/github/dsheirer/instrument/gui/viewer/PlaybackController.java b/src/main/java/io/github/dsheirer/instrument/gui/viewer/PlaybackController.java index ee74737e..25bee580 100644 --- a/src/main/java/io/github/dsheirer/instrument/gui/viewer/PlaybackController.java +++ b/src/main/java/io/github/dsheirer/instrument/gui/viewer/PlaybackController.java @@ -15,13 +15,14 @@ ******************************************************************************/ package io.github.dsheirer.instrument.gui.viewer; -import io.github.dsheirer.instrument.gui.viewer.decoder.DecoderPane; +import io.github.dsheirer.instrument.gui.viewer.decoder.AbstractDecoderPane; import io.github.dsheirer.sample.Listener; +import io.github.dsheirer.sample.buffer.ReusableBuffer; import io.github.dsheirer.sample.buffer.ReusableBufferBroadcaster; -import io.github.dsheirer.sample.buffer.ReusableComplexBuffer; import io.github.dsheirer.source.IControllableFileSource; import io.github.dsheirer.source.IFrameLocationListener; import io.github.dsheirer.source.wave.ComplexWaveSource; +import io.github.dsheirer.source.wave.RealWaveSource; import javafx.event.ActionEvent; import javafx.event.EventHandler; import javafx.geometry.Pos; @@ -40,7 +41,7 @@ import javax.sound.sampled.UnsupportedAudioFileException; import java.io.File; import java.io.IOException; -public class PlaybackController extends HBox implements IFrameLocationListener, Listener +public class PlaybackController extends HBox implements IFrameLocationListener { private final static Logger mLog = LoggerFactory.getLogger(PlaybackController.class); @@ -48,15 +49,17 @@ public class PlaybackController extends HBox implements IFrameLocationListener, private TextField mPlaybackPositionText; private Button mPlay1Button; private Button mPlay10Button; + private Button mPlay30Button; private Button mPlay100Button; private Button mPlay1000Button; + private Button mPlay2000Button; private HBox mControlsBox; private Label mFileLabel; private EventHandler mPlayEventHandler; - private DecoderPane mSampleRateListener; + private AbstractDecoderPane mSampleRateListener; private IControllableFileSource mControllableFileSource; - private ReusableBufferBroadcaster mComplexBufferBroadcaster = new ReusableBufferBroadcaster(); + private ReusableBufferBroadcaster mReusableBufferBroadcaster = new ReusableBufferBroadcaster(); public PlaybackController() { @@ -64,10 +67,10 @@ public class PlaybackController extends HBox implements IFrameLocationListener, getChildren().addAll(getControlsBox(), getFileLabel()); - setControlsEnabled(false); + disableControls(); } - public void setSampleRateListener(DecoderPane decoderPane) + public void setSampleRateListener(AbstractDecoderPane decoderPane) { mSampleRateListener = decoderPane; @@ -81,38 +84,76 @@ public class PlaybackController extends HBox implements IFrameLocationListener, { if(file != null && file.isFile()) { - try + if(ComplexWaveSource.supports(file)) { - mControllableFileSource = new ComplexWaveSource(file); - mControllableFileSource.setListener(this); - ((ComplexWaveSource)mControllableFileSource).setListener((Listener)this); - mControllableFileSource.open(); - - if(mSampleRateListener != null) + try { - mSampleRateListener.setSampleRate(mControllableFileSource.getSampleRate()); + mControllableFileSource = new ComplexWaveSource(file); + mControllableFileSource.setListener(this); + ((ComplexWaveSource)mControllableFileSource).setListener(mReusableBufferBroadcaster); + mControllableFileSource.open(); + + if(mSampleRateListener != null) + { + mSampleRateListener.setSampleRate(mControllableFileSource.getSampleRate()); + } } - } - catch(UnsupportedAudioFileException e) - { - mLog.error("Unsupported Audio File Type [" + (file != null ? file.getAbsolutePath() : "null") + "]"); + catch(UnsupportedAudioFileException e) + { + mLog.error("Unsupported Audio File Type [" + (file != null ? file.getAbsolutePath() : "null") + "]"); - Alert alert = new Alert(Alert.AlertType.ERROR, "The file type is unsupported.", ButtonType.OK); - alert.show(); - return; - } - catch(IOException ioe) - { - mLog.error("Error opening file [" + (file != null ? file.getAbsolutePath() : "null") + "]", ioe); + Alert alert = new Alert(Alert.AlertType.ERROR, "The file type is unsupported.", ButtonType.OK); + alert.show(); + return; + } + catch(IOException ioe) + { + mLog.error("Error opening file [" + (file != null ? file.getAbsolutePath() : "null") + "]", ioe); - Alert alert = new Alert(Alert.AlertType.ERROR, "There was an error opening the file", - ButtonType.OK); - alert.show(); - return; + Alert alert = new Alert(Alert.AlertType.ERROR, "There was an error opening the file", + ButtonType.OK); + alert.show(); + return; + } + + enableControls(); + } + else if(RealWaveSource.supports(file)) + { + try + { + mControllableFileSource = new RealWaveSource(file); + mControllableFileSource.setListener(this); + ((RealWaveSource)mControllableFileSource).setListener(mReusableBufferBroadcaster); + mControllableFileSource.open(); + + if(mSampleRateListener != null) + { + mSampleRateListener.setSampleRate(mControllableFileSource.getSampleRate()); + } + } + catch(UnsupportedAudioFileException e) + { + mLog.error("Unsupported Audio File Type [" + (file != null ? file.getAbsolutePath() : "null") + "]"); + + Alert alert = new Alert(Alert.AlertType.ERROR, "The file type is unsupported.", ButtonType.OK); + alert.show(); + return; + } + catch(IOException ioe) + { + mLog.error("Error opening file [" + (file != null ? file.getAbsolutePath() : "null") + "]", ioe); + + Alert alert = new Alert(Alert.AlertType.ERROR, "There was an error opening the file", + ButtonType.OK); + alert.show(); + return; + } + + enableControls(); } getFileLabel().setText(file.getName()); - setControlsEnabled(true); } } @@ -130,36 +171,49 @@ public class PlaybackController extends HBox implements IFrameLocationListener, } } - setControlsEnabled(false); + disableControls(); } /** * Adds listener to receive complex buffers from this playback */ - public void addListener(Listener listener) + public void addListener(Listener listener) { - mComplexBufferBroadcaster.addListener(listener); + mReusableBufferBroadcaster.addListener(listener); } /** * Removes the listener from receiving complex buffers from this playback */ - public void removeListener(Listener listener) + public void removeListener(Listener listener) { - mComplexBufferBroadcaster.removeListener(listener); + mReusableBufferBroadcaster.removeListener(listener); } /** * Sets all of the playback controls to the specified enabled state. */ - private void setControlsEnabled(boolean enabled) + private void disableControls() { - getRewindButton().setDisable(!enabled); - getPlaybackPositionText().setDisable(!enabled); - getPlay1Button().setDisable(!enabled); - getPlay10Button().setDisable(!enabled); - getPlay100Button().setDisable(!enabled); - getPlay1000Button().setDisable(!enabled); + getRewindButton().setDisable(true); + getPlaybackPositionText().setDisable(true); + getPlay1Button().setDisable(true); + getPlay10Button().setDisable(true); + getPlay100Button().setDisable(true); + getPlay1000Button().setDisable(true); + } + + /** + * Sets all of the playback controls to the specified enabled state. + */ + private void enableControls() + { + getRewindButton().setDisable(false); + getPlaybackPositionText().setDisable(false); + getPlay1Button().setDisable(false); + getPlay10Button().setDisable(false); + getPlay100Button().setDisable(false); + getPlay1000Button().setDisable(false); } private HBox getControlsBox() @@ -169,7 +223,7 @@ public class PlaybackController extends HBox implements IFrameLocationListener, mControlsBox = new HBox(); mControlsBox.getChildren().addAll(getRewindButton(), getPlaybackPositionText(), getPlay1Button(), - getPlay10Button(), getPlay100Button(), getPlay1000Button()); + getPlay10Button(), getPlay30Button(), getPlay100Button(), getPlay1000Button(), getPlay2000Button()); } return mControlsBox; @@ -213,6 +267,19 @@ public class PlaybackController extends HBox implements IFrameLocationListener, return mPlay10Button; } + private Button getPlay30Button() + { + if(mPlay30Button == null) + { + mPlay30Button = new Button("30"); + mPlay30Button.setUserData(30); + mPlay30Button.setOnAction(getPlaybackEventHandler()); + mPlay30Button.setGraphic(new FontIcon(FontAwesome.FAST_FORWARD)); + } + + return mPlay30Button; + } + private Button getPlay100Button() { if(mPlay100Button == null) @@ -239,6 +306,19 @@ public class PlaybackController extends HBox implements IFrameLocationListener, return mPlay1000Button; } + private Button getPlay2000Button() + { + if(mPlay2000Button == null) + { + mPlay2000Button = new Button("2000"); + mPlay2000Button.setUserData(2000); + mPlay2000Button.setOnAction(getPlaybackEventHandler()); + mPlay2000Button.setGraphic(new FontIcon(FontAwesome.FAST_FORWARD)); + } + + return mPlay2000Button; + } + private EventHandler getPlaybackEventHandler() { if(mPlayEventHandler == null) @@ -302,10 +382,4 @@ public class PlaybackController extends HBox implements IFrameLocationListener, { mLog.info("Frame location reset ... ignoring?"); } - - @Override - public void receive(ReusableComplexBuffer complexBuffer) - { - mComplexBufferBroadcaster.broadcast(complexBuffer); - } } diff --git a/src/main/java/io/github/dsheirer/instrument/gui/viewer/ViewerDesktop.java b/src/main/java/io/github/dsheirer/instrument/gui/viewer/ViewerDesktop.java index fcdef3f9..ece3faa5 100644 --- a/src/main/java/io/github/dsheirer/instrument/gui/viewer/ViewerDesktop.java +++ b/src/main/java/io/github/dsheirer/instrument/gui/viewer/ViewerDesktop.java @@ -15,7 +15,7 @@ ******************************************************************************/ package io.github.dsheirer.instrument.gui.viewer; -import io.github.dsheirer.instrument.gui.viewer.decoder.DecoderPane; +import io.github.dsheirer.instrument.gui.viewer.decoder.AbstractDecoderPane; import io.github.dsheirer.instrument.gui.viewer.decoder.DecoderPaneFactory; import io.github.dsheirer.module.decode.DecoderType; import io.github.dsheirer.module.decode.p25.P25Decoder; @@ -26,7 +26,7 @@ import java.io.File; public class ViewerDesktop extends BorderPane { private PlaybackController mPlaybackController; - private DecoderPane mDecoderPane = DecoderPaneFactory.getDefaultPane(); + private AbstractDecoderPane mDecoderPane = DecoderPaneFactory.getDefaultPane(); public ViewerDesktop() { @@ -50,7 +50,7 @@ public class ViewerDesktop extends BorderPane setDecoderPane(DecoderPaneFactory.getP25DecoderPane(modulation)); } - private void setDecoderPane(DecoderPane decoderPane) + private void setDecoderPane(AbstractDecoderPane decoderPane) { getPlaybackController().removeListener(getDecoderPane()); getChildren().remove(getDecoderPane()); @@ -63,7 +63,7 @@ public class ViewerDesktop extends BorderPane /** * Decoder pane */ - public DecoderPane getDecoderPane() + public AbstractDecoderPane getDecoderPane() { if(mDecoderPane == null) { diff --git a/src/main/java/io/github/dsheirer/instrument/gui/viewer/chart/RealSampleLineChart.java b/src/main/java/io/github/dsheirer/instrument/gui/viewer/chart/RealSampleLineChart.java new file mode 100644 index 00000000..94f02060 --- /dev/null +++ b/src/main/java/io/github/dsheirer/instrument/gui/viewer/chart/RealSampleLineChart.java @@ -0,0 +1,85 @@ +/******************************************************************************* + * 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 + * + ******************************************************************************/ +package io.github.dsheirer.instrument.gui.viewer.chart; + +import io.github.dsheirer.buffer.RealCircularBuffer; +import io.github.dsheirer.sample.Listener; +import io.github.dsheirer.sample.buffer.ReusableBuffer; +import javafx.collections.FXCollections; +import javafx.collections.ObservableList; +import javafx.scene.chart.LineChart; +import javafx.scene.chart.NumberAxis; +import javafx.scene.chart.XYChart; +import javafx.scene.layout.StackPane; +import org.slf4j.Logger; +import org.slf4j.LoggerFactory; + +public class RealSampleLineChart extends LineChart implements Listener +{ + private final static Logger mLog = LoggerFactory.getLogger(RealSampleLineChart.class); + private ObservableList> mSamples = FXCollections.observableArrayList(); + private Series mSampleSeries = new Series<>("Samples", mSamples); + private RealCircularBuffer mRealCircularBuffer; + + public RealSampleLineChart(int length, double tickUnit) + { + super(new NumberAxis("Samples", 0, length, tickUnit), + new NumberAxis("Value", -1.0, 1.0, 0.25)); + + ObservableList> observableList = FXCollections.observableArrayList(mSampleSeries); + + setData(observableList); + + init(length); + } + + private void init(int length) + { + mRealCircularBuffer = new RealCircularBuffer(length); + + for(int x = 0; x < length; x++) + { + Data sample = new Data<>(x, 0.0f); + mSamples.add(sample); + } + + //Turn off the data point marker symbols + for(XYChart.Data data: mSampleSeries.getData()) + { + StackPane stackPane = (StackPane)data.getNode(); + stackPane.setVisible(false); + } + } + + @Override + public void receive(ReusableBuffer buffer) + { + for(float sample: buffer.getSamples()) + { + mRealCircularBuffer.put(sample); + } + + float[] bufferSamples = mRealCircularBuffer.get(); + + for(int x = 0; x < bufferSamples.length; x++) + { + Data sample = mSamples.get(x); + sample.setYValue(bufferSamples[x]); + } + + buffer.decrementUserCount(); + } +} diff --git a/src/main/java/io/github/dsheirer/instrument/gui/viewer/chart/SampleBufferChart.java b/src/main/java/io/github/dsheirer/instrument/gui/viewer/chart/SampleBufferChart.java new file mode 100644 index 00000000..799776e9 --- /dev/null +++ b/src/main/java/io/github/dsheirer/instrument/gui/viewer/chart/SampleBufferChart.java @@ -0,0 +1,171 @@ +/******************************************************************************* + * 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 + * + ******************************************************************************/ +package io.github.dsheirer.instrument.gui.viewer.chart; + +import io.github.dsheirer.module.decode.ltrnet.LTRNetDecoderInstrumented; +import javafx.beans.value.ChangeListener; +import javafx.beans.value.ObservableValue; +import javafx.collections.FXCollections; +import javafx.collections.ObservableList; +import javafx.scene.chart.LineChart; +import javafx.scene.chart.NumberAxis; +import javafx.scene.layout.StackPane; +import org.slf4j.Logger; +import org.slf4j.LoggerFactory; + +public class SampleBufferChart extends LineChart +{ + private final static Logger mLog = LoggerFactory.getLogger(SampleBufferChart.class); + private ObservableList> mSamples = FXCollections.observableArrayList(); + private Series mSampleSeries = new Series<>("Samples", mSamples); + + private ObservableList> mSamplePoints = FXCollections.observableArrayList(); + private Series mSamplePointSeries = new Series<>("Pointers", mSamplePoints); + + private ObservableList> mSymbolSamples = FXCollections.observableArrayList(); + private Series mSymbolSamplesSeries = new Series<>("Next Symbol Samples", mSymbolSamples); + + private ObservableList> mSymbol = FXCollections.observableArrayList(); + private Series mSymbolSeries = new Series<>("Previous Symbol", mSymbol); + + private ObservableList> mZeroCrossing = FXCollections.observableArrayList(); + private Series mZeroCrossingSeries = new Series<>("Zero Crossing", mZeroCrossing); + + private LTRNetDecoderInstrumented mLTRNetDecoderInstrumented; + + public SampleBufferChart(LTRNetDecoderInstrumented decoder, int length) + { + super(new NumberAxis("Samples", 0, length, 5), + new NumberAxis("Value", -1.0, 1.0, 0.25)); + + ObservableList observableList = FXCollections.observableArrayList(mSampleSeries, mSamplePointSeries, + mSymbolSamplesSeries, mSymbolSeries, mZeroCrossingSeries); + + setData(observableList); + + mLTRNetDecoderInstrumented = decoder; + decoder.bufferCount.addListener(new BufferChangeListener()); + decoder.getLTRDecoder().getErrorDetector().timingError.addListener(new ErrorChangeListener()); + } + + public void setBuffer(boolean[] buffer) + { + while(mSamples.size() < buffer.length) + { + Data sample = new Data<>(mSamples.size(), 0.0f); + mSamples.add(sample); + + //Turn off the data point marker symbols - yes I know this is inefficient + for(Data data: mSampleSeries.getData()) + { + StackPane stackPane = (StackPane)data.getNode(); + stackPane.setVisible(false); + } + } + + for(int x = 0; x < buffer.length; x++) + { + Data sample = mSamples.get(x); + + sample.setYValue(buffer[x] ? 0.5f : -0.5f); + } + } + + public class BufferChangeListener implements ChangeListener + { + @Override + public void changed(ObservableValue observable, Object oldValue, Object newValue) + { + setBuffer(mLTRNetDecoderInstrumented.getLTRDecoder().getSampleBuffer().getDelayLine()); + + while(mSymbol.size() < 2) + { + Data sample = new Data<>(0.0, -0.55); + mSymbol.add(sample); + } + + while(mZeroCrossing.size() < 2) + { + Data sample = new Data<>(0.0, -0.65); + mZeroCrossing.add(sample); + } + + while(mSamplePoints.size() < 2) + { + Data sample = new Data<>(0.0, -0.75); + mSamplePoints.add(sample); + } + + while(mSymbolSamples.size() < 2) + { + Data sample = new Data<>(0.0, -0.85); + mSymbolSamples.add(sample); + } + + int pointer1 = mLTRNetDecoderInstrumented.getLTRDecoder().getSampleBuffer().getDelayLinePointer(); + int pointer2 = mLTRNetDecoderInstrumented.getLTRDecoder().getSampleBuffer().getDelayLineSecondPointer(); + + mSamplePoints.get(0).setXValue(pointer1); + mSamplePoints.get(1).setXValue(pointer2); + + float samplesRemaining = mLTRNetDecoderInstrumented.getLTRDecoder().getSampleBuffer().getMidSymbolSamplingPoint(); + float samplesPerSymbol = mLTRNetDecoderInstrumented.getLTRDecoder().getSampleBuffer().getSamplesPerSymbol(); + + float start = pointer1 + samplesRemaining; + float end = start + samplesPerSymbol; + + mSymbolSamples.get(0).setXValue(start); + mSymbolSamples.get(1).setXValue(end); + + boolean symbol = mLTRNetDecoderInstrumented.getLTRDecoder().getSampleBuffer().getLastSymbol(); + int symbolStart = mLTRNetDecoderInstrumented.getLTRDecoder().getSampleBuffer().getSymbolStart(); + int symbolEnd = mLTRNetDecoderInstrumented.getLTRDecoder().getSampleBuffer().getSymbolEnd(); + + mSymbol.get(0).setXValue(symbolStart); + mSymbol.get(0).setYValue(symbol ? 0.55f : -0.55f); + mSymbol.get(1).setXValue(symbolEnd); + mSymbol.get(1).setYValue(symbol ? 0.55f : -0.55f); + } + } + + public class ErrorChangeListener implements ChangeListener + { + @Override + public void changed(ObservableValue observable, Object oldValue, Object newValue) + { + while(mZeroCrossing.size() < 2) + { + Data sample = new Data<>(0.0, -0.65); + mZeroCrossing.add(sample); + } + + int pointer1 = mLTRNetDecoderInstrumented.getLTRDecoder().getSampleBuffer().getDelayLinePointer(); + int pointer2 = mLTRNetDecoderInstrumented.getLTRDecoder().getSampleBuffer().getDelayLineSecondPointer(); + + float zeroCrossingIdeal = mLTRNetDecoderInstrumented.getLTRDecoder().getErrorDetector().getZeroCrossingIdeal(); + float detectedZeroCrossing = zeroCrossingIdeal - ((Number)newValue).floatValue(); + + int reference = (zeroCrossingIdeal < pointer1 || detectedZeroCrossing < pointer1 ? pointer2 : pointer1); + reference--; + + float start = reference - zeroCrossingIdeal; + float end = reference - detectedZeroCrossing; + + mZeroCrossing.get(0).setXValue(start); + mZeroCrossing.get(1).setXValue(end); + } + } +} diff --git a/src/main/java/io/github/dsheirer/instrument/gui/viewer/chart/ZeroCrossingErrorDetectorChart.java b/src/main/java/io/github/dsheirer/instrument/gui/viewer/chart/ZeroCrossingErrorDetectorChart.java new file mode 100644 index 00000000..2d9b055b --- /dev/null +++ b/src/main/java/io/github/dsheirer/instrument/gui/viewer/chart/ZeroCrossingErrorDetectorChart.java @@ -0,0 +1,132 @@ +/******************************************************************************* + * 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 + * + ******************************************************************************/ +package io.github.dsheirer.instrument.gui.viewer.chart; + +import io.github.dsheirer.module.decode.ltrnet.LTRNetDecoderInstrumented; +import javafx.beans.value.ChangeListener; +import javafx.beans.value.ObservableValue; +import javafx.collections.FXCollections; +import javafx.collections.ObservableList; +import javafx.scene.chart.LineChart; +import javafx.scene.chart.NumberAxis; +import org.slf4j.Logger; +import org.slf4j.LoggerFactory; + +public class ZeroCrossingErrorDetectorChart extends LineChart +{ + private final static Logger mLog = LoggerFactory.getLogger(ZeroCrossingErrorDetectorChart.class); + + private ObservableList> mCurrentSamples = FXCollections.observableArrayList(); + private Series mCurrentSampleSeries = new Series<>("Samples", mCurrentSamples); + + private ObservableList> mPreviousSamples = FXCollections.observableArrayList(); + private Series mPreviousSampleSeries = new Series<>("Samples", mPreviousSamples); + + private ObservableList> mIdeal = FXCollections.observableArrayList(); + private Series mIdealSeries = new Series<>("Ideal", mIdeal); + + private ObservableList> mDetected = FXCollections.observableArrayList(); + private Series mDetectedSeries = new Series<>("Detected", mDetected); + + private LTRNetDecoderInstrumented mLTRNetDecoderInstrumented; + + public ZeroCrossingErrorDetectorChart(LTRNetDecoderInstrumented decoder, int length) + { + super(new NumberAxis("Samples", 0, length, 5), + new NumberAxis("Value", -1.0, 1.0, 0.25)); + + ObservableList observableList = FXCollections.observableArrayList(mCurrentSampleSeries, + mPreviousSampleSeries, mIdealSeries, mDetectedSeries); + setData(observableList); + + mLTRNetDecoderInstrumented = decoder; + decoder.getLTRDecoder().getErrorDetector().timingError.addListener(new ErrorChangeListener()); + decoder.bufferCount.addListener(new ChangeListener() + { + @Override + public void changed(ObservableValue observable, Object oldValue, Object newValue) + { + updateBuffer(); + } + }); + + for(int x = 0; x < length; x++) + { + Data sample = new Data<>(x, 0.0f); + mCurrentSamples.add(sample); + Data sample2 = new Data<>(x, 0.0f); + mPreviousSamples.add(sample2); + } + + Data ideal1 = new Data<>(5, -0.1f); + mIdeal.add(ideal1); + Data ideal2 = new Data<>(5, -0.9f); + mIdeal.add(ideal2); + + Data detected1 = new Data<>(5, -0.25f); + mDetected.add(detected1); + Data detected2 = new Data<>(5, -0.75f); + mDetected.add(detected2); + } + + private void updateBuffer() + { + boolean[] samples = mLTRNetDecoderInstrumented.getLTRDecoder().getErrorDetector().getBuffer(); + + for(int x = 0; x < samples.length; x++) + { + Data sample = mCurrentSamples.get(x); + sample.setYValue(samples[x] ? 0.7f : 0.3f); + } + } + + public class ErrorChangeListener implements ChangeListener + { + @Override + public void changed(ObservableValue observable, Object oldValue, Object newValue) + { + float error = mLTRNetDecoderInstrumented.getLTRDecoder().getErrorDetector().getError(); + + boolean[] samples = mLTRNetDecoderInstrumented.getLTRDecoder().getErrorDetector().getBuffer(); + + for(int x = 0; x < samples.length; x++) + { + Data sample = mPreviousSamples.get(x); + sample.setYValue(samples[x] ? -0.4f : -0.6f); + } + + Data ideal1 = mIdeal.get(0); + ideal1.setXValue(mLTRNetDecoderInstrumented.getLTRDecoder().getErrorDetector().getZeroCrossingIdeal()); + Data ideal2 = mIdeal.get(1); + ideal2.setXValue(mLTRNetDecoderInstrumented.getLTRDecoder().getErrorDetector().getZeroCrossingIdeal()); + + if(error == 0.0f) + { + Data detected1 = mDetected.get(0); + detected1.setXValue(0.0f); + Data detected2 = mDetected.get(1); + detected2.setXValue(0.0f); + } + else + { + Data detected1 = mDetected.get(0); + detected1.setXValue(mLTRNetDecoderInstrumented.getLTRDecoder().getErrorDetector().getDetectedZeroCrossing()); + Data detected2 = mDetected.get(1); + detected2.setXValue(mLTRNetDecoderInstrumented.getLTRDecoder().getErrorDetector().getDetectedZeroCrossing()); + } + } + } +} diff --git a/src/main/java/io/github/dsheirer/instrument/gui/viewer/decoder/AbstractDecoderPane.java b/src/main/java/io/github/dsheirer/instrument/gui/viewer/decoder/AbstractDecoderPane.java new file mode 100644 index 00000000..1535ec43 --- /dev/null +++ b/src/main/java/io/github/dsheirer/instrument/gui/viewer/decoder/AbstractDecoderPane.java @@ -0,0 +1,30 @@ +/******************************************************************************* + * 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 + * + ******************************************************************************/ +package io.github.dsheirer.instrument.gui.viewer.decoder; + +import io.github.dsheirer.sample.Listener; +import io.github.dsheirer.sample.SampleType; +import io.github.dsheirer.sample.buffer.ReusableBuffer; +import javafx.scene.layout.VBox; + +public abstract class AbstractDecoderPane extends VBox implements Listener +{ + abstract SampleType getSampleType(); + + public void setSampleRate(double sampleRate) + { + } +} diff --git a/src/main/java/io/github/dsheirer/instrument/gui/viewer/decoder/DecoderPane.java b/src/main/java/io/github/dsheirer/instrument/gui/viewer/decoder/ComplexDecoderPane.java similarity index 87% rename from src/main/java/io/github/dsheirer/instrument/gui/viewer/decoder/DecoderPane.java rename to src/main/java/io/github/dsheirer/instrument/gui/viewer/decoder/ComplexDecoderPane.java index 82d3ae86..dc67f4e3 100644 --- a/src/main/java/io/github/dsheirer/instrument/gui/viewer/decoder/DecoderPane.java +++ b/src/main/java/io/github/dsheirer/instrument/gui/viewer/decoder/ComplexDecoderPane.java @@ -17,17 +17,17 @@ package io.github.dsheirer.instrument.gui.viewer.decoder; import io.github.dsheirer.module.decode.DecoderType; import io.github.dsheirer.sample.Listener; +import io.github.dsheirer.sample.SampleType; import io.github.dsheirer.sample.buffer.ReusableBufferBroadcaster; import io.github.dsheirer.sample.buffer.ReusableComplexBuffer; -import javafx.scene.layout.VBox; import javafx.scene.text.Text; -public class DecoderPane extends VBox implements Listener +public class ComplexDecoderPane extends AbstractDecoderPane { private ReusableBufferBroadcaster mComplexBufferBroadcaster = new ReusableBufferBroadcaster(); private DecoderType mDecoderType; - public DecoderPane(DecoderType decoderType) + public ComplexDecoderPane(DecoderType decoderType) { mDecoderType = decoderType; } @@ -35,11 +35,17 @@ public class DecoderPane extends VBox implements Listener /** * Package private - used by factory class to create an empty pane */ - DecoderPane() + ComplexDecoderPane() { getChildren().add(new Text("Please select a decoder from the menu")); } + @Override + SampleType getSampleType() + { + return SampleType.COMPLEX; + } + @Override public void receive(ReusableComplexBuffer reusableComplexBuffer) { @@ -55,8 +61,4 @@ public class DecoderPane extends VBox implements Listener { mComplexBufferBroadcaster.removeListener(listener); } - - public void setSampleRate(double sampleRate) - { - } } diff --git a/src/main/java/io/github/dsheirer/instrument/gui/viewer/decoder/DecoderPaneFactory.java b/src/main/java/io/github/dsheirer/instrument/gui/viewer/decoder/DecoderPaneFactory.java index 03590922..03e61517 100644 --- a/src/main/java/io/github/dsheirer/instrument/gui/viewer/decoder/DecoderPaneFactory.java +++ b/src/main/java/io/github/dsheirer/instrument/gui/viewer/decoder/DecoderPaneFactory.java @@ -22,15 +22,19 @@ import java.util.EnumSet; public class DecoderPaneFactory { - private static final EnumSet SUPPORTED_DECODER_TYPES = EnumSet.of(DecoderType.P25_PHASE1); + private static final EnumSet SUPPORTED_DECODER_TYPES = + EnumSet.of(DecoderType.P25_PHASE1, + DecoderType.LTR_NET); /** * Creates a decoder pane for the decoder type */ - public static DecoderPane getDecoderPane(DecoderType decoderType) + public static AbstractDecoderPane getDecoderPane(DecoderType decoderType) { switch(decoderType) { + case LTR_NET: + return new LTRNetPane(); case P25_PHASE1: throw new IllegalArgumentException("Use the getP25DecoderPane() method for P25 decoder type"); } @@ -41,7 +45,7 @@ public class DecoderPaneFactory /** * Creates a decoder pane for the P25 decoder type */ - public static DecoderPane getP25DecoderPane(P25Decoder.Modulation modulation) + public static AbstractDecoderPane getP25DecoderPane(P25Decoder.Modulation modulation) { switch(modulation) { @@ -57,9 +61,9 @@ public class DecoderPaneFactory /** * Creates an empty decoder pane */ - public static DecoderPane getDefaultPane() + public static AbstractDecoderPane getDefaultPane() { - return new DecoderPane(); + return new ComplexDecoderPane(); } /** diff --git a/src/main/java/io/github/dsheirer/instrument/gui/viewer/decoder/LTRNetPane.java b/src/main/java/io/github/dsheirer/instrument/gui/viewer/decoder/LTRNetPane.java new file mode 100644 index 00000000..1cc29446 --- /dev/null +++ b/src/main/java/io/github/dsheirer/instrument/gui/viewer/decoder/LTRNetPane.java @@ -0,0 +1,151 @@ +/******************************************************************************* + * 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 + * + ******************************************************************************/ +package io.github.dsheirer.instrument.gui.viewer.decoder; + +import io.github.dsheirer.instrument.gui.viewer.chart.RealSampleLineChart; +import io.github.dsheirer.instrument.gui.viewer.chart.SampleBufferChart; +import io.github.dsheirer.instrument.gui.viewer.chart.ZeroCrossingErrorDetectorChart; +import io.github.dsheirer.message.Message; +import io.github.dsheirer.message.MessageDirection; +import io.github.dsheirer.module.decode.DecoderType; +import io.github.dsheirer.module.decode.ltrnet.DecodeConfigLTRNet; +import io.github.dsheirer.module.decode.ltrnet.LTRNetDecoderInstrumented; +import io.github.dsheirer.sample.Listener; +import javafx.scene.layout.HBox; +import javafx.scene.layout.Priority; +import javafx.scene.layout.VBox; +import org.slf4j.Logger; +import org.slf4j.LoggerFactory; + +public class LTRNetPane extends RealDecoderPane +{ + private final static Logger mLog = LoggerFactory.getLogger(LTRNetPane.class); + + private HBox mSampleChartBox; + private RealSampleLineChart mRealSampleLineChart; + private HBox mSymbolDecoderBox; + private SampleBufferChart mSampleBufferChart; + private ZeroCrossingErrorDetectorChart mZeroCrossingErrorDetectorChart; + + private LTRNetDecoderInstrumented mDecoder; + + public LTRNetPane() + { + super(DecoderType.LTR_NET); + init(); + } + + private void init() + { + addListener(getDecoder()); + addListener(getSampleLineChart()); + + HBox.setHgrow(getSampleChartBox(), Priority.ALWAYS); + HBox.setHgrow(getSymbolDecoderBox(), Priority.ALWAYS); + + VBox.setVgrow(getSampleChartBox(), Priority.ALWAYS); + VBox.setVgrow(getSymbolDecoderBox(), Priority.ALWAYS); + + getSampleChartBox().setMaxWidth(Double.MAX_VALUE); + getSymbolDecoderBox().setMaxWidth(Double.MAX_VALUE); + + getChildren().addAll(getSampleChartBox(), getSymbolDecoderBox()); + + } + + private LTRNetDecoderInstrumented getDecoder() + { + if(mDecoder == null) + { + DecodeConfigLTRNet config = new DecodeConfigLTRNet(); + config.setMessageDirection(MessageDirection.OSW); + mDecoder = new LTRNetDecoderInstrumented(config, null); + mDecoder.setMessageListener(new Listener() + { + @Override + public void receive(Message message) + { + mLog.debug(message.getMessage()); + } + }); + } + + return mDecoder; + } + + private HBox getSampleChartBox() + { + if(mSampleChartBox == null) + { + mSampleChartBox = new HBox(); + mSampleChartBox.setMaxHeight(Double.MAX_VALUE); + + getSampleLineChart().setMaxWidth(Double.MAX_VALUE); + HBox.setHgrow(getSampleLineChart(), Priority.ALWAYS); + + mSampleChartBox.getChildren().addAll(getSampleLineChart()); + } + + return mSampleChartBox; + } + + private RealSampleLineChart getSampleLineChart() + { + if(mRealSampleLineChart == null) + { + mRealSampleLineChart = new RealSampleLineChart(500, (8000.0 / 300.0)); + } + + return mRealSampleLineChart; + } + + private HBox getSymbolDecoderBox() + { + if(mSymbolDecoderBox == null) + { + mSymbolDecoderBox = new HBox(); + mSymbolDecoderBox.setMaxWidth(Double.MAX_VALUE); + + getSampleBufferChart().setMaxWidth(Double.MAX_VALUE); + HBox.setHgrow(getSampleBufferChart(), Priority.ALWAYS); + + mSampleChartBox.getChildren().addAll(getSampleBufferChart(), getZeroCrossingErrorDetectorChart()); + } + + return mSymbolDecoderBox; + } + + private SampleBufferChart getSampleBufferChart() + { + if(mSampleBufferChart == null) + { + mSampleBufferChart = new SampleBufferChart(getDecoder(), 106); + } + + return mSampleBufferChart; + } + + private ZeroCrossingErrorDetectorChart getZeroCrossingErrorDetectorChart() + { + if(mZeroCrossingErrorDetectorChart == null) + { + mZeroCrossingErrorDetectorChart = new ZeroCrossingErrorDetectorChart(getDecoder(), + getDecoder().getLTRDecoder().getErrorDetector().getBuffer().length); + } + + return mZeroCrossingErrorDetectorChart; + } +} diff --git a/src/main/java/io/github/dsheirer/instrument/gui/viewer/decoder/P25Phase1C4FMPane.java b/src/main/java/io/github/dsheirer/instrument/gui/viewer/decoder/P25Phase1C4FMPane.java index 56a3cff0..ec35b0de 100644 --- a/src/main/java/io/github/dsheirer/instrument/gui/viewer/decoder/P25Phase1C4FMPane.java +++ b/src/main/java/io/github/dsheirer/instrument/gui/viewer/decoder/P25Phase1C4FMPane.java @@ -30,7 +30,7 @@ import javafx.scene.layout.Priority; import org.slf4j.Logger; import org.slf4j.LoggerFactory; -public class P25Phase1C4FMPane extends DecoderPane +public class P25Phase1C4FMPane extends ComplexDecoderPane { private final static Logger mLog = LoggerFactory.getLogger(P25Phase1C4FMPane.class); diff --git a/src/main/java/io/github/dsheirer/instrument/gui/viewer/decoder/P25Phase1LSMPane.java b/src/main/java/io/github/dsheirer/instrument/gui/viewer/decoder/P25Phase1LSMPane.java index 33b0f7e4..185e26a4 100644 --- a/src/main/java/io/github/dsheirer/instrument/gui/viewer/decoder/P25Phase1LSMPane.java +++ b/src/main/java/io/github/dsheirer/instrument/gui/viewer/decoder/P25Phase1LSMPane.java @@ -30,7 +30,7 @@ import javafx.scene.layout.Priority; import org.slf4j.Logger; import org.slf4j.LoggerFactory; -public class P25Phase1LSMPane extends DecoderPane +public class P25Phase1LSMPane extends ComplexDecoderPane { private final static Logger mLog = LoggerFactory.getLogger(P25Phase1LSMPane.class); diff --git a/src/main/java/io/github/dsheirer/instrument/gui/viewer/decoder/RealDecoderPane.java b/src/main/java/io/github/dsheirer/instrument/gui/viewer/decoder/RealDecoderPane.java new file mode 100644 index 00000000..4d9a4871 --- /dev/null +++ b/src/main/java/io/github/dsheirer/instrument/gui/viewer/decoder/RealDecoderPane.java @@ -0,0 +1,68 @@ +/******************************************************************************* + * 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 + * + ******************************************************************************/ +package io.github.dsheirer.instrument.gui.viewer.decoder; + +import io.github.dsheirer.module.decode.DecoderType; +import io.github.dsheirer.sample.Listener; +import io.github.dsheirer.sample.SampleType; +import io.github.dsheirer.sample.buffer.ReusableBuffer; +import io.github.dsheirer.sample.buffer.ReusableBufferBroadcaster; +import javafx.scene.text.Text; + +public class RealDecoderPane extends AbstractDecoderPane +{ + private ReusableBufferBroadcaster mBufferBroadcaster = new ReusableBufferBroadcaster(); + private DecoderType mDecoderType; + + public RealDecoderPane(DecoderType decoderType) + { + mDecoderType = decoderType; + } + + /** + * Package private - used by factory class to create an empty pane + */ + RealDecoderPane() + { + getChildren().add(new Text("Please select a decoder from the menu")); + } + + @Override + SampleType getSampleType() + { + return SampleType.COMPLEX; + } + + @Override + public void receive(ReusableBuffer reusableBuffer) + { + mBufferBroadcaster.broadcast(reusableBuffer); + } + + public void addListener(Listener listener) + { + mBufferBroadcaster.addListener(listener); + } + + public void removeListener(Listener listener) + { + mBufferBroadcaster.removeListener(listener); + } + + public void setSampleRate(double sampleRate) + { + } +} diff --git a/src/main/java/io/github/dsheirer/instrument/tap/event/SyncDetectTap.java b/src/main/java/io/github/dsheirer/instrument/tap/event/SyncDetectTap.java index c027b596..087393a8 100644 --- a/src/main/java/io/github/dsheirer/instrument/tap/event/SyncDetectTap.java +++ b/src/main/java/io/github/dsheirer/instrument/tap/event/SyncDetectTap.java @@ -1,60 +1,58 @@ /******************************************************************************* * SDR Trunk * Copyright (C) 2014 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 io.github.dsheirer.instrument.tap.event; -import io.github.dsheirer.dsp.symbol.SyncDetectListener; +import io.github.dsheirer.dsp.symbol.ISyncDetectListener; import io.github.dsheirer.dsp.symbol.SyncDetectProvider; import io.github.dsheirer.instrument.tap.Tap; import io.github.dsheirer.instrument.tap.TapListener; import io.github.dsheirer.instrument.tap.TapType; -public class SyncDetectTap extends Tap - implements SyncDetectListener, - SyncDetectProvider +public class SyncDetectTap extends Tap implements ISyncDetectListener, SyncDetectProvider { - private SyncDetectListener mListener; + private ISyncDetectListener mListener; - /** - * Sync detect event tap. Indicates when message sync word has been - * detected within a binary stream of data. - */ - public SyncDetectTap() + /** + * Sync detect event tap. Indicates when message sync word has been + * detected within a binary stream of data. + */ + public SyncDetectTap() { - super( TapType.EVENT_SYNC_DETECT, "Sync Detected", 0 ); + super(TapType.EVENT_SYNC_DETECT, "Sync Detected", 0); } - @Override + @Override public void syncDetected() { - if( mListener != null ) - { - mListener.syncDetected(); - } - - for( TapListener listener: mListeners ) - { - listener.receive( getName() ); - } + if(mListener != null) + { + mListener.syncDetected(); + } + + for(TapListener listener : mListeners) + { + listener.receive(getName()); + } } - @Override - public void setSyncDetectListener( SyncDetectListener listener ) + @Override + public void setSyncDetectListener(ISyncDetectListener listener) { - mListener = listener; + mListener = listener; } } diff --git a/src/main/java/io/github/dsheirer/module/ProcessingChain.java b/src/main/java/io/github/dsheirer/module/ProcessingChain.java index 1a92cfc9..d34adb24 100644 --- a/src/main/java/io/github/dsheirer/module/ProcessingChain.java +++ b/src/main/java/io/github/dsheirer/module/ProcessingChain.java @@ -49,8 +49,12 @@ import io.github.dsheirer.record.wave.ComplexBufferWaveRecorder; import io.github.dsheirer.record.wave.RealBufferWaveRecorder; import io.github.dsheirer.sample.Broadcaster; import io.github.dsheirer.sample.Listener; +import io.github.dsheirer.sample.buffer.IReusableBufferListener; +import io.github.dsheirer.sample.buffer.IReusableBufferProvider; import io.github.dsheirer.sample.buffer.IReusableComplexBufferListener; +import io.github.dsheirer.sample.buffer.ReusableBuffer; import io.github.dsheirer.sample.buffer.ReusableBufferBroadcaster; +import io.github.dsheirer.sample.buffer.ReusableComplexBuffer; import io.github.dsheirer.sample.real.IFilteredRealBufferListener; import io.github.dsheirer.sample.real.IFilteredRealBufferProvider; import io.github.dsheirer.sample.real.IUnFilteredRealBufferListener; @@ -98,7 +102,8 @@ public class ProcessingChain implements IChannelEventListener private Broadcaster mAudioPacketBroadcaster = new Broadcaster<>(); private Broadcaster mCallEventBroadcaster = new Broadcaster<>(); private Broadcaster mChannelEventBroadcaster = new Broadcaster<>(); - private ReusableBufferBroadcaster mComplexBufferBroadcaster = new ReusableBufferBroadcaster(); + private ReusableBufferBroadcaster mComplexBufferBroadcaster = new ReusableBufferBroadcaster(); + private ReusableBufferBroadcaster mDemodulatedAudioBufferBroadcaster = new ReusableBufferBroadcaster(); private Broadcaster mDecoderStateEventBroadcaster = new Broadcaster<>(); private Broadcaster mHeartbeatBroadcaster = new Broadcaster<>(); private Broadcaster mSourceEventBroadcaster = new Broadcaster<>(); @@ -327,9 +332,14 @@ public class ProcessingChain implements IChannelEventListener mMessageBroadcaster.addListener(((IMessageListener) module).getMessageListener()); } + if(module instanceof IReusableBufferListener) + { + mDemodulatedAudioBufferBroadcaster.addListener(((IReusableBufferListener)module).getReusableBufferListener()); + } + if(module instanceof IReusableComplexBufferListener) { - mComplexBufferBroadcaster.addListener(((IReusableComplexBufferListener) module).getReusableComplexBufferListener()); + mComplexBufferBroadcaster.addListener(((IReusableComplexBufferListener)module).getReusableComplexBufferListener()); } if(module instanceof ISourceEventListener) @@ -394,6 +404,11 @@ public class ProcessingChain implements IChannelEventListener mHeartbeatBroadcaster.removeListener(((IHeartbeatListener)module).getHeartbeatListener()); } + if(module instanceof IReusableBufferListener) + { + mDemodulatedAudioBufferBroadcaster.removeListener(((IReusableBufferListener)module).getReusableBufferListener()); + } + if(module instanceof IReusableComplexBufferListener) { mComplexBufferBroadcaster.removeListener(((IReusableComplexBufferListener) module).getReusableComplexBufferListener()); @@ -471,6 +486,11 @@ public class ProcessingChain implements IChannelEventListener ((IMessageProvider) module).setMessageListener(mMessageBroadcaster); } + if(module instanceof IReusableBufferProvider) + { + ((IReusableBufferProvider)module).setBufferListener(mDemodulatedAudioBufferBroadcaster); + } + if(module instanceof ISourceEventProvider) { ((ISourceEventProvider) module).setSourceEventListener(mSourceEventBroadcaster); @@ -538,6 +558,11 @@ public class ProcessingChain implements IChannelEventListener ((IMessageProvider) module).setMessageListener(null); } + if(module instanceof IReusableBufferProvider) + { + ((IReusableBufferProvider)module).setBufferListener(null); + } + if(module instanceof ISourceEventProvider) { ((ISourceEventProvider) module).setSourceEventListener(null); @@ -588,7 +613,7 @@ public class ProcessingChain implements IChannelEventListener ((ComplexSource) mSource).setListener(mComplexBufferBroadcaster); break; case REAL: - ((RealSource) mSource).setListener(mFilteredRealBufferBroadcaster); + ((RealSource) mSource).setListener(mDemodulatedAudioBufferBroadcaster); break; default: throw new IllegalArgumentException("Unrecognized source " @@ -636,7 +661,7 @@ public class ProcessingChain implements IChannelEventListener ((ComplexSource) mSource).removeListener(mComplexBufferBroadcaster); break; case REAL: - ((RealSource) mSource).removeListener(mFilteredRealBufferBroadcaster); + ((RealSource) mSource).removeListener(mDemodulatedAudioBufferBroadcaster); break; default: throw new IllegalArgumentException("Unrecognized source sample type - cannot start processing " + diff --git a/src/main/java/io/github/dsheirer/module/decode/DecoderFactory.java b/src/main/java/io/github/dsheirer/module/decode/DecoderFactory.java index c2020df7..5a3a3308 100644 --- a/src/main/java/io/github/dsheirer/module/decode/DecoderFactory.java +++ b/src/main/java/io/github/dsheirer/module/decode/DecoderFactory.java @@ -20,7 +20,7 @@ package io.github.dsheirer.module.decode; import io.github.dsheirer.alias.AliasList; import io.github.dsheirer.alias.AliasModel; -import io.github.dsheirer.audio.AudioModule2; +import io.github.dsheirer.audio.AudioModule; import io.github.dsheirer.channel.metadata.Metadata; import io.github.dsheirer.channel.state.AlwaysUnsquelchedDecoderState; import io.github.dsheirer.channel.traffic.TrafficChannelManager; @@ -30,8 +30,6 @@ import io.github.dsheirer.controller.channel.ChannelModel; import io.github.dsheirer.controller.channel.ChannelProcessingManager; import io.github.dsheirer.controller.channel.map.ChannelMap; import io.github.dsheirer.controller.channel.map.ChannelMapModel; -import io.github.dsheirer.dsp.filter.FilterFactory; -import io.github.dsheirer.dsp.filter.fir.FIRFilterSpecification; import io.github.dsheirer.filter.AllPassFilter; import io.github.dsheirer.filter.FilterSet; import io.github.dsheirer.filter.IFilter; @@ -101,39 +99,8 @@ public class DecoderFactory { private final static Logger mLog = LoggerFactory.getLogger(DecoderFactory.class); - private static final int NBFM_PASS_FREQUENCY = 6000; - private static final int NBFM_STOP_FREQUENCY = 6500; - - //Low-pass filter with ~60 dB attenuation and 89 taps - private static final FIRFilterSpecification MPT1327_FILTER_SPECIFICATION = FIRFilterSpecification.lowPassBuilder() - .sampleRate( 48000 ).gridDensity( 16 ).passBandCutoff( 3200 ).passBandAmplitude( 1.0 ).passBandRipple( 0.02 ) - .stopBandStart( 4000 ).stopBandAmplitude( 0.0 ).stopBandRipple( 0.03 ).build(); - - private static final FIRFilterSpecification P25_C4FM_IQ_SPECIFICATION = FIRFilterSpecification.lowPassBuilder() - .sampleRate(48000).gridDensity(16).passBandCutoff(6750).passBandAmplitude(1.0).passBandRipple(0.01) - .stopBandStart(7000).stopBandAmplitude(0.0).stopBandRipple(0.008).build(); - - private static final FIRFilterSpecification P25_C4FM_DEMOD_SPECIFICATION = FIRFilterSpecification.lowPassBuilder() - .sampleRate(48000).gridDensity(16).passBandCutoff(2500).passBandAmplitude(1.0).passBandRipple(0.01) - .stopBandStart(4000).stopBandAmplitude(0.0).stopBandRipple(0.008).build(); - - private static float[] MPT1327_LOWPASS_FILTER; - private static float[] P25_C4FM_IQ_FILTER; - private static float[] P25_C4FM_DEMOD_FILTER; - - static - { - try - { - MPT1327_LOWPASS_FILTER = FilterFactory.getTaps(MPT1327_FILTER_SPECIFICATION); - P25_C4FM_IQ_FILTER = FilterFactory.getTaps(P25_C4FM_IQ_SPECIFICATION); - P25_C4FM_DEMOD_FILTER = FilterFactory.getTaps(P25_C4FM_DEMOD_SPECIFICATION); - } - catch(Exception e) - { - mLog.error("Couldn't design startup filter(s)"); - } - } + private static final double CHANNEL_BANDWIDTH = 12500.0; + private static final double DEMODULATED_AUDIO_SAMPLE_RATE = 8000.0; /** * Returns a list of one primary decoder and any auxiliary decoders, as @@ -183,30 +150,28 @@ public class DecoderFactory modules.add(new AMDecoder(decodeConfig)); modules.add(new AlwaysUnsquelchedDecoderState(DecoderType.AM, channel.getName())); modules.add(new AMDemodulatorModule()); +//TODO: update AM demodulator to produce demodulated 8 kHz audio and delete the following filter module modules.add(new DemodulatedAudioFilterModule(4000, 6000)); - modules.add(new AudioModule2(metadata)); + modules.add(new AudioModule(metadata)); break; case NBFM: modules.add(new NBFMDecoder(decodeConfig)); modules.add(new AlwaysUnsquelchedDecoderState(DecoderType.NBFM, channel.getName())); - modules.add(new FMDemodulatorModule(NBFM_PASS_FREQUENCY, NBFM_STOP_FREQUENCY)); - modules.add(new DemodulatedAudioFilterModule(4000, 6000)); - modules.add(new AudioModule2(metadata)); + modules.add(new FMDemodulatorModule(CHANNEL_BANDWIDTH, DEMODULATED_AUDIO_SAMPLE_RATE)); + modules.add(new AudioModule(metadata)); break; case LTR_STANDARD: MessageDirection direction = ((DecodeConfigLTRStandard) decodeConfig).getMessageDirection(); modules.add(new LTRStandardDecoder(aliasList, direction)); modules.add(new LTRStandardDecoderState(aliasList)); - modules.add(new FMDemodulatorModule(NBFM_PASS_FREQUENCY, NBFM_STOP_FREQUENCY)); - modules.add(new DemodulatedAudioFilterModule(4000, 6000)); - modules.add(new AudioModule2(metadata)); + modules.add(new FMDemodulatorModule(CHANNEL_BANDWIDTH, DEMODULATED_AUDIO_SAMPLE_RATE)); + modules.add(new AudioModule(metadata)); break; case LTR_NET: modules.add(new LTRNetDecoder((DecodeConfigLTRNet) decodeConfig, aliasList)); modules.add(new LTRNetDecoderState(aliasList)); - modules.add(new FMDemodulatorModule(NBFM_PASS_FREQUENCY, NBFM_STOP_FREQUENCY)); - modules.add(new DemodulatedAudioFilterModule(4000, 6000)); - modules.add(new AudioModule2(metadata)); + modules.add(new FMDemodulatorModule(CHANNEL_BANDWIDTH, DEMODULATED_AUDIO_SAMPLE_RATE)); + modules.add(new AudioModule(metadata)); break; case MPT1327: DecodeConfigMPT1327 mptConfig = (DecodeConfigMPT1327) decodeConfig; @@ -227,16 +192,14 @@ public class DecoderFactory (aliasList != null ? aliasList.getName() : null), mptConfig.getTrafficChannelPoolSize())); } - modules.add(new FMDemodulatorModule(NBFM_PASS_FREQUENCY, NBFM_STOP_FREQUENCY)); - modules.add(new DemodulatedAudioFilterModule(P25_C4FM_DEMOD_FILTER, 1.0f)); - modules.add(new AudioModule2(metadata)); + modules.add(new FMDemodulatorModule(CHANNEL_BANDWIDTH, DEMODULATED_AUDIO_SAMPLE_RATE)); + modules.add(new AudioModule(metadata)); break; case PASSPORT: modules.add(new PassportDecoder(decodeConfig, aliasList)); modules.add(new PassportDecoderState(aliasList)); - modules.add(new FMDemodulatorModule(NBFM_PASS_FREQUENCY, NBFM_STOP_FREQUENCY)); - modules.add(new DemodulatedAudioFilterModule(4000, 6000)); - modules.add(new AudioModule2(metadata)); + modules.add(new FMDemodulatorModule(CHANNEL_BANDWIDTH, DEMODULATED_AUDIO_SAMPLE_RATE)); + modules.add(new AudioModule(metadata)); break; case P25_PHASE1: DecodeConfigP25Phase1 p25Config = (DecodeConfigP25Phase1) decodeConfig; diff --git a/src/main/java/io/github/dsheirer/module/decode/ltrnet/LTRNetDecoder.java b/src/main/java/io/github/dsheirer/module/decode/ltrnet/LTRNetDecoder.java index 6f5f6d06..fa21ad92 100644 --- a/src/main/java/io/github/dsheirer/module/decode/ltrnet/LTRNetDecoder.java +++ b/src/main/java/io/github/dsheirer/module/decode/ltrnet/LTRNetDecoder.java @@ -1,121 +1,92 @@ /******************************************************************************* - * SDR Trunk - * Copyright (C) 2014 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 + * 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 + * ******************************************************************************/ package io.github.dsheirer.module.decode.ltrnet; import io.github.dsheirer.alias.AliasList; import io.github.dsheirer.bits.MessageFramer; import io.github.dsheirer.bits.SyncPattern; -import io.github.dsheirer.dsp.fsk.LTRFSKDecoder; -import io.github.dsheirer.instrument.Instrumentable; -import io.github.dsheirer.instrument.tap.Tap; -import io.github.dsheirer.instrument.tap.TapGroup; +import io.github.dsheirer.dsp.fsk.LTRDecoder; import io.github.dsheirer.message.MessageDirection; import io.github.dsheirer.module.decode.Decoder; import io.github.dsheirer.module.decode.DecoderType; import io.github.dsheirer.sample.Listener; -import io.github.dsheirer.sample.real.IUnFilteredRealBufferListener; -import io.github.dsheirer.sample.real.RealBuffer; +import io.github.dsheirer.sample.buffer.IReusableBufferListener; +import io.github.dsheirer.sample.buffer.ReusableBuffer; -import java.util.ArrayList; -import java.util.List; - -public class LTRNetDecoder extends Decoder - implements IUnFilteredRealBufferListener, Instrumentable +public class LTRNetDecoder extends Decoder implements IReusableBufferListener, Listener { - public static final int LTR_STANDARD_MESSAGE_LENGTH = 40; - private LTRFSKDecoder mLTRFSKDecoder; - private MessageFramer mLTRMessageFramer; - private LTRNetMessageProcessor mLTRMessageProcessor; + public static final int LTR_NET_MESSAGE_LENGTH = 40; + protected LTRDecoder mLTRDecoder; + private MessageFramer mLTRMessageFramer; + private LTRNetMessageProcessor mLTRMessageProcessor; - private List mAvailableTaps; - /** * LTR-Net Decoder. Decodes unfiltered (e.g. demodulated but with no DC or * audio filtering) samples and produces LTR-Net messages. */ - public LTRNetDecoder( DecodeConfigLTRNet config, AliasList aliasList ) - { - mLTRFSKDecoder = new LTRFSKDecoder(); - - - if( config.getMessageDirection() == MessageDirection.OSW ) - { - mLTRMessageFramer = - new MessageFramer( SyncPattern.LTR_STANDARD_OSW.getPattern(), - LTR_STANDARD_MESSAGE_LENGTH ); - } - else - { - mLTRMessageFramer = - new MessageFramer( SyncPattern.LTR_STANDARD_ISW.getPattern(), - LTR_STANDARD_MESSAGE_LENGTH ); - } - - mLTRFSKDecoder.addListener( mLTRMessageFramer ); - - mLTRMessageProcessor = new LTRNetMessageProcessor( - config.getMessageDirection(), aliasList ); - - mLTRMessageFramer.addMessageListener( mLTRMessageProcessor ); - - mLTRMessageProcessor.setMessageListener( getMessageListener() ); - } - - @Override - public DecoderType getDecoderType() - { - return DecoderType.LTR_NET; - } - - @Override - public Listener getUnFilteredRealBufferListener() - { - return mLTRFSKDecoder; - } - - @Override - public List getTapGroups() + public LTRNetDecoder(DecodeConfigLTRNet config, AliasList aliasList, LTRDecoder ltrDecoder) { - if( mAvailableTaps == null ) + mLTRDecoder = ltrDecoder; + + if(config.getMessageDirection() == MessageDirection.OSW) { - mAvailableTaps = new ArrayList(); - - mAvailableTaps.addAll( mLTRFSKDecoder.getTapGroups() ); + mLTRMessageFramer = new MessageFramer(SyncPattern.LTR_STANDARD_OSW.getPattern(), LTR_NET_MESSAGE_LENGTH); + } + else + { + mLTRMessageFramer = new MessageFramer(SyncPattern.LTR_STANDARD_ISW.getPattern(), LTR_NET_MESSAGE_LENGTH); } - return mAvailableTaps; + mLTRDecoder.setListener(mLTRMessageFramer); + mLTRMessageFramer.setSyncDetectListener(mLTRDecoder); + mLTRMessageProcessor = new LTRNetMessageProcessor(config.getMessageDirection(), aliasList); + mLTRMessageFramer.addMessageListener(mLTRMessageProcessor); + mLTRMessageProcessor.setMessageListener(getMessageListener()); + } + + /** + * LTR-Net Decoder. Decodes unfiltered (e.g. demodulated but with no DC or + * audio filtering) samples and produces LTR-Net messages. + */ + public LTRNetDecoder(DecodeConfigLTRNet config, AliasList aliasList) + { + this(config, aliasList, new LTRDecoder(LTR_NET_MESSAGE_LENGTH)); } @Override - public void registerTap( Tap tap ) + public DecoderType getDecoderType() { - mLTRFSKDecoder.registerTap( tap ); + return DecoderType.LTR_NET; } @Override - public void unregisterTap( Tap tap ) + public Listener getReusableBufferListener() { - mLTRFSKDecoder.unregisterTap( tap ); + return mLTRDecoder; } - @Override - public void reset() - { - mLTRMessageFramer.reset(); - } + @Override + public void receive(ReusableBuffer reusableBuffer) + { + mLTRDecoder.receive(reusableBuffer); + } + + @Override + public void reset() + { + mLTRMessageFramer.reset(); + } } diff --git a/src/main/java/io/github/dsheirer/module/decode/ltrnet/LTRNetDecoderInstrumented.java b/src/main/java/io/github/dsheirer/module/decode/ltrnet/LTRNetDecoderInstrumented.java new file mode 100644 index 00000000..b8970362 --- /dev/null +++ b/src/main/java/io/github/dsheirer/module/decode/ltrnet/LTRNetDecoderInstrumented.java @@ -0,0 +1,44 @@ +/******************************************************************************* + * 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 + * + ******************************************************************************/ +package io.github.dsheirer.module.decode.ltrnet; + +import io.github.dsheirer.alias.AliasList; +import io.github.dsheirer.dsp.fsk.LTRDecoderInstrumented; +import io.github.dsheirer.sample.buffer.ReusableBuffer; +import javafx.beans.property.SimpleIntegerProperty; + +public class LTRNetDecoderInstrumented extends LTRNetDecoder +{ + public SimpleIntegerProperty bufferCount = new SimpleIntegerProperty(); + + public LTRNetDecoderInstrumented(DecodeConfigLTRNet config, AliasList aliasList) + { + super(config, aliasList, new LTRDecoderInstrumented(LTR_NET_MESSAGE_LENGTH)); + } + + public LTRDecoderInstrumented getLTRDecoder() + { + return (LTRDecoderInstrumented)mLTRDecoder; + } + + @Override + public void receive(ReusableBuffer reusableBuffer) + { + super.receive(reusableBuffer); + + bufferCount.setValue(bufferCount.intValue() + 1); + } +} diff --git a/src/main/java/io/github/dsheirer/module/decode/ltrstandard/LTRStandardDecoder.java b/src/main/java/io/github/dsheirer/module/decode/ltrstandard/LTRStandardDecoder.java index 47bbd171..1f2ba7db 100644 --- a/src/main/java/io/github/dsheirer/module/decode/ltrstandard/LTRStandardDecoder.java +++ b/src/main/java/io/github/dsheirer/module/decode/ltrstandard/LTRStandardDecoder.java @@ -1,85 +1,84 @@ /******************************************************************************* - * SDR Trunk - * Copyright (C) 2014,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 + * 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 + * ******************************************************************************/ package io.github.dsheirer.module.decode.ltrstandard; import io.github.dsheirer.alias.AliasList; import io.github.dsheirer.bits.MessageFramer; import io.github.dsheirer.bits.SyncPattern; -import io.github.dsheirer.dsp.fsk.LTRFSKDecoder; +import io.github.dsheirer.dsp.fsk.LTRDecoder; import io.github.dsheirer.message.MessageDirection; import io.github.dsheirer.module.decode.Decoder; import io.github.dsheirer.module.decode.DecoderType; import io.github.dsheirer.sample.Listener; -import io.github.dsheirer.sample.real.IUnFilteredRealBufferListener; -import io.github.dsheirer.sample.real.RealBuffer; +import io.github.dsheirer.sample.buffer.IReusableBufferListener; +import io.github.dsheirer.sample.buffer.ReusableBuffer; -public class LTRStandardDecoder extends Decoder implements IUnFilteredRealBufferListener +public class LTRStandardDecoder extends Decoder implements IReusableBufferListener, Listener { - public static final int LTR_STANDARD_MESSAGE_LENGTH = 40; + public static final int LTR_STANDARD_MESSAGE_LENGTH = 40; + + private LTRDecoder mLTRDecoder; + private MessageFramer mLTRMessageFramer; + private LTRStandardMessageProcessor mLTRMessageProcessor; - private LTRFSKDecoder mLTRFSKDecoder; - private MessageFramer mLTRMessageFramer; - private LTRStandardMessageProcessor mLTRMessageProcessor; - /** * LTR Decoder. Decodes unfiltered (e.g. demodulated but with no DC or * audio filtering) samples and produces LTR Standard messages. */ - public LTRStandardDecoder( AliasList aliasList, - MessageDirection direction ) - { - mLTRFSKDecoder = new LTRFSKDecoder(); + public LTRStandardDecoder(AliasList aliasList, MessageDirection direction) + { + mLTRDecoder = new LTRDecoder(LTR_STANDARD_MESSAGE_LENGTH); - if( direction == MessageDirection.OSW ) - { - mLTRMessageFramer = - new MessageFramer( SyncPattern.LTR_STANDARD_OSW.getPattern(), - LTR_STANDARD_MESSAGE_LENGTH ); - } - else - { - mLTRMessageFramer = - new MessageFramer( SyncPattern.LTR_STANDARD_ISW.getPattern(), - LTR_STANDARD_MESSAGE_LENGTH ); - } - - mLTRFSKDecoder.addListener( mLTRMessageFramer ); + if(direction == MessageDirection.OSW) + { + mLTRMessageFramer = new MessageFramer(SyncPattern.LTR_STANDARD_OSW.getPattern(), LTR_STANDARD_MESSAGE_LENGTH); + } + else + { + mLTRMessageFramer = new MessageFramer(SyncPattern.LTR_STANDARD_ISW.getPattern(), LTR_STANDARD_MESSAGE_LENGTH); + } - mLTRMessageProcessor = new LTRStandardMessageProcessor( direction, aliasList ); - mLTRMessageFramer.addMessageListener( mLTRMessageProcessor ); - mLTRMessageProcessor.setMessageListener( getMessageListener() ); - } + mLTRDecoder.setListener(mLTRMessageFramer); + mLTRMessageFramer.setSyncDetectListener(mLTRDecoder); + mLTRMessageProcessor = new LTRStandardMessageProcessor(direction, aliasList); + mLTRMessageFramer.addMessageListener(mLTRMessageProcessor); + mLTRMessageProcessor.setMessageListener(getMessageListener()); + } - @Override - public DecoderType getDecoderType() - { - return DecoderType.LTR_STANDARD; - } + @Override + public DecoderType getDecoderType() + { + return DecoderType.LTR_STANDARD; + } - @Override - public Listener getUnFilteredRealBufferListener() - { - return mLTRFSKDecoder; - } + @Override + public Listener getReusableBufferListener() + { + return mLTRDecoder; + } - @Override - public void reset() - { - mLTRMessageFramer.reset(); - } + @Override + public void receive(ReusableBuffer reusableBuffer) + { + mLTRDecoder.receive(reusableBuffer); + } + + @Override + public void reset() + { + mLTRMessageFramer.reset(); + } } diff --git a/src/main/java/io/github/dsheirer/module/decode/p25/P25MessageFramer.java b/src/main/java/io/github/dsheirer/module/decode/p25/P25MessageFramer.java index 54047262..7419861d 100644 --- a/src/main/java/io/github/dsheirer/module/decode/p25/P25MessageFramer.java +++ b/src/main/java/io/github/dsheirer/module/decode/p25/P25MessageFramer.java @@ -3,13 +3,13 @@ package io.github.dsheirer.module.decode.p25; import io.github.dsheirer.alias.AliasList; import io.github.dsheirer.bits.BinaryMessage; import io.github.dsheirer.bits.BitSetFullException; -import io.github.dsheirer.bits.ISyncDetectListener; import io.github.dsheirer.bits.MultiSyncPatternMatcher; import io.github.dsheirer.bits.SoftSyncDetector; import io.github.dsheirer.bits.SyncDetector; import io.github.dsheirer.dsp.psk.pll.IPhaseLockedLoop; import io.github.dsheirer.dsp.symbol.Dibit; import io.github.dsheirer.dsp.symbol.FrameSync; +import io.github.dsheirer.dsp.symbol.ISyncDetectListener; import io.github.dsheirer.edac.BCH_63_16_11; import io.github.dsheirer.edac.CRC; import io.github.dsheirer.edac.CRCP25; diff --git a/src/main/java/io/github/dsheirer/module/decode/passport/PassportDecoder.java b/src/main/java/io/github/dsheirer/module/decode/passport/PassportDecoder.java index 42deaed1..131d0a69 100644 --- a/src/main/java/io/github/dsheirer/module/decode/passport/PassportDecoder.java +++ b/src/main/java/io/github/dsheirer/module/decode/passport/PassportDecoder.java @@ -20,7 +20,7 @@ package io.github.dsheirer.module.decode.passport; import io.github.dsheirer.alias.AliasList; import io.github.dsheirer.bits.MessageFramer; import io.github.dsheirer.bits.SyncPattern; -import io.github.dsheirer.dsp.fsk.LTRFSKDecoder; +import io.github.dsheirer.dsp.fsk.LTRFSKDecoderLegacy; import io.github.dsheirer.instrument.Instrumentable; import io.github.dsheirer.instrument.tap.Tap; import io.github.dsheirer.instrument.tap.TapGroup; @@ -40,7 +40,7 @@ public class PassportDecoder extends Decoder public static final int PASSPORT_MESSAGE_LENGTH = 68; public static final int PASSPORT_SYNC_LENGTH = 9; - private LTRFSKDecoder mPassportFSKDecoder; + private LTRFSKDecoderLegacy mPassportFSKDecoder; private MessageFramer mPassportMessageFramer; private PassportMessageProcessor mPassportMessageProcessor; @@ -52,7 +52,7 @@ public class PassportDecoder extends Decoder */ public PassportDecoder(DecodeConfiguration config, AliasList aliasList ) { - mPassportFSKDecoder = new LTRFSKDecoder(); + mPassportFSKDecoder = new LTRFSKDecoderLegacy(); mPassportMessageFramer = new MessageFramer( SyncPattern.PASSPORT.getPattern(), diff --git a/src/main/java/io/github/dsheirer/module/demodulate/audio/DemodulatedAudioFilterModule.java b/src/main/java/io/github/dsheirer/module/demodulate/audio/DemodulatedAudioFilterModule.java index 5560b9dd..57082c6f 100644 --- a/src/main/java/io/github/dsheirer/module/demodulate/audio/DemodulatedAudioFilterModule.java +++ b/src/main/java/io/github/dsheirer/module/demodulate/audio/DemodulatedAudioFilterModule.java @@ -30,6 +30,7 @@ import io.github.dsheirer.sample.real.RealBuffer; import org.slf4j.Logger; import org.slf4j.LoggerFactory; +@Deprecated //Remove once polyphase channelizer enhancement is complete public class DemodulatedAudioFilterModule extends Module implements IUnFilteredRealBufferListener, IFilteredRealBufferProvider { private final static Logger mLog = LoggerFactory.getLogger(DemodulatedAudioFilterModule.class); diff --git a/src/main/java/io/github/dsheirer/module/demodulate/fm/FMDemodulatorModule.java b/src/main/java/io/github/dsheirer/module/demodulate/fm/FMDemodulatorModule.java index b06864c8..d3da75e8 100644 --- a/src/main/java/io/github/dsheirer/module/demodulate/fm/FMDemodulatorModule.java +++ b/src/main/java/io/github/dsheirer/module/demodulate/fm/FMDemodulatorModule.java @@ -19,50 +19,55 @@ 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.FIRFilterSpecification; -import io.github.dsheirer.dsp.filter.fir.complex.ComplexFIRFilter_CB_CB; -import io.github.dsheirer.dsp.fm.FMDemodulator_CB; +import io.github.dsheirer.dsp.filter.fir.complex.ComplexFIRFilter2; +import io.github.dsheirer.dsp.filter.resample.RealResampler; +import io.github.dsheirer.dsp.fm.FMDemodulator; import io.github.dsheirer.module.Module; import io.github.dsheirer.sample.Listener; +import io.github.dsheirer.sample.buffer.IReusableBufferProvider; import io.github.dsheirer.sample.buffer.IReusableComplexBufferListener; +import io.github.dsheirer.sample.buffer.ReusableBuffer; import io.github.dsheirer.sample.buffer.ReusableComplexBuffer; -import io.github.dsheirer.sample.real.IUnFilteredRealBufferProvider; -import io.github.dsheirer.sample.real.RealBuffer; import io.github.dsheirer.source.ISourceEventListener; import io.github.dsheirer.source.SourceEvent; import org.slf4j.Logger; import org.slf4j.LoggerFactory; +/** + * FM Demodulator with integrated fractional resampler. + * + * Note: no filtering is applied to the demodulated audio. + */ public class FMDemodulatorModule extends Module implements ISourceEventListener, IReusableComplexBufferListener, - IUnFilteredRealBufferProvider + Listener, IReusableBufferProvider { private final static Logger mLog = LoggerFactory.getLogger(FMDemodulatorModule.class); - private ComplexFIRFilter_CB_CB mIQFilter; - private FMDemodulator_CB mDemodulator; - private ReusableBufferProcessor mReusableBufferProcessor = new ReusableBufferProcessor(); + private ComplexFIRFilter2 mIQFilter; + private FMDemodulator mDemodulator = new FMDemodulator(); + private RealResampler mResampler; private SourceEventProcessor mSourceEventProcessor = new SourceEventProcessor(); - private int mPassFrequency; - private int mStopFrequency; + private Listener mResampledReusableBufferListener; + private double mChannelBandwidth; + private double mOutputSampleRate; /** - * FM Demodulator with I/Q filter. Demodulated output is unfiltered and may contain a DC component. + * Creates an FM demodulator for the specified channel bandwidth and output sample rate. * - * @param pass - pass frequency for IQ filtering prior to demodulation. This - * frequency should be less than or equal to half of the signal bandwidth since the filter will - * be applied against each of the inphase and quadrature signals and the - * combined pass bandwidth will be twice this value. + * A baseband I/Q filter will be constructed to low pass filter the incoming sample buffer stream to the desired + * channel bandwidth and the demodulated output will be resampled to the desired output sample rate. The input + * low pass filter is constructed at runtime based on receiving a sample rate notification source event. */ - public FMDemodulatorModule(int pass, int stop) + public FMDemodulatorModule(double channelBandwidth, double outputSampleRate) { - mDemodulator = new FMDemodulator_CB(1.0f); - mPassFrequency = pass; - mStopFrequency = stop; + mChannelBandwidth = channelBandwidth; + mOutputSampleRate = outputSampleRate; } @Override public Listener getReusableComplexBufferListener() { - return mReusableBufferProcessor; + return this; } @Override @@ -90,18 +95,6 @@ public class FMDemodulatorModule extends Module implements ISourceEventListener, mDemodulator.reset(); } - @Override - public void setUnFilteredRealBufferListener(Listener listener) - { - mDemodulator.setListener(listener); - } - - @Override - public void removeUnFilteredRealBufferListener() - { - mDemodulator.removeListener(); - } - @Override public void start() { @@ -112,27 +105,38 @@ public class FMDemodulatorModule extends Module implements ISourceEventListener, { } - /** - * Receives complex buffers and sends them through the filtering and demodulation process. Manages accounting for - * the reusable buffers. - */ - public class ReusableBufferProcessor implements Listener + @Override + public void setBufferListener(Listener listener) { - @Override - public void receive(ReusableComplexBuffer reusableComplexBuffer) + mResampledReusableBufferListener = listener; + } + + @Override + public void removeBufferListener() + { + mResampledReusableBufferListener = null; + } + + @Override + public void receive(ReusableComplexBuffer reusableComplexBuffer) + { + if(mIQFilter == null) { - if(mIQFilter == null) - { - reusableComplexBuffer.decrementUserCount(); - throw new IllegalStateException("FM demodulator module must receive a sample rate change source " + - "event before it can process complex sample buffers"); - } - - float[] samples = reusableComplexBuffer.getSamplesCopy(); - - mIQFilter.receive(samples); - reusableComplexBuffer.decrementUserCount(); + throw new IllegalStateException("FM demodulator module must receive a sample rate change source " + + "event before it can process complex sample buffers"); + } + + ReusableComplexBuffer basebandFilteredBuffer = mIQFilter.filter(reusableComplexBuffer); + ReusableBuffer demodulatedBuffer = mDemodulator.demodulate(basebandFilteredBuffer); + + if(mResampler != null) + { + mResampler.resample(demodulatedBuffer); + } + else + { + demodulatedBuffer.decrementUserCount(); } } @@ -146,18 +150,35 @@ public class FMDemodulatorModule extends Module implements ISourceEventListener, { if(sourceEvent.getEvent() == SourceEvent.Event.NOTIFICATION_SAMPLE_RATE_CHANGE) { + if(mIQFilter != null) + { + mIQFilter.dispose(); + mIQFilter = null; + } + double sampleRate = sourceEvent.getValue().doubleValue(); + if((sampleRate < (2.0 * mChannelBandwidth))) + { + throw new IllegalStateException("FM Demodulator with channel bandwidth [" + mChannelBandwidth + + "] requires a channel sample rate of [" + (2.0 * mChannelBandwidth + "] - sample rate of [" + + sampleRate + "] is not supported")); + } + + double cutoff = sampleRate / 4.0; + int passBandStop = (int)cutoff - 500; + int stopBandStart = (int)cutoff + 500; + float[] filterTaps = null; FIRFilterSpecification specification = FIRFilterSpecification.lowPassBuilder() .sampleRate(sampleRate) .gridDensity(16) .oddLength(true) - .passBandCutoff(mPassFrequency) + .passBandCutoff(passBandStop) .passBandAmplitude(1.0) .passBandRipple(0.01) - .stopBandStart(mStopFrequency) + .stopBandStart(stopBandStart) .stopBandAmplitude(0.0) .stopBandRipple(0.028) //Approximately 60 dB attenuation .build(); @@ -169,18 +190,35 @@ public class FMDemodulatorModule extends Module implements ISourceEventListener, catch(FilterDesignException fde) { mLog.error("Couldn't design FM demodulator remez filter for sample rate [" + sampleRate + - "] pass frequency [" + mPassFrequency + "] and stop frequency [" + mStopFrequency + + "] pass frequency [" + passBandStop + "] and stop frequency [" + stopBandStart + "] - using sinc filter"); } if(filterTaps == null) { - filterTaps = FilterFactory.getLowPass(sampleRate, mPassFrequency, mStopFrequency, 60, + filterTaps = FilterFactory.getLowPass(sampleRate, passBandStop, stopBandStart, 60, Window.WindowType.HAMMING, true); } - mIQFilter = new ComplexFIRFilter_CB_CB(filterTaps, 1.0f); - mIQFilter.setListener(mDemodulator); + mIQFilter = new ComplexFIRFilter2(filterTaps); + + mResampler = new RealResampler(sampleRate, mOutputSampleRate); + + mResampler.setListener(new Listener() + { + @Override + public void receive(ReusableBuffer reusableBuffer) + { + if(mResampledReusableBufferListener != null) + { + mResampledReusableBufferListener.receive(reusableBuffer); + } + else + { + reusableBuffer.decrementUserCount(); + } + } + }); } } } diff --git a/src/main/java/io/github/dsheirer/sample/buffer/IReusableBufferListener.java b/src/main/java/io/github/dsheirer/sample/buffer/IReusableBufferListener.java new file mode 100644 index 00000000..4bc8c08c --- /dev/null +++ b/src/main/java/io/github/dsheirer/sample/buffer/IReusableBufferListener.java @@ -0,0 +1,23 @@ +/******************************************************************************* + * 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 + * + ******************************************************************************/ +package io.github.dsheirer.sample.buffer; + +import io.github.dsheirer.sample.Listener; + +public interface IReusableBufferListener +{ + Listener getReusableBufferListener(); +} diff --git a/src/main/java/io/github/dsheirer/sample/buffer/IReusableBufferProvider.java b/src/main/java/io/github/dsheirer/sample/buffer/IReusableBufferProvider.java index afacda85..f2333731 100644 --- a/src/main/java/io/github/dsheirer/sample/buffer/IReusableBufferProvider.java +++ b/src/main/java/io/github/dsheirer/sample/buffer/IReusableBufferProvider.java @@ -17,20 +17,15 @@ package io.github.dsheirer.sample.buffer; import io.github.dsheirer.sample.Listener; -public interface IReusableBufferProvider +public interface IReusableBufferProvider { /** * Adds the listener to receive complex buffer samples */ - void addBufferListener(Listener listener); + void setBufferListener(Listener listener); /** * Removes the listener from receiving complex buffer samples */ - void removeBufferListener(Listener listener); - - /** - * Indicates if there are any complex buffer listeners registered - */ - boolean hasBufferListeners(); + void removeBufferListener(); } diff --git a/src/main/java/io/github/dsheirer/sample/buffer/IReusableComplexBufferListener.java b/src/main/java/io/github/dsheirer/sample/buffer/IReusableComplexBufferListener.java index 47eada60..80de66d5 100644 --- a/src/main/java/io/github/dsheirer/sample/buffer/IReusableComplexBufferListener.java +++ b/src/main/java/io/github/dsheirer/sample/buffer/IReusableComplexBufferListener.java @@ -17,7 +17,7 @@ package io.github.dsheirer.sample.buffer; import io.github.dsheirer.sample.Listener; -public interface IReusableComplexBufferListener +public interface IReusableComplexBufferListener { - Listener getReusableComplexBufferListener(); + Listener getReusableComplexBufferListener(); } diff --git a/src/main/java/io/github/dsheirer/sample/buffer/IReusableComplexBufferProvider.java b/src/main/java/io/github/dsheirer/sample/buffer/IReusableComplexBufferProvider.java new file mode 100644 index 00000000..14316bb3 --- /dev/null +++ b/src/main/java/io/github/dsheirer/sample/buffer/IReusableComplexBufferProvider.java @@ -0,0 +1,36 @@ +/******************************************************************************* + * 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 + * + ******************************************************************************/ +package io.github.dsheirer.sample.buffer; + +import io.github.dsheirer.sample.Listener; + +public interface IReusableComplexBufferProvider +{ + /** + * Adds the listener to receive complex buffer samples + */ + void addBufferListener(Listener listener); + + /** + * Removes the listener from receiving complex buffer samples + */ + void removeBufferListener(Listener listener); + + /** + * Indicates if there are any complex buffer listeners registered + */ + boolean hasBufferListeners(); +} diff --git a/src/main/java/io/github/dsheirer/sample/buffer/ReusableBufferBroadcaster.java b/src/main/java/io/github/dsheirer/sample/buffer/ReusableBufferBroadcaster.java index 2df8493c..3e21f337 100644 --- a/src/main/java/io/github/dsheirer/sample/buffer/ReusableBufferBroadcaster.java +++ b/src/main/java/io/github/dsheirer/sample/buffer/ReusableBufferBroadcaster.java @@ -20,7 +20,7 @@ import io.github.dsheirer.sample.Listener; import org.slf4j.Logger; import org.slf4j.LoggerFactory; -public class ReusableBufferBroadcaster extends Broadcaster +public class ReusableBufferBroadcaster extends Broadcaster { private final static Logger mLog = LoggerFactory.getLogger(ReusableBufferBroadcaster.class); @@ -34,15 +34,15 @@ public class ReusableBufferBroadcaster extends Broadcaster listener : mListeners) + for(Listener listener : mListeners) { - reusableComplexBuffer.incrementUserCount(); - listener.receive(reusableComplexBuffer); + reusableBuffer.incrementUserCount(); + listener.receive(reusableBuffer); } //Decrement user counter for this broadcaster - reusableComplexBuffer.decrementUserCount(); + reusableBuffer.decrementUserCount(); } } diff --git a/src/main/java/io/github/dsheirer/sample/buffer/ReusableBufferQueue.java b/src/main/java/io/github/dsheirer/sample/buffer/ReusableBufferQueue.java index bb96adea..45af48a4 100644 --- a/src/main/java/io/github/dsheirer/sample/buffer/ReusableBufferQueue.java +++ b/src/main/java/io/github/dsheirer/sample/buffer/ReusableBufferQueue.java @@ -27,10 +27,6 @@ public class ReusableBufferQueue extends AbstractReusableBufferQueue +public abstract class RealSource extends Source implements Provider { @Override public SampleType getSampleType() diff --git a/src/main/java/io/github/dsheirer/source/mixer/RealMixerSource.java b/src/main/java/io/github/dsheirer/source/mixer/RealMixerSource.java index 6d6ef037..ba4391d7 100644 --- a/src/main/java/io/github/dsheirer/source/mixer/RealMixerSource.java +++ b/src/main/java/io/github/dsheirer/source/mixer/RealMixerSource.java @@ -19,6 +19,9 @@ package io.github.dsheirer.source.mixer; import io.github.dsheirer.sample.Listener; import io.github.dsheirer.sample.adapter.ISampleAdapter; +import io.github.dsheirer.sample.buffer.ReusableBuffer; +import io.github.dsheirer.sample.buffer.ReusableBufferBroadcaster; +import io.github.dsheirer.sample.buffer.ReusableBufferQueue; import io.github.dsheirer.sample.real.RealBuffer; import io.github.dsheirer.source.RealSource; import io.github.dsheirer.source.SourceEvent; @@ -29,14 +32,14 @@ import javax.sound.sampled.AudioFormat; import javax.sound.sampled.AudioSystem; import javax.sound.sampled.LineUnavailableException; import javax.sound.sampled.TargetDataLine; -import java.util.Iterator; import java.util.concurrent.CopyOnWriteArrayList; import java.util.concurrent.atomic.AtomicBoolean; public class RealMixerSource extends RealSource { private final static Logger mLog = LoggerFactory.getLogger(RealMixerSource.class); - + private ReusableBufferBroadcaster mBufferBroadcaster = new ReusableBufferBroadcaster<>(); + private ReusableBufferQueue mReusableBufferQueue = new ReusableBufferQueue("RealMixerSource"); private long mFrequency = 0; private int mBufferSize = 16384; @@ -103,9 +106,9 @@ public class RealMixerSource extends RealSource { } - public void setListener(Listener listener) + public void setListener(Listener listener) { - mSampleListeners.add(listener); + mBufferBroadcaster.addListener(listener); /* If this is the first listener, start the reader thread */ if(!mBufferReader.isRunning()) @@ -117,38 +120,17 @@ public class RealMixerSource extends RealSource } } - public void removeListener(Listener listener) + public void removeListener(Listener listener) { - mSampleListeners.remove(listener); + mBufferBroadcaster.removeListener(listener); - /* If this is the laster listener, stop the reader thread */ + /* If this is the last listener, stop the reader thread */ if(mSampleListeners.isEmpty()) { mBufferReader.stop(); } } - public void broadcast(RealBuffer samples) - { - Iterator> it = mSampleListeners.iterator(); - - while(it.hasNext()) - { - Listener next = it.next(); - - /* if this is the last (or only) listener, send him the original - * buffer, otherwise send him a copy of the buffer */ - if(it.hasNext()) - { - next.receive(samples.copyOf()); - } - else - { - next.receive(samples); - } - } - } - public double getSampleRate() { if(mTargetDataLine != null) @@ -240,11 +222,14 @@ public class RealMixerSource extends RealSource mTargetDataLine.read(buffer, 0, buffer.length); /* Convert samples to float array */ - float[] samples = - mSampleAdapter.convert(buffer); +//TODO: change this sample adapter over to use reusable buffers and move the buffer queue into the adapter + float[] samples = mSampleAdapter.convert(buffer); /* Dispatch samples to registered listeners */ - broadcast(new RealBuffer(samples)); + ReusableBuffer reusableBuffer = mReusableBufferQueue.getBuffer(samples.length); + reusableBuffer.reloadFrom(samples, System.currentTimeMillis()); + reusableBuffer.incrementUserCount(); + mBufferBroadcaster.broadcast(reusableBuffer); } catch(Exception e) { diff --git a/src/main/java/io/github/dsheirer/source/tuner/TunerController.java b/src/main/java/io/github/dsheirer/source/tuner/TunerController.java index 4d4b3377..e533a6ad 100644 --- a/src/main/java/io/github/dsheirer/source/tuner/TunerController.java +++ b/src/main/java/io/github/dsheirer/source/tuner/TunerController.java @@ -18,7 +18,7 @@ package io.github.dsheirer.source.tuner; import io.github.dsheirer.sample.Listener; -import io.github.dsheirer.sample.buffer.IReusableBufferProvider; +import io.github.dsheirer.sample.buffer.IReusableComplexBufferProvider; import io.github.dsheirer.sample.buffer.ReusableBufferBroadcaster; import io.github.dsheirer.sample.buffer.ReusableComplexBuffer; import io.github.dsheirer.source.ISourceEventListener; @@ -33,7 +33,7 @@ import org.slf4j.Logger; import org.slf4j.LoggerFactory; public abstract class TunerController implements Tunable, ISourceEventProcessor, ISourceEventListener, - IReusableBufferProvider, Listener + IReusableComplexBufferProvider, Listener { private final static Logger mLog = LoggerFactory.getLogger(TunerController.class); protected ReusableBufferBroadcaster mReusableBufferBroadcaster = new ReusableBufferBroadcaster(); diff --git a/src/main/java/io/github/dsheirer/source/wave/ComplexWaveSource.java b/src/main/java/io/github/dsheirer/source/wave/ComplexWaveSource.java index 219a5e1b..0661b797 100644 --- a/src/main/java/io/github/dsheirer/source/wave/ComplexWaveSource.java +++ b/src/main/java/io/github/dsheirer/source/wave/ComplexWaveSource.java @@ -34,7 +34,7 @@ import java.io.File; import java.io.IOException; import java.util.Arrays; -public class ComplexWaveSource extends ComplexSource implements IControllableFileSource +public class ComplexWaveSource extends ComplexSource implements IControllableFileSource, AutoCloseable { private final static Logger mLog = LoggerFactory.getLogger(ComplexWaveSource.class); @@ -275,4 +275,26 @@ public class ComplexWaveSource extends ComplexSource implements IControllableFil { mFrameLocationListener = null; } + + /** + * Indicates if the file is a supported audio file type + */ + public static boolean supports(File file) + { + try(AudioInputStream ais = AudioSystem.getAudioInputStream(file)) + { + AudioFormat format = ais.getFormat(); + + if(format.getChannels() == 2 && format.getSampleSizeInBits() == 16) + { + return true; + } + } + catch(Exception e) + { + //Do nothing, we'll return a default of false + } + + return false; + } } diff --git a/src/main/java/io/github/dsheirer/source/wave/RealWaveSource.java b/src/main/java/io/github/dsheirer/source/wave/RealWaveSource.java index 8a94520c..646065d9 100644 --- a/src/main/java/io/github/dsheirer/source/wave/RealWaveSource.java +++ b/src/main/java/io/github/dsheirer/source/wave/RealWaveSource.java @@ -19,7 +19,8 @@ package io.github.dsheirer.source.wave; import io.github.dsheirer.sample.ConversionUtils; import io.github.dsheirer.sample.Listener; -import io.github.dsheirer.sample.real.RealBuffer; +import io.github.dsheirer.sample.buffer.ReusableBuffer; +import io.github.dsheirer.sample.buffer.ReusableBufferQueue; import io.github.dsheirer.source.IControllableFileSource; import io.github.dsheirer.source.IFrameLocationListener; import io.github.dsheirer.source.RealSource; @@ -39,11 +40,12 @@ public class RealWaveSource extends RealSource implements IControllableFileSourc { private final static Logger mLog = LoggerFactory.getLogger(RealWaveSource.class); + private ReusableBufferQueue mReusableBufferQueue = new ReusableBufferQueue("RealWaveSource"); private IFrameLocationListener mFrameLocationListener; private int mBytesPerFrame; private int mFrameCounter = 0; private long mFrequency = 0; - private Listener mListener; + private Listener mListener; private AudioInputStream mInputStream; private File mFile; @@ -52,7 +54,7 @@ public class RealWaveSource extends RealSource implements IControllableFileSourc mFile = file; } - @Override + @Override public void setSourceEventListener(Listener listener) { //Not implemented @@ -226,20 +228,22 @@ public class RealWaveSource extends RealSource implements IControllableFileSourc buffer = Arrays.copyOf(buffer, samplesRead); } - float[] samples = ConversionUtils - .convertFromSigned16BitSamples(buffer); + float[] samples = ConversionUtils.convertFromSigned16BitSamples(buffer); - mListener.receive(new RealBuffer(samples)); + ReusableBuffer reusableBuffer = mReusableBufferQueue.getBuffer(samples.length); + reusableBuffer.reloadFrom(samples, System.currentTimeMillis()); + reusableBuffer.incrementUserCount(); + + mListener.receive(reusableBuffer); } } } /** - * Registers the listener to receive sample buffers as they are read from - * the wave file + * Registers the listener to receive sample buffers as they are read from the wave file */ @Override - public void setListener(Listener listener) + public void setListener(Listener listener) { mListener = listener; } @@ -247,7 +251,8 @@ public class RealWaveSource extends RealSource implements IControllableFileSourc /** * Unregisters the listener from receiving sample buffers */ - public void removeListener(Listener listener) + @Override + public void removeListener(Listener listener) { mListener = null; } @@ -285,4 +290,26 @@ public class RealWaveSource extends RealSource implements IControllableFileSourc { mFrameLocationListener = null; } + + /** + * Indicates if the file is a supported audio file type + */ + public static boolean supports(File file) + { + try(AudioInputStream ais = AudioSystem.getAudioInputStream(file)) + { + AudioFormat format = ais.getFormat(); + + if(format.getChannels() == 1 && format.getSampleSizeInBits() == 16) + { + return true; + } + } + catch(Exception e) + { + //Do nothing, we'll return a default of false + } + + return false; + } } From e4209c8e4fee926d688839940c4ae43c4b9de10e Mon Sep 17 00:00:00 2001 From: Denny Date: Sat, 2 Jun 2018 09:31:49 -0400 Subject: [PATCH 69/87] [WIP] Updates the passport decoder to use the new LTRDecoder. Deprecates the Listener interface method in the MessageFramer to optimize for primitive booleans and reduce impact on memory. Will remove this interface once the AFSK1200 decoders are updated. --- .../github/dsheirer/bits/MessageFramer.java | 10 +- .../github/dsheirer/dsp/fsk/LTRDecoder.java | 15 +- .../dsheirer/dsp/fsk/LTRFSKDecoderLegacy.java | 253 ------------------ .../module/decode/ltrnet/LTRNetDecoder.java | 2 +- .../ltrstandard/LTRStandardDecoder.java | 2 +- .../decode/passport/PassportDecoder.java | 127 ++++----- 6 files changed, 65 insertions(+), 344 deletions(-) delete mode 100644 src/main/java/io/github/dsheirer/dsp/fsk/LTRFSKDecoderLegacy.java diff --git a/src/main/java/io/github/dsheirer/bits/MessageFramer.java b/src/main/java/io/github/dsheirer/bits/MessageFramer.java index 591858c5..3a3acc64 100644 --- a/src/main/java/io/github/dsheirer/bits/MessageFramer.java +++ b/src/main/java/io/github/dsheirer/bits/MessageFramer.java @@ -66,8 +66,7 @@ public class MessageFramer implements Listener, SyncDetectProvider mMessageAssemblers.clear(); } - @Override - public void receive(Boolean bit) + public void receive(boolean bit) { mMatcher.receive(bit); @@ -105,6 +104,13 @@ public class MessageFramer implements Listener, SyncDetectProvider } } + @Deprecated //Legacy support ... remove once all producers are converted to use receive(boolean bit) method + @Override + public void receive(Boolean bit) + { + receive(bit); + } + /** * Causes all messages currently under assembly to be forcibly * sent (ie flushed) to all registered message listeners, and diff --git a/src/main/java/io/github/dsheirer/dsp/fsk/LTRDecoder.java b/src/main/java/io/github/dsheirer/dsp/fsk/LTRDecoder.java index b4a48545..2317af18 100644 --- a/src/main/java/io/github/dsheirer/dsp/fsk/LTRDecoder.java +++ b/src/main/java/io/github/dsheirer/dsp/fsk/LTRDecoder.java @@ -15,6 +15,7 @@ ******************************************************************************/ package io.github.dsheirer.dsp.fsk; +import io.github.dsheirer.bits.MessageFramer; import io.github.dsheirer.dsp.filter.dc.IIRSinglePoleDCRemovalFilter; import io.github.dsheirer.dsp.filter.design.FilterDesignException; import io.github.dsheirer.dsp.filter.fir.FIRFilterSpecification; @@ -89,7 +90,7 @@ public class LTRDecoder implements Listener, ISyncDetectListener protected SynchronizationMonitor mSynchronizationMonitor; private IIRSinglePoleDCRemovalFilter mDCFilter = new IIRSinglePoleDCRemovalFilter(0.99999f); private RealFIRFilter2 mLowPassFilter = new RealFIRFilter2(sLowPassFilterCoefficients); - private Listener mSymbolDecisionListener; + private MessageFramer mMessageFramer; private boolean mSampleDecision; @@ -155,9 +156,9 @@ public class LTRDecoder implements Listener, ISyncDetectListener if(mSampleBuffer.hasSymbol()) { - if(mSymbolDecisionListener != null) + if(mMessageFramer != null) { - mSymbolDecisionListener.receive(mSampleBuffer.getSymbol()); + mMessageFramer.receive(mSampleBuffer.getSymbol()); } mSampleBuffer.resetAndAdjust(-mTimingErrorDetector.getError()); @@ -172,11 +173,11 @@ public class LTRDecoder implements Listener, ISyncDetectListener /** * Registers a listener to receive decoded LTR symbols. * - * @param listener to receive symbols. + * @param messageFramer to receive symbols. */ - public void setListener(Listener listener) + public void setMessageFramer(MessageFramer messageFramer) { - mSymbolDecisionListener = listener; + mMessageFramer = messageFramer; } /** @@ -184,7 +185,7 @@ public class LTRDecoder implements Listener, ISyncDetectListener */ public void removeListener() { - mSymbolDecisionListener = null; + mMessageFramer = null; } diff --git a/src/main/java/io/github/dsheirer/dsp/fsk/LTRFSKDecoderLegacy.java b/src/main/java/io/github/dsheirer/dsp/fsk/LTRFSKDecoderLegacy.java deleted file mode 100644 index 34812d2e..00000000 --- a/src/main/java/io/github/dsheirer/dsp/fsk/LTRFSKDecoderLegacy.java +++ /dev/null @@ -1,253 +0,0 @@ -/******************************************************************************* - * SDR Trunk - * Copyright (C) 2014 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 io.github.dsheirer.dsp.fsk; - -import io.github.dsheirer.dsp.filter.Filters; -import io.github.dsheirer.dsp.filter.FloatHalfBandFilter; -import io.github.dsheirer.dsp.filter.FloatHalfBandNoDecimateFilter; -import io.github.dsheirer.dsp.filter.LTRPulseShapingFilter; -import io.github.dsheirer.dsp.filter.SquaringFilter; -import io.github.dsheirer.dsp.filter.Window.WindowType; -import io.github.dsheirer.dsp.filter.cic.RealPrimeCICDecimate; -import io.github.dsheirer.dsp.filter.dc.IIRSinglePoleDCRemovalFilter; -import io.github.dsheirer.dsp.symbol.Slicer; -import io.github.dsheirer.dsp.symbol.Slicer.Output; -import io.github.dsheirer.instrument.Instrumentable; -import io.github.dsheirer.instrument.tap.Tap; -import io.github.dsheirer.instrument.tap.TapGroup; -import io.github.dsheirer.instrument.tap.stream.BinaryTap; -import io.github.dsheirer.instrument.tap.stream.FloatBufferTap; -import io.github.dsheirer.instrument.tap.stream.FloatTap; -import io.github.dsheirer.instrument.tap.stream.SymbolEventTap; -import io.github.dsheirer.sample.Listener; -import io.github.dsheirer.sample.real.RealBuffer; - -import java.util.ArrayList; -import java.util.List; - -public class LTRFSKDecoderLegacy implements Listener, Instrumentable -{ - private RealPrimeCICDecimate mCICDecimator; - - private FloatHalfBandFilter mHBFilter1; - private FloatHalfBandFilter mHBFilter2; - private FloatHalfBandFilter mHBFilter3; - private FloatHalfBandFilter mHBFilter4; - private FloatHalfBandFilter mHBFilter5; - private FloatHalfBandNoDecimateFilter mHBFilter6; - private IIRSinglePoleDCRemovalFilter mDCFilter; - private SquaringFilter mSquaringFilter; - private LTRPulseShapingFilter mPulseShaper; - private Slicer mSlicer; - - private List mAvailableTaps; - private final String TAP_F1_F2 = "LTR FSK Demod Filter1 >< Filter2"; - private final String TAP_F2_F3 = "LTR FSK Demod Filter2 >< Filter3"; - private final String TAP_F3_F4 = "LTR FSK Demod Filter3 >< Filter4"; - private final String TAP_F4_F5 = "LTR FSK Demod Filter4 >< Filter5"; - private final String TAP_F5_F6 = "LTR FSK Demod Filter5 >< Filter6"; - private final String TAP_F6_DT = "LTR FSK Demod Filter6 >< DC Filter"; - private final String TAP_CIC = "LTR FSK Demod CIC Decimator"; - private final String TAP_DT_SF1 = "LTR FSK Demod DC Filter >< Squaring Filter"; - private final String TAP_SF1_PS = "LTR FSK Demod Squaring Filter >< Pulse Shaper"; - private final String TAP_PS_SLICER = "LTR FSK Demod Pulse Shaper >< Slicer"; - private final String TAP_SYMBOL_EVENT = "LTR FSK Demod Symbol Event"; - - /** - * Implements a Logic Trunked Radio sub-audible 300 baud FSK signaling - * decoder. Expects a 48000 sample rate input. - */ - public LTRFSKDecoderLegacy() - { - mCICDecimator = new RealPrimeCICDecimate( 32, 1, 375, 60, WindowType.HAMMING, 2 ); - - mHBFilter1 = new FloatHalfBandFilter( - Filters.FIR_HALF_BAND_31T_ONE_EIGHTH_FCO, 1.1002f ); - - mHBFilter2 = new FloatHalfBandFilter( - Filters.FIR_HALF_BAND_31T_ONE_EIGHTH_FCO, 1.1002f ); - mHBFilter1.setListener( mHBFilter2 ); - - mHBFilter3 = new FloatHalfBandFilter( - Filters.FIR_HALF_BAND_31T_ONE_EIGHTH_FCO, 1.1002f ); - mHBFilter2.setListener( mHBFilter3 ); - - mHBFilter4 = new FloatHalfBandFilter( - Filters.FIR_HALF_BAND_31T_ONE_EIGHTH_FCO, 1.1002f ); - mHBFilter3.setListener( mHBFilter4 ); - - mHBFilter5 = new FloatHalfBandFilter( - Filters.FIR_HALF_BAND_31T_ONE_EIGHTH_FCO, 1.1002f ); - mHBFilter4.setListener( mHBFilter5 ); - - mHBFilter6 = new FloatHalfBandNoDecimateFilter( - Filters.FIR_HALF_BAND_31T_ONE_EIGHTH_FCO, 1.1002f ); - mHBFilter5.setListener( mHBFilter6 ); - - mDCFilter = new IIRSinglePoleDCRemovalFilter( 0.9946f ); - mHBFilter6.setListener( mDCFilter ); - - mSquaringFilter = new SquaringFilter(); - mDCFilter.setListener( mSquaringFilter ); - - mPulseShaper = new LTRPulseShapingFilter(); - mSquaringFilter.setListener( mPulseShaper ); - - mSlicer = new Slicer( Output.NORMAL, 5 ); - mPulseShaper.setListener( mSlicer ); - } - - @Override - public void receive( RealBuffer buffer ) - { - for( float sample: buffer.getSamples() ) - { - mHBFilter1.receive( sample ); - } - - mCICDecimator.receive(buffer); - } - - public void addListener( Listener listener ) - { - mSlicer.setListener( listener ); - } - - public void removeListener( Listener listener ) - { - mSlicer.removeListener( listener ); - } - - @Override - public List getTapGroups() - { - if( mAvailableTaps == null ) - { - mAvailableTaps = new ArrayList(); - - TapGroup group = new TapGroup( "LTR FSK Decoder" ); - - group.add( new FloatTap( TAP_F1_F2, 0, 1.0f ) ); - group.add( new FloatTap( TAP_F2_F3, 8, 0.5f ) ); - group.add( new FloatTap( TAP_F3_F4, 12, 0.25f ) ); - group.add( new FloatTap( TAP_F4_F5, 14, 0.125f ) ); - group.add( new FloatTap( TAP_F5_F6, 15, 0.0625f ) ); - group.add( new FloatTap( TAP_F6_DT, 31, 0.0625f ) ); - group.add( new FloatBufferTap( TAP_CIC, 31, 0.0625f ) ); - group.add( new FloatTap( TAP_DT_SF1, 31, 0.0625f ) ); - group.add( new BinaryTap( TAP_SF1_PS, 33, 0.0625f ) ); - group.add( new BinaryTap( TAP_PS_SLICER, 33, 0.0625f ) ); - group.add( new SymbolEventTap( TAP_SYMBOL_EVENT, 7, 0.0125f ) ); - - mAvailableTaps.add( group ); - } - - return mAvailableTaps; - } - - @Override - public void registerTap( Tap tap ) - { - switch( tap.getName() ) - { - case TAP_CIC: - mCICDecimator.setListener( (FloatBufferTap)tap ); - break; - case TAP_F1_F2: - mHBFilter1.setListener( (FloatTap)tap ); - ((FloatTap)tap).setListener( mHBFilter2 ); - break; - case TAP_F2_F3: - mHBFilter2.setListener( (FloatTap)tap ); - ((FloatTap)tap).setListener( mHBFilter3 ); - break; - case TAP_F3_F4: - mHBFilter3.setListener( (FloatTap)tap ); - ((FloatTap)tap).setListener( mHBFilter4 ); - break; - case TAP_F4_F5: - mHBFilter4.setListener( (FloatTap)tap ); - ((FloatTap)tap).setListener( mHBFilter5 ); - break; - case TAP_F5_F6: - mHBFilter5.setListener( (FloatTap)tap ); - ((FloatTap)tap).setListener( mHBFilter6 ); - break; - case TAP_F6_DT: - mHBFilter6.setListener( (FloatTap)tap ); - ((FloatTap)tap).setListener( mDCFilter ); - break; - case TAP_DT_SF1: - mDCFilter.setListener( (FloatTap)tap ); - ((FloatTap)tap).setListener( mSquaringFilter ); - break; - case TAP_SF1_PS: - mSquaringFilter.setListener( (BinaryTap)tap ); - ((BinaryTap)tap).setListener( mPulseShaper ); - break; - case TAP_PS_SLICER: - mPulseShaper.setListener( (BinaryTap)tap ); - ((BinaryTap)tap).setListener( mSlicer ); - break; - case TAP_SYMBOL_EVENT: - mSlicer.addTap( (SymbolEventTap)tap ); - break; - } - } - - @Override - public void unregisterTap( Tap tap ) - { - switch( tap.getName() ) - { - case TAP_CIC: - mCICDecimator.removeListener(); - break; - case TAP_F1_F2: - mHBFilter1.setListener( mHBFilter2 ); - break; - case TAP_F2_F3: - mHBFilter2.setListener( mHBFilter3 ); - break; - case TAP_F3_F4: - mHBFilter3.setListener( mHBFilter4 ); - break; - case TAP_F4_F5: - mHBFilter4.setListener( mHBFilter5 ); - break; - case TAP_F5_F6: - mHBFilter5.setListener( mHBFilter6 ); - break; - case TAP_F6_DT: - mHBFilter6.setListener( mDCFilter ); - break; - case TAP_DT_SF1: - mDCFilter.setListener( mSquaringFilter ); - break; - case TAP_SF1_PS: - mSquaringFilter.setListener( mPulseShaper ); - break; - case TAP_PS_SLICER: - mPulseShaper.setListener( mSlicer ); - break; - case TAP_SYMBOL_EVENT: - mSlicer.removeTap(); - break; - } - } -} diff --git a/src/main/java/io/github/dsheirer/module/decode/ltrnet/LTRNetDecoder.java b/src/main/java/io/github/dsheirer/module/decode/ltrnet/LTRNetDecoder.java index fa21ad92..b1d35001 100644 --- a/src/main/java/io/github/dsheirer/module/decode/ltrnet/LTRNetDecoder.java +++ b/src/main/java/io/github/dsheirer/module/decode/ltrnet/LTRNetDecoder.java @@ -50,7 +50,7 @@ public class LTRNetDecoder extends Decoder implements IReusableBufferListener, L mLTRMessageFramer = new MessageFramer(SyncPattern.LTR_STANDARD_ISW.getPattern(), LTR_NET_MESSAGE_LENGTH); } - mLTRDecoder.setListener(mLTRMessageFramer); + mLTRDecoder.setMessageFramer(mLTRMessageFramer); mLTRMessageFramer.setSyncDetectListener(mLTRDecoder); mLTRMessageProcessor = new LTRNetMessageProcessor(config.getMessageDirection(), aliasList); mLTRMessageFramer.addMessageListener(mLTRMessageProcessor); diff --git a/src/main/java/io/github/dsheirer/module/decode/ltrstandard/LTRStandardDecoder.java b/src/main/java/io/github/dsheirer/module/decode/ltrstandard/LTRStandardDecoder.java index 1f2ba7db..89043350 100644 --- a/src/main/java/io/github/dsheirer/module/decode/ltrstandard/LTRStandardDecoder.java +++ b/src/main/java/io/github/dsheirer/module/decode/ltrstandard/LTRStandardDecoder.java @@ -51,7 +51,7 @@ public class LTRStandardDecoder extends Decoder implements IReusableBufferListen mLTRMessageFramer = new MessageFramer(SyncPattern.LTR_STANDARD_ISW.getPattern(), LTR_STANDARD_MESSAGE_LENGTH); } - mLTRDecoder.setListener(mLTRMessageFramer); + mLTRDecoder.setMessageFramer(mLTRMessageFramer); mLTRMessageFramer.setSyncDetectListener(mLTRDecoder); mLTRMessageProcessor = new LTRStandardMessageProcessor(direction, aliasList); mLTRMessageFramer.addMessageListener(mLTRMessageProcessor); diff --git a/src/main/java/io/github/dsheirer/module/decode/passport/PassportDecoder.java b/src/main/java/io/github/dsheirer/module/decode/passport/PassportDecoder.java index 131d0a69..cc30e36b 100644 --- a/src/main/java/io/github/dsheirer/module/decode/passport/PassportDecoder.java +++ b/src/main/java/io/github/dsheirer/module/decode/passport/PassportDecoder.java @@ -1,110 +1,77 @@ /******************************************************************************* - * SDR Trunk - * Copyright (C) 2014 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 + * 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 + * ******************************************************************************/ package io.github.dsheirer.module.decode.passport; import io.github.dsheirer.alias.AliasList; import io.github.dsheirer.bits.MessageFramer; import io.github.dsheirer.bits.SyncPattern; -import io.github.dsheirer.dsp.fsk.LTRFSKDecoderLegacy; -import io.github.dsheirer.instrument.Instrumentable; -import io.github.dsheirer.instrument.tap.Tap; -import io.github.dsheirer.instrument.tap.TapGroup; +import io.github.dsheirer.dsp.fsk.LTRDecoder; import io.github.dsheirer.module.decode.Decoder; import io.github.dsheirer.module.decode.DecoderType; import io.github.dsheirer.module.decode.config.DecodeConfiguration; import io.github.dsheirer.sample.Listener; -import io.github.dsheirer.sample.real.IUnFilteredRealBufferListener; -import io.github.dsheirer.sample.real.RealBuffer; +import io.github.dsheirer.sample.buffer.IReusableBufferListener; +import io.github.dsheirer.sample.buffer.ReusableBuffer; -import java.util.ArrayList; -import java.util.List; - -public class PassportDecoder extends Decoder - implements IUnFilteredRealBufferListener, Instrumentable +public class PassportDecoder extends Decoder implements IReusableBufferListener, Listener { - public static final int PASSPORT_MESSAGE_LENGTH = 68; - public static final int PASSPORT_SYNC_LENGTH = 9; + public static final int PASSPORT_MESSAGE_LENGTH = 68; - private LTRFSKDecoderLegacy mPassportFSKDecoder; - private MessageFramer mPassportMessageFramer; - private PassportMessageProcessor mPassportMessageProcessor; - - private List mAvailableTaps; + private LTRDecoder mLTRDecoder; + private MessageFramer mPassportMessageFramer; + private PassportMessageProcessor mPassportMessageProcessor; /** * Passport Decoder. Decodes unfiltered (e.g. demodulated but with no DC or * audio filtering) samples and produces Passport messages. */ - public PassportDecoder(DecodeConfiguration config, AliasList aliasList ) - { - mPassportFSKDecoder = new LTRFSKDecoderLegacy(); - - mPassportMessageFramer = - new MessageFramer( SyncPattern.PASSPORT.getPattern(), - PASSPORT_MESSAGE_LENGTH ); - - mPassportFSKDecoder.addListener( mPassportMessageFramer ); - - mPassportMessageProcessor = new PassportMessageProcessor( aliasList ); - mPassportMessageFramer.addMessageListener( mPassportMessageProcessor ); - mPassportMessageProcessor.setMessageListener( getMessageListener() ); - } - - @Override - public Listener getUnFilteredRealBufferListener() - { - return mPassportFSKDecoder; - } - - @Override - public DecoderType getDecoderType() - { - return DecoderType.PASSPORT; - } - - @Override - public List getTapGroups() + public PassportDecoder(DecodeConfiguration config, AliasList aliasList) { - if( mAvailableTaps == null ) - { - mAvailableTaps = new ArrayList<>(); - - mAvailableTaps.addAll( mPassportFSKDecoder.getTapGroups() ); - } + mLTRDecoder = new LTRDecoder(PASSPORT_MESSAGE_LENGTH); - return mAvailableTaps; + mPassportMessageFramer = new MessageFramer(SyncPattern.PASSPORT.getPattern(), PASSPORT_MESSAGE_LENGTH); + + mLTRDecoder.setMessageFramer(mPassportMessageFramer); + mPassportMessageFramer.setSyncDetectListener(mLTRDecoder); + mPassportMessageProcessor = new PassportMessageProcessor(aliasList); + mPassportMessageFramer.addMessageListener(mPassportMessageProcessor); + mPassportMessageProcessor.setMessageListener(getMessageListener()); } - @Override - public void registerTap( Tap tap ) + @Override + public Listener getReusableBufferListener() { - mPassportFSKDecoder.registerTap( tap ); + return mLTRDecoder; } - @Override - public void unregisterTap( Tap tap ) + @Override + public void receive(ReusableBuffer reusableBuffer) { - mPassportFSKDecoder.unregisterTap( tap ); + mLTRDecoder.receive(reusableBuffer); } - @Override - public void reset() - { - mPassportMessageFramer.reset(); - } + @Override + public DecoderType getDecoderType() + { + return DecoderType.PASSPORT; + } + + @Override + public void reset() + { + mPassportMessageFramer.reset(); + } } From a5c85529375fd0e6cad215bdd1d3d65970bcb81e Mon Sep 17 00:00:00 2001 From: Denny Date: Sat, 9 Jun 2018 04:38:47 -0400 Subject: [PATCH 70/87] [WIP] new AFSK1200 decoder. --- .../io/github/dsheirer/bits/SyncPattern.java | 2 +- .../dsheirer/bits/SyncPatternMatcher.java | 201 ++++++----- .../buffer/BooleanAveragingBuffer.java | 138 ++++---- .../dsp/filter/design/FilterViewer.java | 34 +- .../dsp/filter/resample/RealResampler.java | 26 +- .../dsheirer/dsp/fsk/AFSK1200Decoder.java | 320 ++++++++++++++++++ .../dsp/fsk/AFSK1200DecoderInstrumented.java | 55 +++ .../io/github/dsheirer/dsp/fsk/AFSKTest.java | 122 +++++++ .../dsp/fsk/ZeroCrossingErrorDetector.java | 3 +- .../dsp/mixer/AbstractOscillator.java | 11 + .../dsheirer/dsp/mixer/IOscillator.java | 9 + .../gui/viewer/chart/DecodedSymbolChart.java | 73 ++++ .../chart/Fleetsync2SampleBufferChart.java | 171 ++++++++++ ...eetsyncZeroCrossingErrorDetectorChart.java | 132 ++++++++ ...hart.java => LTRNetSampleBufferChart.java} | 6 +- .../viewer/decoder/DecoderPaneFactory.java | 5 +- .../gui/viewer/decoder/Fleetsync2Pane.java | 185 ++++++++++ .../gui/viewer/decoder/LTRNetPane.java | 12 +- .../dsheirer/module/decode/DecoderType.java | 2 +- .../decode/fleetsync2/Fleetsync2Decoder.java | 178 +++------- .../Fleetsync2DecoderInstrumented.java | 49 +++ .../demodulate/fm/FMDemodulatorModule.java | 2 +- 22 files changed, 1384 insertions(+), 352 deletions(-) create mode 100644 src/main/java/io/github/dsheirer/dsp/fsk/AFSK1200Decoder.java create mode 100644 src/main/java/io/github/dsheirer/dsp/fsk/AFSK1200DecoderInstrumented.java create mode 100644 src/main/java/io/github/dsheirer/dsp/fsk/AFSKTest.java create mode 100644 src/main/java/io/github/dsheirer/instrument/gui/viewer/chart/DecodedSymbolChart.java create mode 100644 src/main/java/io/github/dsheirer/instrument/gui/viewer/chart/Fleetsync2SampleBufferChart.java create mode 100644 src/main/java/io/github/dsheirer/instrument/gui/viewer/chart/FleetsyncZeroCrossingErrorDetectorChart.java rename src/main/java/io/github/dsheirer/instrument/gui/viewer/chart/{SampleBufferChart.java => LTRNetSampleBufferChart.java} (96%) create mode 100644 src/main/java/io/github/dsheirer/instrument/gui/viewer/decoder/Fleetsync2Pane.java create mode 100644 src/main/java/io/github/dsheirer/module/decode/fleetsync2/Fleetsync2DecoderInstrumented.java diff --git a/src/main/java/io/github/dsheirer/bits/SyncPattern.java b/src/main/java/io/github/dsheirer/bits/SyncPattern.java index 5ef8f4be..8f589bbb 100644 --- a/src/main/java/io/github/dsheirer/bits/SyncPattern.java +++ b/src/main/java/io/github/dsheirer/bits/SyncPattern.java @@ -29,7 +29,7 @@ public enum SyncPattern false, false, false, false //0000 0x0 } ), - /* Revs (0x0A) and Sync (0x23EB) = 10101 0010001111101011 */ + /* Revs (0x0A) and Sync (0x23EB) = 0101 000 1 000 11111 0 1 0 11 */ FLEETSYNC2( new boolean[] { false, true, false, true, false, //End of bit revs diff --git a/src/main/java/io/github/dsheirer/bits/SyncPatternMatcher.java b/src/main/java/io/github/dsheirer/bits/SyncPatternMatcher.java index 3748fa4e..7adf426f 100644 --- a/src/main/java/io/github/dsheirer/bits/SyncPatternMatcher.java +++ b/src/main/java/io/github/dsheirer/bits/SyncPatternMatcher.java @@ -1,17 +1,17 @@ /******************************************************************************* * SDR Trunk * Copyright (C) 2014 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 ******************************************************************************/ @@ -20,113 +20,112 @@ package io.github.dsheirer.bits; /** * Sync pattern matcher. - * + * * Note: works for sync patterns up to 63 bits (integer size - 1 ) long. * If a 64 bit or larger sync pattern is applied, then the wrapping feature * of the Long.rotate methods will wrap the MSB around to the LSB and * corrupt the matching value. - * */ public class SyncPatternMatcher { - private long mBits = 0; - private long mMask = 0; - private long mSync = 0; - private boolean mSoftMode = false; - private int mSoftModeErrorThreshold = 0; - - public SyncPatternMatcher( boolean[] syncPattern ) - { - //Setup a bit mask of all ones the length of the sync pattern - mMask = (long)( ( Math.pow( 2, syncPattern.length ) ) - 1 ); - - //Convert the sync bits into a long value for comparison - for( int x = 0; x < syncPattern.length; x++ ) - { - if( syncPattern[ x ] ) - { - mSync += 1l << ( syncPattern.length - 1 - x ); - } - } - } - - public SyncPatternMatcher( long sync ) - { - mSync = sync; - mMask = getMask( sync ); - } + private long mBits = 0; + private long mMask = 0; + private long mSync = 0; + private boolean mSoftMode = false; + private int mSoftModeErrorThreshold = 0; - /** - * Constructs the sync pattern matcher and defines the soft mode maximum - * number of bit errors to allow in the pattern match. - * - * @param sync pattern - * @param softModeErrorThreshold - maximum bit errors (ie pattern mismatch - * positions) when considering a sync match - */ - public SyncPatternMatcher( long sync, int softModeErrorThreshold ) - { - this( sync ); - - mSoftModeErrorThreshold = softModeErrorThreshold; - } + public SyncPatternMatcher(boolean[] syncPattern) + { + //Setup a bit mask of all ones the length of the sync pattern + mMask = (long)((Math.pow(2, syncPattern.length)) - 1); - /** - * Generates an all ones mask corresponding to the sync pattern - */ - private static long getMask( long sync ) - { - long mask = sync; - - for( int x = 1; x < 64; x *= 2 ) - { - mask |= ( mask >> x ); - } - - return mask; - } - - - public void receive( boolean bit ) - { - //Left shift the previous value - mBits = Long.rotateLeft( mBits, 1 ); - - //Apply the mask to erase the previous MSB - mBits &= mMask; - - //Add in the new bit - if( bit ) - { - mBits += 1; - } - } + //Convert the sync bits into a long value for comparison + for(int x = 0; x < syncPattern.length; x++) + { + if(syncPattern[x]) + { + mSync += 1l << (syncPattern.length - 1 - x); + } + } + } - /** - * Indicates if the most recently received bit sequence matches the - * sync pattern - */ - public boolean matches() - { - if( mSoftMode ) - { - long difference = mBits ^ mSync; - - return difference == 0 || - Long.bitCount( difference ) <= mSoftModeErrorThreshold; - } - else - { - return ( mBits == mSync ); - } - } + public SyncPatternMatcher(long sync) + { + mSync = sync; + mMask = getMask(sync); + } - /** - * Sets the sync pattern matcher in soft mode which allows up to the soft - * mode threshold number of bit errors when considering a sync pattern match - */ - public void setSoftMode( boolean softMode ) - { - mSoftMode = softMode; - } + /** + * Constructs the sync pattern matcher and defines the soft mode maximum + * number of bit errors to allow in the pattern match. + * + * @param sync pattern + * @param softModeErrorThreshold - maximum bit errors (ie pattern mismatch + * positions) when considering a sync match + */ + public SyncPatternMatcher(long sync, int softModeErrorThreshold) + { + this(sync); + + mSoftModeErrorThreshold = softModeErrorThreshold; + } + + /** + * Generates an all ones mask corresponding to the sync pattern + */ + private static long getMask(long sync) + { + long mask = sync; + + for(int x = 1; x < 64; x *= 2) + { + mask |= (mask >> x); + } + + return mask; + } + + + public void receive(boolean bit) + { + //Left shift the previous value + mBits = Long.rotateLeft(mBits, 1); + + //Apply the mask to erase the previous MSB + mBits &= mMask; + + //Add in the new bit + if(bit) + { + mBits += 1; + } + } + + /** + * Indicates if the most recently received bit sequence matches the + * sync pattern + */ + public boolean matches() + { + if(mSoftMode) + { + long difference = mBits ^ mSync; + + return difference == 0 || + Long.bitCount(difference) <= mSoftModeErrorThreshold; + } + else + { + return (mBits == mSync); + } + } + + /** + * Sets the sync pattern matcher in soft mode which allows up to the soft + * mode threshold number of bit errors when considering a sync pattern match + */ + public void setSoftMode(boolean softMode) + { + mSoftMode = softMode; + } } diff --git a/src/main/java/io/github/dsheirer/buffer/BooleanAveragingBuffer.java b/src/main/java/io/github/dsheirer/buffer/BooleanAveragingBuffer.java index 5117972a..9d37d2e3 100644 --- a/src/main/java/io/github/dsheirer/buffer/BooleanAveragingBuffer.java +++ b/src/main/java/io/github/dsheirer/buffer/BooleanAveragingBuffer.java @@ -1,107 +1,89 @@ /******************************************************************************* * SDR Trunk * Copyright (C) 2014 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 io.github.dsheirer.buffer; -import java.util.Arrays; - - /** * Circular Buffer - implements a circular buffer with a method to get the * current head of the buffer, and put a new value in its place at the same * time - * + * * Also provides averaging over the elements in the buffer */ -public class BooleanAveragingBuffer { +public class BooleanAveragingBuffer +{ + private boolean[] mBuffer; + private int mBufferPointer; + private int mThreshold; + private int mTrueCount; - private boolean[] mBuffer; - private int mBufferPointer; - - public BooleanAveragingBuffer( int length ) - { - mBuffer = new boolean[ length ]; - - //Preload the array with false values - Arrays.fill( mBuffer, false ); - } - - public boolean get( boolean newValue ) - { - //Fetch current boolean value - boolean retVal = mBuffer[ mBufferPointer ]; + public BooleanAveragingBuffer(int length) + { + mBuffer = new boolean[length]; + mThreshold = length / 2; + } - //Load the new value into the buffer - put( newValue ); - - return retVal; - } + public boolean get(boolean newValue) + { + //Fetch current boolean value + boolean retVal = mBuffer[mBufferPointer]; - /** - * Loads the newValue into this buffer and adjusts the buffer pointer - * to prepare for the next get/put cycle - */ - private void put( boolean newValue ) - { - //Store the new value to the buffer - mBuffer[ mBufferPointer ] = newValue; - - //Increment the buffer pointer - mBufferPointer++; + //Load the new value into the buffer + put(newValue); - //Wrap the buffer pointer around to 0 when necessary - if( mBufferPointer >= mBuffer.length ) - { - mBufferPointer = 0; - } - } + return retVal; + } - /** - * Loads the newValue into the buffer, calculates the average - * and returns that average from this method - * - * This effectively performs low-pass filtering - */ - public boolean getAverage( boolean newValue ) - { - //Load the new value into the buffer - put( newValue ); - - int trueCount = 0; - - for( int x = 0; x < mBuffer.length; x++ ) - { - if( mBuffer[ x ] ) - { - trueCount++; - } - } + /** + * Loads the newValue into this buffer and adjusts the buffer pointer + * to prepare for the next get/put cycle + */ + private void put(boolean newValue) + { + //Store the new value to the buffer + mBuffer[mBufferPointer] = newValue; - /** - * If the number of true values in the buffer is - * more than half, return true, otherwise, false - */ - if( trueCount > (int)( mBuffer.length / 2 ) ) - { - return true; - } - else - { - return false; - } - } + //Increment the buffer pointer + mBufferPointer++; + + mBufferPointer = mBufferPointer % mBuffer.length; + } + + /** + * Loads the newValue into the buffer, calculates the average + * and returns that average from this method + * + * This effectively performs low-pass filtering + */ + public boolean getAverage(boolean newValue) + { + //Load the new value into the buffer + put(newValue); + + mTrueCount = 0; + + for(int x = 0; x < mBuffer.length; x++) + { + if(mBuffer[x]) + { + mTrueCount++; + } + } + + return mTrueCount > mThreshold; + } } diff --git a/src/main/java/io/github/dsheirer/dsp/filter/design/FilterViewer.java b/src/main/java/io/github/dsheirer/dsp/filter/design/FilterViewer.java index 821c432b..9cf838eb 100644 --- a/src/main/java/io/github/dsheirer/dsp/filter/design/FilterViewer.java +++ b/src/main/java/io/github/dsheirer/dsp/filter/design/FilterViewer.java @@ -51,26 +51,26 @@ public class FilterViewer extends Application // .stopBandRipple(0.03) //Approximately 60 dB attenuation // .build(); // - FIRFilterSpecification specification = FIRFilterSpecification.highPassBuilder() - .sampleRate(8000) - .stopBandCutoff(200) - .stopBandAmplitude(0.0) - .stopBandRipple(0.025) - .passBandStart(300) - .passBandAmplitude(1.0) - .passBandRipple(0.01) - .build(); - -// FIRFilterSpecification specification = FIRFilterSpecification.bandPassBuilder() +// FIRFilterSpecification specification = FIRFilterSpecification.highPassBuilder() // .sampleRate(8000) -// .stopFrequency1(200) -// .passFrequencyBegin(300) -// .passFrequencyEnd(3800) -// .stopFrequency2(3900) -// .stopRipple(0.01) -// .passRipple(0.01) +// .stopBandCutoff(200) +// .stopBandAmplitude(0.0) +// .stopBandRipple(0.025) +// .passBandStart(300) +// .passBandAmplitude(1.0) +// .passBandRipple(0.01) // .build(); + FIRFilterSpecification specification = FIRFilterSpecification.bandPassBuilder() + .sampleRate(8000) + .stopFrequency1(1000) + .passFrequencyBegin(1100) + .passFrequencyEnd(1900) + .stopFrequency2(2000) + .stopRipple(0.000001) + .passRipple(0.00001) + .build(); + float[] taps = null; // try diff --git a/src/main/java/io/github/dsheirer/dsp/filter/resample/RealResampler.java b/src/main/java/io/github/dsheirer/dsp/filter/resample/RealResampler.java index 39238e8c..496c786c 100644 --- a/src/main/java/io/github/dsheirer/dsp/filter/resample/RealResampler.java +++ b/src/main/java/io/github/dsheirer/dsp/filter/resample/RealResampler.java @@ -33,7 +33,7 @@ public class RealResampler private ReusableBufferQueue mReusableBufferQueue = new ReusableBufferQueue("RealResampler"); private Resampler mResampler; private Listener mResampledListener; - private BufferManager mBufferManager = new BufferManager(); + private BufferManager mBufferManager; private double mResampleFactor; /** @@ -41,10 +41,11 @@ public class RealResampler * @param inputRate * @param outputRate */ - public RealResampler(double inputRate, double outputRate) + public RealResampler(double inputRate, double outputRate, int inputBufferSize, int outputBufferSize) { mResampleFactor = outputRate / inputRate; mResampler = new Resampler(true, mResampleFactor, mResampleFactor); + mBufferManager = new BufferManager(inputBufferSize, outputBufferSize); } /** @@ -72,16 +73,17 @@ public class RealResampler */ public class BufferManager implements SampleBuffers { - private int mBufferLength = 8000; - private FloatBuffer mInputBuffer = ByteBuffer.allocate(mBufferLength).asFloatBuffer(); - private FloatBuffer mOutputBuffer = ByteBuffer.allocate(mBufferLength).asFloatBuffer(); + private int mOutputBufferSize; + private FloatBuffer mInputBuffer; + private FloatBuffer mOutputBuffer; - /** - * Current length of the input and output buffers - */ - public int getBufferLength() + public BufferManager(int inputBufferSize, int outputBufferSize) { - return mBufferLength; + mOutputBufferSize = outputBufferSize; + + //The size of the underlying byte buffer in bytes is 4 times the requested input buffer size in floats + mInputBuffer = ByteBuffer.allocate(inputBufferSize * 4).asFloatBuffer(); + mOutputBuffer = ByteBuffer.allocate(inputBufferSize * 4).asFloatBuffer(); } /** @@ -124,9 +126,9 @@ public class RealResampler { mOutputBuffer.put(samples, offset, length); - while(mOutputBuffer.position() > 1000) + while(mOutputBuffer.position() > mOutputBufferSize) { - ReusableBuffer outputBuffer = mReusableBufferQueue.getBuffer(1000); + ReusableBuffer outputBuffer = mReusableBufferQueue.getBuffer(mOutputBufferSize); outputBuffer.incrementUserCount(); mOutputBuffer.flip(); diff --git a/src/main/java/io/github/dsheirer/dsp/fsk/AFSK1200Decoder.java b/src/main/java/io/github/dsheirer/dsp/fsk/AFSK1200Decoder.java new file mode 100644 index 00000000..be69104e --- /dev/null +++ b/src/main/java/io/github/dsheirer/dsp/fsk/AFSK1200Decoder.java @@ -0,0 +1,320 @@ +/******************************************************************************* + * 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 + * + ******************************************************************************/ +package io.github.dsheirer.dsp.fsk; + +import io.github.dsheirer.bits.MessageFramer; +import io.github.dsheirer.buffer.BooleanAveragingBuffer; +import io.github.dsheirer.buffer.FloatAveragingBuffer; +import io.github.dsheirer.dsp.filter.design.FilterDesignException; +import io.github.dsheirer.dsp.filter.fir.FIRFilterSpecification; +import io.github.dsheirer.dsp.filter.fir.real.RealFIRFilter2; +import io.github.dsheirer.dsp.filter.fir.remez.RemezFIRFilterDesigner; +import io.github.dsheirer.dsp.filter.resample.RealResampler; +import io.github.dsheirer.dsp.mixer.IOscillator; +import io.github.dsheirer.dsp.mixer.Oscillator; +import io.github.dsheirer.dsp.symbol.ISyncDetectListener; +import io.github.dsheirer.sample.Listener; +import io.github.dsheirer.sample.buffer.ReusableBuffer; +import org.slf4j.Logger; +import org.slf4j.LoggerFactory; + +/** + * Binary Frequency Shift Keying (2FSK) correlation decoder. Provides normal or inverted decoded output. + */ +public class AFSK1200Decoder implements Listener, ISyncDetectListener, ISyncStateListener +{ + private final static Logger mLog = LoggerFactory.getLogger(AFSK1200Decoder.class); + + public enum Output + { + NORMAL, INVERTED + } + + public static final double TARGET_SAMPLE_RATE = 7200.0d; + public static final int SAMPLES_PER_SYMBOL = 6; //TARGET_SAMPLE_RATE / 1200; + public static final float COARSE_TIMING_GAIN = 1.0f / 3.0f; + protected static final float MEDIUM_TIMING_GAIN = 1.0f / 4.0f; + protected static final float FINE_TIMING_GAIN = 1.0f / 5.0f; + + private static float[] sBandPassFilterCoefficients; + + static + { + FIRFilterSpecification specification = FIRFilterSpecification.bandPassBuilder() + .sampleRate(8000) + .stopFrequency1(1000) + .passFrequencyBegin(1100) + .passFrequencyEnd(1900) + .stopFrequency2(2000) + .stopRipple(0.000001) + .passRipple(0.00001) + .build(); + + try + { + RemezFIRFilterDesigner designer = new RemezFIRFilterDesigner(specification); + + if(designer.isValid()) + { + sBandPassFilterCoefficients = designer.getImpulseResponse(); + } + } + catch(FilterDesignException fde) + { + mLog.error("Filter design error", fde); + } + } + + private Correlator mCorrelator1200 = new Correlator(TARGET_SAMPLE_RATE, 1200.0, SAMPLES_PER_SYMBOL); + private Correlator mCorrelator1800 = new Correlator(TARGET_SAMPLE_RATE, 1800.0, SAMPLES_PER_SYMBOL); + private float[] mCorrelationValues1200; + private float[] mCorrelationValues1800; + + protected boolean mNormalOutput; + protected float mSymbolTimingGain = COARSE_TIMING_GAIN; + protected SampleBuffer mSampleBuffer; + protected ZeroCrossingErrorDetector mTimingErrorDetector = new ZeroCrossingErrorDetector(SAMPLES_PER_SYMBOL); + protected SynchronizationMonitor mSynchronizationMonitor; + private RealFIRFilter2 mBandPassFilter = new RealFIRFilter2(sBandPassFilterCoefficients); + protected MessageFramer mMessageFramer; + private boolean mSampleDecision; + + //Resample to an integral of the baud rate 1200 baud * 6 samples per symbol = 7200.0 Hertz + private RealResampler mResampler = new RealResampler(8000.0, TARGET_SAMPLE_RATE, 2000, 1); + + public AFSK1200Decoder(int messageLength, SampleBuffer sampleBuffer, ZeroCrossingErrorDetector detector, boolean invertOutput) + { + mSynchronizationMonitor = new SynchronizationMonitor(messageLength); + mSynchronizationMonitor.setListener(this); + mTimingErrorDetector = detector; + mSampleBuffer = sampleBuffer; + mSampleBuffer.setTimingGain(mSymbolTimingGain); + mResampler.setListener(new Decoder(SAMPLES_PER_SYMBOL)); + mNormalOutput = !invertOutput; + } + + public AFSK1200Decoder(int messageLength, boolean invertOutput) + { + this(messageLength, new SampleBuffer(SAMPLES_PER_SYMBOL, COARSE_TIMING_GAIN), + new ZeroCrossingErrorDetector(SAMPLES_PER_SYMBOL), invertOutput); + } + + /** + * Disposes of all references to prepare for garbage collection + */ + public void dispose() + { + } + + /** + * Implements the ISyncDetectedListener interface to be notified of message sync detection events. + * + * This allows the internal timing error detector to adjust symbol timing error gain levels according to the + * message synchronization state to quickly adjust to initial signal streams or to reduce gain levels once + * synchronization has been achieved. + */ + @Override + public void syncDetected() + { + mSynchronizationMonitor.syncDetected(); + } + + /** + * Processes the buffer samples by converting all samples to boolean values reflecting if the sample value is + * greater than zero (or not). Average symbol timing offset is calculated for the full buffer and the offset is + * adjusted and then each of the symbols are decoded using a simple majority decision. + * + * @param buffer containing 8.0 kHz unfiltered FM demodulated audio samples with sub-audible LTR signalling. + */ + @Override + public void receive(ReusableBuffer buffer) + { +// ReusableBuffer bandPassFiltered = mBandPassFilter.filter(buffer); +// mResampler.resample(bandPassFiltered); + mResampler.resample(buffer); + } + + protected void dispatch(boolean symbol) + { + if(mMessageFramer != null) + { + mMessageFramer.receive(mNormalOutput ? symbol : !symbol); + } + } + + /** + * Registers a listener to receive decoded LTR symbols. + * + * @param messageFramer to receive symbols. + */ + public void setMessageFramer(MessageFramer messageFramer) + { + mMessageFramer = messageFramer; + } + + /** + * Removes the symbol listener from receiving decoded LTR symbols. + */ + public void removeListener() + { + mMessageFramer = null; + } + + + /** + * Implements the ISyncStateListener interface to receive synchronization state events and adjust the symbol timing + * error gain levels on the internal timing error detector. + * + * Gain levels are defined relative to a unit gain of 1.0 over a number of symbol periods. LTR uses an initial + * ramp-up symbol reversal pattern of 0101 which provides three zero crossing detection opportunities for the timing + * error detector, therefore we react to a COARSE gain state to average timing error over 3 symbols. + * + * @param syncState from an external synchronization state monitor + */ + @Override + public void setSyncState(SyncState syncState) + { + switch(syncState) + { + case FINE: + mSymbolTimingGain = FINE_TIMING_GAIN; + break; + case MEDIUM: + mSymbolTimingGain = MEDIUM_TIMING_GAIN; + break; + case COARSE: + mSymbolTimingGain = COARSE_TIMING_GAIN; + break; + default: + throw new IllegalArgumentException("Unrecognized sync state level: " + syncState.name()); + } + + mSampleBuffer.setTimingGain(mSymbolTimingGain); + } + + public class Decoder implements Listener + { + private BooleanAveragingBuffer mSampleAveragingBuffer; + + private Decoder(int samplesPerSymbol) + { + mSampleAveragingBuffer = new BooleanAveragingBuffer(samplesPerSymbol); + } + + @Override + public void receive(ReusableBuffer buffer) + { + //Calculate correlation values against each 1200/1800 reference signal + mCorrelationValues1200 = mCorrelator1200.process(buffer); + mCorrelationValues1800 = mCorrelator1800.process(buffer); + + buffer.decrementUserCount(); + + for(int x = 0; x < mCorrelationValues1200.length; x++) + { + //1200 = Mark (1) and 1800 = Space (0) + mSampleDecision = mCorrelationValues1200[x] > mCorrelationValues1800[x]; +// mSampleDecision = mSampleAveragingBuffer.getAverage(mSampleDecision); + mSampleBuffer.receive(mSampleDecision); + mTimingErrorDetector.receive(mSampleDecision); + + if(mSampleBuffer.hasSymbol()) + { + dispatch(mSampleBuffer.getSymbol()); + +// mSampleBuffer.resetAndAdjust(-mTimingErrorDetector.getError()); + mSampleBuffer.resetAndAdjust(0.0f); + + mSynchronizationMonitor.increment(); + } + } + } + } + + /** + * Generates a correlation value for each FM demodulated signal against an established reference frequency, + * correlated and averaged over separate periods relative to the samples per symbol period. + */ + public class Correlator + { + private FloatAveragingBuffer mAveragingBuffer; + private float[] mReferenceSamples; + private float[] mDemodulatedSamples; + private float[] mCorrelationValues; + private float mCorrelationAccumulator; + + /** + * Constructs a correlator instance for the specified parameters + * @param sampleRate of the incoming sample stream. Note: this should be an integral of the symbol rate. + * @param frequency of the mark or space symbol to test for correlation + * @param samplesPerSymbol to determine the correlation and averaging periods + */ + public Correlator(double sampleRate, double frequency, int samplesPerSymbol) + { + //Adjust correlation period and averaging period to provide optimal correlation for a given samples/symbol. + //These values are optimized for 6 samples per symbol with averaging over 7 samples and correlation over an + //8 sample period. + int correlationPeriod = samplesPerSymbol + 2; + + mAveragingBuffer = new FloatAveragingBuffer(samplesPerSymbol + 1); + + mDemodulatedSamples = new float[correlationPeriod]; + + IOscillator referenceGenerator = new Oscillator(frequency, sampleRate); + mReferenceSamples = referenceGenerator.generateReal(correlationPeriod); + } + + /** + * Processes each sample in the incoming sample buffer against a generated reference sample set for one symbol + * period to derive a correlation value that is in-turn averaged over one symbol period. + * + * @param reusableBuffer containing FM demodulated samples + * @return a reusable array of correlation values for each sample + */ + public float[] process(ReusableBuffer reusableBuffer) + { + if(mCorrelationValues == null || mCorrelationValues.length != reusableBuffer.getSampleCount()) + { + mCorrelationValues = new float[reusableBuffer.getSampleCount()]; + } + + float[] samples = reusableBuffer.getSamples(); + + int y; + + for(int x = 0; x < samples.length; x++) + { + //Note: we're using array copy and dot product structures that the JRE can promote to SIMD intrinsics + //when the host processor provides SIMD capabilities + System.arraycopy(mDemodulatedSamples, 1, mDemodulatedSamples, 0, mDemodulatedSamples.length - 1); + mDemodulatedSamples[mDemodulatedSamples.length - 1] = samples[x]; + + mCorrelationAccumulator = 0.0f; + + for(y = 0; y < mDemodulatedSamples.length; y++) + { + mCorrelationAccumulator += mDemodulatedSamples[y] * mReferenceSamples[y]; + } + + //Add the absolute value of correlation accumulator value to the averaging buffer and store the current + // average as the correlation value for this sample + mCorrelationValues[x] = mAveragingBuffer.get(Math.abs(mCorrelationAccumulator)); + } + + return mCorrelationValues; + } + } +} diff --git a/src/main/java/io/github/dsheirer/dsp/fsk/AFSK1200DecoderInstrumented.java b/src/main/java/io/github/dsheirer/dsp/fsk/AFSK1200DecoderInstrumented.java new file mode 100644 index 00000000..659918b7 --- /dev/null +++ b/src/main/java/io/github/dsheirer/dsp/fsk/AFSK1200DecoderInstrumented.java @@ -0,0 +1,55 @@ +/******************************************************************************* + * 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 + * + ******************************************************************************/ +package io.github.dsheirer.dsp.fsk; + +import io.github.dsheirer.sample.Broadcaster; +import io.github.dsheirer.sample.Listener; + +public class AFSK1200DecoderInstrumented extends AFSK1200Decoder +{ + private Broadcaster mSymbolBroadcaster = new Broadcaster<>(); + + public AFSK1200DecoderInstrumented(int messageLength, SampleBuffer sampleBuffer, ZeroCrossingErrorDetector detector, boolean invertOutput) + { + super(messageLength, new SampleBufferInstrumented(SAMPLES_PER_SYMBOL, COARSE_TIMING_GAIN), + new ZeroCrossingErrorDetectorInstrumented(SAMPLES_PER_SYMBOL), invertOutput); + } + + public SampleBufferInstrumented getSampleBuffer() + { + return (SampleBufferInstrumented)mSampleBuffer; + } + + public ZeroCrossingErrorDetectorInstrumented getErrorDetector() + { + return (ZeroCrossingErrorDetectorInstrumented)mTimingErrorDetector; + } + + protected void dispatch(boolean symbol) + { + if(mMessageFramer != null) + { + mMessageFramer.receive(mNormalOutput ? symbol : !symbol); + } + + mSymbolBroadcaster.broadcast(symbol); + } + + public void addListener(Listener listener) + { + mSymbolBroadcaster.addListener(listener); + } +} diff --git a/src/main/java/io/github/dsheirer/dsp/fsk/AFSKTest.java b/src/main/java/io/github/dsheirer/dsp/fsk/AFSKTest.java new file mode 100644 index 00000000..d34a3f62 --- /dev/null +++ b/src/main/java/io/github/dsheirer/dsp/fsk/AFSKTest.java @@ -0,0 +1,122 @@ +/******************************************************************************* + * 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 + * + ******************************************************************************/ +package io.github.dsheirer.dsp.fsk; + +import io.github.dsheirer.buffer.FloatAveragingBuffer; +import io.github.dsheirer.dsp.mixer.IOscillator; +import io.github.dsheirer.dsp.mixer.Oscillator; +import io.github.dsheirer.sample.complex.Complex; +import org.slf4j.Logger; +import org.slf4j.LoggerFactory; + +import java.text.DecimalFormat; + +public class AFSKTest +{ + private final static Logger mLog = LoggerFactory.getLogger(AFSK1200Decoder.class); + + private static final DecimalFormat df = new DecimalFormat("00.000"); + + public static String format(double value) + { + if(value > 0) + { + return " " + df.format(value); + } + else + { + return df.format(value); + } + } + + public static float[] generateAFSK1200(boolean[] symbols, int samplesPerSymbol, float gain) + { + float[] samples = new float[symbols.length * samplesPerSymbol]; + + Oscillator oscillator = new Oscillator(1200.0, (double)samplesPerSymbol * 1200); + + for(int x = 0; x < symbols.length; x++) + { + oscillator.setFrequency(symbols[x] ? 1200.0 : 1800.0); + + for(int y = 0; y < samplesPerSymbol; y++) + { + Complex sample = new Complex(oscillator.inphase(), oscillator.quadrature()); + sample.multiply(gain); + samples[x * samplesPerSymbol + y] = sample.inphase(); + oscillator.rotate(); + } + } + + return samples; + } + + public static void main(String[] args) + { + int sampleSize = 6; + + FloatAveragingBuffer avg1200 = new FloatAveragingBuffer(sampleSize); + FloatAveragingBuffer avg1800 = new FloatAveragingBuffer(sampleSize); + + IOscillator oscillator1200 = new Oscillator(1200.0, 7200.0); + IOscillator oscillator1800 = new Oscillator(1800.0, 7200.0); + oscillator1200.generateReal(1); + oscillator1800.generateReal(1); + + float[] reference1200 = oscillator1200.generateReal(sampleSize); + float[] reference1800 = oscillator1800.generateReal(sampleSize); + + boolean[] symbols = new boolean[]{true,false,true,false,true,false,true,false,true,true,true,false,true,false, + false,false,true,false,false,false,false,true,false,true,true,true,true,false,true,true,false,true,false, + true,false,true,false,true,false,true,false,true,false,true,false,true,false,true,false,true,false,true, + false,true,false,true,false,true,false,true,false,true,false,true,false,true,false,true,false,true,false, + true,false,true,false,true,false,true,false,true,false,true,false,true,false,true,false,true,false,true, + false,true,false,true,false,true,false,true,false,true,false,true,false,true,false,true,false,true,false, + true,false,true,false,true,false,true,false,true,false,true,false,true,false,true,false,true,false,true, + false,true,false,true,false,true,false,true,false,true,false,true,false,true,false,true,false,true,false, + true,false,false,false,false,false,false,false,false,false,false,false,false,false,false,false,false, + false,false,false,false,false,false,false,false,false,false,false,false,false,false,false,false,false}; + + float[] samples = generateAFSK1200(symbols, 6, 0.5f); + + mLog.debug("Length:" + samples.length); + + int offset = 0; + + float correlation1200 = 0.0f; + float correlation1800 = 0.0f; + + while(offset + sampleSize < samples.length) + { + correlation1200 = 0.0f; + correlation1800 = 0.0f; + + for(int x = 0; x < sampleSize; x++) + { + correlation1200 += reference1200[x] * samples[x + offset]; + correlation1800 += reference1800[x] * samples[x + offset]; + } + + correlation1200 = avg1200.get(Math.abs(correlation1200)); + correlation1800 = avg1800.get(Math.abs(correlation1800)); + + mLog.debug(offset + " 1200:" + format(correlation1200) + " 1800:" + format(correlation1800) + + " Decision: " + (correlation1200 > correlation1800 ? "0" : "1")); + + offset++; + } + } +} diff --git a/src/main/java/io/github/dsheirer/dsp/fsk/ZeroCrossingErrorDetector.java b/src/main/java/io/github/dsheirer/dsp/fsk/ZeroCrossingErrorDetector.java index c93a09a1..ded627d8 100644 --- a/src/main/java/io/github/dsheirer/dsp/fsk/ZeroCrossingErrorDetector.java +++ b/src/main/java/io/github/dsheirer/dsp/fsk/ZeroCrossingErrorDetector.java @@ -46,7 +46,8 @@ public class ZeroCrossingErrorDetector public ZeroCrossingErrorDetector(float samplesPerSymbol) { mBuffer = new boolean[(int)Math.ceil(samplesPerSymbol)]; - mZeroCrossingIdeal = mBuffer.length - 1.5f - (samplesPerSymbol / 2.0f); +// mZeroCrossingIdeal = mBuffer.length - 1.5f - (samplesPerSymbol / 2.0f); //Preserve this for LTR + mZeroCrossingIdeal = (samplesPerSymbol / 2.0f); } /** diff --git a/src/main/java/io/github/dsheirer/dsp/mixer/AbstractOscillator.java b/src/main/java/io/github/dsheirer/dsp/mixer/AbstractOscillator.java index 3f7df3f8..fa012fe4 100644 --- a/src/main/java/io/github/dsheirer/dsp/mixer/AbstractOscillator.java +++ b/src/main/java/io/github/dsheirer/dsp/mixer/AbstractOscillator.java @@ -15,6 +15,7 @@ ******************************************************************************/ package io.github.dsheirer.dsp.mixer; +import io.github.dsheirer.sample.buffer.ReusableBuffer; import io.github.dsheirer.sample.buffer.ReusableComplexBuffer; import io.github.dsheirer.sample.complex.Complex; import org.slf4j.Logger; @@ -178,4 +179,14 @@ public abstract class AbstractOscillator implements IOscillator reusableComplexBuffer.reloadFrom(mSampleBuffer, System.currentTimeMillis()); } + + /** + * Generates enough real samples to fill the reusable buffer + * @param reusableBuffer to fill with real samples + */ + @Override + public void generateReal(ReusableBuffer reusableBuffer) + { + reusableBuffer.reloadFrom(generateReal(reusableBuffer.getSampleCount()), System.currentTimeMillis()); + } } diff --git a/src/main/java/io/github/dsheirer/dsp/mixer/IOscillator.java b/src/main/java/io/github/dsheirer/dsp/mixer/IOscillator.java index 18f522d5..2d0b4930 100644 --- a/src/main/java/io/github/dsheirer/dsp/mixer/IOscillator.java +++ b/src/main/java/io/github/dsheirer/dsp/mixer/IOscillator.java @@ -15,6 +15,7 @@ ******************************************************************************/ package io.github.dsheirer.dsp.mixer; +import io.github.dsheirer.sample.buffer.ReusableBuffer; import io.github.dsheirer.sample.buffer.ReusableComplexBuffer; /** @@ -97,6 +98,14 @@ public interface IOscillator */ void generateComplex(ReusableComplexBuffer reusableComplexBuffer); + /** + * Generates real samples and fills the reusable buffer using the current system time in millis for + * the update timestamp. + * + * @param reusableBuffer to fill with complex samples + */ + void generateReal(ReusableBuffer reusableBuffer); + /** * Mixes (heterodynes) the complex sample array using the current settings of this oscillator. * @param complexSamples to mix to a new frequency diff --git a/src/main/java/io/github/dsheirer/instrument/gui/viewer/chart/DecodedSymbolChart.java b/src/main/java/io/github/dsheirer/instrument/gui/viewer/chart/DecodedSymbolChart.java new file mode 100644 index 00000000..f2b20124 --- /dev/null +++ b/src/main/java/io/github/dsheirer/instrument/gui/viewer/chart/DecodedSymbolChart.java @@ -0,0 +1,73 @@ +/******************************************************************************* + * 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 + * + ******************************************************************************/ +package io.github.dsheirer.instrument.gui.viewer.chart; + +import io.github.dsheirer.sample.Listener; +import javafx.collections.FXCollections; +import javafx.collections.ObservableList; +import javafx.scene.chart.LineChart; +import javafx.scene.chart.NumberAxis; +import org.slf4j.Logger; +import org.slf4j.LoggerFactory; + +public class DecodedSymbolChart extends LineChart implements Listener +{ + private final static Logger mLog = LoggerFactory.getLogger(DecodedSymbolChart.class); + + private ObservableList> mSymbols = FXCollections.observableArrayList(); + private Series mSymbolSeries = new Series<>("Samples", mSymbols); + private boolean[] mSymbolBuffer; + private int mSymbolBufferPointer; + + public DecodedSymbolChart(int length) + { + super(new NumberAxis("Symbols", 0, length, 5), + new NumberAxis("Value", -1.0, 1.0, 0.25)); + + ObservableList observableList = FXCollections.observableArrayList(mSymbolSeries); + setData(observableList); + + for(int x = 0; x < length; x++) + { + Data symbol = new Data<>(x, -0.5f); + mSymbols.add(symbol); + } + + mSymbolBuffer = new boolean[length]; + } + + @Override + public void receive(Boolean symbol) + { + mSymbolBuffer[mSymbolBufferPointer++] = symbol; + mSymbolBufferPointer %= mSymbolBuffer.length; + + update(); + } + + private void update() + { + for(int x = 0; x < mSymbolBuffer.length; x++) + { + Data symbol = mSymbols.get(x); + + int offset = x + mSymbolBufferPointer; + offset %= mSymbolBuffer.length; + + symbol.setYValue(mSymbolBuffer[offset] ? 0.5f : -0.5f); + } + } +} diff --git a/src/main/java/io/github/dsheirer/instrument/gui/viewer/chart/Fleetsync2SampleBufferChart.java b/src/main/java/io/github/dsheirer/instrument/gui/viewer/chart/Fleetsync2SampleBufferChart.java new file mode 100644 index 00000000..bb3a8ff2 --- /dev/null +++ b/src/main/java/io/github/dsheirer/instrument/gui/viewer/chart/Fleetsync2SampleBufferChart.java @@ -0,0 +1,171 @@ +/******************************************************************************* + * 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 + * + ******************************************************************************/ +package io.github.dsheirer.instrument.gui.viewer.chart; + +import io.github.dsheirer.module.decode.fleetsync2.Fleetsync2DecoderInstrumented; +import javafx.beans.value.ChangeListener; +import javafx.beans.value.ObservableValue; +import javafx.collections.FXCollections; +import javafx.collections.ObservableList; +import javafx.scene.chart.LineChart; +import javafx.scene.chart.NumberAxis; +import javafx.scene.layout.StackPane; +import org.slf4j.Logger; +import org.slf4j.LoggerFactory; + +public class Fleetsync2SampleBufferChart extends LineChart +{ + private final static Logger mLog = LoggerFactory.getLogger(Fleetsync2SampleBufferChart.class); + private ObservableList> mSamples = FXCollections.observableArrayList(); + private Series mSampleSeries = new Series<>("Samples", mSamples); + + private ObservableList> mSamplePoints = FXCollections.observableArrayList(); + private Series mSamplePointSeries = new Series<>("Pointers", mSamplePoints); + + private ObservableList> mSymbolSamples = FXCollections.observableArrayList(); + private Series mSymbolSamplesSeries = new Series<>("Next Symbol Samples", mSymbolSamples); + + private ObservableList> mSymbol = FXCollections.observableArrayList(); + private Series mSymbolSeries = new Series<>("Previous Symbol", mSymbol); + + private ObservableList> mZeroCrossing = FXCollections.observableArrayList(); + private Series mZeroCrossingSeries = new Series<>("Zero Crossing", mZeroCrossing); + + private Fleetsync2DecoderInstrumented mFleetsync2DecoderInstrumented; + + public Fleetsync2SampleBufferChart(Fleetsync2DecoderInstrumented decoder, int length) + { + super(new NumberAxis("Samples", 0, length, 5), + new NumberAxis("Value", -1.0, 1.0, 0.25)); + + ObservableList observableList = FXCollections.observableArrayList(mSampleSeries, mSamplePointSeries, + mSymbolSamplesSeries, mSymbolSeries, mZeroCrossingSeries); + + setData(observableList); + + mFleetsync2DecoderInstrumented = decoder; + decoder.bufferCount.addListener(new BufferChangeListener()); + decoder.getAFSK1200Decoder().getErrorDetector().timingError.addListener(new ErrorChangeListener()); + } + + public void setBuffer(boolean[] buffer) + { + while(mSamples.size() < buffer.length) + { + Data sample = new Data<>(mSamples.size(), 0.0f); + mSamples.add(sample); + + //Turn off the data point marker symbols - yes I know this is inefficient + for(Data data: mSampleSeries.getData()) + { + StackPane stackPane = (StackPane)data.getNode(); + stackPane.setVisible(false); + } + } + + for(int x = 0; x < buffer.length; x++) + { + Data sample = mSamples.get(x); + + sample.setYValue(buffer[x] ? 0.5f : -0.5f); + } + } + + public class BufferChangeListener implements ChangeListener + { + @Override + public void changed(ObservableValue observable, Object oldValue, Object newValue) + { + setBuffer(mFleetsync2DecoderInstrumented.getAFSK1200Decoder().getSampleBuffer().getDelayLine()); + + while(mSymbol.size() < 2) + { + Data sample = new Data<>(0.0, -0.55); + mSymbol.add(sample); + } + + while(mZeroCrossing.size() < 2) + { + Data sample = new Data<>(0.0, -0.65); + mZeroCrossing.add(sample); + } + + while(mSamplePoints.size() < 2) + { + Data sample = new Data<>(0.0, -0.75); + mSamplePoints.add(sample); + } + + while(mSymbolSamples.size() < 2) + { + Data sample = new Data<>(0.0, -0.85); + mSymbolSamples.add(sample); + } + + int pointer1 = mFleetsync2DecoderInstrumented.getAFSK1200Decoder().getSampleBuffer().getDelayLinePointer(); + int pointer2 = mFleetsync2DecoderInstrumented.getAFSK1200Decoder().getSampleBuffer().getDelayLineSecondPointer(); + + mSamplePoints.get(0).setXValue(pointer1); + mSamplePoints.get(1).setXValue(pointer2); + + float samplesRemaining = mFleetsync2DecoderInstrumented.getAFSK1200Decoder().getSampleBuffer().getMidSymbolSamplingPoint(); + float samplesPerSymbol = mFleetsync2DecoderInstrumented.getAFSK1200Decoder().getSampleBuffer().getSamplesPerSymbol(); + + float start = pointer1 + samplesRemaining; + float end = start + samplesPerSymbol; + + mSymbolSamples.get(0).setXValue(start); + mSymbolSamples.get(1).setXValue(end); + + boolean symbol = mFleetsync2DecoderInstrumented.getAFSK1200Decoder().getSampleBuffer().getLastSymbol(); + int symbolStart = mFleetsync2DecoderInstrumented.getAFSK1200Decoder().getSampleBuffer().getSymbolStart(); + int symbolEnd = mFleetsync2DecoderInstrumented.getAFSK1200Decoder().getSampleBuffer().getSymbolEnd(); + + mSymbol.get(0).setXValue(symbolStart); + mSymbol.get(0).setYValue(symbol ? 0.55f : -0.55f); + mSymbol.get(1).setXValue(symbolEnd); + mSymbol.get(1).setYValue(symbol ? 0.55f : -0.55f); + } + } + + public class ErrorChangeListener implements ChangeListener + { + @Override + public void changed(ObservableValue observable, Object oldValue, Object newValue) + { + while(mZeroCrossing.size() < 2) + { + Data sample = new Data<>(0.0, -0.65); + mZeroCrossing.add(sample); + } + + int pointer1 = mFleetsync2DecoderInstrumented.getAFSK1200Decoder().getSampleBuffer().getDelayLinePointer(); + int pointer2 = mFleetsync2DecoderInstrumented.getAFSK1200Decoder().getSampleBuffer().getDelayLineSecondPointer(); + + float zeroCrossingIdeal = mFleetsync2DecoderInstrumented.getAFSK1200Decoder().getErrorDetector().getZeroCrossingIdeal(); + float detectedZeroCrossing = zeroCrossingIdeal - ((Number)newValue).floatValue(); + + int reference = (zeroCrossingIdeal < pointer1 || detectedZeroCrossing < pointer1 ? pointer2 : pointer1); + reference--; + + float start = reference - zeroCrossingIdeal; + float end = reference - detectedZeroCrossing; + + mZeroCrossing.get(0).setXValue(start); + mZeroCrossing.get(1).setXValue(end); + } + } +} diff --git a/src/main/java/io/github/dsheirer/instrument/gui/viewer/chart/FleetsyncZeroCrossingErrorDetectorChart.java b/src/main/java/io/github/dsheirer/instrument/gui/viewer/chart/FleetsyncZeroCrossingErrorDetectorChart.java new file mode 100644 index 00000000..7891c036 --- /dev/null +++ b/src/main/java/io/github/dsheirer/instrument/gui/viewer/chart/FleetsyncZeroCrossingErrorDetectorChart.java @@ -0,0 +1,132 @@ +/******************************************************************************* + * 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 + * + ******************************************************************************/ +package io.github.dsheirer.instrument.gui.viewer.chart; + +import io.github.dsheirer.module.decode.fleetsync2.Fleetsync2DecoderInstrumented; +import javafx.beans.value.ChangeListener; +import javafx.beans.value.ObservableValue; +import javafx.collections.FXCollections; +import javafx.collections.ObservableList; +import javafx.scene.chart.LineChart; +import javafx.scene.chart.NumberAxis; +import org.slf4j.Logger; +import org.slf4j.LoggerFactory; + +public class FleetsyncZeroCrossingErrorDetectorChart extends LineChart +{ + private final static Logger mLog = LoggerFactory.getLogger(FleetsyncZeroCrossingErrorDetectorChart.class); + + private ObservableList> mCurrentSamples = FXCollections.observableArrayList(); + private Series mCurrentSampleSeries = new Series<>("Samples", mCurrentSamples); + + private ObservableList> mPreviousSamples = FXCollections.observableArrayList(); + private Series mPreviousSampleSeries = new Series<>("Samples", mPreviousSamples); + + private ObservableList> mIdeal = FXCollections.observableArrayList(); + private Series mIdealSeries = new Series<>("Ideal", mIdeal); + + private ObservableList> mDetected = FXCollections.observableArrayList(); + private Series mDetectedSeries = new Series<>("Detected", mDetected); + + private Fleetsync2DecoderInstrumented mFleetsync2DecoderInstrumented; + + public FleetsyncZeroCrossingErrorDetectorChart(Fleetsync2DecoderInstrumented decoder, int length) + { + super(new NumberAxis("Samples", 0, length, 5), + new NumberAxis("Value", -1.0, 1.0, 0.25)); + + ObservableList observableList = FXCollections.observableArrayList(mCurrentSampleSeries, + mPreviousSampleSeries, mIdealSeries, mDetectedSeries); + setData(observableList); + + mFleetsync2DecoderInstrumented = decoder; + decoder.getAFSK1200Decoder().getErrorDetector().timingError.addListener(new ErrorChangeListener()); + decoder.bufferCount.addListener(new ChangeListener() + { + @Override + public void changed(ObservableValue observable, Object oldValue, Object newValue) + { + updateBuffer(); + } + }); + + for(int x = 0; x < length; x++) + { + Data sample = new Data<>(x, 0.0f); + mCurrentSamples.add(sample); + Data sample2 = new Data<>(x, 0.0f); + mPreviousSamples.add(sample2); + } + + Data ideal1 = new Data<>(5, -0.1f); + mIdeal.add(ideal1); + Data ideal2 = new Data<>(5, -0.9f); + mIdeal.add(ideal2); + + Data detected1 = new Data<>(5, -0.25f); + mDetected.add(detected1); + Data detected2 = new Data<>(5, -0.75f); + mDetected.add(detected2); + } + + private void updateBuffer() + { + boolean[] samples = mFleetsync2DecoderInstrumented.getAFSK1200Decoder().getErrorDetector().getBuffer(); + + for(int x = 0; x < samples.length; x++) + { + Data sample = mCurrentSamples.get(x); + sample.setYValue(samples[x] ? 0.7f : 0.3f); + } + } + + public class ErrorChangeListener implements ChangeListener + { + @Override + public void changed(ObservableValue observable, Object oldValue, Object newValue) + { + float error = mFleetsync2DecoderInstrumented.getAFSK1200Decoder().getErrorDetector().getError(); + + boolean[] samples = mFleetsync2DecoderInstrumented.getAFSK1200Decoder().getErrorDetector().getBuffer(); + + for(int x = 0; x < samples.length; x++) + { + Data sample = mPreviousSamples.get(x); + sample.setYValue(samples[x] ? -0.4f : -0.6f); + } + + Data ideal1 = mIdeal.get(0); + ideal1.setXValue(mFleetsync2DecoderInstrumented.getAFSK1200Decoder().getErrorDetector().getZeroCrossingIdeal()); + Data ideal2 = mIdeal.get(1); + ideal2.setXValue(mFleetsync2DecoderInstrumented.getAFSK1200Decoder().getErrorDetector().getZeroCrossingIdeal()); + + if(error == 0.0f) + { + Data detected1 = mDetected.get(0); + detected1.setXValue(0.0f); + Data detected2 = mDetected.get(1); + detected2.setXValue(0.0f); + } + else + { + Data detected1 = mDetected.get(0); + detected1.setXValue(mFleetsync2DecoderInstrumented.getAFSK1200Decoder().getErrorDetector().getDetectedZeroCrossing()); + Data detected2 = mDetected.get(1); + detected2.setXValue(mFleetsync2DecoderInstrumented.getAFSK1200Decoder().getErrorDetector().getDetectedZeroCrossing()); + } + } + } +} diff --git a/src/main/java/io/github/dsheirer/instrument/gui/viewer/chart/SampleBufferChart.java b/src/main/java/io/github/dsheirer/instrument/gui/viewer/chart/LTRNetSampleBufferChart.java similarity index 96% rename from src/main/java/io/github/dsheirer/instrument/gui/viewer/chart/SampleBufferChart.java rename to src/main/java/io/github/dsheirer/instrument/gui/viewer/chart/LTRNetSampleBufferChart.java index 799776e9..ade62f19 100644 --- a/src/main/java/io/github/dsheirer/instrument/gui/viewer/chart/SampleBufferChart.java +++ b/src/main/java/io/github/dsheirer/instrument/gui/viewer/chart/LTRNetSampleBufferChart.java @@ -26,9 +26,9 @@ import javafx.scene.layout.StackPane; import org.slf4j.Logger; import org.slf4j.LoggerFactory; -public class SampleBufferChart extends LineChart +public class LTRNetSampleBufferChart extends LineChart { - private final static Logger mLog = LoggerFactory.getLogger(SampleBufferChart.class); + private final static Logger mLog = LoggerFactory.getLogger(LTRNetSampleBufferChart.class); private ObservableList> mSamples = FXCollections.observableArrayList(); private Series mSampleSeries = new Series<>("Samples", mSamples); @@ -46,7 +46,7 @@ public class SampleBufferChart extends LineChart private LTRNetDecoderInstrumented mLTRNetDecoderInstrumented; - public SampleBufferChart(LTRNetDecoderInstrumented decoder, int length) + public LTRNetSampleBufferChart(LTRNetDecoderInstrumented decoder, int length) { super(new NumberAxis("Samples", 0, length, 5), new NumberAxis("Value", -1.0, 1.0, 0.25)); diff --git a/src/main/java/io/github/dsheirer/instrument/gui/viewer/decoder/DecoderPaneFactory.java b/src/main/java/io/github/dsheirer/instrument/gui/viewer/decoder/DecoderPaneFactory.java index 03e61517..11327c27 100644 --- a/src/main/java/io/github/dsheirer/instrument/gui/viewer/decoder/DecoderPaneFactory.java +++ b/src/main/java/io/github/dsheirer/instrument/gui/viewer/decoder/DecoderPaneFactory.java @@ -23,8 +23,7 @@ import java.util.EnumSet; public class DecoderPaneFactory { private static final EnumSet SUPPORTED_DECODER_TYPES = - EnumSet.of(DecoderType.P25_PHASE1, - DecoderType.LTR_NET); + EnumSet.of(DecoderType.P25_PHASE1, DecoderType.LTR_NET, DecoderType.FLEETSYNC2); /** * Creates a decoder pane for the decoder type @@ -33,6 +32,8 @@ public class DecoderPaneFactory { switch(decoderType) { + case FLEETSYNC2: + return new Fleetsync2Pane(); case LTR_NET: return new LTRNetPane(); case P25_PHASE1: diff --git a/src/main/java/io/github/dsheirer/instrument/gui/viewer/decoder/Fleetsync2Pane.java b/src/main/java/io/github/dsheirer/instrument/gui/viewer/decoder/Fleetsync2Pane.java new file mode 100644 index 00000000..647015b6 --- /dev/null +++ b/src/main/java/io/github/dsheirer/instrument/gui/viewer/decoder/Fleetsync2Pane.java @@ -0,0 +1,185 @@ +/******************************************************************************* + * 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 + * + ******************************************************************************/ +package io.github.dsheirer.instrument.gui.viewer.decoder; + +import io.github.dsheirer.dsp.symbol.ISyncDetectListener; +import io.github.dsheirer.instrument.gui.viewer.chart.DecodedSymbolChart; +import io.github.dsheirer.instrument.gui.viewer.chart.Fleetsync2SampleBufferChart; +import io.github.dsheirer.instrument.gui.viewer.chart.FleetsyncZeroCrossingErrorDetectorChart; +import io.github.dsheirer.instrument.gui.viewer.chart.RealSampleLineChart; +import io.github.dsheirer.message.Message; +import io.github.dsheirer.module.decode.DecoderType; +import io.github.dsheirer.module.decode.fleetsync2.Fleetsync2DecoderInstrumented; +import io.github.dsheirer.sample.Listener; +import javafx.scene.layout.HBox; +import javafx.scene.layout.Priority; +import javafx.scene.layout.VBox; +import org.slf4j.Logger; +import org.slf4j.LoggerFactory; + +public class Fleetsync2Pane extends RealDecoderPane +{ + private final static Logger mLog = LoggerFactory.getLogger(Fleetsync2Pane.class); + + private HBox mSampleChartBox; + private RealSampleLineChart mRealSampleLineChart; + private HBox mSymbolDecoderBox; + private Fleetsync2SampleBufferChart mSampleBufferChart; + private FleetsyncZeroCrossingErrorDetectorChart mZeroCrossingErrorDetectorChart; + private DecodedSymbolChart mDecodedSymbolChart; + + private Fleetsync2DecoderInstrumented mDecoder; + + public Fleetsync2Pane() + { + super(DecoderType.LTR_NET); + init(); + } + + private void init() + { + addListener(getDecoder()); + addListener(getSampleLineChart()); + + HBox.setHgrow(getSampleChartBox(), Priority.ALWAYS); + HBox.setHgrow(getSymbolDecoderBox(), Priority.ALWAYS); + + VBox.setVgrow(getSampleChartBox(), Priority.ALWAYS); + VBox.setVgrow(getSymbolDecoderBox(), Priority.ALWAYS); + + getSampleChartBox().setMaxWidth(Double.MAX_VALUE); + getSymbolDecoderBox().setMaxWidth(Double.MAX_VALUE); + + getChildren().addAll(getSampleChartBox(), getSymbolDecoderBox()); + + getDecoder().getAFSK1200Decoder().addListener(getDecodedSymbolChart()); + + getDecoder().setSyncDetectListener(new ISyncDetectListener() + { + @Override + public void syncDetected() + { + mLog.debug("Sync Detected!!!!!"); + } + }); + + getDecoder().setMessageListener(new Listener() + { + @Override + public void receive(Message message) + { + mLog.debug(message.getMessage()); + } + }); + } + + private Fleetsync2DecoderInstrumented getDecoder() + { + if(mDecoder == null) + { + mDecoder = new Fleetsync2DecoderInstrumented(null); + mDecoder.setMessageListener(new Listener() + { + @Override + public void receive(Message message) + { + mLog.debug(message.getMessage()); + } + }); + } + + return mDecoder; + } + + private HBox getSampleChartBox() + { + if(mSampleChartBox == null) + { + mSampleChartBox = new HBox(); + mSampleChartBox.setMaxHeight(Double.MAX_VALUE); + + getSampleLineChart().setMaxWidth(Double.MAX_VALUE); + getDecodedSymbolChart().setMaxWidth(Double.MAX_VALUE); + + HBox.setHgrow(getSampleLineChart(), Priority.ALWAYS); + HBox.setHgrow(getDecodedSymbolChart(), Priority.ALWAYS); + + mSampleChartBox.getChildren().addAll(getSampleLineChart(), getDecodedSymbolChart()); + } + + return mSampleChartBox; + } + + private RealSampleLineChart getSampleLineChart() + { + if(mRealSampleLineChart == null) + { + mRealSampleLineChart = new RealSampleLineChart(200, (10.0)); + } + + return mRealSampleLineChart; + } + + private HBox getSymbolDecoderBox() + { + if(mSymbolDecoderBox == null) + { + mSymbolDecoderBox = new HBox(); + mSymbolDecoderBox.setMaxWidth(Double.MAX_VALUE); + + getSampleBufferChart().setMaxWidth(Double.MAX_VALUE); + getZeroCrossingErrorDetectorChart().setMaxWidth(Double.MAX_VALUE); + + HBox.setHgrow(getSampleBufferChart(), Priority.ALWAYS); + HBox.setHgrow(getZeroCrossingErrorDetectorChart(), Priority.ALWAYS); + + mSymbolDecoderBox.getChildren().addAll(getSampleBufferChart(), getZeroCrossingErrorDetectorChart()); + } + + return mSymbolDecoderBox; + } + + private Fleetsync2SampleBufferChart getSampleBufferChart() + { + if(mSampleBufferChart == null) + { + mSampleBufferChart = new Fleetsync2SampleBufferChart(getDecoder(), 106); + } + + return mSampleBufferChart; + } + + private FleetsyncZeroCrossingErrorDetectorChart getZeroCrossingErrorDetectorChart() + { + if(mZeroCrossingErrorDetectorChart == null) + { + mZeroCrossingErrorDetectorChart = new FleetsyncZeroCrossingErrorDetectorChart(getDecoder(), + getDecoder().getAFSK1200Decoder().getErrorDetector().getBuffer().length); + } + + return mZeroCrossingErrorDetectorChart; + } + + private DecodedSymbolChart getDecodedSymbolChart() + { + if(mDecodedSymbolChart == null) + { + mDecodedSymbolChart = new DecodedSymbolChart(50); + } + + return mDecodedSymbolChart; + } +} diff --git a/src/main/java/io/github/dsheirer/instrument/gui/viewer/decoder/LTRNetPane.java b/src/main/java/io/github/dsheirer/instrument/gui/viewer/decoder/LTRNetPane.java index 1cc29446..ee8162f2 100644 --- a/src/main/java/io/github/dsheirer/instrument/gui/viewer/decoder/LTRNetPane.java +++ b/src/main/java/io/github/dsheirer/instrument/gui/viewer/decoder/LTRNetPane.java @@ -15,8 +15,8 @@ ******************************************************************************/ package io.github.dsheirer.instrument.gui.viewer.decoder; +import io.github.dsheirer.instrument.gui.viewer.chart.LTRNetSampleBufferChart; import io.github.dsheirer.instrument.gui.viewer.chart.RealSampleLineChart; -import io.github.dsheirer.instrument.gui.viewer.chart.SampleBufferChart; import io.github.dsheirer.instrument.gui.viewer.chart.ZeroCrossingErrorDetectorChart; import io.github.dsheirer.message.Message; import io.github.dsheirer.message.MessageDirection; @@ -37,7 +37,7 @@ public class LTRNetPane extends RealDecoderPane private HBox mSampleChartBox; private RealSampleLineChart mRealSampleLineChart; private HBox mSymbolDecoderBox; - private SampleBufferChart mSampleBufferChart; + private LTRNetSampleBufferChart mLTRNetSampleBufferChart; private ZeroCrossingErrorDetectorChart mZeroCrossingErrorDetectorChart; private LTRNetDecoderInstrumented mDecoder; @@ -128,14 +128,14 @@ public class LTRNetPane extends RealDecoderPane return mSymbolDecoderBox; } - private SampleBufferChart getSampleBufferChart() + private LTRNetSampleBufferChart getSampleBufferChart() { - if(mSampleBufferChart == null) + if(mLTRNetSampleBufferChart == null) { - mSampleBufferChart = new SampleBufferChart(getDecoder(), 106); + mLTRNetSampleBufferChart = new LTRNetSampleBufferChart(getDecoder(), 106); } - return mSampleBufferChart; + return mLTRNetSampleBufferChart; } private ZeroCrossingErrorDetectorChart getZeroCrossingErrorDetectorChart() diff --git a/src/main/java/io/github/dsheirer/module/decode/DecoderType.java b/src/main/java/io/github/dsheirer/module/decode/DecoderType.java index 42b05e81..689ba6fc 100644 --- a/src/main/java/io/github/dsheirer/module/decode/DecoderType.java +++ b/src/main/java/io/github/dsheirer/module/decode/DecoderType.java @@ -30,7 +30,7 @@ public enum DecoderType AM( "AM", "AM", "images/am.png", 10000 ), APRS( "APRS", "APRS", "images/aprs.png", 12500 ), DMR( "DMR", "DMR", "images/dmr.png", 12500 ), - FLEETSYNC2( "Fleetsync II", "Fsync2", "images/fm.png", 12500 ), + FLEETSYNC2( "Fleetsync II", "Fleetsync2", "images/fm.png", 12500 ), LJ_1200( "LJ1200 173.075", "LJ1200", "images/lj1200.png", 12500 ), LTR_STANDARD( "LTR-Standard", "LTR", "images/ltr.png", 10000 ), LTR_NET( "LTR-Net", "LTR-Net", "images/ltr_net.png", 12500 ), diff --git a/src/main/java/io/github/dsheirer/module/decode/fleetsync2/Fleetsync2Decoder.java b/src/main/java/io/github/dsheirer/module/decode/fleetsync2/Fleetsync2Decoder.java index bbd992cd..f05d9366 100644 --- a/src/main/java/io/github/dsheirer/module/decode/fleetsync2/Fleetsync2Decoder.java +++ b/src/main/java/io/github/dsheirer/module/decode/fleetsync2/Fleetsync2Decoder.java @@ -1,17 +1,17 @@ /******************************************************************************* * SDR Trunk * Copyright (C) 2014,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 ******************************************************************************/ @@ -20,156 +20,76 @@ package io.github.dsheirer.module.decode.fleetsync2; import io.github.dsheirer.alias.AliasList; import io.github.dsheirer.bits.MessageFramer; import io.github.dsheirer.bits.SyncPattern; -import io.github.dsheirer.dsp.filter.Filters; -import io.github.dsheirer.dsp.filter.fir.real.RealFIRFilter_RB_RB; -import io.github.dsheirer.dsp.filter.halfband.real.HalfBandFilter_RB_RB; -import io.github.dsheirer.dsp.fsk.FSK2Decoder; -import io.github.dsheirer.dsp.fsk.FSK2Decoder.Output; -import io.github.dsheirer.instrument.Instrumentable; -import io.github.dsheirer.instrument.tap.Tap; -import io.github.dsheirer.instrument.tap.TapGroup; -import io.github.dsheirer.instrument.tap.stream.BinaryTap; -import io.github.dsheirer.instrument.tap.stream.FloatBufferTap; +import io.github.dsheirer.dsp.fsk.AFSK1200Decoder; +import io.github.dsheirer.dsp.symbol.ISyncDetectListener; +import io.github.dsheirer.dsp.symbol.SyncDetectProvider; import io.github.dsheirer.module.decode.Decoder; import io.github.dsheirer.module.decode.DecoderType; import io.github.dsheirer.sample.Listener; -import io.github.dsheirer.sample.real.IFilteredRealBufferListener; -import io.github.dsheirer.sample.real.RealBuffer; - -import java.util.ArrayList; -import java.util.List; +import io.github.dsheirer.sample.buffer.IReusableBufferListener; +import io.github.dsheirer.sample.buffer.ReusableBuffer; /** - * Fleetsync II Decoder - 1200 baud 2FSK decoder that can process 48k sample rate - * floating point samples and output fully framed Fleetsync II messages + * Fleetsync II Decoder */ -public class Fleetsync2Decoder extends Decoder implements IFilteredRealBufferListener, - Instrumentable +public class Fleetsync2Decoder extends Decoder implements IReusableBufferListener, Listener, + SyncDetectProvider { - /* Decimated sample rate ( 48,000 / 2 = 24,000 ) feeding the decoder */ - private static final int sDECIMATED_SAMPLE_RATE = 24000; - - /* Baud or Symbol Rate */ - private static final int sSYMBOL_RATE = 1200; + /* Message length - 5 x REVS + 16 x SYNC + 8 x 64Bit Blocks */ + protected static final int MESSAGE_LENGTH = 537; - /* Message length - 5 x REVS + 16 x SYNC + 8 x 64Bit Blocks */ - private static final int sMESSAGE_LENGTH = 537; - - /* Instrumentation Taps */ - private ArrayList mAvailableTaps; - private static final String INSTRUMENT_BANDPASS_FILTER_TO_FSK2_DEMOD = - "Tap Point: Bandpass Filter > < FSK2 Decoder"; - private static final String INSTRUMENT_FSK2_DECODER_TO_MESSAGE_FRAMER = - "Tap Point: FSK2 Decoder > < Message Framer"; - - private FSK2Decoder mFSKDecoder; - private HalfBandFilter_RB_RB mDecimationFilter; - private RealFIRFilter_RB_RB mBandPassFilter; + protected AFSK1200Decoder mAFSK1200Decoder; private MessageFramer mMessageFramer; private Fleetsync2MessageProcessor mMessageProcessor; - - public Fleetsync2Decoder( AliasList aliasList ) - { - mDecimationFilter = new HalfBandFilter_RB_RB( - Filters.FIR_HALF_BAND_31T_ONE_EIGHTH_FCO.getCoefficients(), 1.0f, true ); - - mBandPassFilter = new RealFIRFilter_RB_RB( - Filters.FIRBP_1200FSK_24000FS.getCoefficients(), 1.02f ); - mDecimationFilter.setListener( mBandPassFilter ); + public Fleetsync2Decoder(AFSK1200Decoder afsk1200Decoder, AliasList aliasList) + { + mAFSK1200Decoder = afsk1200Decoder; + mMessageFramer = new MessageFramer(SyncPattern.FLEETSYNC2.getPattern(), MESSAGE_LENGTH); + mAFSK1200Decoder.setMessageFramer(mMessageFramer); + mMessageFramer.setSyncDetectListener(mAFSK1200Decoder); + mMessageProcessor = new Fleetsync2MessageProcessor(aliasList); + mMessageFramer.addMessageListener(mMessageProcessor); + mMessageProcessor.setMessageListener(getMessageListener()); + } - mFSKDecoder = new FSK2Decoder( sDECIMATED_SAMPLE_RATE, - sSYMBOL_RATE, Output.INVERTED ); - mBandPassFilter.setListener( mFSKDecoder ); + public Fleetsync2Decoder(AliasList aliasList) + { + this(new AFSK1200Decoder(MESSAGE_LENGTH, false), aliasList); + } - mMessageFramer = new MessageFramer( - SyncPattern.FLEETSYNC2.getPattern(), sMESSAGE_LENGTH ); - mFSKDecoder.setListener( mMessageFramer ); - - mMessageProcessor = new Fleetsync2MessageProcessor( aliasList ); - mMessageFramer.addMessageListener( mMessageProcessor ); - - mMessageProcessor.setMessageListener( getMessageListener() ); - } - public void dispose() { - super.dispose(); - - mBandPassFilter.dispose(); - mDecimationFilter.dispose(); - mFSKDecoder.dispose(); - mMessageFramer.dispose(); - mMessageProcessor.dispose(); + super.dispose(); } - @Override - public DecoderType getDecoderType() - { - return DecoderType.FLEETSYNC2; - } - - @Override - public List getTapGroups() + @Override + public DecoderType getDecoderType() { - if( mAvailableTaps == null ) - { - mAvailableTaps = new ArrayList(); - - TapGroup group = new TapGroup( "Fleetsync 2 Decoder" ); - - group.add( new FloatBufferTap( - INSTRUMENT_BANDPASS_FILTER_TO_FSK2_DEMOD, 0, 0.5f ) ); - group.add( new BinaryTap( - INSTRUMENT_FSK2_DECODER_TO_MESSAGE_FRAMER, 0, 0.025f ) ); - - mAvailableTaps.add( group ); - - mAvailableTaps.addAll( mFSKDecoder.getTapGroups() ); - } - - return mAvailableTaps; + return DecoderType.FLEETSYNC2; } - @Override - public void registerTap( Tap tap ) + @Override + public Listener getReusableBufferListener() { - mFSKDecoder.registerTap( tap ); - - switch( tap.getName() ) - { - case INSTRUMENT_BANDPASS_FILTER_TO_FSK2_DEMOD: - FloatBufferTap bpTap = (FloatBufferTap)tap; - mBandPassFilter.setListener( bpTap ); - bpTap.setListener( mFSKDecoder ); - break; - case INSTRUMENT_FSK2_DECODER_TO_MESSAGE_FRAMER: - BinaryTap decoderTap = (BinaryTap)tap; - mFSKDecoder.setListener( decoderTap ); - decoderTap.setListener( mMessageFramer ); - break; - } + return mAFSK1200Decoder; } - @Override - public void unregisterTap( Tap tap ) + @Override + public void receive(ReusableBuffer reusableBuffer) { - mFSKDecoder.unregisterTap( tap ); - - switch( tap.getName() ) - { - case INSTRUMENT_BANDPASS_FILTER_TO_FSK2_DEMOD: - mBandPassFilter.setListener( mFSKDecoder ); - break; - case INSTRUMENT_FSK2_DECODER_TO_MESSAGE_FRAMER: - mFSKDecoder.setListener( mMessageFramer ); - break; - } + mAFSK1200Decoder.receive(reusableBuffer); } - @Override - public Listener getFilteredRealBufferListener() - { - return mDecimationFilter; - } + @Override + public void reset() + { + mMessageFramer.reset(); + } + + @Override + public void setSyncDetectListener(ISyncDetectListener listener) + { + mMessageFramer.setSyncDetectListener(listener); + } } diff --git a/src/main/java/io/github/dsheirer/module/decode/fleetsync2/Fleetsync2DecoderInstrumented.java b/src/main/java/io/github/dsheirer/module/decode/fleetsync2/Fleetsync2DecoderInstrumented.java new file mode 100644 index 00000000..e2b845af --- /dev/null +++ b/src/main/java/io/github/dsheirer/module/decode/fleetsync2/Fleetsync2DecoderInstrumented.java @@ -0,0 +1,49 @@ +/******************************************************************************* + * 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 + * + ******************************************************************************/ +package io.github.dsheirer.module.decode.fleetsync2; + +import io.github.dsheirer.alias.AliasList; +import io.github.dsheirer.dsp.fsk.AFSK1200Decoder; +import io.github.dsheirer.dsp.fsk.AFSK1200DecoderInstrumented; +import io.github.dsheirer.dsp.fsk.SampleBufferInstrumented; +import io.github.dsheirer.dsp.fsk.ZeroCrossingErrorDetectorInstrumented; +import io.github.dsheirer.sample.buffer.ReusableBuffer; +import javafx.beans.property.SimpleIntegerProperty; + +public class Fleetsync2DecoderInstrumented extends Fleetsync2Decoder +{ + public SimpleIntegerProperty bufferCount = new SimpleIntegerProperty(); + + public Fleetsync2DecoderInstrumented(AliasList aliasList) + { + super(new AFSK1200DecoderInstrumented(MESSAGE_LENGTH, + new SampleBufferInstrumented(AFSK1200Decoder.SAMPLES_PER_SYMBOL, AFSK1200Decoder.COARSE_TIMING_GAIN), + new ZeroCrossingErrorDetectorInstrumented(AFSK1200Decoder.SAMPLES_PER_SYMBOL), false), aliasList); + } + + public AFSK1200DecoderInstrumented getAFSK1200Decoder() + { + return (AFSK1200DecoderInstrumented)mAFSK1200Decoder; + } + + @Override + public void receive(ReusableBuffer reusableBuffer) + { + super.receive(reusableBuffer); + + bufferCount.setValue(bufferCount.intValue() + 1); + } +} diff --git a/src/main/java/io/github/dsheirer/module/demodulate/fm/FMDemodulatorModule.java b/src/main/java/io/github/dsheirer/module/demodulate/fm/FMDemodulatorModule.java index d3da75e8..7dfc7a6e 100644 --- a/src/main/java/io/github/dsheirer/module/demodulate/fm/FMDemodulatorModule.java +++ b/src/main/java/io/github/dsheirer/module/demodulate/fm/FMDemodulatorModule.java @@ -202,7 +202,7 @@ public class FMDemodulatorModule extends Module implements ISourceEventListener, mIQFilter = new ComplexFIRFilter2(filterTaps); - mResampler = new RealResampler(sampleRate, mOutputSampleRate); + mResampler = new RealResampler(sampleRate, mOutputSampleRate, 2000, 1000); mResampler.setListener(new Listener() { From 9dda1f95a171762c715861e8d01ec371955c171d Mon Sep 17 00:00:00 2001 From: Denny Date: Sat, 9 Jun 2018 08:17:18 -0400 Subject: [PATCH 71/87] [WIP] Creates a new AFSK-1200 correlation decoder and updates the Fleetsync II decoder to use it. Adds instrumented version of the decoder and a new Fleetsync2 FX pane to the DemodulatorViewerFX tool. Note: the remaining AFSK-1200 based decoders will all be ported to use this new AFSK1200 decoder (MDC1200, Tait1200, LowJack1200). --- .idea/dictionaries/denny.xml | 1 + .../dsp/{fsk => afsk}/AFSK1200Decoder.java | 164 +++++++----------- .../AFSK1200DecoderInstrumented.java | 16 +- .../dsheirer/dsp/afsk/AFSKSampleBuffer.java | 121 +++++++++++++ .../afsk/AFSKSampleBufferInstrumented.java | 90 ++++++++++ .../dsheirer/dsp/{fsk => afsk}/AFSKTest.java | 2 +- .../dsp/afsk/AFSKTimingErrorDetector.java | 103 +++++++++++ .../AFSKTimingErrorDetectorInstrumented.java | 53 ++++++ .../chart/Fleetsync2SampleBufferChart.java | 51 +++--- ...eetsyncZeroCrossingErrorDetectorChart.java | 15 +- .../gui/viewer/decoder/Fleetsync2Pane.java | 12 +- .../decode/afsk/AbstractAFSKDecoder.java | 61 +++++++ .../decode/fleetsync2/Fleetsync2Decoder.java | 48 +---- .../Fleetsync2DecoderInstrumented.java | 17 +- 14 files changed, 547 insertions(+), 207 deletions(-) rename src/main/java/io/github/dsheirer/dsp/{fsk => afsk}/AFSK1200Decoder.java (61%) rename src/main/java/io/github/dsheirer/dsp/{fsk => afsk}/AFSK1200DecoderInstrumented.java (70%) create mode 100644 src/main/java/io/github/dsheirer/dsp/afsk/AFSKSampleBuffer.java create mode 100644 src/main/java/io/github/dsheirer/dsp/afsk/AFSKSampleBufferInstrumented.java rename src/main/java/io/github/dsheirer/dsp/{fsk => afsk}/AFSKTest.java (99%) create mode 100644 src/main/java/io/github/dsheirer/dsp/afsk/AFSKTimingErrorDetector.java create mode 100644 src/main/java/io/github/dsheirer/dsp/afsk/AFSKTimingErrorDetectorInstrumented.java create mode 100644 src/main/java/io/github/dsheirer/module/decode/afsk/AbstractAFSKDecoder.java diff --git a/.idea/dictionaries/denny.xml b/.idea/dictionaries/denny.xml index 35e8bffe..fc474e43 100644 --- a/.idea/dictionaries/denny.xml +++ b/.idea/dictionaries/denny.xml @@ -3,6 +3,7 @@ bahroo broadcastify + correlator dinf esnid fleetsync diff --git a/src/main/java/io/github/dsheirer/dsp/fsk/AFSK1200Decoder.java b/src/main/java/io/github/dsheirer/dsp/afsk/AFSK1200Decoder.java similarity index 61% rename from src/main/java/io/github/dsheirer/dsp/fsk/AFSK1200Decoder.java rename to src/main/java/io/github/dsheirer/dsp/afsk/AFSK1200Decoder.java index be69104e..fd022a49 100644 --- a/src/main/java/io/github/dsheirer/dsp/fsk/AFSK1200Decoder.java +++ b/src/main/java/io/github/dsheirer/dsp/afsk/AFSK1200Decoder.java @@ -13,10 +13,9 @@ * If not, see * ******************************************************************************/ -package io.github.dsheirer.dsp.fsk; +package io.github.dsheirer.dsp.afsk; import io.github.dsheirer.bits.MessageFramer; -import io.github.dsheirer.buffer.BooleanAveragingBuffer; import io.github.dsheirer.buffer.FloatAveragingBuffer; import io.github.dsheirer.dsp.filter.design.FilterDesignException; import io.github.dsheirer.dsp.filter.fir.FIRFilterSpecification; @@ -25,16 +24,21 @@ import io.github.dsheirer.dsp.filter.fir.remez.RemezFIRFilterDesigner; import io.github.dsheirer.dsp.filter.resample.RealResampler; import io.github.dsheirer.dsp.mixer.IOscillator; import io.github.dsheirer.dsp.mixer.Oscillator; -import io.github.dsheirer.dsp.symbol.ISyncDetectListener; import io.github.dsheirer.sample.Listener; import io.github.dsheirer.sample.buffer.ReusableBuffer; import org.slf4j.Logger; import org.slf4j.LoggerFactory; /** - * Binary Frequency Shift Keying (2FSK) correlation decoder. Provides normal or inverted decoded output. + * Audio Frequency Shift Keying (AFSK) 1200-baud correlation decoder for decoding 8 kHz FM-demodulated audio samples. + * + * Note: internally the incoming 8 kHz sample rate is resampled to 7200 Hz to be an integral of the 1200 Hz baud rate + * which equates to six samples per symbol. Each symbol is correlated over an eight sample period with correlation + * values averaged over a seven correlation value period. + * + * Provides normal or inverted decoded output. */ -public class AFSK1200Decoder implements Listener, ISyncDetectListener, ISyncStateListener +public class AFSK1200Decoder implements Listener { private final static Logger mLog = LoggerFactory.getLogger(AFSK1200Decoder.class); @@ -43,11 +47,15 @@ public class AFSK1200Decoder implements Listener, ISyncDetectLis NORMAL, INVERTED } - public static final double TARGET_SAMPLE_RATE = 7200.0d; - public static final int SAMPLES_PER_SYMBOL = 6; //TARGET_SAMPLE_RATE / 1200; - public static final float COARSE_TIMING_GAIN = 1.0f / 3.0f; - protected static final float MEDIUM_TIMING_GAIN = 1.0f / 4.0f; - protected static final float FINE_TIMING_GAIN = 1.0f / 5.0f; + public static final double SAMPLE_RATE = 7200.0d; + public static final int SAMPLES_PER_SYMBOL = 6; //SAMPLE_RATE / 1200 baud; + + //Correlation period of 8 samples works well and should align nicely with SIMD intrinsic vector sizes + public static final int CORRELATION_PERIOD = SAMPLES_PER_SYMBOL + 2; + public static final int AVERAGING_PERIOD = SAMPLES_PER_SYMBOL + 1; + public static final double MARK = 1200.0; + public static final double SPACE = 1800.0; + public static final float TIMING_ERROR_GAIN = 1.0f / 3.0f; //Timing error adjustments over 3 symbol periods private static float[] sBandPassFilterCoefficients; @@ -78,38 +86,47 @@ public class AFSK1200Decoder implements Listener, ISyncDetectLis } } - private Correlator mCorrelator1200 = new Correlator(TARGET_SAMPLE_RATE, 1200.0, SAMPLES_PER_SYMBOL); - private Correlator mCorrelator1800 = new Correlator(TARGET_SAMPLE_RATE, 1800.0, SAMPLES_PER_SYMBOL); + private Correlator mCorrelator1200 = new Correlator(SAMPLE_RATE, MARK, AVERAGING_PERIOD, CORRELATION_PERIOD); + private Correlator mCorrelator1800 = new Correlator(SAMPLE_RATE, SPACE, AVERAGING_PERIOD, CORRELATION_PERIOD); private float[] mCorrelationValues1200; private float[] mCorrelationValues1800; protected boolean mNormalOutput; - protected float mSymbolTimingGain = COARSE_TIMING_GAIN; - protected SampleBuffer mSampleBuffer; - protected ZeroCrossingErrorDetector mTimingErrorDetector = new ZeroCrossingErrorDetector(SAMPLES_PER_SYMBOL); - protected SynchronizationMonitor mSynchronizationMonitor; + protected float mSymbolTimingGain = TIMING_ERROR_GAIN; + protected AFSKSampleBuffer mSampleBuffer; + protected AFSKTimingErrorDetector mTimingErrorDetector = new AFSKTimingErrorDetector(SAMPLES_PER_SYMBOL); private RealFIRFilter2 mBandPassFilter = new RealFIRFilter2(sBandPassFilterCoefficients); protected MessageFramer mMessageFramer; private boolean mSampleDecision; //Resample to an integral of the baud rate 1200 baud * 6 samples per symbol = 7200.0 Hertz - private RealResampler mResampler = new RealResampler(8000.0, TARGET_SAMPLE_RATE, 2000, 1); + private RealResampler mResampler = new RealResampler(8000.0, SAMPLE_RATE, 2000, 1); - public AFSK1200Decoder(int messageLength, SampleBuffer sampleBuffer, ZeroCrossingErrorDetector detector, boolean invertOutput) + /** + * Constructs a decoder using the provided arguments. + * + * @param sampleBuffer for storing incoming samples and calculating symbols + * @param detector for timing error alignment + * @param output NORMAL: 1200Hz = Mark(1) and 1800Hz = Space(0), or INVERTED (vice-versa) + */ + public AFSK1200Decoder(AFSKSampleBuffer sampleBuffer, AFSKTimingErrorDetector detector, Output output) { - mSynchronizationMonitor = new SynchronizationMonitor(messageLength); - mSynchronizationMonitor.setListener(this); mTimingErrorDetector = detector; mSampleBuffer = sampleBuffer; mSampleBuffer.setTimingGain(mSymbolTimingGain); - mResampler.setListener(new Decoder(SAMPLES_PER_SYMBOL)); - mNormalOutput = !invertOutput; + mResampler.setListener(new Decoder()); + mNormalOutput = (output == Output.NORMAL); } - public AFSK1200Decoder(int messageLength, boolean invertOutput) + /** + * Constructs a decoder using the provided arguments. + * + * @param output NORMAL: 1200Hz = Mark(1) and 1800Hz = Space(0), or INVERTED (vice-versa) + */ + public AFSK1200Decoder(Output output) { - this(messageLength, new SampleBuffer(SAMPLES_PER_SYMBOL, COARSE_TIMING_GAIN), - new ZeroCrossingErrorDetector(SAMPLES_PER_SYMBOL), invertOutput); + this(new AFSKSampleBuffer(SAMPLES_PER_SYMBOL, TIMING_ERROR_GAIN), + new AFSKTimingErrorDetector(SAMPLES_PER_SYMBOL), output); } /** @@ -119,19 +136,6 @@ public class AFSK1200Decoder implements Listener, ISyncDetectLis { } - /** - * Implements the ISyncDetectedListener interface to be notified of message sync detection events. - * - * This allows the internal timing error detector to adjust symbol timing error gain levels according to the - * message synchronization state to quickly adjust to initial signal streams or to reduce gain levels once - * synchronization has been achieved. - */ - @Override - public void syncDetected() - { - mSynchronizationMonitor.syncDetected(); - } - /** * Processes the buffer samples by converting all samples to boolean values reflecting if the sample value is * greater than zero (or not). Average symbol timing offset is calculated for the full buffer and the offset is @@ -142,6 +146,8 @@ public class AFSK1200Decoder implements Listener, ISyncDetectLis @Override public void receive(ReusableBuffer buffer) { +//TODO: remove the passband filter ... the correlators should be able to deal with the noise and the filters may be +//TODO: introducing noise anyway // ReusableBuffer bandPassFiltered = mBandPassFilter.filter(buffer); // mResampler.resample(bandPassFiltered); mResampler.resample(buffer); @@ -174,46 +180,8 @@ public class AFSK1200Decoder implements Listener, ISyncDetectLis } - /** - * Implements the ISyncStateListener interface to receive synchronization state events and adjust the symbol timing - * error gain levels on the internal timing error detector. - * - * Gain levels are defined relative to a unit gain of 1.0 over a number of symbol periods. LTR uses an initial - * ramp-up symbol reversal pattern of 0101 which provides three zero crossing detection opportunities for the timing - * error detector, therefore we react to a COARSE gain state to average timing error over 3 symbols. - * - * @param syncState from an external synchronization state monitor - */ - @Override - public void setSyncState(SyncState syncState) - { - switch(syncState) - { - case FINE: - mSymbolTimingGain = FINE_TIMING_GAIN; - break; - case MEDIUM: - mSymbolTimingGain = MEDIUM_TIMING_GAIN; - break; - case COARSE: - mSymbolTimingGain = COARSE_TIMING_GAIN; - break; - default: - throw new IllegalArgumentException("Unrecognized sync state level: " + syncState.name()); - } - - mSampleBuffer.setTimingGain(mSymbolTimingGain); - } - public class Decoder implements Listener { - private BooleanAveragingBuffer mSampleAveragingBuffer; - - private Decoder(int samplesPerSymbol) - { - mSampleAveragingBuffer = new BooleanAveragingBuffer(samplesPerSymbol); - } - @Override public void receive(ReusableBuffer buffer) { @@ -227,26 +195,30 @@ public class AFSK1200Decoder implements Listener, ISyncDetectLis { //1200 = Mark (1) and 1800 = Space (0) mSampleDecision = mCorrelationValues1200[x] > mCorrelationValues1800[x]; -// mSampleDecision = mSampleAveragingBuffer.getAverage(mSampleDecision); mSampleBuffer.receive(mSampleDecision); mTimingErrorDetector.receive(mSampleDecision); if(mSampleBuffer.hasSymbol()) { dispatch(mSampleBuffer.getSymbol()); - -// mSampleBuffer.resetAndAdjust(-mTimingErrorDetector.getError()); - mSampleBuffer.resetAndAdjust(0.0f); - - mSynchronizationMonitor.increment(); + mSampleBuffer.resetAndAdjust(mTimingErrorDetector.getError()); } } } } /** - * Generates a correlation value for each FM demodulated signal against an established reference frequency, - * correlated and averaged over separate periods relative to the samples per symbol period. + * Generates a correlation value for each FM demodulated sample against samples generated for an established + * reference frequency, correlated and averaged over separate periods relative to the samples per symbol period. + * + * The sample rate of the correlator should be an integral value of the target frequency for optimal performance, + * This correlator only works across integral sample periods and does not provide intra-sample interpolation. + * + * Correlation period (ie number of samples) should be relative to the samples per symbol. You may have to + * experiment with various values of (samplesPerSymbol +/- 1, 2 or 3) to obtain the optimal decoder performance. + * + * Averaging period defines the number of correlation values to average before producing the final correlation + * value for each sample. */ public class Correlator { @@ -257,24 +229,21 @@ public class AFSK1200Decoder implements Listener, ISyncDetectLis private float mCorrelationAccumulator; /** - * Constructs a correlator instance for the specified parameters + * Constructs a correlator instance, + * * @param sampleRate of the incoming sample stream. Note: this should be an integral of the symbol rate. * @param frequency of the mark or space symbol to test for correlation - * @param samplesPerSymbol to determine the correlation and averaging periods + * @param averagingPeriod is the number of correlation values to average each period + * @param correlationPeriod is the number of samples to correlate each period */ - public Correlator(double sampleRate, double frequency, int samplesPerSymbol) + public Correlator(double sampleRate, double frequency, int averagingPeriod, int correlationPeriod) { - //Adjust correlation period and averaging period to provide optimal correlation for a given samples/symbol. - //These values are optimized for 6 samples per symbol with averaging over 7 samples and correlation over an - //8 sample period. - int correlationPeriod = samplesPerSymbol + 2; + mAveragingBuffer = new FloatAveragingBuffer(averagingPeriod); - mAveragingBuffer = new FloatAveragingBuffer(samplesPerSymbol + 1); + IOscillator referenceSignalGenerator = new Oscillator(frequency, sampleRate); + mReferenceSamples = referenceSignalGenerator.generateReal(correlationPeriod); mDemodulatedSamples = new float[correlationPeriod]; - - IOscillator referenceGenerator = new Oscillator(frequency, sampleRate); - mReferenceSamples = referenceGenerator.generateReal(correlationPeriod); } /** @@ -298,7 +267,7 @@ public class AFSK1200Decoder implements Listener, ISyncDetectLis for(int x = 0; x < samples.length; x++) { //Note: we're using array copy and dot product structures that the JRE can promote to SIMD intrinsics - //when the host processor provides SIMD capabilities + //when the host processor supports SIMD instructions System.arraycopy(mDemodulatedSamples, 1, mDemodulatedSamples, 0, mDemodulatedSamples.length - 1); mDemodulatedSamples[mDemodulatedSamples.length - 1] = samples[x]; @@ -310,7 +279,8 @@ public class AFSK1200Decoder implements Listener, ISyncDetectLis } //Add the absolute value of correlation accumulator value to the averaging buffer and store the current - // average as the correlation value for this sample + //average as the correlation value for this sample. We use absolute value because we don't care if the + //signal is out of phase with the reference samples mCorrelationValues[x] = mAveragingBuffer.get(Math.abs(mCorrelationAccumulator)); } diff --git a/src/main/java/io/github/dsheirer/dsp/fsk/AFSK1200DecoderInstrumented.java b/src/main/java/io/github/dsheirer/dsp/afsk/AFSK1200DecoderInstrumented.java similarity index 70% rename from src/main/java/io/github/dsheirer/dsp/fsk/AFSK1200DecoderInstrumented.java rename to src/main/java/io/github/dsheirer/dsp/afsk/AFSK1200DecoderInstrumented.java index 659918b7..5aef1105 100644 --- a/src/main/java/io/github/dsheirer/dsp/fsk/AFSK1200DecoderInstrumented.java +++ b/src/main/java/io/github/dsheirer/dsp/afsk/AFSK1200DecoderInstrumented.java @@ -13,7 +13,7 @@ * If not, see * ******************************************************************************/ -package io.github.dsheirer.dsp.fsk; +package io.github.dsheirer.dsp.afsk; import io.github.dsheirer.sample.Broadcaster; import io.github.dsheirer.sample.Listener; @@ -22,20 +22,20 @@ public class AFSK1200DecoderInstrumented extends AFSK1200Decoder { private Broadcaster mSymbolBroadcaster = new Broadcaster<>(); - public AFSK1200DecoderInstrumented(int messageLength, SampleBuffer sampleBuffer, ZeroCrossingErrorDetector detector, boolean invertOutput) + public AFSK1200DecoderInstrumented(boolean invertOutput) { - super(messageLength, new SampleBufferInstrumented(SAMPLES_PER_SYMBOL, COARSE_TIMING_GAIN), - new ZeroCrossingErrorDetectorInstrumented(SAMPLES_PER_SYMBOL), invertOutput); + super(new AFSKSampleBufferInstrumented(SAMPLES_PER_SYMBOL, TIMING_ERROR_GAIN), + new AFSKTimingErrorDetectorInstrumented(SAMPLES_PER_SYMBOL), Output.NORMAL); } - public SampleBufferInstrumented getSampleBuffer() + public AFSKSampleBufferInstrumented getSampleBuffer() { - return (SampleBufferInstrumented)mSampleBuffer; + return (AFSKSampleBufferInstrumented)mSampleBuffer; } - public ZeroCrossingErrorDetectorInstrumented getErrorDetector() + public AFSKTimingErrorDetectorInstrumented getErrorDetector() { - return (ZeroCrossingErrorDetectorInstrumented)mTimingErrorDetector; + return (AFSKTimingErrorDetectorInstrumented)mTimingErrorDetector; } protected void dispatch(boolean symbol) diff --git a/src/main/java/io/github/dsheirer/dsp/afsk/AFSKSampleBuffer.java b/src/main/java/io/github/dsheirer/dsp/afsk/AFSKSampleBuffer.java new file mode 100644 index 00000000..674e7734 --- /dev/null +++ b/src/main/java/io/github/dsheirer/dsp/afsk/AFSKSampleBuffer.java @@ -0,0 +1,121 @@ +/******************************************************************************* + * 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 + * + ******************************************************************************/ +package io.github.dsheirer.dsp.afsk; + +import org.slf4j.Logger; +import org.slf4j.LoggerFactory; + +public class AFSKSampleBuffer +{ + private final static Logger mLog = LoggerFactory.getLogger(AFSKSampleBuffer.class); + + private final static float SYMBOL_SAMPLE_THRESHOLD = 1.0f; + protected boolean[] mDelayLine; + protected int mDelayLinePointer = 0; + + private int mHalfIntegerSamplesPerSymbol; + private int mPositiveSampleDecisionCount; + protected int mTwiceSamplesPerSymbol; + protected int mSymbolStart; + protected int mSymbolEnd; + protected int mSamplesPerSymbol; + protected float mSymbolSamplingPoint; + protected float mSymbolTimingGain; + + /** + * Buffer to store complex sample data and produce interpolated samples. + * @param samplesPerSymbol + */ + public AFSKSampleBuffer(int samplesPerSymbol, float symbolTimingGain) + { + mSamplesPerSymbol = samplesPerSymbol; + mSymbolTimingGain = symbolTimingGain; + + mHalfIntegerSamplesPerSymbol = samplesPerSymbol / 2; + mSymbolSamplingPoint = samplesPerSymbol; + mTwiceSamplesPerSymbol = 2 * samplesPerSymbol; + mDelayLine = new boolean[2 * mTwiceSamplesPerSymbol]; + } + + /** + * Stores the sample in the sample buffer to support making symbol decisions. + * + * Note: use the hasSymbol() method to determine if sufficient samples have been collected to make a symbol + * determination. + * + * @param sample to store in the sample buffer + */ + public void receive(boolean sample) + { + mSymbolSamplingPoint--; + + mDelayLine[mDelayLinePointer] = sample; + mDelayLine[mDelayLinePointer + mTwiceSamplesPerSymbol] = sample; + + //Increment pointer and keep pointer in bounds + mDelayLinePointer++; + mDelayLinePointer = mDelayLinePointer % mTwiceSamplesPerSymbol; + } + + /** + * Adjusts samples per symbol and symbol timing counters and increments the sample counter to collect another + * symbol. + * + * @param symbolTimingError from a symbol timing error detector + */ + public void resetAndAdjust(float symbolTimingError) + { + mSymbolSamplingPoint += (mSamplesPerSymbol + (symbolTimingError * mSymbolTimingGain)); + } + + public void setTimingGain(float gain) + { + mSymbolTimingGain = gain; + } + + /** + * Indicates if this buffer has accumulated enough samples to represent a full symbol + */ + public boolean hasSymbol() + { + return mSymbolSamplingPoint < SYMBOL_SAMPLE_THRESHOLD; + } + + /** + * Calculates the symbol as a majority decision where the number of positive/true samples are greater than half + * of the samples per symbol. Symbol decision is based on the samples collected over the preceeding 1.0 to 2.0 + * symbol periods. + * + * @return a true symbol if the majority of the samples were true or positive, otherwise a false symbol. + */ + public boolean getSymbol() + { + mPositiveSampleDecisionCount = 0; + + mSymbolStart = mDelayLinePointer; + mSymbolEnd = mDelayLinePointer + mSamplesPerSymbol - 1; + + for(int x = mSymbolStart; x <= mSymbolEnd; x++) + { + if(mDelayLine[x]) + { + mPositiveSampleDecisionCount++; + } + } + + return mPositiveSampleDecisionCount > mHalfIntegerSamplesPerSymbol; + } +} diff --git a/src/main/java/io/github/dsheirer/dsp/afsk/AFSKSampleBufferInstrumented.java b/src/main/java/io/github/dsheirer/dsp/afsk/AFSKSampleBufferInstrumented.java new file mode 100644 index 00000000..714e7065 --- /dev/null +++ b/src/main/java/io/github/dsheirer/dsp/afsk/AFSKSampleBufferInstrumented.java @@ -0,0 +1,90 @@ +/******************************************************************************* + * 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 + * + ******************************************************************************/ +package io.github.dsheirer.dsp.afsk; + +public class AFSKSampleBufferInstrumented extends AFSKSampleBuffer +{ + public boolean mLastSymbol; + + /** + * Buffer to store complex sample data and produce interpolated samples. + * + * @param samplesPerSymbol + * @param symbolTimingGain + */ + public AFSKSampleBufferInstrumented(int samplesPerSymbol, float symbolTimingGain) + { + super(samplesPerSymbol, symbolTimingGain); + } + + /** + * Sample delay buffer containing boolean (ie positive or negative) samples. + */ + public boolean[] getDelayLine() + { + return mDelayLine; + } + + /** + * Current delay buffer pointer which points to where the next sample will be stored and/or the oldest sample in + * the buffer. + */ + public int getDelayLinePointer() + { + return mDelayLinePointer; + } + + public int getDelayLineSecondPointer() + { + return mDelayLinePointer + mTwiceSamplesPerSymbol; + } + + /** + * Current symbol timing gain value + * @return + */ + public float getSymbolTimingGain() + { + return mSymbolTimingGain; + } + + public float getSamplesPerSymbol() + { + return mSamplesPerSymbol; + } + + @Override + public boolean getSymbol() + { + mLastSymbol = super.getSymbol(); + return mLastSymbol; + } + + public boolean getLastSymbol() + { + return mLastSymbol; + } + + public int getSymbolStart() + { + return mSymbolStart; + } + + public int getSymbolEnd() + { + return mSymbolEnd; + } +} diff --git a/src/main/java/io/github/dsheirer/dsp/fsk/AFSKTest.java b/src/main/java/io/github/dsheirer/dsp/afsk/AFSKTest.java similarity index 99% rename from src/main/java/io/github/dsheirer/dsp/fsk/AFSKTest.java rename to src/main/java/io/github/dsheirer/dsp/afsk/AFSKTest.java index d34a3f62..3e215f0c 100644 --- a/src/main/java/io/github/dsheirer/dsp/fsk/AFSKTest.java +++ b/src/main/java/io/github/dsheirer/dsp/afsk/AFSKTest.java @@ -13,7 +13,7 @@ * If not, see * ******************************************************************************/ -package io.github.dsheirer.dsp.fsk; +package io.github.dsheirer.dsp.afsk; import io.github.dsheirer.buffer.FloatAveragingBuffer; import io.github.dsheirer.dsp.mixer.IOscillator; diff --git a/src/main/java/io/github/dsheirer/dsp/afsk/AFSKTimingErrorDetector.java b/src/main/java/io/github/dsheirer/dsp/afsk/AFSKTimingErrorDetector.java new file mode 100644 index 00000000..92447773 --- /dev/null +++ b/src/main/java/io/github/dsheirer/dsp/afsk/AFSKTimingErrorDetector.java @@ -0,0 +1,103 @@ +/******************************************************************************* + * 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 + * + ******************************************************************************/ +package io.github.dsheirer.dsp.afsk; + +/** + * Zero crossing error detector. Provides symbol timing error correction values by detecting zero-crossings within + * each symbol period. Ideally there should be only 0 or 1 zero crossings within each symbol period. However, when + * there is a slight DC offset in the demodulated samples, there can be up to 2 zero crossings within a symbol + * period. + * + * This detector will only provide error corrections when there are 1 or 2 zero crossings detected. If the number + * of zero crossings is greater than 2, then it is assumed that there is no signal present and the returned timing + * error value is zero. This detector provides an error correction value relative to (approximately) the middle of + * a symbol period and therefore continuously attempts to align zero crossings to the middle of the period. + * + * Since this detector does NOT use interpolation, the error signal will contain minor levels of symbol timing + * jitter. Error signal values are reported as integrals of the sampling period. + */ +public class AFSKTimingErrorDetector +{ + protected boolean[] mBuffer; + protected int mDetectedZeroCrossing; + protected float mSymbolMiddle; + private int mZeroCrossingCount; + + /** + * Constructs the detector for the specified samples per symbol. + * + * @param samplesPerSymbol + */ + public AFSKTimingErrorDetector(int samplesPerSymbol) + { + mBuffer = new boolean[samplesPerSymbol]; + mSymbolMiddle = (float)(samplesPerSymbol - 1) / 2.0f; + } + + /** + * Stores the sample in the buffer. Use the getError() method once enough samples have been processed to determine + * the error for a symbol period. + * + * @param sample + */ + public void receive(boolean sample) + { + //Shift all samples left and store new sample at the end + System.arraycopy(mBuffer, 1, mBuffer, 0, mBuffer.length - 1); + mBuffer[mBuffer.length - 1] = sample; + } + + /** + * Determines the symbol timing error for the samples processed thus far. This method should be invoked once per + * symbol period, triggered via an external samples per symbol tracking process. + * + * @return symbol timing error calculated as the distance between the approximate symbol center and the next closest + * zero crossing index. An error value of zero is returned if the number of zero crossings is less than one or + * greater than two. + */ + public int getError() + { + mZeroCrossingCount = 0; + + for(int x = 0; x < mBuffer.length - 1; x++) + { + if(mBuffer[x] ^ mBuffer[x + 1]) + { + mDetectedZeroCrossing = x + 1; + mZeroCrossingCount++; + + if(mZeroCrossingCount > 1) + { + return 0; //Too many crossings + } + } + } + + if(mZeroCrossingCount == 1) + { + if(mDetectedZeroCrossing > mSymbolMiddle) + { + return -(mBuffer.length - mDetectedZeroCrossing); + } + else + { + return mDetectedZeroCrossing + 1; + } + } + + return 0; + } +} diff --git a/src/main/java/io/github/dsheirer/dsp/afsk/AFSKTimingErrorDetectorInstrumented.java b/src/main/java/io/github/dsheirer/dsp/afsk/AFSKTimingErrorDetectorInstrumented.java new file mode 100644 index 00000000..9824f654 --- /dev/null +++ b/src/main/java/io/github/dsheirer/dsp/afsk/AFSKTimingErrorDetectorInstrumented.java @@ -0,0 +1,53 @@ +/******************************************************************************* + * 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 + * + ******************************************************************************/ +package io.github.dsheirer.dsp.afsk; + +import javafx.beans.property.SimpleIntegerProperty; + +public class AFSKTimingErrorDetectorInstrumented extends AFSKTimingErrorDetector +{ + public SimpleIntegerProperty timingError = new SimpleIntegerProperty(); + + /** + * Constructs the detector for the specified samples per symbol. + * + * @param samplesPerSymbol + */ + public AFSKTimingErrorDetectorInstrumented(int samplesPerSymbol) + { + super(samplesPerSymbol); + } + + @Override + public int getError() + { + int error = super.getError(); + + timingError.setValue(error); + + return error; + } + + public int getDetectedZeroCrossing() + { + return mDetectedZeroCrossing; + } + + public boolean[] getBuffer() + { + return mBuffer; + } +} diff --git a/src/main/java/io/github/dsheirer/instrument/gui/viewer/chart/Fleetsync2SampleBufferChart.java b/src/main/java/io/github/dsheirer/instrument/gui/viewer/chart/Fleetsync2SampleBufferChart.java index bb3a8ff2..a6fb9a81 100644 --- a/src/main/java/io/github/dsheirer/instrument/gui/viewer/chart/Fleetsync2SampleBufferChart.java +++ b/src/main/java/io/github/dsheirer/instrument/gui/viewer/chart/Fleetsync2SampleBufferChart.java @@ -38,8 +38,8 @@ public class Fleetsync2SampleBufferChart extends LineChart private ObservableList> mSymbolSamples = FXCollections.observableArrayList(); private Series mSymbolSamplesSeries = new Series<>("Next Symbol Samples", mSymbolSamples); - private ObservableList> mSymbol = FXCollections.observableArrayList(); - private Series mSymbolSeries = new Series<>("Previous Symbol", mSymbol); + private ObservableList> mPreviousSymbol = FXCollections.observableArrayList(); + private Series mPreviousSymbolSeries = new Series<>("Previous Symbol", mPreviousSymbol); private ObservableList> mZeroCrossing = FXCollections.observableArrayList(); private Series mZeroCrossingSeries = new Series<>("Zero Crossing", mZeroCrossing); @@ -52,7 +52,7 @@ public class Fleetsync2SampleBufferChart extends LineChart new NumberAxis("Value", -1.0, 1.0, 0.25)); ObservableList observableList = FXCollections.observableArrayList(mSampleSeries, mSamplePointSeries, - mSymbolSamplesSeries, mSymbolSeries, mZeroCrossingSeries); + mSymbolSamplesSeries, mPreviousSymbolSeries, mZeroCrossingSeries); setData(observableList); @@ -91,10 +91,10 @@ public class Fleetsync2SampleBufferChart extends LineChart { setBuffer(mFleetsync2DecoderInstrumented.getAFSK1200Decoder().getSampleBuffer().getDelayLine()); - while(mSymbol.size() < 2) + while(mPreviousSymbol.size() < 2) { Data sample = new Data<>(0.0, -0.55); - mSymbol.add(sample); + mPreviousSymbol.add(sample); } while(mZeroCrossing.size() < 2) @@ -121,23 +121,23 @@ public class Fleetsync2SampleBufferChart extends LineChart mSamplePoints.get(0).setXValue(pointer1); mSamplePoints.get(1).setXValue(pointer2); - float samplesRemaining = mFleetsync2DecoderInstrumented.getAFSK1200Decoder().getSampleBuffer().getMidSymbolSamplingPoint(); float samplesPerSymbol = mFleetsync2DecoderInstrumented.getAFSK1200Decoder().getSampleBuffer().getSamplesPerSymbol(); - float start = pointer1 + samplesRemaining; - float end = start + samplesPerSymbol; - - mSymbolSamples.get(0).setXValue(start); - mSymbolSamples.get(1).setXValue(end); - boolean symbol = mFleetsync2DecoderInstrumented.getAFSK1200Decoder().getSampleBuffer().getLastSymbol(); int symbolStart = mFleetsync2DecoderInstrumented.getAFSK1200Decoder().getSampleBuffer().getSymbolStart(); int symbolEnd = mFleetsync2DecoderInstrumented.getAFSK1200Decoder().getSampleBuffer().getSymbolEnd(); - mSymbol.get(0).setXValue(symbolStart); - mSymbol.get(0).setYValue(symbol ? 0.55f : -0.55f); - mSymbol.get(1).setXValue(symbolEnd); - mSymbol.get(1).setYValue(symbol ? 0.55f : -0.55f); + float start = pointer1 + samplesPerSymbol - 1; + float end = symbolEnd; + + mSymbolSamples.get(0).setXValue(start); + mSymbolSamples.get(1).setXValue(end); + + + mPreviousSymbol.get(0).setXValue(symbolStart); + mPreviousSymbol.get(0).setYValue(symbol ? 0.55f : -0.55f); + mPreviousSymbol.get(1).setXValue(symbolEnd); + mPreviousSymbol.get(1).setYValue(symbol ? 0.55f : -0.55f); } } @@ -155,17 +155,16 @@ public class Fleetsync2SampleBufferChart extends LineChart int pointer1 = mFleetsync2DecoderInstrumented.getAFSK1200Decoder().getSampleBuffer().getDelayLinePointer(); int pointer2 = mFleetsync2DecoderInstrumented.getAFSK1200Decoder().getSampleBuffer().getDelayLineSecondPointer(); - float zeroCrossingIdeal = mFleetsync2DecoderInstrumented.getAFSK1200Decoder().getErrorDetector().getZeroCrossingIdeal(); - float detectedZeroCrossing = zeroCrossingIdeal - ((Number)newValue).floatValue(); + int detectedZeroCrossing = ((Number)newValue).intValue(); - int reference = (zeroCrossingIdeal < pointer1 || detectedZeroCrossing < pointer1 ? pointer2 : pointer1); - reference--; - - float start = reference - zeroCrossingIdeal; - float end = reference - detectedZeroCrossing; - - mZeroCrossing.get(0).setXValue(start); - mZeroCrossing.get(1).setXValue(end); +// int reference = (zeroCrossingIdeal < pointer1 || detectedZeroCrossing < pointer1 ? pointer2 : pointer1); +// reference--; +// +// float start = reference - zeroCrossingIdeal; +// float end = reference - detectedZeroCrossing; +// +// mZeroCrossing.get(0).setXValue(start); +// mZeroCrossing.get(1).setXValue(end); } } } diff --git a/src/main/java/io/github/dsheirer/instrument/gui/viewer/chart/FleetsyncZeroCrossingErrorDetectorChart.java b/src/main/java/io/github/dsheirer/instrument/gui/viewer/chart/FleetsyncZeroCrossingErrorDetectorChart.java index 7891c036..16c90e1f 100644 --- a/src/main/java/io/github/dsheirer/instrument/gui/viewer/chart/FleetsyncZeroCrossingErrorDetectorChart.java +++ b/src/main/java/io/github/dsheirer/instrument/gui/viewer/chart/FleetsyncZeroCrossingErrorDetectorChart.java @@ -35,9 +35,6 @@ public class FleetsyncZeroCrossingErrorDetectorChart extends LineChart private ObservableList> mPreviousSamples = FXCollections.observableArrayList(); private Series mPreviousSampleSeries = new Series<>("Samples", mPreviousSamples); - private ObservableList> mIdeal = FXCollections.observableArrayList(); - private Series mIdealSeries = new Series<>("Ideal", mIdeal); - private ObservableList> mDetected = FXCollections.observableArrayList(); private Series mDetectedSeries = new Series<>("Detected", mDetected); @@ -49,7 +46,7 @@ public class FleetsyncZeroCrossingErrorDetectorChart extends LineChart new NumberAxis("Value", -1.0, 1.0, 0.25)); ObservableList observableList = FXCollections.observableArrayList(mCurrentSampleSeries, - mPreviousSampleSeries, mIdealSeries, mDetectedSeries); + mPreviousSampleSeries, mDetectedSeries); setData(observableList); mFleetsync2DecoderInstrumented = decoder; @@ -71,11 +68,6 @@ public class FleetsyncZeroCrossingErrorDetectorChart extends LineChart mPreviousSamples.add(sample2); } - Data ideal1 = new Data<>(5, -0.1f); - mIdeal.add(ideal1); - Data ideal2 = new Data<>(5, -0.9f); - mIdeal.add(ideal2); - Data detected1 = new Data<>(5, -0.25f); mDetected.add(detected1); Data detected2 = new Data<>(5, -0.75f); @@ -108,11 +100,6 @@ public class FleetsyncZeroCrossingErrorDetectorChart extends LineChart sample.setYValue(samples[x] ? -0.4f : -0.6f); } - Data ideal1 = mIdeal.get(0); - ideal1.setXValue(mFleetsync2DecoderInstrumented.getAFSK1200Decoder().getErrorDetector().getZeroCrossingIdeal()); - Data ideal2 = mIdeal.get(1); - ideal2.setXValue(mFleetsync2DecoderInstrumented.getAFSK1200Decoder().getErrorDetector().getZeroCrossingIdeal()); - if(error == 0.0f) { Data detected1 = mDetected.get(0); diff --git a/src/main/java/io/github/dsheirer/instrument/gui/viewer/decoder/Fleetsync2Pane.java b/src/main/java/io/github/dsheirer/instrument/gui/viewer/decoder/Fleetsync2Pane.java index 647015b6..1d4f39d0 100644 --- a/src/main/java/io/github/dsheirer/instrument/gui/viewer/decoder/Fleetsync2Pane.java +++ b/src/main/java/io/github/dsheirer/instrument/gui/viewer/decoder/Fleetsync2Pane.java @@ -15,7 +15,6 @@ ******************************************************************************/ package io.github.dsheirer.instrument.gui.viewer.decoder; -import io.github.dsheirer.dsp.symbol.ISyncDetectListener; import io.github.dsheirer.instrument.gui.viewer.chart.DecodedSymbolChart; import io.github.dsheirer.instrument.gui.viewer.chart.Fleetsync2SampleBufferChart; import io.github.dsheirer.instrument.gui.viewer.chart.FleetsyncZeroCrossingErrorDetectorChart; @@ -67,15 +66,6 @@ public class Fleetsync2Pane extends RealDecoderPane getDecoder().getAFSK1200Decoder().addListener(getDecodedSymbolChart()); - getDecoder().setSyncDetectListener(new ISyncDetectListener() - { - @Override - public void syncDetected() - { - mLog.debug("Sync Detected!!!!!"); - } - }); - getDecoder().setMessageListener(new Listener() { @Override @@ -90,7 +80,7 @@ public class Fleetsync2Pane extends RealDecoderPane { if(mDecoder == null) { - mDecoder = new Fleetsync2DecoderInstrumented(null); + mDecoder = new Fleetsync2DecoderInstrumented(); mDecoder.setMessageListener(new Listener() { @Override diff --git a/src/main/java/io/github/dsheirer/module/decode/afsk/AbstractAFSKDecoder.java b/src/main/java/io/github/dsheirer/module/decode/afsk/AbstractAFSKDecoder.java new file mode 100644 index 00000000..466642e9 --- /dev/null +++ b/src/main/java/io/github/dsheirer/module/decode/afsk/AbstractAFSKDecoder.java @@ -0,0 +1,61 @@ +/******************************************************************************* + * 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 + * + ******************************************************************************/ +package io.github.dsheirer.module.decode.afsk; + +import io.github.dsheirer.dsp.afsk.AFSK1200Decoder; +import io.github.dsheirer.module.decode.Decoder; +import io.github.dsheirer.sample.Listener; +import io.github.dsheirer.sample.buffer.IReusableBufferListener; +import io.github.dsheirer.sample.buffer.ReusableBuffer; + +/** + * Abstract class for Audio Frequency Shift Keying (AFSK) 1200-baud decoder based decoder modules. + * + * This class handles the AFSK 1200 decoder and incoming sample stream buffer management. + * + * Sub-class implementations should invoke getDecoder().setListener() to receive decoded symbol stream. + */ +public abstract class AbstractAFSKDecoder extends Decoder implements IReusableBufferListener, Listener +{ + private AFSK1200Decoder mAFSK1200Decoder; + + public AbstractAFSKDecoder(AFSK1200Decoder decoder) + { + mAFSK1200Decoder = decoder; + } + + public AbstractAFSKDecoder(AFSK1200Decoder.Output output) + { + mAFSK1200Decoder = new AFSK1200Decoder(output); + } + + protected AFSK1200Decoder getDecoder() + { + return mAFSK1200Decoder; + } + + @Override + public void receive(ReusableBuffer reusableBuffer) + { + mAFSK1200Decoder.receive(reusableBuffer); + } + + @Override + public Listener getReusableBufferListener() + { + return this; + } +} diff --git a/src/main/java/io/github/dsheirer/module/decode/fleetsync2/Fleetsync2Decoder.java b/src/main/java/io/github/dsheirer/module/decode/fleetsync2/Fleetsync2Decoder.java index f05d9366..5196376f 100644 --- a/src/main/java/io/github/dsheirer/module/decode/fleetsync2/Fleetsync2Decoder.java +++ b/src/main/java/io/github/dsheirer/module/decode/fleetsync2/Fleetsync2Decoder.java @@ -20,34 +20,25 @@ package io.github.dsheirer.module.decode.fleetsync2; import io.github.dsheirer.alias.AliasList; import io.github.dsheirer.bits.MessageFramer; import io.github.dsheirer.bits.SyncPattern; -import io.github.dsheirer.dsp.fsk.AFSK1200Decoder; -import io.github.dsheirer.dsp.symbol.ISyncDetectListener; -import io.github.dsheirer.dsp.symbol.SyncDetectProvider; -import io.github.dsheirer.module.decode.Decoder; +import io.github.dsheirer.dsp.afsk.AFSK1200Decoder; import io.github.dsheirer.module.decode.DecoderType; -import io.github.dsheirer.sample.Listener; -import io.github.dsheirer.sample.buffer.IReusableBufferListener; -import io.github.dsheirer.sample.buffer.ReusableBuffer; +import io.github.dsheirer.module.decode.afsk.AbstractAFSKDecoder; /** * Fleetsync II Decoder */ -public class Fleetsync2Decoder extends Decoder implements IReusableBufferListener, Listener, - SyncDetectProvider +public class Fleetsync2Decoder extends AbstractAFSKDecoder { - /* Message length - 5 x REVS + 16 x SYNC + 8 x 64Bit Blocks */ - protected static final int MESSAGE_LENGTH = 537; - - protected AFSK1200Decoder mAFSK1200Decoder; + //Message length - 5 x REVS + 16 x SYNC + 8 x 64Bit Blocks + private static final int MESSAGE_LENGTH = 537; private MessageFramer mMessageFramer; private Fleetsync2MessageProcessor mMessageProcessor; public Fleetsync2Decoder(AFSK1200Decoder afsk1200Decoder, AliasList aliasList) { - mAFSK1200Decoder = afsk1200Decoder; + super(afsk1200Decoder); mMessageFramer = new MessageFramer(SyncPattern.FLEETSYNC2.getPattern(), MESSAGE_LENGTH); - mAFSK1200Decoder.setMessageFramer(mMessageFramer); - mMessageFramer.setSyncDetectListener(mAFSK1200Decoder); + getDecoder().setMessageFramer(mMessageFramer); mMessageProcessor = new Fleetsync2MessageProcessor(aliasList); mMessageFramer.addMessageListener(mMessageProcessor); mMessageProcessor.setMessageListener(getMessageListener()); @@ -55,12 +46,7 @@ public class Fleetsync2Decoder extends Decoder implements IReusableBufferListene public Fleetsync2Decoder(AliasList aliasList) { - this(new AFSK1200Decoder(MESSAGE_LENGTH, false), aliasList); - } - - public void dispose() - { - super.dispose(); + this(new AFSK1200Decoder(AFSK1200Decoder.Output.NORMAL), aliasList); } @Override @@ -69,27 +55,9 @@ public class Fleetsync2Decoder extends Decoder implements IReusableBufferListene return DecoderType.FLEETSYNC2; } - @Override - public Listener getReusableBufferListener() - { - return mAFSK1200Decoder; - } - - @Override - public void receive(ReusableBuffer reusableBuffer) - { - mAFSK1200Decoder.receive(reusableBuffer); - } - @Override public void reset() { mMessageFramer.reset(); } - - @Override - public void setSyncDetectListener(ISyncDetectListener listener) - { - mMessageFramer.setSyncDetectListener(listener); - } } diff --git a/src/main/java/io/github/dsheirer/module/decode/fleetsync2/Fleetsync2DecoderInstrumented.java b/src/main/java/io/github/dsheirer/module/decode/fleetsync2/Fleetsync2DecoderInstrumented.java index e2b845af..5dcf8476 100644 --- a/src/main/java/io/github/dsheirer/module/decode/fleetsync2/Fleetsync2DecoderInstrumented.java +++ b/src/main/java/io/github/dsheirer/module/decode/fleetsync2/Fleetsync2DecoderInstrumented.java @@ -15,28 +15,25 @@ ******************************************************************************/ package io.github.dsheirer.module.decode.fleetsync2; -import io.github.dsheirer.alias.AliasList; -import io.github.dsheirer.dsp.fsk.AFSK1200Decoder; -import io.github.dsheirer.dsp.fsk.AFSK1200DecoderInstrumented; -import io.github.dsheirer.dsp.fsk.SampleBufferInstrumented; -import io.github.dsheirer.dsp.fsk.ZeroCrossingErrorDetectorInstrumented; +import io.github.dsheirer.dsp.afsk.AFSK1200DecoderInstrumented; import io.github.dsheirer.sample.buffer.ReusableBuffer; import javafx.beans.property.SimpleIntegerProperty; +/** + * Instrumented version of the AFSK-1200 decoder for use with DemodulatorViewerFX + */ public class Fleetsync2DecoderInstrumented extends Fleetsync2Decoder { public SimpleIntegerProperty bufferCount = new SimpleIntegerProperty(); - public Fleetsync2DecoderInstrumented(AliasList aliasList) + public Fleetsync2DecoderInstrumented() { - super(new AFSK1200DecoderInstrumented(MESSAGE_LENGTH, - new SampleBufferInstrumented(AFSK1200Decoder.SAMPLES_PER_SYMBOL, AFSK1200Decoder.COARSE_TIMING_GAIN), - new ZeroCrossingErrorDetectorInstrumented(AFSK1200Decoder.SAMPLES_PER_SYMBOL), false), aliasList); + super(new AFSK1200DecoderInstrumented(false), null); } public AFSK1200DecoderInstrumented getAFSK1200Decoder() { - return (AFSK1200DecoderInstrumented)mAFSK1200Decoder; + return (AFSK1200DecoderInstrumented)getDecoder(); } @Override From 1fdcb623a61e9c17e42fa61f63619d17b97a3e4d Mon Sep 17 00:00:00 2001 From: Denny Date: Mon, 11 Jun 2018 04:32:30 -0400 Subject: [PATCH 72/87] [WIP]Updates the MPT-1327 decoder to use the new AFSK1200 decoder class. --- .../dsheirer/bits/IBinarySymbolProcessor.java | 21 ++ .../github/dsheirer/bits/MessageFramer.java | 2 +- .../dsheirer/dsp/afsk/AFSK1200Decoder.java | 16 +- .../dsp/afsk/AFSK1200DecoderInstrumented.java | 6 +- .../module/decode/DecoderFactory.java | 12 +- .../decode/fleetsync2/Fleetsync2Decoder.java | 31 +- .../decode/mpt1327/DecodeConfigMPT1327.java | 1 - .../module/decode/mpt1327/MPT1327Decoder.java | 274 +++--------------- .../decode/mpt1327/MPT1327DecoderLegacy.java | 253 ++++++++++++++++ .../dsheirer/module/decode/mpt1327/Sync.java | 55 ++++ 10 files changed, 400 insertions(+), 271 deletions(-) create mode 100644 src/main/java/io/github/dsheirer/bits/IBinarySymbolProcessor.java create mode 100644 src/main/java/io/github/dsheirer/module/decode/mpt1327/MPT1327DecoderLegacy.java create mode 100644 src/main/java/io/github/dsheirer/module/decode/mpt1327/Sync.java diff --git a/src/main/java/io/github/dsheirer/bits/IBinarySymbolProcessor.java b/src/main/java/io/github/dsheirer/bits/IBinarySymbolProcessor.java new file mode 100644 index 00000000..a46185ad --- /dev/null +++ b/src/main/java/io/github/dsheirer/bits/IBinarySymbolProcessor.java @@ -0,0 +1,21 @@ +/******************************************************************************* + * 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 + * + ******************************************************************************/ +package io.github.dsheirer.bits; + +public interface IBinarySymbolProcessor +{ + void receive(boolean symbol); +} diff --git a/src/main/java/io/github/dsheirer/bits/MessageFramer.java b/src/main/java/io/github/dsheirer/bits/MessageFramer.java index 3a3acc64..27bdcabe 100644 --- a/src/main/java/io/github/dsheirer/bits/MessageFramer.java +++ b/src/main/java/io/github/dsheirer/bits/MessageFramer.java @@ -32,7 +32,7 @@ import java.util.List; * Will extract multiple messages simultaneously, for each sync pattern that is * encountered within the bitset bit stream. */ -public class MessageFramer implements Listener, SyncDetectProvider +public class MessageFramer implements IBinarySymbolProcessor, Listener, SyncDetectProvider { private boolean[] mSyncPattern; private int mMessageLength; diff --git a/src/main/java/io/github/dsheirer/dsp/afsk/AFSK1200Decoder.java b/src/main/java/io/github/dsheirer/dsp/afsk/AFSK1200Decoder.java index fd022a49..ca72ac66 100644 --- a/src/main/java/io/github/dsheirer/dsp/afsk/AFSK1200Decoder.java +++ b/src/main/java/io/github/dsheirer/dsp/afsk/AFSK1200Decoder.java @@ -15,7 +15,7 @@ ******************************************************************************/ package io.github.dsheirer.dsp.afsk; -import io.github.dsheirer.bits.MessageFramer; +import io.github.dsheirer.bits.IBinarySymbolProcessor; import io.github.dsheirer.buffer.FloatAveragingBuffer; import io.github.dsheirer.dsp.filter.design.FilterDesignException; import io.github.dsheirer.dsp.filter.fir.FIRFilterSpecification; @@ -96,7 +96,7 @@ public class AFSK1200Decoder implements Listener protected AFSKSampleBuffer mSampleBuffer; protected AFSKTimingErrorDetector mTimingErrorDetector = new AFSKTimingErrorDetector(SAMPLES_PER_SYMBOL); private RealFIRFilter2 mBandPassFilter = new RealFIRFilter2(sBandPassFilterCoefficients); - protected MessageFramer mMessageFramer; + protected IBinarySymbolProcessor mBinarySymbolProcessor; private boolean mSampleDecision; //Resample to an integral of the baud rate 1200 baud * 6 samples per symbol = 7200.0 Hertz @@ -155,20 +155,20 @@ public class AFSK1200Decoder implements Listener protected void dispatch(boolean symbol) { - if(mMessageFramer != null) + if(mBinarySymbolProcessor != null) { - mMessageFramer.receive(mNormalOutput ? symbol : !symbol); + mBinarySymbolProcessor.receive(mNormalOutput ? symbol : !symbol); } } /** * Registers a listener to receive decoded LTR symbols. * - * @param messageFramer to receive symbols. + * @param binarySymbolProcessor to receive symbols. */ - public void setMessageFramer(MessageFramer messageFramer) + public void setSymbolProcessor(IBinarySymbolProcessor binarySymbolProcessor) { - mMessageFramer = messageFramer; + mBinarySymbolProcessor = binarySymbolProcessor; } /** @@ -176,7 +176,7 @@ public class AFSK1200Decoder implements Listener */ public void removeListener() { - mMessageFramer = null; + mBinarySymbolProcessor = null; } diff --git a/src/main/java/io/github/dsheirer/dsp/afsk/AFSK1200DecoderInstrumented.java b/src/main/java/io/github/dsheirer/dsp/afsk/AFSK1200DecoderInstrumented.java index 5aef1105..dd0e444f 100644 --- a/src/main/java/io/github/dsheirer/dsp/afsk/AFSK1200DecoderInstrumented.java +++ b/src/main/java/io/github/dsheirer/dsp/afsk/AFSK1200DecoderInstrumented.java @@ -40,11 +40,7 @@ public class AFSK1200DecoderInstrumented extends AFSK1200Decoder protected void dispatch(boolean symbol) { - if(mMessageFramer != null) - { - mMessageFramer.receive(mNormalOutput ? symbol : !symbol); - } - + super.dispatch(symbol); mSymbolBroadcaster.broadcast(symbol); } diff --git a/src/main/java/io/github/dsheirer/module/decode/DecoderFactory.java b/src/main/java/io/github/dsheirer/module/decode/DecoderFactory.java index 5a3a3308..f0cfe14e 100644 --- a/src/main/java/io/github/dsheirer/module/decode/DecoderFactory.java +++ b/src/main/java/io/github/dsheirer/module/decode/DecoderFactory.java @@ -64,10 +64,10 @@ import io.github.dsheirer.module.decode.mdc1200.MDCDecoderState; import io.github.dsheirer.module.decode.mdc1200.MDCMessageFilter; import io.github.dsheirer.module.decode.mpt1327.DecodeConfigMPT1327; import io.github.dsheirer.module.decode.mpt1327.MPT1327Decoder; -import io.github.dsheirer.module.decode.mpt1327.MPT1327Decoder.Sync; import io.github.dsheirer.module.decode.mpt1327.MPT1327DecoderEditor; import io.github.dsheirer.module.decode.mpt1327.MPT1327DecoderState; import io.github.dsheirer.module.decode.mpt1327.MPT1327MessageFilter; +import io.github.dsheirer.module.decode.mpt1327.Sync; import io.github.dsheirer.module.decode.nbfm.DecodeConfigNBFM; import io.github.dsheirer.module.decode.nbfm.NBFMDecoder; import io.github.dsheirer.module.decode.nbfm.NBFMDecoderEditor; @@ -175,15 +175,12 @@ public class DecoderFactory break; case MPT1327: DecodeConfigMPT1327 mptConfig = (DecodeConfigMPT1327) decodeConfig; - ChannelMap channelMap = channelMapModel.getChannelMap(mptConfig.getChannelMapName()); - Sync sync = mptConfig.getSync(); - modules.add(new MPT1327Decoder(aliasList, sync)); - - modules.add(new MPT1327DecoderState(aliasList, channelMap, channelType, mptConfig.getCallTimeout() * 1000)); + modules.add(new FMDemodulatorModule(CHANNEL_BANDWIDTH, DEMODULATED_AUDIO_SAMPLE_RATE)); + modules.add(new AudioModule(metadata)); if(channelType == ChannelType.STANDARD) { @@ -191,9 +188,6 @@ public class DecoderFactory channel.getRecordConfiguration(), channel.getSystem(), channel.getSite(), (aliasList != null ? aliasList.getName() : null), mptConfig.getTrafficChannelPoolSize())); } - - modules.add(new FMDemodulatorModule(CHANNEL_BANDWIDTH, DEMODULATED_AUDIO_SAMPLE_RATE)); - modules.add(new AudioModule(metadata)); break; case PASSPORT: modules.add(new PassportDecoder(decodeConfig, aliasList)); diff --git a/src/main/java/io/github/dsheirer/module/decode/fleetsync2/Fleetsync2Decoder.java b/src/main/java/io/github/dsheirer/module/decode/fleetsync2/Fleetsync2Decoder.java index 5196376f..dc3f52af 100644 --- a/src/main/java/io/github/dsheirer/module/decode/fleetsync2/Fleetsync2Decoder.java +++ b/src/main/java/io/github/dsheirer/module/decode/fleetsync2/Fleetsync2Decoder.java @@ -34,21 +34,40 @@ public class Fleetsync2Decoder extends AbstractAFSKDecoder private MessageFramer mMessageFramer; private Fleetsync2MessageProcessor mMessageProcessor; + /** + * Constructs a decoder for Fleetsync II protocol + * @param afsk1200Decoder to use for decoding the symbols + * @param aliasList to assign aliases to decoded values + */ public Fleetsync2Decoder(AFSK1200Decoder afsk1200Decoder, AliasList aliasList) { super(afsk1200Decoder); + init(aliasList); + } + + /** + * Constructs a decoder for Fleetsync II protocol + * @param aliasList to assign aliases to decoded values + */ + public Fleetsync2Decoder(AliasList aliasList) + { + super(AFSK1200Decoder.Output.NORMAL); + init(aliasList); + } + + /** + * Initializes the decoding chain. + * @param aliasList to assign to the message processor + */ + private void init(AliasList aliasList) + { mMessageFramer = new MessageFramer(SyncPattern.FLEETSYNC2.getPattern(), MESSAGE_LENGTH); - getDecoder().setMessageFramer(mMessageFramer); + getDecoder().setSymbolProcessor(mMessageFramer); mMessageProcessor = new Fleetsync2MessageProcessor(aliasList); mMessageFramer.addMessageListener(mMessageProcessor); mMessageProcessor.setMessageListener(getMessageListener()); } - public Fleetsync2Decoder(AliasList aliasList) - { - this(new AFSK1200Decoder(AFSK1200Decoder.Output.NORMAL), aliasList); - } - @Override public DecoderType getDecoderType() { diff --git a/src/main/java/io/github/dsheirer/module/decode/mpt1327/DecodeConfigMPT1327.java b/src/main/java/io/github/dsheirer/module/decode/mpt1327/DecodeConfigMPT1327.java index 172ee2e7..40b5a3c6 100644 --- a/src/main/java/io/github/dsheirer/module/decode/mpt1327/DecodeConfigMPT1327.java +++ b/src/main/java/io/github/dsheirer/module/decode/mpt1327/DecodeConfigMPT1327.java @@ -20,7 +20,6 @@ package io.github.dsheirer.module.decode.mpt1327; import com.fasterxml.jackson.dataformat.xml.annotation.JacksonXmlProperty; import io.github.dsheirer.module.decode.DecoderType; import io.github.dsheirer.module.decode.config.DecodeConfiguration; -import io.github.dsheirer.module.decode.mpt1327.MPT1327Decoder.Sync; public class DecodeConfigMPT1327 extends DecodeConfiguration { diff --git a/src/main/java/io/github/dsheirer/module/decode/mpt1327/MPT1327Decoder.java b/src/main/java/io/github/dsheirer/module/decode/mpt1327/MPT1327Decoder.java index 6dc1fc84..eef7ba35 100644 --- a/src/main/java/io/github/dsheirer/module/decode/mpt1327/MPT1327Decoder.java +++ b/src/main/java/io/github/dsheirer/module/decode/mpt1327/MPT1327Decoder.java @@ -1,161 +1,65 @@ /******************************************************************************* - * 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 + * You should have received a copy of the GNU General Public License along with this program. + * If not, see * ******************************************************************************/ package io.github.dsheirer.module.decode.mpt1327; import io.github.dsheirer.alias.AliasList; +import io.github.dsheirer.bits.IBinarySymbolProcessor; import io.github.dsheirer.bits.MessageFramer; -import io.github.dsheirer.bits.SyncPattern; -import io.github.dsheirer.dsp.filter.FilterFactory; -import io.github.dsheirer.dsp.filter.Filters; -import io.github.dsheirer.dsp.filter.fir.FIRFilterSpecification; -import io.github.dsheirer.dsp.filter.fir.real.RealFIRFilter_RB_RB; -import io.github.dsheirer.dsp.filter.halfband.real.HalfBandFilter_RB_RB; -import io.github.dsheirer.dsp.fsk.FSK2Decoder; -import io.github.dsheirer.instrument.Instrumentable; -import io.github.dsheirer.instrument.tap.Tap; -import io.github.dsheirer.instrument.tap.TapGroup; -import io.github.dsheirer.instrument.tap.stream.BinaryTap; -import io.github.dsheirer.instrument.tap.stream.FloatBufferTap; -import io.github.dsheirer.instrument.tap.stream.FloatTap; -import io.github.dsheirer.module.decode.Decoder; +import io.github.dsheirer.dsp.afsk.AFSK1200Decoder; import io.github.dsheirer.module.decode.DecoderType; -import io.github.dsheirer.sample.Broadcaster; -import io.github.dsheirer.sample.Listener; -import io.github.dsheirer.sample.real.IFilteredRealBufferListener; -import io.github.dsheirer.sample.real.RealBuffer; +import io.github.dsheirer.module.decode.afsk.AbstractAFSKDecoder; import org.slf4j.Logger; import org.slf4j.LoggerFactory; -import java.util.ArrayList; -import java.util.List; - -/** - * MPT1327 Decoder - 1200 baud 2FSK decoder that can process 48k sample rate - * complex or floating point samples and output fully framed MPT1327 control and - * traffic messages. - */ -public class MPT1327Decoder extends Decoder implements IFilteredRealBufferListener, Instrumentable +public class MPT1327Decoder extends AbstractAFSKDecoder implements IBinarySymbolProcessor { private final static Logger mLog = LoggerFactory.getLogger(MPT1327Decoder.class); - private static final FIRFilterSpecification HIGH_PASS_SPECIFICATION = FIRFilterSpecification.highPassBuilder() - .sampleRate( 24000 ) - .stopBandCutoff( 800 ) - .stopBandAmplitude( 0.0 ) - .stopBandRipple( 0.03 ) - .passBandStart( 1000 ) - .passBandAmplitude( 1.0 ) - .passBandRipple( 0.08 ) - .build(); - - private static float[] HIGHPASS_FILTER; - - static - { - try - { - HIGHPASS_FILTER = FilterFactory.getTaps(HIGH_PASS_SPECIFICATION); - } - catch(Exception e) - { - mLog.error("Couldn't design MPT1327 highpass filter"); - } - } - - /* Decimated sample rate ( 48,000 / 2 = 24,000 ) feeding the decoder */ - private static final int sDECIMATED_SAMPLE_RATE = 24000; - - /* Baud or Symbol Rate */ - private static final int sSYMBOL_RATE = 1200; - /* Message length -- longest possible message is: * 4xREVS + 16xSYNC + 64xADD1 + 64xDCW1 + 64xDCW2 + 64xDCW3 + 64xDCW4 */ - private static final int sMESSAGE_LENGTH = 350; + private static final int MESSAGE_LENGTH = 350; - /* Instrumentation Taps */ - private static final String INSTRUMENT_HB1_FILTER_TO_HIGH_PASS = - "Tap Point: Half-band Decimation Filter > < Low Pass Filter"; - private static final String INSTRUMENT_HIGH_PASS_TO_DECODER = - "Tap Point: Low Pass Filter > < Decoder"; - private static final String INSTRUMENT_DECODER_TO_FRAMER = - "Tap Point: Decoder > < Sync Detect/Message Framer"; - private List mAvailableTaps; - - private HalfBandFilter_RB_RB mDecimationFilter; - private RealFIRFilter_RB_RB mHighPassFilter; - private FSK2Decoder mFSKDecoder; - private Broadcaster mSymbolBroadcaster; private MessageFramer mControlMessageFramer; private MessageFramer mTrafficMessageFramer; private MPT1327MessageProcessor mMessageProcessor; public MPT1327Decoder(AliasList aliasList, Sync sync) { - /** - * Normal: 2FSK Decoder with inverted output - * French: 2FSK Decoder with normal output - */ - if(sync == Sync.NORMAL) - { - mFSKDecoder = new FSK2Decoder(24000, 1200, FSK2Decoder.Output.INVERTED); - } - else if(sync == Sync.FRENCH) - { - mFSKDecoder = new FSK2Decoder(24000, 1200, FSK2Decoder.Output.NORMAL); - } - else - { - throw new IllegalArgumentException("MPT1327 Decoder - unrecognized Sync type"); - } + super((sync == Sync.NORMAL ? AFSK1200Decoder.Output.NORMAL : AFSK1200Decoder.Output.INVERTED)); - /* Decimation filter - 48000 / 2 = 24000 output */ - mDecimationFilter = new HalfBandFilter_RB_RB(Filters.FIR_HALF_BAND_31T_ONE_EIGHTH_FCO.getCoefficients(), 1.0002f, true); + getDecoder().setSymbolProcessor(this); - //High-pass filter the audio to remove any DC component or CTCSS tones. The audio has already been low-pass - //filtered by the AudioDemodulation filter cutoff of 3400 Hz. - mHighPassFilter = new RealFIRFilter_RB_RB(HIGHPASS_FILTER, 1.0f); - mDecimationFilter.setListener(mHighPassFilter); + //Message framer for control channel messages + mControlMessageFramer = new MessageFramer(sync.getControlSyncPattern().getPattern(), MESSAGE_LENGTH); - mHighPassFilter.setListener(mFSKDecoder); + //Message framer for traffic channel massages + mTrafficMessageFramer = new MessageFramer(sync.getTrafficSyncPattern().getPattern(), MESSAGE_LENGTH); - mSymbolBroadcaster = new Broadcaster(); - mFSKDecoder.setListener(mSymbolBroadcaster); - - /* Message framer for control channel messages */ - mControlMessageFramer = new MessageFramer(sync.getControlSyncPattern().getPattern(), sMESSAGE_LENGTH); - mSymbolBroadcaster.addListener(mControlMessageFramer); - - /* Message framer for traffic channel massages */ - mTrafficMessageFramer = new MessageFramer(sync.getTrafficSyncPattern().getPattern(), sMESSAGE_LENGTH); - mSymbolBroadcaster.addListener(mTrafficMessageFramer); - - /* Fully decoded and framed messages processor */ + //Fully decoded and framed messages processor mMessageProcessor = new MPT1327MessageProcessor(aliasList); mMessageProcessor.setMessageListener(getMessageListener()); + mControlMessageFramer.addMessageListener(mMessageProcessor); mTrafficMessageFramer.addMessageListener(mMessageProcessor); } - @Override - public Listener getFilteredRealBufferListener() + public void receive(boolean symbol) { - return mDecimationFilter; + mControlMessageFramer.receive(symbol); + mTrafficMessageFramer.receive(symbol); } @Override @@ -164,6 +68,13 @@ public class MPT1327Decoder extends Decoder implements IFilteredRealBufferListen return DecoderType.MPT1327; } + @Override + public void reset() + { + mControlMessageFramer.reset(); + mTrafficMessageFramer.reset(); + } + /** * Cleanup method */ @@ -171,128 +82,9 @@ public class MPT1327Decoder extends Decoder implements IFilteredRealBufferListen { super.dispose(); - mSymbolBroadcaster.dispose(); - - mControlMessageFramer.dispose(); - - mFSKDecoder.dispose(); - - mDecimationFilter.dispose(); - mMessageProcessor.dispose(); - + mControlMessageFramer.dispose(); mTrafficMessageFramer.dispose(); } - /* Instrumentation Taps */ - @Override - public List getTapGroups() - { - if(mAvailableTaps == null) - { - mAvailableTaps = new ArrayList(); - - TapGroup group = new TapGroup("MPT-1327 Decoder"); - - group.add(new FloatTap(INSTRUMENT_HB1_FILTER_TO_HIGH_PASS, 0, 0.5f)); - group.add(new FloatTap(INSTRUMENT_HIGH_PASS_TO_DECODER, 0, 0.5f)); - group.add(new BinaryTap(INSTRUMENT_DECODER_TO_FRAMER, 0, 0.025f)); - - mAvailableTaps.add(group); - - /* Add the taps from the FSK decoder */ - mAvailableTaps.addAll(mFSKDecoder.getTapGroups()); - } - - return mAvailableTaps; - } - - @Override - public void registerTap(Tap tap) - { - /* Send request to decoder */ - mFSKDecoder.registerTap(tap); - - switch(tap.getName()) - { - case INSTRUMENT_HB1_FILTER_TO_HIGH_PASS: - FloatBufferTap hb1Tap = (FloatBufferTap) tap; - mDecimationFilter.setListener(hb1Tap); - hb1Tap.setListener(mHighPassFilter); - break; - case INSTRUMENT_HIGH_PASS_TO_DECODER: - FloatBufferTap lowTap = (FloatBufferTap) tap; - mHighPassFilter.setListener(lowTap); - lowTap.setListener(mFSKDecoder); - break; - case INSTRUMENT_DECODER_TO_FRAMER: - BinaryTap decoderTap = (BinaryTap) tap; - mFSKDecoder.setListener(decoderTap); - decoderTap.setListener(mSymbolBroadcaster); - break; - } - } - - @Override - public void unregisterTap(Tap tap) - { - mFSKDecoder.unregisterTap(tap); - - switch(tap.getName()) - { - case INSTRUMENT_HB1_FILTER_TO_HIGH_PASS: - mDecimationFilter.setListener(mHighPassFilter); - break; - case INSTRUMENT_HIGH_PASS_TO_DECODER: - mHighPassFilter.setListener(mFSKDecoder); - break; - case INSTRUMENT_DECODER_TO_FRAMER: - mFSKDecoder.setListener(mSymbolBroadcaster); - break; - } - } - - public enum Sync - { - NORMAL("Normal", SyncPattern.MPT1327_CONTROL, SyncPattern.MPT1327_TRAFFIC), - FRENCH("French", SyncPattern.MPT1327_CONTROL_FRENCH, SyncPattern.MPT1327_TRAFFIC_FRENCH); - - private String mLabel; - private SyncPattern mControlSyncPattern; - private SyncPattern mTrafficSyncPattern; - - private Sync(String label, SyncPattern controlPattern, SyncPattern trafficPattern) - { - mLabel = label; - mControlSyncPattern = controlPattern; - mTrafficSyncPattern = trafficPattern; - } - - public String getLabel() - { - return mLabel; - } - - public SyncPattern getControlSyncPattern() - { - return mControlSyncPattern; - } - - public SyncPattern getTrafficSyncPattern() - { - return mTrafficSyncPattern; - } - - public String toString() - { - return getLabel(); - } - } - - @Override - public void reset() - { - mControlMessageFramer.reset(); - mTrafficMessageFramer.reset(); - } } diff --git a/src/main/java/io/github/dsheirer/module/decode/mpt1327/MPT1327DecoderLegacy.java b/src/main/java/io/github/dsheirer/module/decode/mpt1327/MPT1327DecoderLegacy.java new file mode 100644 index 00000000..be8fef25 --- /dev/null +++ b/src/main/java/io/github/dsheirer/module/decode/mpt1327/MPT1327DecoderLegacy.java @@ -0,0 +1,253 @@ +/******************************************************************************* + * 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 io.github.dsheirer.module.decode.mpt1327; + +import io.github.dsheirer.alias.AliasList; +import io.github.dsheirer.bits.MessageFramer; +import io.github.dsheirer.dsp.filter.FilterFactory; +import io.github.dsheirer.dsp.filter.Filters; +import io.github.dsheirer.dsp.filter.fir.FIRFilterSpecification; +import io.github.dsheirer.dsp.filter.fir.real.RealFIRFilter_RB_RB; +import io.github.dsheirer.dsp.filter.halfband.real.HalfBandFilter_RB_RB; +import io.github.dsheirer.dsp.fsk.FSK2Decoder; +import io.github.dsheirer.instrument.Instrumentable; +import io.github.dsheirer.instrument.tap.Tap; +import io.github.dsheirer.instrument.tap.TapGroup; +import io.github.dsheirer.instrument.tap.stream.BinaryTap; +import io.github.dsheirer.instrument.tap.stream.FloatBufferTap; +import io.github.dsheirer.instrument.tap.stream.FloatTap; +import io.github.dsheirer.module.decode.Decoder; +import io.github.dsheirer.module.decode.DecoderType; +import io.github.dsheirer.sample.Broadcaster; +import io.github.dsheirer.sample.Listener; +import io.github.dsheirer.sample.real.IFilteredRealBufferListener; +import io.github.dsheirer.sample.real.RealBuffer; +import org.slf4j.Logger; +import org.slf4j.LoggerFactory; + +import java.util.ArrayList; +import java.util.List; + +/** + * MPT1327 Decoder - 1200 baud 2FSK decoder that can process 48k sample rate + * complex or floating point samples and output fully framed MPT1327 control and + * traffic messages. + */ +public class MPT1327DecoderLegacy extends Decoder implements IFilteredRealBufferListener, Instrumentable +{ + private final static Logger mLog = LoggerFactory.getLogger(MPT1327DecoderLegacy.class); + + private static final FIRFilterSpecification HIGH_PASS_SPECIFICATION = FIRFilterSpecification.highPassBuilder() + .sampleRate( 24000 ) + .stopBandCutoff( 800 ) + .stopBandAmplitude( 0.0 ) + .stopBandRipple( 0.03 ) + .passBandStart( 1000 ) + .passBandAmplitude( 1.0 ) + .passBandRipple( 0.08 ) + .build(); + + private static float[] HIGHPASS_FILTER; + + static + { + try + { + HIGHPASS_FILTER = FilterFactory.getTaps(HIGH_PASS_SPECIFICATION); + } + catch(Exception e) + { + mLog.error("Couldn't design MPT1327 highpass filter"); + } + } + + /* Decimated sample rate ( 48,000 / 2 = 24,000 ) feeding the decoder */ + private static final int sDECIMATED_SAMPLE_RATE = 24000; + + /* Baud or Symbol Rate */ + private static final int sSYMBOL_RATE = 1200; + + /* Message length -- longest possible message is: + * 4xREVS + 16xSYNC + 64xADD1 + 64xDCW1 + 64xDCW2 + 64xDCW3 + 64xDCW4 */ + private static final int sMESSAGE_LENGTH = 350; + + /* Instrumentation Taps */ + private static final String INSTRUMENT_HB1_FILTER_TO_HIGH_PASS = + "Tap Point: Half-band Decimation Filter > < Low Pass Filter"; + private static final String INSTRUMENT_HIGH_PASS_TO_DECODER = + "Tap Point: Low Pass Filter > < Decoder"; + private static final String INSTRUMENT_DECODER_TO_FRAMER = + "Tap Point: Decoder > < Sync Detect/Message Framer"; + private List mAvailableTaps; + + private HalfBandFilter_RB_RB mDecimationFilter; + private RealFIRFilter_RB_RB mHighPassFilter; + private FSK2Decoder mFSKDecoder; + private Broadcaster mSymbolBroadcaster; + private MessageFramer mControlMessageFramer; + private MessageFramer mTrafficMessageFramer; + private MPT1327MessageProcessor mMessageProcessor; + + public MPT1327DecoderLegacy(AliasList aliasList, Sync sync) + { + /** + * Normal: 2FSK Decoder with inverted output + * French: 2FSK Decoder with normal output + */ + if(sync == Sync.NORMAL) + { + mFSKDecoder = new FSK2Decoder(24000, 1200, FSK2Decoder.Output.INVERTED); + } + else if(sync == Sync.FRENCH) + { + mFSKDecoder = new FSK2Decoder(24000, 1200, FSK2Decoder.Output.NORMAL); + } + else + { + throw new IllegalArgumentException("MPT1327 Decoder - unrecognized Sync type"); + } + + /* Decimation filter - 48000 / 2 = 24000 output */ + mDecimationFilter = new HalfBandFilter_RB_RB(Filters.FIR_HALF_BAND_31T_ONE_EIGHTH_FCO.getCoefficients(), 1.0002f, true); + + //High-pass filter the audio to remove any DC component or CTCSS tones. The audio has already been low-pass + //filtered by the AudioDemodulation filter cutoff of 3400 Hz. + mHighPassFilter = new RealFIRFilter_RB_RB(HIGHPASS_FILTER, 1.0f); + mDecimationFilter.setListener(mHighPassFilter); + + mHighPassFilter.setListener(mFSKDecoder); + + mSymbolBroadcaster = new Broadcaster(); + mFSKDecoder.setListener(mSymbolBroadcaster); + + /* Message framer for control channel messages */ + mControlMessageFramer = new MessageFramer(sync.getControlSyncPattern().getPattern(), sMESSAGE_LENGTH); + mSymbolBroadcaster.addListener(mControlMessageFramer); + + /* Message framer for traffic channel massages */ + mTrafficMessageFramer = new MessageFramer(sync.getTrafficSyncPattern().getPattern(), sMESSAGE_LENGTH); + mSymbolBroadcaster.addListener(mTrafficMessageFramer); + + /* Fully decoded and framed messages processor */ + mMessageProcessor = new MPT1327MessageProcessor(aliasList); + mMessageProcessor.setMessageListener(getMessageListener()); + mControlMessageFramer.addMessageListener(mMessageProcessor); + mTrafficMessageFramer.addMessageListener(mMessageProcessor); + } + + @Override + public Listener getFilteredRealBufferListener() + { + return mDecimationFilter; + } + + @Override + public DecoderType getDecoderType() + { + return DecoderType.MPT1327; + } + + /** + * Cleanup method + */ + public void dispose() + { + super.dispose(); + + mSymbolBroadcaster.dispose(); + + mControlMessageFramer.dispose(); + + mFSKDecoder.dispose(); + + mDecimationFilter.dispose(); + + mMessageProcessor.dispose(); + + mTrafficMessageFramer.dispose(); + } + + /* Instrumentation Taps */ + @Override + public List getTapGroups() + { + if(mAvailableTaps == null) + { + mAvailableTaps = new ArrayList(); + + TapGroup group = new TapGroup("MPT-1327 Decoder"); + + group.add(new FloatTap(INSTRUMENT_HB1_FILTER_TO_HIGH_PASS, 0, 0.5f)); + group.add(new FloatTap(INSTRUMENT_HIGH_PASS_TO_DECODER, 0, 0.5f)); + group.add(new BinaryTap(INSTRUMENT_DECODER_TO_FRAMER, 0, 0.025f)); + + mAvailableTaps.add(group); + + /* Add the taps from the FSK decoder */ + mAvailableTaps.addAll(mFSKDecoder.getTapGroups()); + } + + return mAvailableTaps; + } + + @Override + public void registerTap(Tap tap) + { + /* Send request to decoder */ + mFSKDecoder.registerTap(tap); + + switch(tap.getName()) + { + case INSTRUMENT_HB1_FILTER_TO_HIGH_PASS: + FloatBufferTap hb1Tap = (FloatBufferTap) tap; + mDecimationFilter.setListener(hb1Tap); + hb1Tap.setListener(mHighPassFilter); + break; + case INSTRUMENT_HIGH_PASS_TO_DECODER: + FloatBufferTap lowTap = (FloatBufferTap) tap; + mHighPassFilter.setListener(lowTap); + lowTap.setListener(mFSKDecoder); + break; + case INSTRUMENT_DECODER_TO_FRAMER: + BinaryTap decoderTap = (BinaryTap) tap; + mFSKDecoder.setListener(decoderTap); + decoderTap.setListener(mSymbolBroadcaster); + break; + } + } + + @Override + public void unregisterTap(Tap tap) + { + mFSKDecoder.unregisterTap(tap); + + switch(tap.getName()) + { + case INSTRUMENT_HB1_FILTER_TO_HIGH_PASS: + mDecimationFilter.setListener(mHighPassFilter); + break; + case INSTRUMENT_HIGH_PASS_TO_DECODER: + mHighPassFilter.setListener(mFSKDecoder); + break; + case INSTRUMENT_DECODER_TO_FRAMER: + mFSKDecoder.setListener(mSymbolBroadcaster); + break; + } + } +} diff --git a/src/main/java/io/github/dsheirer/module/decode/mpt1327/Sync.java b/src/main/java/io/github/dsheirer/module/decode/mpt1327/Sync.java new file mode 100644 index 00000000..33adc048 --- /dev/null +++ b/src/main/java/io/github/dsheirer/module/decode/mpt1327/Sync.java @@ -0,0 +1,55 @@ +/******************************************************************************* + * 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 + * + ******************************************************************************/ +package io.github.dsheirer.module.decode.mpt1327; + +import io.github.dsheirer.bits.SyncPattern; + +public enum Sync +{ + NORMAL("Normal", SyncPattern.MPT1327_CONTROL, SyncPattern.MPT1327_TRAFFIC), + FRENCH("French", SyncPattern.MPT1327_CONTROL_FRENCH, SyncPattern.MPT1327_TRAFFIC_FRENCH); + + private String mLabel; + private SyncPattern mControlSyncPattern; + private SyncPattern mTrafficSyncPattern; + + Sync(String label, SyncPattern controlPattern, SyncPattern trafficPattern) + { + mLabel = label; + mControlSyncPattern = controlPattern; + mTrafficSyncPattern = trafficPattern; + } + + public String getLabel() + { + return mLabel; + } + + public SyncPattern getControlSyncPattern() + { + return mControlSyncPattern; + } + + public SyncPattern getTrafficSyncPattern() + { + return mTrafficSyncPattern; + } + + public String toString() + { + return getLabel(); + } +} From b410cf0dfeeee2f6914ac495383ac68a53cfc3ac Mon Sep 17 00:00:00 2001 From: Denny Date: Fri, 15 Jun 2018 06:19:50 -0400 Subject: [PATCH 73/87] [WIP]Updates the LJ-1200 decoder to use the new AFSK1200 decoder class. Adds Demodulator Viewer FX panes for MPT-1327 and LJ-1200 decoders. --- ...rt.java => AFSK1200SampleBufferChart.java} | 31 ++- ...SK1200ZeroCrossingErrorDetectorChart.java} | 23 +- .../chart/IInstrumentedAFSK1200Decoder.java | 30 +++ .../viewer/decoder/AbstractAFSK1200Pane.java | 162 +++++++++++++ .../viewer/decoder/DecoderPaneFactory.java | 12 +- .../gui/viewer/decoder/Fleetsync2Pane.java | 138 +---------- .../gui/viewer/decoder/LJ1200Pane.java | 60 +++++ .../gui/viewer/decoder/MPT1327Pane.java | 60 +++++ .../Fleetsync2DecoderInstrumented.java | 9 +- .../module/decode/lj1200/LJ1200Decoder.java | 225 +++++------------- .../lj1200/LJ1200DecoderInstrumented.java | 53 +++++ .../module/decode/mpt1327/MPT1327Decoder.java | 22 ++ .../mpt1327/MPT1327DecoderInstrumented.java | 53 +++++ 13 files changed, 549 insertions(+), 329 deletions(-) rename src/main/java/io/github/dsheirer/instrument/gui/viewer/chart/{Fleetsync2SampleBufferChart.java => AFSK1200SampleBufferChart.java} (78%) rename src/main/java/io/github/dsheirer/instrument/gui/viewer/chart/{FleetsyncZeroCrossingErrorDetectorChart.java => AFSK1200ZeroCrossingErrorDetectorChart.java} (77%) create mode 100644 src/main/java/io/github/dsheirer/instrument/gui/viewer/chart/IInstrumentedAFSK1200Decoder.java create mode 100644 src/main/java/io/github/dsheirer/instrument/gui/viewer/decoder/AbstractAFSK1200Pane.java create mode 100644 src/main/java/io/github/dsheirer/instrument/gui/viewer/decoder/LJ1200Pane.java create mode 100644 src/main/java/io/github/dsheirer/instrument/gui/viewer/decoder/MPT1327Pane.java create mode 100644 src/main/java/io/github/dsheirer/module/decode/lj1200/LJ1200DecoderInstrumented.java create mode 100644 src/main/java/io/github/dsheirer/module/decode/mpt1327/MPT1327DecoderInstrumented.java diff --git a/src/main/java/io/github/dsheirer/instrument/gui/viewer/chart/Fleetsync2SampleBufferChart.java b/src/main/java/io/github/dsheirer/instrument/gui/viewer/chart/AFSK1200SampleBufferChart.java similarity index 78% rename from src/main/java/io/github/dsheirer/instrument/gui/viewer/chart/Fleetsync2SampleBufferChart.java rename to src/main/java/io/github/dsheirer/instrument/gui/viewer/chart/AFSK1200SampleBufferChart.java index a6fb9a81..9014ac5e 100644 --- a/src/main/java/io/github/dsheirer/instrument/gui/viewer/chart/Fleetsync2SampleBufferChart.java +++ b/src/main/java/io/github/dsheirer/instrument/gui/viewer/chart/AFSK1200SampleBufferChart.java @@ -15,7 +15,6 @@ ******************************************************************************/ package io.github.dsheirer.instrument.gui.viewer.chart; -import io.github.dsheirer.module.decode.fleetsync2.Fleetsync2DecoderInstrumented; import javafx.beans.value.ChangeListener; import javafx.beans.value.ObservableValue; import javafx.collections.FXCollections; @@ -26,9 +25,9 @@ import javafx.scene.layout.StackPane; import org.slf4j.Logger; import org.slf4j.LoggerFactory; -public class Fleetsync2SampleBufferChart extends LineChart +public class AFSK1200SampleBufferChart extends LineChart { - private final static Logger mLog = LoggerFactory.getLogger(Fleetsync2SampleBufferChart.class); + private final static Logger mLog = LoggerFactory.getLogger(AFSK1200SampleBufferChart.class); private ObservableList> mSamples = FXCollections.observableArrayList(); private Series mSampleSeries = new Series<>("Samples", mSamples); @@ -44,9 +43,9 @@ public class Fleetsync2SampleBufferChart extends LineChart private ObservableList> mZeroCrossing = FXCollections.observableArrayList(); private Series mZeroCrossingSeries = new Series<>("Zero Crossing", mZeroCrossing); - private Fleetsync2DecoderInstrumented mFleetsync2DecoderInstrumented; + private IInstrumentedAFSK1200Decoder mIInstrumentedAFSK1200Decoder; - public Fleetsync2SampleBufferChart(Fleetsync2DecoderInstrumented decoder, int length) + public AFSK1200SampleBufferChart(IInstrumentedAFSK1200Decoder decoder, int length) { super(new NumberAxis("Samples", 0, length, 5), new NumberAxis("Value", -1.0, 1.0, 0.25)); @@ -56,8 +55,8 @@ public class Fleetsync2SampleBufferChart extends LineChart setData(observableList); - mFleetsync2DecoderInstrumented = decoder; - decoder.bufferCount.addListener(new BufferChangeListener()); + mIInstrumentedAFSK1200Decoder = decoder; + decoder.getBufferCountProperty().addListener(new BufferChangeListener()); decoder.getAFSK1200Decoder().getErrorDetector().timingError.addListener(new ErrorChangeListener()); } @@ -89,7 +88,7 @@ public class Fleetsync2SampleBufferChart extends LineChart @Override public void changed(ObservableValue observable, Object oldValue, Object newValue) { - setBuffer(mFleetsync2DecoderInstrumented.getAFSK1200Decoder().getSampleBuffer().getDelayLine()); + setBuffer(mIInstrumentedAFSK1200Decoder.getAFSK1200Decoder().getSampleBuffer().getDelayLine()); while(mPreviousSymbol.size() < 2) { @@ -115,17 +114,17 @@ public class Fleetsync2SampleBufferChart extends LineChart mSymbolSamples.add(sample); } - int pointer1 = mFleetsync2DecoderInstrumented.getAFSK1200Decoder().getSampleBuffer().getDelayLinePointer(); - int pointer2 = mFleetsync2DecoderInstrumented.getAFSK1200Decoder().getSampleBuffer().getDelayLineSecondPointer(); + int pointer1 = mIInstrumentedAFSK1200Decoder.getAFSK1200Decoder().getSampleBuffer().getDelayLinePointer(); + int pointer2 = mIInstrumentedAFSK1200Decoder.getAFSK1200Decoder().getSampleBuffer().getDelayLineSecondPointer(); mSamplePoints.get(0).setXValue(pointer1); mSamplePoints.get(1).setXValue(pointer2); - float samplesPerSymbol = mFleetsync2DecoderInstrumented.getAFSK1200Decoder().getSampleBuffer().getSamplesPerSymbol(); + float samplesPerSymbol = mIInstrumentedAFSK1200Decoder.getAFSK1200Decoder().getSampleBuffer().getSamplesPerSymbol(); - boolean symbol = mFleetsync2DecoderInstrumented.getAFSK1200Decoder().getSampleBuffer().getLastSymbol(); - int symbolStart = mFleetsync2DecoderInstrumented.getAFSK1200Decoder().getSampleBuffer().getSymbolStart(); - int symbolEnd = mFleetsync2DecoderInstrumented.getAFSK1200Decoder().getSampleBuffer().getSymbolEnd(); + boolean symbol = mIInstrumentedAFSK1200Decoder.getAFSK1200Decoder().getSampleBuffer().getLastSymbol(); + int symbolStart = mIInstrumentedAFSK1200Decoder.getAFSK1200Decoder().getSampleBuffer().getSymbolStart(); + int symbolEnd = mIInstrumentedAFSK1200Decoder.getAFSK1200Decoder().getSampleBuffer().getSymbolEnd(); float start = pointer1 + samplesPerSymbol - 1; float end = symbolEnd; @@ -152,8 +151,8 @@ public class Fleetsync2SampleBufferChart extends LineChart mZeroCrossing.add(sample); } - int pointer1 = mFleetsync2DecoderInstrumented.getAFSK1200Decoder().getSampleBuffer().getDelayLinePointer(); - int pointer2 = mFleetsync2DecoderInstrumented.getAFSK1200Decoder().getSampleBuffer().getDelayLineSecondPointer(); + int pointer1 = mIInstrumentedAFSK1200Decoder.getAFSK1200Decoder().getSampleBuffer().getDelayLinePointer(); + int pointer2 = mIInstrumentedAFSK1200Decoder.getAFSK1200Decoder().getSampleBuffer().getDelayLineSecondPointer(); int detectedZeroCrossing = ((Number)newValue).intValue(); diff --git a/src/main/java/io/github/dsheirer/instrument/gui/viewer/chart/FleetsyncZeroCrossingErrorDetectorChart.java b/src/main/java/io/github/dsheirer/instrument/gui/viewer/chart/AFSK1200ZeroCrossingErrorDetectorChart.java similarity index 77% rename from src/main/java/io/github/dsheirer/instrument/gui/viewer/chart/FleetsyncZeroCrossingErrorDetectorChart.java rename to src/main/java/io/github/dsheirer/instrument/gui/viewer/chart/AFSK1200ZeroCrossingErrorDetectorChart.java index 16c90e1f..202cb607 100644 --- a/src/main/java/io/github/dsheirer/instrument/gui/viewer/chart/FleetsyncZeroCrossingErrorDetectorChart.java +++ b/src/main/java/io/github/dsheirer/instrument/gui/viewer/chart/AFSK1200ZeroCrossingErrorDetectorChart.java @@ -15,7 +15,6 @@ ******************************************************************************/ package io.github.dsheirer.instrument.gui.viewer.chart; -import io.github.dsheirer.module.decode.fleetsync2.Fleetsync2DecoderInstrumented; import javafx.beans.value.ChangeListener; import javafx.beans.value.ObservableValue; import javafx.collections.FXCollections; @@ -25,9 +24,9 @@ import javafx.scene.chart.NumberAxis; import org.slf4j.Logger; import org.slf4j.LoggerFactory; -public class FleetsyncZeroCrossingErrorDetectorChart extends LineChart +public class AFSK1200ZeroCrossingErrorDetectorChart extends LineChart { - private final static Logger mLog = LoggerFactory.getLogger(FleetsyncZeroCrossingErrorDetectorChart.class); + private final static Logger mLog = LoggerFactory.getLogger(AFSK1200ZeroCrossingErrorDetectorChart.class); private ObservableList> mCurrentSamples = FXCollections.observableArrayList(); private Series mCurrentSampleSeries = new Series<>("Samples", mCurrentSamples); @@ -38,9 +37,9 @@ public class FleetsyncZeroCrossingErrorDetectorChart extends LineChart private ObservableList> mDetected = FXCollections.observableArrayList(); private Series mDetectedSeries = new Series<>("Detected", mDetected); - private Fleetsync2DecoderInstrumented mFleetsync2DecoderInstrumented; + private IInstrumentedAFSK1200Decoder mIInstrumentedAFSK1200Decoder; - public FleetsyncZeroCrossingErrorDetectorChart(Fleetsync2DecoderInstrumented decoder, int length) + public AFSK1200ZeroCrossingErrorDetectorChart(IInstrumentedAFSK1200Decoder decoder, int length) { super(new NumberAxis("Samples", 0, length, 5), new NumberAxis("Value", -1.0, 1.0, 0.25)); @@ -49,9 +48,9 @@ public class FleetsyncZeroCrossingErrorDetectorChart extends LineChart mPreviousSampleSeries, mDetectedSeries); setData(observableList); - mFleetsync2DecoderInstrumented = decoder; + mIInstrumentedAFSK1200Decoder = decoder; decoder.getAFSK1200Decoder().getErrorDetector().timingError.addListener(new ErrorChangeListener()); - decoder.bufferCount.addListener(new ChangeListener() + decoder.getBufferCountProperty().addListener(new ChangeListener() { @Override public void changed(ObservableValue observable, Object oldValue, Object newValue) @@ -76,7 +75,7 @@ public class FleetsyncZeroCrossingErrorDetectorChart extends LineChart private void updateBuffer() { - boolean[] samples = mFleetsync2DecoderInstrumented.getAFSK1200Decoder().getErrorDetector().getBuffer(); + boolean[] samples = mIInstrumentedAFSK1200Decoder.getAFSK1200Decoder().getErrorDetector().getBuffer(); for(int x = 0; x < samples.length; x++) { @@ -90,9 +89,9 @@ public class FleetsyncZeroCrossingErrorDetectorChart extends LineChart @Override public void changed(ObservableValue observable, Object oldValue, Object newValue) { - float error = mFleetsync2DecoderInstrumented.getAFSK1200Decoder().getErrorDetector().getError(); + float error = mIInstrumentedAFSK1200Decoder.getAFSK1200Decoder().getErrorDetector().getError(); - boolean[] samples = mFleetsync2DecoderInstrumented.getAFSK1200Decoder().getErrorDetector().getBuffer(); + boolean[] samples = mIInstrumentedAFSK1200Decoder.getAFSK1200Decoder().getErrorDetector().getBuffer(); for(int x = 0; x < samples.length; x++) { @@ -110,9 +109,9 @@ public class FleetsyncZeroCrossingErrorDetectorChart extends LineChart else { Data detected1 = mDetected.get(0); - detected1.setXValue(mFleetsync2DecoderInstrumented.getAFSK1200Decoder().getErrorDetector().getDetectedZeroCrossing()); + detected1.setXValue(mIInstrumentedAFSK1200Decoder.getAFSK1200Decoder().getErrorDetector().getDetectedZeroCrossing()); Data detected2 = mDetected.get(1); - detected2.setXValue(mFleetsync2DecoderInstrumented.getAFSK1200Decoder().getErrorDetector().getDetectedZeroCrossing()); + detected2.setXValue(mIInstrumentedAFSK1200Decoder.getAFSK1200Decoder().getErrorDetector().getDetectedZeroCrossing()); } } } diff --git a/src/main/java/io/github/dsheirer/instrument/gui/viewer/chart/IInstrumentedAFSK1200Decoder.java b/src/main/java/io/github/dsheirer/instrument/gui/viewer/chart/IInstrumentedAFSK1200Decoder.java new file mode 100644 index 00000000..c8236d15 --- /dev/null +++ b/src/main/java/io/github/dsheirer/instrument/gui/viewer/chart/IInstrumentedAFSK1200Decoder.java @@ -0,0 +1,30 @@ +/******************************************************************************* + * 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 + * + ******************************************************************************/ +package io.github.dsheirer.instrument.gui.viewer.chart; + +import io.github.dsheirer.dsp.afsk.AFSK1200DecoderInstrumented; +import io.github.dsheirer.message.Message; +import io.github.dsheirer.sample.Listener; +import javafx.beans.property.SimpleIntegerProperty; + +public interface IInstrumentedAFSK1200Decoder +{ + public SimpleIntegerProperty getBufferCountProperty(); + + public AFSK1200DecoderInstrumented getAFSK1200Decoder(); + + public void setMessageListener(Listener listener); +} diff --git a/src/main/java/io/github/dsheirer/instrument/gui/viewer/decoder/AbstractAFSK1200Pane.java b/src/main/java/io/github/dsheirer/instrument/gui/viewer/decoder/AbstractAFSK1200Pane.java new file mode 100644 index 00000000..1ad275b8 --- /dev/null +++ b/src/main/java/io/github/dsheirer/instrument/gui/viewer/decoder/AbstractAFSK1200Pane.java @@ -0,0 +1,162 @@ +/******************************************************************************* + * 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 + * + ******************************************************************************/ +package io.github.dsheirer.instrument.gui.viewer.decoder; + +import io.github.dsheirer.instrument.gui.viewer.chart.AFSK1200SampleBufferChart; +import io.github.dsheirer.instrument.gui.viewer.chart.AFSK1200ZeroCrossingErrorDetectorChart; +import io.github.dsheirer.instrument.gui.viewer.chart.DecodedSymbolChart; +import io.github.dsheirer.instrument.gui.viewer.chart.IInstrumentedAFSK1200Decoder; +import io.github.dsheirer.instrument.gui.viewer.chart.RealSampleLineChart; +import io.github.dsheirer.message.Message; +import io.github.dsheirer.module.decode.DecoderType; +import io.github.dsheirer.sample.Listener; +import io.github.dsheirer.sample.buffer.ReusableBuffer; +import javafx.scene.layout.HBox; +import javafx.scene.layout.Priority; +import javafx.scene.layout.VBox; +import org.slf4j.Logger; +import org.slf4j.LoggerFactory; + +public abstract class AbstractAFSK1200Pane extends RealDecoderPane +{ + private final static Logger mLog = LoggerFactory.getLogger(AbstractAFSK1200Pane.class); + + private HBox mSampleChartBox; + private RealSampleLineChart mRealSampleLineChart; + private HBox mSymbolDecoderBox; + private AFSK1200SampleBufferChart mSampleBufferChart; + private AFSK1200ZeroCrossingErrorDetectorChart mZeroCrossingErrorDetectorChart; + private DecodedSymbolChart mDecodedSymbolChart; + + private IInstrumentedAFSK1200Decoder mDecoder; + + public AbstractAFSK1200Pane(DecoderType decoderType) + { + super(decoderType); + init(); + } + + private void init() + { + //This is force-cast without type checking .... + addListener((Listener)getDecoder()); + + addListener(getSampleLineChart()); + + HBox.setHgrow(getSampleChartBox(), Priority.ALWAYS); + HBox.setHgrow(getSymbolDecoderBox(), Priority.ALWAYS); + + VBox.setVgrow(getSampleChartBox(), Priority.ALWAYS); + VBox.setVgrow(getSymbolDecoderBox(), Priority.ALWAYS); + + getSampleChartBox().setMaxWidth(Double.MAX_VALUE); + getSymbolDecoderBox().setMaxWidth(Double.MAX_VALUE); + + getChildren().addAll(getSampleChartBox(), getSymbolDecoderBox()); + + getDecoder().getAFSK1200Decoder().addListener(getDecodedSymbolChart()); + + getDecoder().setMessageListener(new Listener() + { + @Override + public void receive(Message message) + { + mLog.debug((message.isValid() ? "PASS " : "FAIL ") + message.getMessage()); + } + }); + } + + protected abstract IInstrumentedAFSK1200Decoder getDecoder(); + + private HBox getSampleChartBox() + { + if(mSampleChartBox == null) + { + mSampleChartBox = new HBox(); + mSampleChartBox.setMaxHeight(Double.MAX_VALUE); + + getSampleLineChart().setMaxWidth(Double.MAX_VALUE); + getDecodedSymbolChart().setMaxWidth(Double.MAX_VALUE); + + HBox.setHgrow(getSampleLineChart(), Priority.ALWAYS); + HBox.setHgrow(getDecodedSymbolChart(), Priority.ALWAYS); + + mSampleChartBox.getChildren().addAll(getSampleLineChart(), getDecodedSymbolChart()); + } + + return mSampleChartBox; + } + + private RealSampleLineChart getSampleLineChart() + { + if(mRealSampleLineChart == null) + { + mRealSampleLineChart = new RealSampleLineChart(200, (10.0)); + } + + return mRealSampleLineChart; + } + + private HBox getSymbolDecoderBox() + { + if(mSymbolDecoderBox == null) + { + mSymbolDecoderBox = new HBox(); + mSymbolDecoderBox.setMaxWidth(Double.MAX_VALUE); + + getSampleBufferChart().setMaxWidth(Double.MAX_VALUE); + getZeroCrossingErrorDetectorChart().setMaxWidth(Double.MAX_VALUE); + + HBox.setHgrow(getSampleBufferChart(), Priority.ALWAYS); + HBox.setHgrow(getZeroCrossingErrorDetectorChart(), Priority.ALWAYS); + + mSymbolDecoderBox.getChildren().addAll(getSampleBufferChart(), getZeroCrossingErrorDetectorChart()); + } + + return mSymbolDecoderBox; + } + + private AFSK1200SampleBufferChart getSampleBufferChart() + { + if(mSampleBufferChart == null) + { + mSampleBufferChart = new AFSK1200SampleBufferChart(getDecoder(), 106); + } + + return mSampleBufferChart; + } + + private AFSK1200ZeroCrossingErrorDetectorChart getZeroCrossingErrorDetectorChart() + { + if(mZeroCrossingErrorDetectorChart == null) + { + mZeroCrossingErrorDetectorChart = new AFSK1200ZeroCrossingErrorDetectorChart(getDecoder(), + getDecoder().getAFSK1200Decoder().getErrorDetector().getBuffer().length); + } + + return mZeroCrossingErrorDetectorChart; + } + + private DecodedSymbolChart getDecodedSymbolChart() + { + if(mDecodedSymbolChart == null) + { + mDecodedSymbolChart = new DecodedSymbolChart(50); + } + + return mDecodedSymbolChart; + } +} diff --git a/src/main/java/io/github/dsheirer/instrument/gui/viewer/decoder/DecoderPaneFactory.java b/src/main/java/io/github/dsheirer/instrument/gui/viewer/decoder/DecoderPaneFactory.java index 11327c27..f1895372 100644 --- a/src/main/java/io/github/dsheirer/instrument/gui/viewer/decoder/DecoderPaneFactory.java +++ b/src/main/java/io/github/dsheirer/instrument/gui/viewer/decoder/DecoderPaneFactory.java @@ -22,8 +22,12 @@ import java.util.EnumSet; public class DecoderPaneFactory { - private static final EnumSet SUPPORTED_DECODER_TYPES = - EnumSet.of(DecoderType.P25_PHASE1, DecoderType.LTR_NET, DecoderType.FLEETSYNC2); + private static final EnumSet SUPPORTED_DECODER_TYPES = EnumSet.of( + DecoderType.P25_PHASE1, + DecoderType.LJ_1200, + DecoderType.LTR_NET, + DecoderType.MPT1327, + DecoderType.FLEETSYNC2); /** * Creates a decoder pane for the decoder type @@ -34,8 +38,12 @@ public class DecoderPaneFactory { case FLEETSYNC2: return new Fleetsync2Pane(); + case LJ_1200: + return new LJ1200Pane(); case LTR_NET: return new LTRNetPane(); + case MPT1327: + return new MPT1327Pane(); case P25_PHASE1: throw new IllegalArgumentException("Use the getP25DecoderPane() method for P25 decoder type"); } diff --git a/src/main/java/io/github/dsheirer/instrument/gui/viewer/decoder/Fleetsync2Pane.java b/src/main/java/io/github/dsheirer/instrument/gui/viewer/decoder/Fleetsync2Pane.java index 1d4f39d0..a2f45b38 100644 --- a/src/main/java/io/github/dsheirer/instrument/gui/viewer/decoder/Fleetsync2Pane.java +++ b/src/main/java/io/github/dsheirer/instrument/gui/viewer/decoder/Fleetsync2Pane.java @@ -15,161 +15,29 @@ ******************************************************************************/ package io.github.dsheirer.instrument.gui.viewer.decoder; -import io.github.dsheirer.instrument.gui.viewer.chart.DecodedSymbolChart; -import io.github.dsheirer.instrument.gui.viewer.chart.Fleetsync2SampleBufferChart; -import io.github.dsheirer.instrument.gui.viewer.chart.FleetsyncZeroCrossingErrorDetectorChart; -import io.github.dsheirer.instrument.gui.viewer.chart.RealSampleLineChart; -import io.github.dsheirer.message.Message; import io.github.dsheirer.module.decode.DecoderType; import io.github.dsheirer.module.decode.fleetsync2.Fleetsync2DecoderInstrumented; -import io.github.dsheirer.sample.Listener; -import javafx.scene.layout.HBox; -import javafx.scene.layout.Priority; -import javafx.scene.layout.VBox; import org.slf4j.Logger; import org.slf4j.LoggerFactory; -public class Fleetsync2Pane extends RealDecoderPane +public class Fleetsync2Pane extends AbstractAFSK1200Pane { private final static Logger mLog = LoggerFactory.getLogger(Fleetsync2Pane.class); - private HBox mSampleChartBox; - private RealSampleLineChart mRealSampleLineChart; - private HBox mSymbolDecoderBox; - private Fleetsync2SampleBufferChart mSampleBufferChart; - private FleetsyncZeroCrossingErrorDetectorChart mZeroCrossingErrorDetectorChart; - private DecodedSymbolChart mDecodedSymbolChart; - private Fleetsync2DecoderInstrumented mDecoder; public Fleetsync2Pane() { - super(DecoderType.LTR_NET); - init(); + super(DecoderType.FLEETSYNC2); } - private void init() - { - addListener(getDecoder()); - addListener(getSampleLineChart()); - - HBox.setHgrow(getSampleChartBox(), Priority.ALWAYS); - HBox.setHgrow(getSymbolDecoderBox(), Priority.ALWAYS); - - VBox.setVgrow(getSampleChartBox(), Priority.ALWAYS); - VBox.setVgrow(getSymbolDecoderBox(), Priority.ALWAYS); - - getSampleChartBox().setMaxWidth(Double.MAX_VALUE); - getSymbolDecoderBox().setMaxWidth(Double.MAX_VALUE); - - getChildren().addAll(getSampleChartBox(), getSymbolDecoderBox()); - - getDecoder().getAFSK1200Decoder().addListener(getDecodedSymbolChart()); - - getDecoder().setMessageListener(new Listener() - { - @Override - public void receive(Message message) - { - mLog.debug(message.getMessage()); - } - }); - } - - private Fleetsync2DecoderInstrumented getDecoder() + protected Fleetsync2DecoderInstrumented getDecoder() { if(mDecoder == null) { mDecoder = new Fleetsync2DecoderInstrumented(); - mDecoder.setMessageListener(new Listener() - { - @Override - public void receive(Message message) - { - mLog.debug(message.getMessage()); - } - }); } return mDecoder; } - - private HBox getSampleChartBox() - { - if(mSampleChartBox == null) - { - mSampleChartBox = new HBox(); - mSampleChartBox.setMaxHeight(Double.MAX_VALUE); - - getSampleLineChart().setMaxWidth(Double.MAX_VALUE); - getDecodedSymbolChart().setMaxWidth(Double.MAX_VALUE); - - HBox.setHgrow(getSampleLineChart(), Priority.ALWAYS); - HBox.setHgrow(getDecodedSymbolChart(), Priority.ALWAYS); - - mSampleChartBox.getChildren().addAll(getSampleLineChart(), getDecodedSymbolChart()); - } - - return mSampleChartBox; - } - - private RealSampleLineChart getSampleLineChart() - { - if(mRealSampleLineChart == null) - { - mRealSampleLineChart = new RealSampleLineChart(200, (10.0)); - } - - return mRealSampleLineChart; - } - - private HBox getSymbolDecoderBox() - { - if(mSymbolDecoderBox == null) - { - mSymbolDecoderBox = new HBox(); - mSymbolDecoderBox.setMaxWidth(Double.MAX_VALUE); - - getSampleBufferChart().setMaxWidth(Double.MAX_VALUE); - getZeroCrossingErrorDetectorChart().setMaxWidth(Double.MAX_VALUE); - - HBox.setHgrow(getSampleBufferChart(), Priority.ALWAYS); - HBox.setHgrow(getZeroCrossingErrorDetectorChart(), Priority.ALWAYS); - - mSymbolDecoderBox.getChildren().addAll(getSampleBufferChart(), getZeroCrossingErrorDetectorChart()); - } - - return mSymbolDecoderBox; - } - - private Fleetsync2SampleBufferChart getSampleBufferChart() - { - if(mSampleBufferChart == null) - { - mSampleBufferChart = new Fleetsync2SampleBufferChart(getDecoder(), 106); - } - - return mSampleBufferChart; - } - - private FleetsyncZeroCrossingErrorDetectorChart getZeroCrossingErrorDetectorChart() - { - if(mZeroCrossingErrorDetectorChart == null) - { - mZeroCrossingErrorDetectorChart = new FleetsyncZeroCrossingErrorDetectorChart(getDecoder(), - getDecoder().getAFSK1200Decoder().getErrorDetector().getBuffer().length); - } - - return mZeroCrossingErrorDetectorChart; - } - - private DecodedSymbolChart getDecodedSymbolChart() - { - if(mDecodedSymbolChart == null) - { - mDecodedSymbolChart = new DecodedSymbolChart(50); - } - - return mDecodedSymbolChart; - } } diff --git a/src/main/java/io/github/dsheirer/instrument/gui/viewer/decoder/LJ1200Pane.java b/src/main/java/io/github/dsheirer/instrument/gui/viewer/decoder/LJ1200Pane.java new file mode 100644 index 00000000..2b5fa55b --- /dev/null +++ b/src/main/java/io/github/dsheirer/instrument/gui/viewer/decoder/LJ1200Pane.java @@ -0,0 +1,60 @@ +/******************************************************************************* + * 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 + * + ******************************************************************************/ +package io.github.dsheirer.instrument.gui.viewer.decoder; + +import io.github.dsheirer.dsp.symbol.ISyncDetectListener; +import io.github.dsheirer.module.decode.DecoderType; +import io.github.dsheirer.module.decode.lj1200.LJ1200DecoderInstrumented; +import org.slf4j.Logger; +import org.slf4j.LoggerFactory; + +public class LJ1200Pane extends AbstractAFSK1200Pane +{ + private final static Logger mLog = LoggerFactory.getLogger(LJ1200Pane.class); + + private LJ1200DecoderInstrumented mDecoder; + + public LJ1200Pane() + { + super(DecoderType.LJ_1200); + } + + protected LJ1200DecoderInstrumented getDecoder() + { + if(mDecoder == null) + { + mDecoder = new LJ1200DecoderInstrumented(); + mDecoder.getTowerMessageFramer().setSyncDetectListener(new ISyncDetectListener() + { + @Override + public void syncDetected() + { + mLog.debug("Tower Sync Detected!"); + } + }); + mDecoder.getTransponderMessageFramer().setSyncDetectListener(new ISyncDetectListener() + { + @Override + public void syncDetected() + { + mLog.debug("Transponder Sync Detected!"); + } + }); + } + + return mDecoder; + } +} diff --git a/src/main/java/io/github/dsheirer/instrument/gui/viewer/decoder/MPT1327Pane.java b/src/main/java/io/github/dsheirer/instrument/gui/viewer/decoder/MPT1327Pane.java new file mode 100644 index 00000000..dca8662b --- /dev/null +++ b/src/main/java/io/github/dsheirer/instrument/gui/viewer/decoder/MPT1327Pane.java @@ -0,0 +1,60 @@ +/******************************************************************************* + * 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 + * + ******************************************************************************/ +package io.github.dsheirer.instrument.gui.viewer.decoder; + +import io.github.dsheirer.dsp.symbol.ISyncDetectListener; +import io.github.dsheirer.module.decode.DecoderType; +import io.github.dsheirer.module.decode.mpt1327.MPT1327DecoderInstrumented; +import org.slf4j.Logger; +import org.slf4j.LoggerFactory; + +public class MPT1327Pane extends AbstractAFSK1200Pane +{ + private final static Logger mLog = LoggerFactory.getLogger(MPT1327Pane.class); + + private MPT1327DecoderInstrumented mDecoder; + + public MPT1327Pane() + { + super(DecoderType.MPT1327); + } + + protected MPT1327DecoderInstrumented getDecoder() + { + if(mDecoder == null) + { + mDecoder = new MPT1327DecoderInstrumented(); + mDecoder.getControlMessageFramer().setSyncDetectListener(new ISyncDetectListener() + { + @Override + public void syncDetected() + { +// mLog.debug("Control Sync Detected!"); + } + }); + mDecoder.getTrafficMessageFramer().setSyncDetectListener(new ISyncDetectListener() + { + @Override + public void syncDetected() + { +// mLog.debug("Traffic Sync Detected!"); + } + }); + } + + return mDecoder; + } +} diff --git a/src/main/java/io/github/dsheirer/module/decode/fleetsync2/Fleetsync2DecoderInstrumented.java b/src/main/java/io/github/dsheirer/module/decode/fleetsync2/Fleetsync2DecoderInstrumented.java index 5dcf8476..58a31ca8 100644 --- a/src/main/java/io/github/dsheirer/module/decode/fleetsync2/Fleetsync2DecoderInstrumented.java +++ b/src/main/java/io/github/dsheirer/module/decode/fleetsync2/Fleetsync2DecoderInstrumented.java @@ -16,13 +16,14 @@ package io.github.dsheirer.module.decode.fleetsync2; import io.github.dsheirer.dsp.afsk.AFSK1200DecoderInstrumented; +import io.github.dsheirer.instrument.gui.viewer.chart.IInstrumentedAFSK1200Decoder; import io.github.dsheirer.sample.buffer.ReusableBuffer; import javafx.beans.property.SimpleIntegerProperty; /** * Instrumented version of the AFSK-1200 decoder for use with DemodulatorViewerFX */ -public class Fleetsync2DecoderInstrumented extends Fleetsync2Decoder +public class Fleetsync2DecoderInstrumented extends Fleetsync2Decoder implements IInstrumentedAFSK1200Decoder { public SimpleIntegerProperty bufferCount = new SimpleIntegerProperty(); @@ -31,6 +32,12 @@ public class Fleetsync2DecoderInstrumented extends Fleetsync2Decoder super(new AFSK1200DecoderInstrumented(false), null); } + @Override + public SimpleIntegerProperty getBufferCountProperty() + { + return bufferCount; + } + public AFSK1200DecoderInstrumented getAFSK1200Decoder() { return (AFSK1200DecoderInstrumented)getDecoder(); diff --git a/src/main/java/io/github/dsheirer/module/decode/lj1200/LJ1200Decoder.java b/src/main/java/io/github/dsheirer/module/decode/lj1200/LJ1200Decoder.java index 8e82ffdf..62842393 100644 --- a/src/main/java/io/github/dsheirer/module/decode/lj1200/LJ1200Decoder.java +++ b/src/main/java/io/github/dsheirer/module/decode/lj1200/LJ1200Decoder.java @@ -1,193 +1,92 @@ /******************************************************************************* - * SDR Trunk - * Copyright (C) 2014,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 + * 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 + * ******************************************************************************/ package io.github.dsheirer.module.decode.lj1200; import io.github.dsheirer.alias.AliasList; +import io.github.dsheirer.bits.IBinarySymbolProcessor; import io.github.dsheirer.bits.MessageFramer; import io.github.dsheirer.bits.SyncPattern; -import io.github.dsheirer.dsp.filter.Filters; -import io.github.dsheirer.dsp.filter.fir.real.RealFIRFilter_RB_RB; -import io.github.dsheirer.dsp.filter.halfband.real.HalfBandFilter_RB_RB; -import io.github.dsheirer.dsp.fsk.FSK2Decoder; -import io.github.dsheirer.dsp.fsk.FSK2Decoder.Output; -import io.github.dsheirer.instrument.Instrumentable; -import io.github.dsheirer.instrument.tap.Tap; -import io.github.dsheirer.instrument.tap.TapGroup; -import io.github.dsheirer.instrument.tap.stream.BinaryTap; -import io.github.dsheirer.instrument.tap.stream.FloatBufferTap; -import io.github.dsheirer.instrument.tap.stream.FloatTap; -import io.github.dsheirer.module.decode.Decoder; +import io.github.dsheirer.dsp.afsk.AFSK1200Decoder; import io.github.dsheirer.module.decode.DecoderType; -import io.github.dsheirer.sample.Broadcaster; -import io.github.dsheirer.sample.Listener; -import io.github.dsheirer.sample.real.IFilteredRealBufferListener; -import io.github.dsheirer.sample.real.RealBuffer; -import io.github.dsheirer.sample.real.RealSampleListener; - -import java.util.ArrayList; -import java.util.List; +import io.github.dsheirer.module.decode.afsk.AbstractAFSKDecoder; /** - * LJ1200 - 1200 baud 2FSK decoder + * LJ1200 - 1200 baud AFSK decoder */ -public class LJ1200Decoder extends Decoder implements IFilteredRealBufferListener, - Instrumentable +public class LJ1200Decoder extends AbstractAFSKDecoder implements IBinarySymbolProcessor { - /* Decimated sample rate ( 48,000 / 2 = 24,000 ) feeding the decoder */ - private static final int DECIMATED_SAMPLE_RATE = 24000; - - /* Baud or Symbol Rate */ - private static final int SYMBOL_RATE = 1200; - - /* Message length - 16-bit sync plus 64 bit message */ + /* Message length - 16-bit sync plus 64 bit message */ private static final int MESSAGE_LENGTH = 80; - - /* Instrumentation Taps */ - private ArrayList mAvailableTaps; - private static final String INSTRUMENT_BANDPASS_FILTER_TO_FSK2_DEMOD = - "Tap Point: Bandpass Filter > < FSK2 Decoder"; - private static final String INSTRUMENT_FSK2_DECODER_TO_MESSAGE_FRAMER = - "Tap Point: FSK2 Decoder > < Message Framer"; - - private FSK2Decoder mFSKDecoder; - private Broadcaster mFSKDecoderBroadcaster = - new Broadcaster(); - - private HalfBandFilter_RB_RB mDecimationFilter; - private RealFIRFilter_RB_RB mBandPassFilter; + private MessageFramer mTowerMessageFramer; private MessageFramer mTransponderMessageFramer; private LJ1200MessageProcessor mMessageProcessor; - - public LJ1200Decoder( AliasList aliasList ) - { - mDecimationFilter = new HalfBandFilter_RB_RB( - Filters.FIR_HALF_BAND_31T_ONE_EIGHTH_FCO.getCoefficients(), 1.0f, true ); - mBandPassFilter = new RealFIRFilter_RB_RB( - Filters.FIRBP_1200FSK_24000FS.getCoefficients(), 1.02f ); - mDecimationFilter.setListener( mBandPassFilter ); - - mFSKDecoder = new FSK2Decoder( DECIMATED_SAMPLE_RATE, - SYMBOL_RATE, Output.INVERTED ); - mBandPassFilter.setListener( mFSKDecoder ); - - mFSKDecoder.setListener( mFSKDecoderBroadcaster ); - - mTowerMessageFramer = new MessageFramer( - SyncPattern.LJ1200.getPattern(), MESSAGE_LENGTH ); - mTransponderMessageFramer = new MessageFramer( - SyncPattern.LJ1200_TRANSPONDER.getPattern(), MESSAGE_LENGTH ); - mFSKDecoderBroadcaster.addListener( mTowerMessageFramer ); - mFSKDecoderBroadcaster.addListener( mTransponderMessageFramer ); - - mMessageProcessor = new LJ1200MessageProcessor( aliasList ); - mTowerMessageFramer.addMessageListener( mMessageProcessor ); - mTransponderMessageFramer.addMessageListener( mMessageProcessor ); - mMessageProcessor.setMessageListener( getMessageListener() ); - } - - @Override - public DecoderType getDecoderType() - { - return DecoderType.LJ_1200; - } - - public void dispose() + protected LJ1200Decoder(AFSK1200Decoder decoder, AliasList aliasList) { - super.dispose(); - - mDecimationFilter.dispose(); - mBandPassFilter.dispose(); - mFSKDecoder.dispose(); - mTowerMessageFramer.dispose(); - mMessageProcessor.dispose(); + super(decoder); + init(aliasList); } - /** - * Returns a float listener interface for connecting this decoder to a - * float stream provider - */ - public RealSampleListener getRealReceiver() - { - return (RealSampleListener)mDecimationFilter; - } - - @Override - public List getTapGroups() + public LJ1200Decoder(AliasList aliasList) { - if( mAvailableTaps == null ) - { - mAvailableTaps = new ArrayList(); - - TapGroup group = new TapGroup( "LJ-1200 Decoder" ); - - group.add( new FloatTap( - INSTRUMENT_BANDPASS_FILTER_TO_FSK2_DEMOD, 0, 0.5f ) ); - group.add( new BinaryTap( - INSTRUMENT_FSK2_DECODER_TO_MESSAGE_FRAMER, 0, 0.025f ) ); - - mAvailableTaps.add( group ); - mAvailableTaps.addAll( mFSKDecoder.getTapGroups() ); - } - - return mAvailableTaps; + super(AFSK1200Decoder.Output.NORMAL); + init(aliasList); } - @Override - public void registerTap( Tap tap ) - { - mFSKDecoder.registerTap( tap ); - switch( tap.getName() ) - { - case INSTRUMENT_BANDPASS_FILTER_TO_FSK2_DEMOD: - FloatBufferTap bpTap = (FloatBufferTap)tap; - mBandPassFilter.setListener( bpTap ); - bpTap.setListener( mFSKDecoder ); - break; - case INSTRUMENT_FSK2_DECODER_TO_MESSAGE_FRAMER: - BinaryTap decoderTap = (BinaryTap)tap; - mFSKDecoder.setListener( decoderTap ); - decoderTap.setListener( mTowerMessageFramer ); - break; - } + private void init(AliasList aliasList) + { + getDecoder().setSymbolProcessor(this); + + mTowerMessageFramer = new MessageFramer(SyncPattern.LJ1200.getPattern(), MESSAGE_LENGTH); + mTransponderMessageFramer = new MessageFramer(SyncPattern.LJ1200_TRANSPONDER.getPattern(), MESSAGE_LENGTH); + mMessageProcessor = new LJ1200MessageProcessor(aliasList); + mTowerMessageFramer.addMessageListener(mMessageProcessor); + mTransponderMessageFramer.addMessageListener(mMessageProcessor); + mMessageProcessor.setMessageListener(getMessageListener()); } - @Override - public void unregisterTap( Tap tap ) + @Override + public DecoderType getDecoderType() { - mFSKDecoder.unregisterTap( tap ); - - switch( tap.getName() ) - { - case INSTRUMENT_BANDPASS_FILTER_TO_FSK2_DEMOD: - mBandPassFilter.setListener( mFSKDecoder ); - break; - case INSTRUMENT_FSK2_DECODER_TO_MESSAGE_FRAMER: - mFSKDecoder.setListener( mTowerMessageFramer ); - break; - } + return DecoderType.LJ_1200; } - @Override - public Listener getFilteredRealBufferListener() - { - return mDecimationFilter; - } + public void dispose() + { + super.dispose(); + mTowerMessageFramer.dispose(); + mMessageProcessor.dispose(); + } + + @Override + public void receive(boolean symbol) + { + mTowerMessageFramer.receive(symbol); + mTransponderMessageFramer.receive(symbol); + } + + public MessageFramer getTowerMessageFramer() + { + return mTowerMessageFramer; + } + + public MessageFramer getTransponderMessageFramer() + { + return mTransponderMessageFramer; + } } diff --git a/src/main/java/io/github/dsheirer/module/decode/lj1200/LJ1200DecoderInstrumented.java b/src/main/java/io/github/dsheirer/module/decode/lj1200/LJ1200DecoderInstrumented.java new file mode 100644 index 00000000..468b117b --- /dev/null +++ b/src/main/java/io/github/dsheirer/module/decode/lj1200/LJ1200DecoderInstrumented.java @@ -0,0 +1,53 @@ +/******************************************************************************* + * 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 + * + ******************************************************************************/ +package io.github.dsheirer.module.decode.lj1200; + +import io.github.dsheirer.dsp.afsk.AFSK1200DecoderInstrumented; +import io.github.dsheirer.instrument.gui.viewer.chart.IInstrumentedAFSK1200Decoder; +import io.github.dsheirer.sample.buffer.ReusableBuffer; +import javafx.beans.property.SimpleIntegerProperty; + +/** + * Instrumented version of the AFSK-1200 decoder for use with DemodulatorViewerFX + */ +public class LJ1200DecoderInstrumented extends LJ1200Decoder implements IInstrumentedAFSK1200Decoder +{ + public SimpleIntegerProperty bufferCount = new SimpleIntegerProperty(); + + public LJ1200DecoderInstrumented() + { + super(new AFSK1200DecoderInstrumented(false), null); + } + + @Override + public SimpleIntegerProperty getBufferCountProperty() + { + return bufferCount; + } + + public AFSK1200DecoderInstrumented getAFSK1200Decoder() + { + return (AFSK1200DecoderInstrumented)getDecoder(); + } + + @Override + public void receive(ReusableBuffer reusableBuffer) + { + super.receive(reusableBuffer); + + bufferCount.setValue(bufferCount.intValue() + 1); + } +} diff --git a/src/main/java/io/github/dsheirer/module/decode/mpt1327/MPT1327Decoder.java b/src/main/java/io/github/dsheirer/module/decode/mpt1327/MPT1327Decoder.java index eef7ba35..2c0ae2a2 100644 --- a/src/main/java/io/github/dsheirer/module/decode/mpt1327/MPT1327Decoder.java +++ b/src/main/java/io/github/dsheirer/module/decode/mpt1327/MPT1327Decoder.java @@ -24,6 +24,9 @@ import io.github.dsheirer.module.decode.afsk.AbstractAFSKDecoder; import org.slf4j.Logger; import org.slf4j.LoggerFactory; +/** + * Instrumented version of the MPT1327 decoder. Exposes properties for instrumented manual decoding of a signal. + */ public class MPT1327Decoder extends AbstractAFSKDecoder implements IBinarySymbolProcessor { private final static Logger mLog = LoggerFactory.getLogger(MPT1327Decoder.class); @@ -36,10 +39,20 @@ public class MPT1327Decoder extends AbstractAFSKDecoder implements IBinarySymbol private MessageFramer mTrafficMessageFramer; private MPT1327MessageProcessor mMessageProcessor; + protected MPT1327Decoder(AFSK1200Decoder decoder, AliasList aliasList, Sync sync) + { + super(decoder); + init(aliasList, sync); + } + public MPT1327Decoder(AliasList aliasList, Sync sync) { super((sync == Sync.NORMAL ? AFSK1200Decoder.Output.NORMAL : AFSK1200Decoder.Output.INVERTED)); + init(aliasList, sync); + } + private void init(AliasList aliasList, Sync sync) + { getDecoder().setSymbolProcessor(this); //Message framer for control channel messages @@ -87,4 +100,13 @@ public class MPT1327Decoder extends AbstractAFSKDecoder implements IBinarySymbol mTrafficMessageFramer.dispose(); } + public MessageFramer getControlMessageFramer() + { + return mControlMessageFramer; + } + + public MessageFramer getTrafficMessageFramer() + { + return mTrafficMessageFramer; + } } diff --git a/src/main/java/io/github/dsheirer/module/decode/mpt1327/MPT1327DecoderInstrumented.java b/src/main/java/io/github/dsheirer/module/decode/mpt1327/MPT1327DecoderInstrumented.java new file mode 100644 index 00000000..33413a78 --- /dev/null +++ b/src/main/java/io/github/dsheirer/module/decode/mpt1327/MPT1327DecoderInstrumented.java @@ -0,0 +1,53 @@ +/******************************************************************************* + * 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 + * + ******************************************************************************/ +package io.github.dsheirer.module.decode.mpt1327; + +import io.github.dsheirer.dsp.afsk.AFSK1200DecoderInstrumented; +import io.github.dsheirer.instrument.gui.viewer.chart.IInstrumentedAFSK1200Decoder; +import io.github.dsheirer.sample.buffer.ReusableBuffer; +import javafx.beans.property.SimpleIntegerProperty; + +/** + * Instrumented version of the AFSK-1200 decoder for use with DemodulatorViewerFX + */ +public class MPT1327DecoderInstrumented extends MPT1327Decoder implements IInstrumentedAFSK1200Decoder +{ + public SimpleIntegerProperty bufferCount = new SimpleIntegerProperty(); + + public MPT1327DecoderInstrumented() + { + super(new AFSK1200DecoderInstrumented(true), null, Sync.FRENCH); + } + + @Override + public SimpleIntegerProperty getBufferCountProperty() + { + return bufferCount; + } + + public AFSK1200DecoderInstrumented getAFSK1200Decoder() + { + return (AFSK1200DecoderInstrumented)getDecoder(); + } + + @Override + public void receive(ReusableBuffer reusableBuffer) + { + super.receive(reusableBuffer); + + bufferCount.setValue(bufferCount.intValue() + 1); + } +} From 18f50485d3043e6f7674719075b33d80c7e9de4d Mon Sep 17 00:00:00 2001 From: Denny Date: Fri, 15 Jun 2018 06:56:44 -0400 Subject: [PATCH 74/87] [WIP]Updates the Tait1200 decoder to use the new AFSK1200 decoder class. Adds Demodulator Viewer FX pane for Tait1200 decoder. --- .../viewer/decoder/DecoderPaneFactory.java | 5 +- .../gui/viewer/decoder/Tait1200Pane.java | 60 +++++ .../decode/fleetsync2/Fleetsync2Decoder.java | 2 +- .../module/decode/tait/Tait1200Decoder.java | 214 +++++------------- .../tait/Tait1200DecoderInstrumented.java | 53 +++++ .../decode/tait/Tait1200GPSMessage.java | 2 - 6 files changed, 179 insertions(+), 157 deletions(-) create mode 100644 src/main/java/io/github/dsheirer/instrument/gui/viewer/decoder/Tait1200Pane.java create mode 100644 src/main/java/io/github/dsheirer/module/decode/tait/Tait1200DecoderInstrumented.java diff --git a/src/main/java/io/github/dsheirer/instrument/gui/viewer/decoder/DecoderPaneFactory.java b/src/main/java/io/github/dsheirer/instrument/gui/viewer/decoder/DecoderPaneFactory.java index f1895372..15153790 100644 --- a/src/main/java/io/github/dsheirer/instrument/gui/viewer/decoder/DecoderPaneFactory.java +++ b/src/main/java/io/github/dsheirer/instrument/gui/viewer/decoder/DecoderPaneFactory.java @@ -24,10 +24,11 @@ public class DecoderPaneFactory { private static final EnumSet SUPPORTED_DECODER_TYPES = EnumSet.of( DecoderType.P25_PHASE1, + DecoderType.FLEETSYNC2, DecoderType.LJ_1200, DecoderType.LTR_NET, DecoderType.MPT1327, - DecoderType.FLEETSYNC2); + DecoderType.TAIT_1200); /** * Creates a decoder pane for the decoder type @@ -44,6 +45,8 @@ public class DecoderPaneFactory return new LTRNetPane(); case MPT1327: return new MPT1327Pane(); + case TAIT_1200: + return new Tait1200Pane(); case P25_PHASE1: throw new IllegalArgumentException("Use the getP25DecoderPane() method for P25 decoder type"); } diff --git a/src/main/java/io/github/dsheirer/instrument/gui/viewer/decoder/Tait1200Pane.java b/src/main/java/io/github/dsheirer/instrument/gui/viewer/decoder/Tait1200Pane.java new file mode 100644 index 00000000..07b877e3 --- /dev/null +++ b/src/main/java/io/github/dsheirer/instrument/gui/viewer/decoder/Tait1200Pane.java @@ -0,0 +1,60 @@ +/******************************************************************************* + * 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 + * + ******************************************************************************/ +package io.github.dsheirer.instrument.gui.viewer.decoder; + +import io.github.dsheirer.dsp.symbol.ISyncDetectListener; +import io.github.dsheirer.module.decode.DecoderType; +import io.github.dsheirer.module.decode.tait.Tait1200DecoderInstrumented; +import org.slf4j.Logger; +import org.slf4j.LoggerFactory; + +public class Tait1200Pane extends AbstractAFSK1200Pane +{ + private final static Logger mLog = LoggerFactory.getLogger(Tait1200Pane.class); + + private Tait1200DecoderInstrumented mDecoder; + + public Tait1200Pane() + { + super(DecoderType.TAIT_1200); + } + + protected Tait1200DecoderInstrumented getDecoder() + { + if(mDecoder == null) + { + mDecoder = new Tait1200DecoderInstrumented(); + mDecoder.getANIMessageFramer().setSyncDetectListener(new ISyncDetectListener() + { + @Override + public void syncDetected() + { + mLog.debug("ANI Sync Detected!"); + } + }); + mDecoder.getGPSMessageFramer().setSyncDetectListener(new ISyncDetectListener() + { + @Override + public void syncDetected() + { + mLog.debug("GPS Sync Detected!"); + } + }); + } + + return mDecoder; + } +} diff --git a/src/main/java/io/github/dsheirer/module/decode/fleetsync2/Fleetsync2Decoder.java b/src/main/java/io/github/dsheirer/module/decode/fleetsync2/Fleetsync2Decoder.java index dc3f52af..5d5676d7 100644 --- a/src/main/java/io/github/dsheirer/module/decode/fleetsync2/Fleetsync2Decoder.java +++ b/src/main/java/io/github/dsheirer/module/decode/fleetsync2/Fleetsync2Decoder.java @@ -39,7 +39,7 @@ public class Fleetsync2Decoder extends AbstractAFSKDecoder * @param afsk1200Decoder to use for decoding the symbols * @param aliasList to assign aliases to decoded values */ - public Fleetsync2Decoder(AFSK1200Decoder afsk1200Decoder, AliasList aliasList) + protected Fleetsync2Decoder(AFSK1200Decoder afsk1200Decoder, AliasList aliasList) { super(afsk1200Decoder); init(aliasList); diff --git a/src/main/java/io/github/dsheirer/module/decode/tait/Tait1200Decoder.java b/src/main/java/io/github/dsheirer/module/decode/tait/Tait1200Decoder.java index f268e52e..3fd46b95 100644 --- a/src/main/java/io/github/dsheirer/module/decode/tait/Tait1200Decoder.java +++ b/src/main/java/io/github/dsheirer/module/decode/tait/Tait1200Decoder.java @@ -1,191 +1,99 @@ /******************************************************************************* - * SDR Trunk - * Copyright (C) 2014,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 + * 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 + * ******************************************************************************/ package io.github.dsheirer.module.decode.tait; import io.github.dsheirer.alias.AliasList; +import io.github.dsheirer.bits.IBinarySymbolProcessor; import io.github.dsheirer.bits.MessageFramer; import io.github.dsheirer.bits.SyncPattern; -import io.github.dsheirer.dsp.filter.Filters; -import io.github.dsheirer.dsp.filter.fir.real.RealFIRFilter_RB_RB; -import io.github.dsheirer.dsp.filter.halfband.real.HalfBandFilter_RB_RB; -import io.github.dsheirer.dsp.fsk.FSK2Decoder; -import io.github.dsheirer.dsp.fsk.FSK2Decoder.Output; -import io.github.dsheirer.instrument.Instrumentable; -import io.github.dsheirer.instrument.tap.Tap; -import io.github.dsheirer.instrument.tap.TapGroup; -import io.github.dsheirer.instrument.tap.stream.BinaryTap; -import io.github.dsheirer.instrument.tap.stream.FloatBufferTap; -import io.github.dsheirer.instrument.tap.stream.FloatTap; -import io.github.dsheirer.module.decode.Decoder; +import io.github.dsheirer.dsp.afsk.AFSK1200Decoder; import io.github.dsheirer.module.decode.DecoderType; -import io.github.dsheirer.sample.Broadcaster; -import io.github.dsheirer.sample.Listener; -import io.github.dsheirer.sample.real.IFilteredRealBufferListener; -import io.github.dsheirer.sample.real.RealBuffer; - -import java.util.ArrayList; -import java.util.List; +import io.github.dsheirer.module.decode.afsk.AbstractAFSKDecoder; /** - * TAIT 1200 - 1200 baud 2FSK decoder + * TAIT 1200 - 1200 baud AFSK decoder */ -public class Tait1200Decoder extends Decoder implements IFilteredRealBufferListener, - Instrumentable +public class Tait1200Decoder extends AbstractAFSKDecoder implements IBinarySymbolProcessor { - /* Decimated sample rate ( 48,000 / 2 = 24,000 ) feeding the decoder */ - private static final int DECIMATED_SAMPLE_RATE = 24000; - - /* Baud or Symbol Rate */ - private static final int SYMBOL_RATE = 1200; - - /* Message length ... */ + /* Message length ... */ private static final int MESSAGE_LENGTH = 440; - - /* Instrumentation Taps */ - private ArrayList mAvailableTaps; - private static final String INSTRUMENT_BANDPASS_FILTER_TO_FSK2_DEMOD = - "Tap Point: Bandpass Filter > < FSK2 Decoder"; - private static final String INSTRUMENT_FSK2_DECODER_TO_MESSAGE_FRAMER = - "Tap Point: FSK2 Decoder > < Message Framer"; - - private FSK2Decoder mFSKDecoder; - - private HalfBandFilter_RB_RB mDecimationFilter; - private RealFIRFilter_RB_RB mBandPassFilter; + private MessageFramer mMessageFramerGPS; private MessageFramer mMessageFramerANI; - private Broadcaster mFSKBroadcaster = new Broadcaster(); private Tait1200GPSMessageProcessor mMessageAProcessor; private Tait1200ANIMessageProcessor mMessageBProcessor; - - public Tait1200Decoder( AliasList aliasList ) - { - mDecimationFilter = new HalfBandFilter_RB_RB( - Filters.FIR_HALF_BAND_31T_ONE_EIGHTH_FCO.getCoefficients(), 1.0002f, true ); - mBandPassFilter = new RealFIRFilter_RB_RB( - Filters.FIRBP_1200FSK_24000FS.getCoefficients(), 1.02f ); - mDecimationFilter.setListener( mBandPassFilter ); + protected Tait1200Decoder(AFSK1200Decoder decoder, AliasList aliasList) + { + super(decoder); + init(aliasList); + } - mFSKDecoder = new FSK2Decoder( DECIMATED_SAMPLE_RATE, - SYMBOL_RATE, Output.INVERTED ); - mBandPassFilter.setListener( mFSKDecoder ); + public Tait1200Decoder(AliasList aliasList) + { + super(AFSK1200Decoder.Output.NORMAL); + init(aliasList); + } + private void init(AliasList aliasList) + { + getDecoder().setSymbolProcessor(this); - mFSKDecoder.setListener( mFSKBroadcaster ); + mMessageFramerGPS = new MessageFramer(SyncPattern.TAIT_CCDI_GPS_MESSAGE.getPattern(), MESSAGE_LENGTH); + mMessageFramerANI = new MessageFramer(SyncPattern.TAIT_SELCAL_MESSAGE.getPattern(), MESSAGE_LENGTH); - mMessageFramerGPS = new MessageFramer( - SyncPattern.TAIT_CCDI_GPS_MESSAGE.getPattern(), MESSAGE_LENGTH ); - mMessageFramerANI = new MessageFramer( - SyncPattern.TAIT_SELCAL_MESSAGE.getPattern(), MESSAGE_LENGTH ); - - mFSKBroadcaster.addListener( mMessageFramerGPS ); - mFSKBroadcaster.addListener( mMessageFramerANI ); + mMessageAProcessor = new Tait1200GPSMessageProcessor(aliasList); + mMessageBProcessor = new Tait1200ANIMessageProcessor(aliasList); + + mMessageFramerGPS.addMessageListener(mMessageAProcessor); + mMessageFramerANI.addMessageListener(mMessageBProcessor); - mMessageAProcessor = new Tait1200GPSMessageProcessor( aliasList ); - mMessageBProcessor = new Tait1200ANIMessageProcessor( aliasList ); - - mMessageFramerGPS.addMessageListener( mMessageAProcessor ); - mMessageFramerANI.addMessageListener( mMessageBProcessor ); - mMessageAProcessor.setMessageListener(getMessageListener()); mMessageBProcessor.setMessageListener(getMessageListener()); - } - + } + + @Override + public DecoderType getDecoderType() + { + return DecoderType.TAIT_1200; + } + public void dispose() { - super.dispose(); - - mDecimationFilter.dispose(); - mBandPassFilter.dispose(); - mFSKDecoder.dispose(); - mMessageFramerGPS.dispose(); - mMessageFramerANI.dispose(); - mMessageAProcessor.dispose(); - mMessageBProcessor.dispose(); + super.dispose(); + mMessageFramerGPS.dispose(); + mMessageFramerANI.dispose(); + mMessageAProcessor.dispose(); + mMessageBProcessor.dispose(); } - @Override - public List getTapGroups() + @Override + public void receive(boolean symbol) { - if( mAvailableTaps == null ) - { - mAvailableTaps = new ArrayList<>(); - - TapGroup group = new TapGroup( "Tait 1200 Decoder" ); - - group.add( new FloatTap( - INSTRUMENT_BANDPASS_FILTER_TO_FSK2_DEMOD, 0, 0.5f ) ); - group.add( new BinaryTap( - INSTRUMENT_FSK2_DECODER_TO_MESSAGE_FRAMER, 0, 0.025f ) ); - - mAvailableTaps.add( group ); - - mAvailableTaps.addAll( mFSKDecoder.getTapGroups() ); - } - - return mAvailableTaps; + mMessageFramerANI.receive(symbol); + mMessageFramerGPS.receive(symbol); } - @Override - public void registerTap( Tap tap ) + public MessageFramer getANIMessageFramer() { - mFSKDecoder.registerTap( tap ); - - switch( tap.getName() ) - { - case INSTRUMENT_BANDPASS_FILTER_TO_FSK2_DEMOD: - FloatBufferTap bpTap = (FloatBufferTap)tap; - mBandPassFilter.setListener( bpTap ); - bpTap.setListener( mFSKDecoder ); - break; - case INSTRUMENT_FSK2_DECODER_TO_MESSAGE_FRAMER: - BinaryTap decoderTap = (BinaryTap)tap; - mFSKBroadcaster.addListener( decoderTap ); - break; - } + return mMessageFramerANI; } - @Override - public void unregisterTap( Tap tap ) + public MessageFramer getGPSMessageFramer() { - mFSKDecoder.unregisterTap( tap ); - - switch( tap.getName() ) - { - case INSTRUMENT_BANDPASS_FILTER_TO_FSK2_DEMOD: - mBandPassFilter.setListener( mFSKDecoder ); - break; - case INSTRUMENT_FSK2_DECODER_TO_MESSAGE_FRAMER: - mFSKBroadcaster.removeListener( (BinaryTap)tap ); - break; - } + return mMessageFramerGPS; } - - @Override - public Listener getFilteredRealBufferListener() - { - return mDecimationFilter; - } - - @Override - public DecoderType getDecoderType() - { - return DecoderType.TAIT_1200; - } } diff --git a/src/main/java/io/github/dsheirer/module/decode/tait/Tait1200DecoderInstrumented.java b/src/main/java/io/github/dsheirer/module/decode/tait/Tait1200DecoderInstrumented.java new file mode 100644 index 00000000..c6ce15f9 --- /dev/null +++ b/src/main/java/io/github/dsheirer/module/decode/tait/Tait1200DecoderInstrumented.java @@ -0,0 +1,53 @@ +/******************************************************************************* + * 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 + * + ******************************************************************************/ +package io.github.dsheirer.module.decode.tait; + +import io.github.dsheirer.dsp.afsk.AFSK1200DecoderInstrumented; +import io.github.dsheirer.instrument.gui.viewer.chart.IInstrumentedAFSK1200Decoder; +import io.github.dsheirer.sample.buffer.ReusableBuffer; +import javafx.beans.property.SimpleIntegerProperty; + +/** + * Instrumented version of the AFSK-1200 decoder for use with DemodulatorViewerFX + */ +public class Tait1200DecoderInstrumented extends Tait1200Decoder implements IInstrumentedAFSK1200Decoder +{ + public SimpleIntegerProperty bufferCount = new SimpleIntegerProperty(); + + public Tait1200DecoderInstrumented() + { + super(new AFSK1200DecoderInstrumented(false), null); + } + + @Override + public SimpleIntegerProperty getBufferCountProperty() + { + return bufferCount; + } + + public AFSK1200DecoderInstrumented getAFSK1200Decoder() + { + return (AFSK1200DecoderInstrumented)getDecoder(); + } + + @Override + public void receive(ReusableBuffer reusableBuffer) + { + super.receive(reusableBuffer); + + bufferCount.setValue(bufferCount.intValue() + 1); + } +} diff --git a/src/main/java/io/github/dsheirer/module/decode/tait/Tait1200GPSMessage.java b/src/main/java/io/github/dsheirer/module/decode/tait/Tait1200GPSMessage.java index 6158f1c6..9d321fac 100644 --- a/src/main/java/io/github/dsheirer/module/decode/tait/Tait1200GPSMessage.java +++ b/src/main/java/io/github/dsheirer/module/decode/tait/Tait1200GPSMessage.java @@ -121,8 +121,6 @@ public class Tait1200GPSMessage extends Message { mMessage = message; mAliasList = list; - - mLog.debug( toString() ); } public boolean isValid() From 2cd3b73633fcd1a7ff08fda9f5824aba7343db67 Mon Sep 17 00:00:00 2001 From: Denny Date: Fri, 15 Jun 2018 07:11:07 -0400 Subject: [PATCH 75/87] [WIP]Updates the MDC1200 decoder to use the new AFSK1200 decoder class. Adds Demodulator Viewer FX pane for MDC1200 decoder. --- .../io/github/dsheirer/dsp/NRZDecoder.java | 127 +++++----- .../viewer/decoder/DecoderPaneFactory.java | 3 + .../gui/viewer/decoder/MDC1200Pane.java | 52 +++++ .../module/decode/mdc1200/MDCDecoder.java | 219 ++++-------------- .../mdc1200/MDCDecoderInstrumented.java | 53 +++++ 5 files changed, 218 insertions(+), 236 deletions(-) create mode 100644 src/main/java/io/github/dsheirer/instrument/gui/viewer/decoder/MDC1200Pane.java create mode 100644 src/main/java/io/github/dsheirer/module/decode/mdc1200/MDCDecoderInstrumented.java diff --git a/src/main/java/io/github/dsheirer/dsp/NRZDecoder.java b/src/main/java/io/github/dsheirer/dsp/NRZDecoder.java index 2c1e9e68..a93e9c01 100644 --- a/src/main/java/io/github/dsheirer/dsp/NRZDecoder.java +++ b/src/main/java/io/github/dsheirer/dsp/NRZDecoder.java @@ -1,77 +1,76 @@ /******************************************************************************* - * SDR Trunk - * Copyright (C) 2014 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 + * 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 + * ******************************************************************************/ package io.github.dsheirer.dsp; +import io.github.dsheirer.bits.IBinarySymbolProcessor; import io.github.dsheirer.sample.Listener; -public class NRZDecoder implements Listener +public class NRZDecoder implements IBinarySymbolProcessor { - public final static boolean MODE_NORMAL = true; - public final static boolean MODE_INVERTED = false; - - private Listener mListener; - private boolean mMode = MODE_NORMAL; - private boolean mPrevious; + public final static boolean MODE_NORMAL = true; + public final static boolean MODE_INVERTED = false; - /** - * Non Return To Zero (NRZ) and NRZ Inverted (NRZ-I) Decoder. - * - * Use default constructor for NRZ output. Use the alternate constructor - * with the MODE_INVERTED argument for NRZ-I output. - * - * Performs XOR of incoming bit with previous output and sends the result - * to the registered listener. - */ - public NRZDecoder( boolean mode ) - { - mMode = mode; - } - - public NRZDecoder() - { - this( MODE_NORMAL ); - } - - public void dispose() - { - mListener = null; - } + private IBinarySymbolProcessor mListener; + private boolean mMode = MODE_NORMAL; + private boolean mPrevious; - @Override - public void receive( Boolean bit ) + /** + * Non Return To Zero (NRZ) and NRZ Inverted (NRZ-I) Decoder. + * + * Use default constructor for NRZ output. Use the alternate constructor + * with the MODE_INVERTED argument for NRZ-I output. + * + * Performs XOR of incoming bit with previous output and sends the result + * to the registered listener. + */ + public NRZDecoder(boolean mode) { - boolean result = mPrevious ^ bit; - - if( mListener != null ) - { - mListener.receive( mMode ? result : !result ); - } - - mPrevious = result; + mMode = mode; } - public void setListener( Listener listener ) - { - mListener = listener; - } - - public void removeListener( Listener listener ) - { - mListener = null; - } + public NRZDecoder() + { + this(MODE_NORMAL); + } + + public void dispose() + { + mListener = null; + } + + @Override + public void receive(boolean symbol) + { + boolean result = mPrevious ^ symbol; + + if(mListener != null) + { + mListener.receive(mMode ? result : !result); + } + + mPrevious = result; + } + + public void setListener(IBinarySymbolProcessor listener) + { + mListener = listener; + } + + public void removeListener(Listener listener) + { + mListener = null; + } } diff --git a/src/main/java/io/github/dsheirer/instrument/gui/viewer/decoder/DecoderPaneFactory.java b/src/main/java/io/github/dsheirer/instrument/gui/viewer/decoder/DecoderPaneFactory.java index 15153790..a78202c8 100644 --- a/src/main/java/io/github/dsheirer/instrument/gui/viewer/decoder/DecoderPaneFactory.java +++ b/src/main/java/io/github/dsheirer/instrument/gui/viewer/decoder/DecoderPaneFactory.java @@ -27,6 +27,7 @@ public class DecoderPaneFactory DecoderType.FLEETSYNC2, DecoderType.LJ_1200, DecoderType.LTR_NET, + DecoderType.MDC1200, DecoderType.MPT1327, DecoderType.TAIT_1200); @@ -43,6 +44,8 @@ public class DecoderPaneFactory return new LJ1200Pane(); case LTR_NET: return new LTRNetPane(); + case MDC1200: + return new MDC1200Pane(); case MPT1327: return new MPT1327Pane(); case TAIT_1200: diff --git a/src/main/java/io/github/dsheirer/instrument/gui/viewer/decoder/MDC1200Pane.java b/src/main/java/io/github/dsheirer/instrument/gui/viewer/decoder/MDC1200Pane.java new file mode 100644 index 00000000..80953de9 --- /dev/null +++ b/src/main/java/io/github/dsheirer/instrument/gui/viewer/decoder/MDC1200Pane.java @@ -0,0 +1,52 @@ +/******************************************************************************* + * 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 + * + ******************************************************************************/ +package io.github.dsheirer.instrument.gui.viewer.decoder; + +import io.github.dsheirer.dsp.symbol.ISyncDetectListener; +import io.github.dsheirer.module.decode.DecoderType; +import io.github.dsheirer.module.decode.mdc1200.MDCDecoderInstrumented; +import org.slf4j.Logger; +import org.slf4j.LoggerFactory; + +public class MDC1200Pane extends AbstractAFSK1200Pane +{ + private final static Logger mLog = LoggerFactory.getLogger(MDC1200Pane.class); + + private MDCDecoderInstrumented mDecoder; + + public MDC1200Pane() + { + super(DecoderType.MDC1200); + } + + protected MDCDecoderInstrumented getDecoder() + { + if(mDecoder == null) + { + mDecoder = new MDCDecoderInstrumented(); + mDecoder.getMessageFramer().setSyncDetectListener(new ISyncDetectListener() + { + @Override + public void syncDetected() + { + mLog.debug("Sync Detected!"); + } + }); + } + + return mDecoder; + } +} diff --git a/src/main/java/io/github/dsheirer/module/decode/mdc1200/MDCDecoder.java b/src/main/java/io/github/dsheirer/module/decode/mdc1200/MDCDecoder.java index 735871fa..f4910c4a 100644 --- a/src/main/java/io/github/dsheirer/module/decode/mdc1200/MDCDecoder.java +++ b/src/main/java/io/github/dsheirer/module/decode/mdc1200/MDCDecoder.java @@ -1,19 +1,17 @@ /******************************************************************************* - * SDR Trunk - * Copyright (C) 2014 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 + * 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 + * ******************************************************************************/ package io.github.dsheirer.module.decode.mdc1200; @@ -21,183 +19,60 @@ import io.github.dsheirer.alias.AliasList; import io.github.dsheirer.bits.MessageFramer; import io.github.dsheirer.bits.SyncPattern; import io.github.dsheirer.dsp.NRZDecoder; -import io.github.dsheirer.dsp.filter.Filters; -import io.github.dsheirer.dsp.filter.fir.real.RealFIRFilter_RB_RB; -import io.github.dsheirer.dsp.filter.halfband.real.HalfBandFilter_RB_RB; -import io.github.dsheirer.dsp.fsk.FSK2Decoder; -import io.github.dsheirer.dsp.fsk.FSK2Decoder.Output; -import io.github.dsheirer.instrument.Instrumentable; -import io.github.dsheirer.instrument.tap.Tap; -import io.github.dsheirer.instrument.tap.TapGroup; -import io.github.dsheirer.instrument.tap.stream.BinaryTap; -import io.github.dsheirer.instrument.tap.stream.FloatBufferTap; -import io.github.dsheirer.instrument.tap.stream.FloatTap; -import io.github.dsheirer.module.decode.Decoder; +import io.github.dsheirer.dsp.afsk.AFSK1200Decoder; import io.github.dsheirer.module.decode.DecoderType; -import io.github.dsheirer.sample.Listener; -import io.github.dsheirer.sample.real.IFilteredRealBufferListener; -import io.github.dsheirer.sample.real.RealBuffer; - -import java.util.ArrayList; -import java.util.List; +import io.github.dsheirer.module.decode.afsk.AbstractAFSKDecoder; /** - * MDC1200 Decoder - 1200 baud 2FSK decoder that can process 48k sample rate - * complex or floating point samples and output fully framed MDC1200 messages + * MDC1200 Decoder - 1200 baud 2FSK decoder */ -public class MDCDecoder extends Decoder implements IFilteredRealBufferListener, - Instrumentable +public class MDCDecoder extends AbstractAFSKDecoder { - /* Decimated sample rate ( 48,000 / 2 = 24,000 ) feeding the decoder */ - private static final int sDECIMATED_SAMPLE_RATE = 24000; - - /* Baud or Symbol Rate */ - private static final int sSYMBOL_RATE = 1200; + private static final int MESSAGE_LENGTH = 304; - /* Message length */ - private static final int sMESSAGE_LENGTH = 304; - - /* Instrumentation Taps */ - private ArrayList mAvailableTaps; - private static final String INSTRUMENT_BANDPASS_FILTER_TO_FSK2_DEMOD = - "Tap Point: Bandpass Filter > < FSK2 Decoder"; - private static final String INSTRUMENT_FSK2_DECODER_TO_NRZ_DECODER = - "Tap Point: FSK2 Decoder > < NRZI Decoder"; - private static final String INSTRUMENT_NRZI_DECODER_TO_MESSAGE_FRAMER = - "Tap Point: NRZ Decoder > < Message Framer"; - - private FSK2Decoder mFSKDecoder; - private HalfBandFilter_RB_RB mDecimationFilter; - private RealFIRFilter_RB_RB mBandPassFilter; private NRZDecoder mNRZDecoder; private MessageFramer mMessageFramer; private MDCMessageProcessor mMessageProcessor; - - public MDCDecoder( AliasList aliasList ) - { - /* Decimation filter: 48000 / 2 = 24000 */ - mDecimationFilter = new HalfBandFilter_RB_RB( - Filters.FIR_HALF_BAND_31T_ONE_EIGHTH_FCO.getCoefficients(), 1.0f, true ); - /* Bandpass filter - predefined */ - mBandPassFilter = new RealFIRFilter_RB_RB( - Filters.FIRBP_1200FSK_24000FS.getCoefficients(), 1.02f ); - mDecimationFilter.setListener( mBandPassFilter ); + public MDCDecoder(AliasList aliasList) + { + super(AFSK1200Decoder.Output.INVERTED); + init(aliasList); + } - /* 2FSK Decoder */ - mFSKDecoder = new FSK2Decoder( sDECIMATED_SAMPLE_RATE, - sSYMBOL_RATE, Output.NORMAL ); - mBandPassFilter.setListener( mFSKDecoder ); + protected MDCDecoder(AFSK1200Decoder decoder, AliasList aliasList) + { + super(decoder); + init(aliasList); + } - /* NRZ Decoder */ - mNRZDecoder = new NRZDecoder( NRZDecoder.MODE_INVERTED ); - mFSKDecoder.setListener( mNRZDecoder ); - - /* Message Framer */ - mMessageFramer = new MessageFramer( SyncPattern.MDC1200.getPattern(), - sMESSAGE_LENGTH ); - mNRZDecoder.setListener( mMessageFramer ); - - /* Message Processor */ - mMessageProcessor = new MDCMessageProcessor( aliasList ); - mMessageFramer.addMessageListener( mMessageProcessor ); + private void init(AliasList aliasList) + { + mNRZDecoder = new NRZDecoder(NRZDecoder.MODE_INVERTED); + getDecoder().setSymbolProcessor(mNRZDecoder); + mMessageFramer = new MessageFramer(SyncPattern.MDC1200.getPattern(), MESSAGE_LENGTH); + mNRZDecoder.setListener(mMessageFramer); + mMessageProcessor = new MDCMessageProcessor(aliasList); + mMessageFramer.addMessageListener(mMessageProcessor); mMessageProcessor.addMessageListener(getMessageListener()); - } - + } + public void dispose() { - super.dispose(); - - mDecimationFilter.dispose(); - mBandPassFilter.dispose(); - mFSKDecoder.dispose(); - mNRZDecoder.dispose(); - mMessageFramer.dispose(); - mMessageProcessor.dispose(); + super.dispose(); + mNRZDecoder.dispose(); + mMessageFramer.dispose(); + mMessageProcessor.dispose(); } - /* Instrumentation */ - @Override - public List getTapGroups() + @Override + public DecoderType getDecoderType() { - if( mAvailableTaps == null ) - { - mAvailableTaps = new ArrayList(); - - TapGroup group = new TapGroup( "MDC-1200 Decoder" ); - - group.add( - new FloatTap( INSTRUMENT_BANDPASS_FILTER_TO_FSK2_DEMOD, 0, 0.5f ) ); - group.add( - new BinaryTap( INSTRUMENT_FSK2_DECODER_TO_NRZ_DECODER, 0, 0.025f ) ); - group.add( - new BinaryTap( INSTRUMENT_NRZI_DECODER_TO_MESSAGE_FRAMER, 0, 0.025f ) ); - - mAvailableTaps.add( group ); - mAvailableTaps.addAll( mFSKDecoder.getTapGroups() ); - } - - return mAvailableTaps; + return DecoderType.MDC1200; } - @Override - public void registerTap( Tap tap ) + public MessageFramer getMessageFramer() { - mFSKDecoder.registerTap( tap ); - - switch( tap.getName() ) - { - case INSTRUMENT_BANDPASS_FILTER_TO_FSK2_DEMOD: - /* wire the tap between the bandpass filter and the demod */ - FloatBufferTap floatTap = (FloatBufferTap)tap; - mBandPassFilter.setListener( floatTap ); - floatTap.setListener( mFSKDecoder ); - break; - case INSTRUMENT_FSK2_DECODER_TO_NRZ_DECODER: - /* wire between FSK2 decoder and NRZ decoder */ - BinaryTap binTap = (BinaryTap)tap; - mFSKDecoder.setListener( binTap ); - binTap.setListener( mNRZDecoder ); - break; - case INSTRUMENT_NRZI_DECODER_TO_MESSAGE_FRAMER: - /* wire between demod and framer */ - BinaryTap binInvTap = (BinaryTap)tap; - mNRZDecoder.setListener( binInvTap ); - binInvTap.setListener( mMessageFramer ); - break; - default: - } + return mMessageFramer; } - - @Override - public void unregisterTap( Tap tap ) - { - mFSKDecoder.unregisterTap( tap ); - - switch( tap.getName() ) - { - case INSTRUMENT_BANDPASS_FILTER_TO_FSK2_DEMOD: - mBandPassFilter.setListener( mFSKDecoder ); - break; - case INSTRUMENT_FSK2_DECODER_TO_NRZ_DECODER: - mFSKDecoder.setListener( mNRZDecoder ); - break; - case INSTRUMENT_NRZI_DECODER_TO_MESSAGE_FRAMER: - mNRZDecoder.setListener( mMessageFramer ); - break; - default: - } - } - - @Override - public DecoderType getDecoderType() - { - return DecoderType.MDC1200; - } - - @Override - public Listener getFilteredRealBufferListener() - { - return mDecimationFilter; - } } diff --git a/src/main/java/io/github/dsheirer/module/decode/mdc1200/MDCDecoderInstrumented.java b/src/main/java/io/github/dsheirer/module/decode/mdc1200/MDCDecoderInstrumented.java new file mode 100644 index 00000000..1e2bdafd --- /dev/null +++ b/src/main/java/io/github/dsheirer/module/decode/mdc1200/MDCDecoderInstrumented.java @@ -0,0 +1,53 @@ +/******************************************************************************* + * 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 + * + ******************************************************************************/ +package io.github.dsheirer.module.decode.mdc1200; + +import io.github.dsheirer.dsp.afsk.AFSK1200DecoderInstrumented; +import io.github.dsheirer.instrument.gui.viewer.chart.IInstrumentedAFSK1200Decoder; +import io.github.dsheirer.sample.buffer.ReusableBuffer; +import javafx.beans.property.SimpleIntegerProperty; + +/** + * Instrumented version of the AFSK-1200 decoder for use with DemodulatorViewerFX + */ +public class MDCDecoderInstrumented extends MDCDecoder implements IInstrumentedAFSK1200Decoder +{ + public SimpleIntegerProperty bufferCount = new SimpleIntegerProperty(); + + public MDCDecoderInstrumented() + { + super(new AFSK1200DecoderInstrumented(false), null); + } + + @Override + public SimpleIntegerProperty getBufferCountProperty() + { + return bufferCount; + } + + public AFSK1200DecoderInstrumented getAFSK1200Decoder() + { + return (AFSK1200DecoderInstrumented)getDecoder(); + } + + @Override + public void receive(ReusableBuffer reusableBuffer) + { + super.receive(reusableBuffer); + + bufferCount.setValue(bufferCount.intValue() + 1); + } +} From f4793f3cfd536d2d3a5a2ea589efdff9a27c0821 Mon Sep 17 00:00:00 2001 From: Denny Date: Fri, 15 Jun 2018 07:27:17 -0400 Subject: [PATCH 76/87] [WIP] Cleanup. Removes the legacy Instrumentable, Taps, etc and related gui components. Refactors the new DecoderFX application to the instrument package. --- .../github/dsheirer/dsp/fsk/FSK2Decoder.java | 302 --------------- .../io/github/dsheirer/dsp/symbol/Slicer.java | 149 -------- .../{gui/viewer => }/DemodulatorViewerFX.java | 4 +- .../dsheirer/instrument/Instrumentable.java | 54 --- .../InstrumentableProcessingChain.java | 82 ---- .../{gui/viewer => }/PlaybackController.java | 4 +- .../{gui/viewer => }/RecentFilesMenu.java | 17 +- .../{gui/viewer => }/ViewerDesktop.java | 6 +- .../chart/AFSK1200SampleBufferChart.java | 2 +- ...FSK1200ZeroCrossingErrorDetectorChart.java | 2 +- .../chart/ComplexPhaseLineChart.java | 2 +- .../chart/ComplexSampleLineChart.java | 2 +- .../viewer => }/chart/DecodedSymbolChart.java | 2 +- .../viewer => }/chart/DoubleLineChart.java | 2 +- .../viewer => }/chart/EyeDiagramChart.java | 2 +- .../chart/FMDemodulatedLineChart.java | 2 +- .../chart/IInstrumentedAFSK1200Decoder.java | 2 +- .../chart/LTRNetSampleBufferChart.java | 2 +- .../viewer => }/chart/PhaseLineChart.java | 2 +- .../chart/RealSampleLineChart.java | 2 +- .../{gui/viewer => }/chart/SymbolChart.java | 2 +- .../chart/ZeroCrossingErrorDetectorChart.java | 2 +- .../decoder/AbstractAFSK1200Pane.java | 12 +- .../decoder/AbstractDecoderPane.java | 2 +- .../decoder/ComplexDecoderPane.java | 2 +- .../decoder/DecoderPaneFactory.java | 2 +- .../viewer => }/decoder/Fleetsync2Pane.java | 2 +- .../{gui/viewer => }/decoder/LJ1200Pane.java | 2 +- .../{gui/viewer => }/decoder/LTRNetPane.java | 8 +- .../{gui/viewer => }/decoder/MDC1200Pane.java | 2 +- .../{gui/viewer => }/decoder/MPT1327Pane.java | 2 +- .../decoder/P25Phase1C4FMPane.java | 12 +- .../viewer => }/decoder/P25Phase1LSMPane.java | 12 +- .../viewer => }/decoder/RealDecoderPane.java | 2 +- .../viewer => }/decoder/Tait1200Pane.java | 2 +- .../instrument/gui/ConstellationPanel.java | 77 ---- .../instrument/gui/DecoderSelectionFrame.java | 102 ----- .../instrument/gui/DecoderViewFrame.java | 355 ------------------ .../dsheirer/instrument/gui/SampleModel.java | 101 ----- .../instrument/gui/SourceControllerFrame.java | 215 ----------- .../dsheirer/instrument/gui/Viewer.java | 293 --------------- .../github/dsheirer/instrument/tap/Tap.java | 96 ----- .../dsheirer/instrument/tap/TapGroup.java | 35 -- .../dsheirer/instrument/tap/TapListener.java | 23 -- .../dsheirer/instrument/tap/TapType.java | 31 -- .../dsheirer/instrument/tap/TapViewPanel.java | 131 ------- .../instrument/tap/event/SyncDetectTap.java | 58 --- .../tap/stream/AdditiveFloatTap.java | 40 -- .../instrument/tap/stream/BinaryTap.java | 59 --- .../tap/stream/BinaryTapViewPanel.java | 156 -------- .../tap/stream/ComplexSampleTap.java | 64 ---- .../tap/stream/ComplexSampleTapViewPanel.java | 150 -------- .../instrument/tap/stream/ComplexTap.java | 64 ---- .../tap/stream/ComplexTapViewPanel.java | 150 -------- .../instrument/tap/stream/DibitTap.java | 60 --- .../tap/stream/DibitTapViewPanel.java | 186 --------- .../instrument/tap/stream/EyeDiagramData.java | 46 --- .../tap/stream/EyeDiagramDataTap.java | 60 --- .../tap/stream/EyeDiagramDataTapPanel.java | 221 ----------- .../instrument/tap/stream/FloatBufferTap.java | 43 --- .../instrument/tap/stream/FloatTap.java | 58 --- .../tap/stream/FloatTapPhaseViewPanel.java | 129 ------- .../tap/stream/FloatTapViewPanel.java | 127 ------- .../instrument/tap/stream/GainFloatTap.java | 40 -- .../instrument/tap/stream/QPSKTap.java | 11 - .../instrument/tap/stream/QPSKTapPanel.java | 157 -------- .../instrument/tap/stream/StreamTap.java | 41 -- .../instrument/tap/stream/SymbolEventTap.java | 58 --- .../tap/stream/SymbolEventTapViewPanel.java | 187 --------- .../Fleetsync2DecoderInstrumented.java | 2 +- .../lj1200/LJ1200DecoderInstrumented.java | 2 +- .../mdc1200/MDCDecoderInstrumented.java | 2 +- .../mpt1327/MPT1327DecoderInstrumented.java | 2 +- .../decode/mpt1327/MPT1327DecoderLegacy.java | 253 ------------- .../module/decode/p25/C4FMSymbolFilter.java | 60 +-- .../tait/Tait1200DecoderInstrumented.java | 2 +- 76 files changed, 74 insertions(+), 4581 deletions(-) delete mode 100644 src/main/java/io/github/dsheirer/dsp/fsk/FSK2Decoder.java delete mode 100644 src/main/java/io/github/dsheirer/dsp/symbol/Slicer.java rename src/main/java/io/github/dsheirer/instrument/{gui/viewer => }/DemodulatorViewerFX.java (98%) delete mode 100644 src/main/java/io/github/dsheirer/instrument/Instrumentable.java delete mode 100644 src/main/java/io/github/dsheirer/instrument/InstrumentableProcessingChain.java rename src/main/java/io/github/dsheirer/instrument/{gui/viewer => }/PlaybackController.java (98%) rename src/main/java/io/github/dsheirer/instrument/{gui/viewer => }/RecentFilesMenu.java (86%) rename src/main/java/io/github/dsheirer/instrument/{gui/viewer => }/ViewerDesktop.java (92%) rename src/main/java/io/github/dsheirer/instrument/{gui/viewer => }/chart/AFSK1200SampleBufferChart.java (99%) rename src/main/java/io/github/dsheirer/instrument/{gui/viewer => }/chart/AFSK1200ZeroCrossingErrorDetectorChart.java (98%) rename src/main/java/io/github/dsheirer/instrument/{gui/viewer => }/chart/ComplexPhaseLineChart.java (98%) rename src/main/java/io/github/dsheirer/instrument/{gui/viewer => }/chart/ComplexSampleLineChart.java (98%) rename src/main/java/io/github/dsheirer/instrument/{gui/viewer => }/chart/DecodedSymbolChart.java (97%) rename src/main/java/io/github/dsheirer/instrument/{gui/viewer => }/chart/DoubleLineChart.java (97%) rename src/main/java/io/github/dsheirer/instrument/{gui/viewer => }/chart/EyeDiagramChart.java (98%) rename src/main/java/io/github/dsheirer/instrument/{gui/viewer => }/chart/FMDemodulatedLineChart.java (98%) rename src/main/java/io/github/dsheirer/instrument/{gui/viewer => }/chart/IInstrumentedAFSK1200Decoder.java (95%) rename src/main/java/io/github/dsheirer/instrument/{gui/viewer => }/chart/LTRNetSampleBufferChart.java (99%) rename src/main/java/io/github/dsheirer/instrument/{gui/viewer => }/chart/PhaseLineChart.java (97%) rename src/main/java/io/github/dsheirer/instrument/{gui/viewer => }/chart/RealSampleLineChart.java (98%) rename src/main/java/io/github/dsheirer/instrument/{gui/viewer => }/chart/SymbolChart.java (98%) rename src/main/java/io/github/dsheirer/instrument/{gui/viewer => }/chart/ZeroCrossingErrorDetectorChart.java (99%) rename src/main/java/io/github/dsheirer/instrument/{gui/viewer => }/decoder/AbstractAFSK1200Pane.java (91%) rename src/main/java/io/github/dsheirer/instrument/{gui/viewer => }/decoder/AbstractDecoderPane.java (95%) rename src/main/java/io/github/dsheirer/instrument/{gui/viewer => }/decoder/ComplexDecoderPane.java (97%) rename src/main/java/io/github/dsheirer/instrument/{gui/viewer => }/decoder/DecoderPaneFactory.java (98%) rename src/main/java/io/github/dsheirer/instrument/{gui/viewer => }/decoder/Fleetsync2Pane.java (96%) rename src/main/java/io/github/dsheirer/instrument/{gui/viewer => }/decoder/LJ1200Pane.java (97%) rename src/main/java/io/github/dsheirer/instrument/{gui/viewer => }/decoder/LTRNetPane.java (94%) rename src/main/java/io/github/dsheirer/instrument/{gui/viewer => }/decoder/MDC1200Pane.java (96%) rename src/main/java/io/github/dsheirer/instrument/{gui/viewer => }/decoder/MPT1327Pane.java (97%) rename src/main/java/io/github/dsheirer/instrument/{gui/viewer => }/decoder/P25Phase1C4FMPane.java (94%) rename src/main/java/io/github/dsheirer/instrument/{gui/viewer => }/decoder/P25Phase1LSMPane.java (94%) rename src/main/java/io/github/dsheirer/instrument/{gui/viewer => }/decoder/RealDecoderPane.java (97%) rename src/main/java/io/github/dsheirer/instrument/{gui/viewer => }/decoder/Tait1200Pane.java (97%) delete mode 100644 src/main/java/io/github/dsheirer/instrument/gui/ConstellationPanel.java delete mode 100644 src/main/java/io/github/dsheirer/instrument/gui/DecoderSelectionFrame.java delete mode 100644 src/main/java/io/github/dsheirer/instrument/gui/DecoderViewFrame.java delete mode 100644 src/main/java/io/github/dsheirer/instrument/gui/SampleModel.java delete mode 100644 src/main/java/io/github/dsheirer/instrument/gui/SourceControllerFrame.java delete mode 100644 src/main/java/io/github/dsheirer/instrument/gui/Viewer.java delete mode 100644 src/main/java/io/github/dsheirer/instrument/tap/Tap.java delete mode 100644 src/main/java/io/github/dsheirer/instrument/tap/TapGroup.java delete mode 100644 src/main/java/io/github/dsheirer/instrument/tap/TapListener.java delete mode 100644 src/main/java/io/github/dsheirer/instrument/tap/TapType.java delete mode 100644 src/main/java/io/github/dsheirer/instrument/tap/TapViewPanel.java delete mode 100644 src/main/java/io/github/dsheirer/instrument/tap/event/SyncDetectTap.java delete mode 100644 src/main/java/io/github/dsheirer/instrument/tap/stream/AdditiveFloatTap.java delete mode 100644 src/main/java/io/github/dsheirer/instrument/tap/stream/BinaryTap.java delete mode 100644 src/main/java/io/github/dsheirer/instrument/tap/stream/BinaryTapViewPanel.java delete mode 100644 src/main/java/io/github/dsheirer/instrument/tap/stream/ComplexSampleTap.java delete mode 100644 src/main/java/io/github/dsheirer/instrument/tap/stream/ComplexSampleTapViewPanel.java delete mode 100644 src/main/java/io/github/dsheirer/instrument/tap/stream/ComplexTap.java delete mode 100644 src/main/java/io/github/dsheirer/instrument/tap/stream/ComplexTapViewPanel.java delete mode 100644 src/main/java/io/github/dsheirer/instrument/tap/stream/DibitTap.java delete mode 100644 src/main/java/io/github/dsheirer/instrument/tap/stream/DibitTapViewPanel.java delete mode 100644 src/main/java/io/github/dsheirer/instrument/tap/stream/EyeDiagramData.java delete mode 100644 src/main/java/io/github/dsheirer/instrument/tap/stream/EyeDiagramDataTap.java delete mode 100644 src/main/java/io/github/dsheirer/instrument/tap/stream/EyeDiagramDataTapPanel.java delete mode 100644 src/main/java/io/github/dsheirer/instrument/tap/stream/FloatBufferTap.java delete mode 100644 src/main/java/io/github/dsheirer/instrument/tap/stream/FloatTap.java delete mode 100644 src/main/java/io/github/dsheirer/instrument/tap/stream/FloatTapPhaseViewPanel.java delete mode 100644 src/main/java/io/github/dsheirer/instrument/tap/stream/FloatTapViewPanel.java delete mode 100644 src/main/java/io/github/dsheirer/instrument/tap/stream/GainFloatTap.java delete mode 100644 src/main/java/io/github/dsheirer/instrument/tap/stream/QPSKTap.java delete mode 100644 src/main/java/io/github/dsheirer/instrument/tap/stream/QPSKTapPanel.java delete mode 100644 src/main/java/io/github/dsheirer/instrument/tap/stream/StreamTap.java delete mode 100644 src/main/java/io/github/dsheirer/instrument/tap/stream/SymbolEventTap.java delete mode 100644 src/main/java/io/github/dsheirer/instrument/tap/stream/SymbolEventTapViewPanel.java delete mode 100644 src/main/java/io/github/dsheirer/module/decode/mpt1327/MPT1327DecoderLegacy.java diff --git a/src/main/java/io/github/dsheirer/dsp/fsk/FSK2Decoder.java b/src/main/java/io/github/dsheirer/dsp/fsk/FSK2Decoder.java deleted file mode 100644 index 142201f5..00000000 --- a/src/main/java/io/github/dsheirer/dsp/fsk/FSK2Decoder.java +++ /dev/null @@ -1,302 +0,0 @@ -/******************************************************************************* - * SDR Trunk - * Copyright (C) 2014 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 io.github.dsheirer.dsp.fsk; - -import io.github.dsheirer.buffer.BooleanAveragingBuffer; -import io.github.dsheirer.dsp.symbol.SymbolEvent; -import io.github.dsheirer.dsp.symbol.SymbolEvent.Shift; -import io.github.dsheirer.instrument.Instrumentable; -import io.github.dsheirer.instrument.tap.Tap; -import io.github.dsheirer.instrument.tap.TapGroup; -import io.github.dsheirer.instrument.tap.stream.SymbolEventTap; -import io.github.dsheirer.sample.Listener; -import io.github.dsheirer.sample.real.RealBuffer; -import org.slf4j.Logger; -import org.slf4j.LoggerFactory; - -import java.util.ArrayList; -import java.util.BitSet; -import java.util.List; - -/** - * Binary Frequency Shift Keying (FSK) decoder. Implements a BFSK correlation - * decoder. Provides normal or inverted decoded output. Requires the symbol - * rate to be an integer multiple of the sample rate. - * - * Implements a correlation decoder that correlates a modulated FSK signal upon - * against a one-baud delayed version of itself, converting all float samples to - * binary (0 or 1), and uses XOR to produce the correlation value. Low pass - * filtering smoothes the correlated output. - * - * Automatically aligns to symbol timing during symbol transitions (0 to 1, or - * 1 to 0) by inspecting the samples at the symbol edges and advancing or - * retarding symbol window to maintain continuous symbol alignment. - * - * Use a DC-removal filter prior to this decoder to ensure samples don't have - * a DC component. - * - * Implements instrumentable interface, so that slice events can be received - * externally to analyze decoder performance. - */ -public class FSK2Decoder implements Instrumentable, Listener -{ - private final static Logger mLog = LoggerFactory.getLogger(FSK2Decoder.class); - - public enum Output - { - NORMAL, INVERTED - } - - /* Instrumentation taps */ - private static final String INSTRUMENT_DECISION = - "Tap Point: FSK2 Symbol Decision"; - private List mAvailableTaps; - private ArrayList mTaps = new ArrayList(); - - private Listener mListener; - private BooleanAveragingBuffer mDelayBuffer; - private BooleanAveragingBuffer mLowPassFilter; - private Slicer mSlicer; - private boolean mNormalOutput; - private int mSamplesPerSymbol; - private int mSymbolRate; - - public FSK2Decoder(int sampleRate, int symbolRate, Output output) - { - /* Ensure we're using an integral symbolRate of the sampleRate */ - assert (sampleRate % symbolRate == 0); - - mSamplesPerSymbol = (int) (sampleRate / symbolRate); - mNormalOutput = (output == Output.NORMAL); - mSymbolRate = symbolRate; - - mDelayBuffer = new BooleanAveragingBuffer(mSamplesPerSymbol); - mLowPassFilter = new BooleanAveragingBuffer(mSamplesPerSymbol); - mSlicer = new Slicer(mSamplesPerSymbol); - } - - /** - * Disposes of all references to prepare for garbage collection - */ - public void dispose() - { - mListener = null; - } - - /** - * Primary sample input - */ - @Override - public void receive(RealBuffer buffer) - { - for(float sample : buffer.getSamples()) - { - /* Square the sample. Greater than zero is a 1 (true) and less than - * zero is a 0 (false) */ - boolean bitSample = (sample >= 0.0f); - - /* Feed the delay buffer and fetch the one-baud delayed sample */ - boolean delayedBitSample = mDelayBuffer.get(bitSample); - - /* Correlation: xor current bit with delayed bit */ - boolean softBit = bitSample ^ delayedBitSample; - - /* Low pass filter to smooth the correlated values */ - boolean filteredSoftBit = mLowPassFilter.getAverage(softBit); - - /* Send the filtered correlated bit to the slicer */ - mSlicer.receive(filteredSoftBit); - } - } - - /** - * Registers a listener to receive the decoded FSK bits - */ - public void setListener(Listener listener) - { - mListener = listener; - } - - /** - * Removes the listener - */ - public void removeListener(Listener listener) - { - mListener = null; - } - - /** - * Symbol slicer with auto-aligning baud timing - */ - public class Slicer - { - private BitSet mBitSet = new BitSet(); - private int mSymbolLength; - private int mDecisionThreshold; - private int mSampleCounter; - - public Slicer(int samplesPerSymbol) - { - mSymbolLength = samplesPerSymbol; - - mDecisionThreshold = (int) (mSymbolLength / 2); - - /* Adjust for an odd number of samples per baud */ - if(mSymbolLength % 2 == 1) - { - mDecisionThreshold++; - } - } - - public void receive(boolean softBit) - { - if(mSampleCounter >= 0) - { - if(softBit) - { - mBitSet.set(mSampleCounter); - } - else - { - mBitSet.clear(mSampleCounter); - } - } - - mSampleCounter++; - - if(mSampleCounter >= mSymbolLength) - { - boolean decision = mBitSet.cardinality() >= mDecisionThreshold; - - send(decision); - - /* Shift timing left if the left bit in the bitset is opposite - * the decision and the right bit is the same */ - if((mBitSet.get(0) ^ decision) && - (!(mBitSet.get(mSymbolLength - 1) ^ decision))) - { - sendTapEvent(mBitSet, Shift.LEFT, decision); - - reset(); - - mSampleCounter--; - } - /* Shift timing right if the left bit is the same as the - * decision and the right bit is opposite */ - else if((!(mBitSet.get(0) ^ decision)) && - (mBitSet.get(mSymbolLength - 1) ^ decision)) - { - sendTapEvent(mBitSet, Shift.RIGHT, decision); - - /* Last bit from previous symbol to pre-fill next symbol */ - boolean previousSoftBit = mBitSet.get(mSymbolLength - 1); - - reset(); - - if(previousSoftBit) - { - mBitSet.set(0); - } - - mSampleCounter++; - } - /* No shift */ - else - { - sendTapEvent(mBitSet, Shift.NONE, decision); - - reset(); - } - } - } - - /** - * Sends the bit decision to the listener - */ - private void send(boolean decision) - { - if(mListener != null) - { - mListener.receive(mNormalOutput ? decision : !decision); - } - } - - private void reset() - { - mBitSet.clear(); - mSampleCounter = 0; - } - - /** - * Sends instrumentation tap event to all registered listeners - */ - private void sendTapEvent(BitSet bitset, Shift shift, boolean decision) - { - for(SymbolEventTap tap : mTaps) - { - SymbolEvent event = - new SymbolEvent(bitset.get(0, mSymbolLength), - mSymbolLength, - decision, - shift); - - tap.receive(event); - } - } - } - - /** - * Get instrumentation taps - */ - @Override - public List getTapGroups() - { - if(mAvailableTaps == null) - { - mAvailableTaps = new ArrayList(); - - TapGroup tapGroup = new TapGroup("FSK2 Decoder"); - tapGroup.add(new SymbolEventTap(INSTRUMENT_DECISION, 0, .025f)); - - mAvailableTaps.add(tapGroup); - } - - return mAvailableTaps; - } - - /** - * Add instrumentation tap - */ - @Override - public void registerTap(Tap tap) - { - if(tap instanceof SymbolEventTap && !mTaps.contains(tap)) - { - mTaps.add((SymbolEventTap) tap); - } - } - - /** - * Remove instrumentation tap - */ - @Override - public void unregisterTap(Tap tap) - { - mTaps.remove(tap); - } -} diff --git a/src/main/java/io/github/dsheirer/dsp/symbol/Slicer.java b/src/main/java/io/github/dsheirer/dsp/symbol/Slicer.java deleted file mode 100644 index a122ee42..00000000 --- a/src/main/java/io/github/dsheirer/dsp/symbol/Slicer.java +++ /dev/null @@ -1,149 +0,0 @@ -package io.github.dsheirer.dsp.symbol; - -import io.github.dsheirer.instrument.tap.stream.SymbolEventTap; -import io.github.dsheirer.sample.Listener; - -import java.util.BitSet; - -/** - * Symbol slicer with auto-aligning baud timing - */ -public class Slicer implements Listener -{ - public enum Output{ NORMAL, INVERTED }; - - private BitSet mBitSet = new BitSet(); - private int mSymbolLength; - private int mDecisionThreshold; - private int mSampleCounter; - private SymbolEventTap mSymbolEventTap; - - private boolean mNormalOutput = true; - private Listener mListener; - - public Slicer( Output output, int samplesPerSymbol ) - { - mNormalOutput = ( output == Output.NORMAL ); - - mSymbolLength = samplesPerSymbol; - - /* Round up */ - mDecisionThreshold = (int)( mSymbolLength / 2 ) + ( mSymbolLength % 2 ); - } - - public void receive( Boolean sample ) - { - if( mSampleCounter >= 0 ) - { - if( sample ) - { - mBitSet.set( mSampleCounter ); - } - else - { - mBitSet.clear( mSampleCounter ); - } - } - - mSampleCounter++; - - if( mSampleCounter >= mSymbolLength ) - { - boolean decision = mBitSet.cardinality() >= mDecisionThreshold; - - send( decision ); - - /* Shift timing left if the left bit in the bitset is opposite - * the decision and the right bit is the same */ - if( ( mBitSet.get( 0 ) ^ decision ) && - ( !( mBitSet.get( mSymbolLength - 1 ) ^ decision ) ) ) - { - sendTapEvent( mBitSet, SymbolEvent.Shift.LEFT, decision ); - - reset(); - - mSampleCounter--; - } - /* Shift timing right if the left bit is the same as the - * decision and the right bit is opposite */ - else if( ( !( mBitSet.get( 0 ) ^ decision ) ) && - ( mBitSet.get( mSymbolLength - 1 ) ^ decision ) ) - { - sendTapEvent( mBitSet, SymbolEvent.Shift.RIGHT, decision ); - - /* Last bit from previous symbol to pre-fill next symbol */ - boolean previousSoftBit = mBitSet.get( mSymbolLength - 1 ); - - reset(); - - if( previousSoftBit ) - { - mBitSet.set( 0 ); - } - - mSampleCounter++; - } - /* No shift */ - else - { - sendTapEvent( mBitSet, SymbolEvent.Shift.NONE, decision ); - - reset(); - } - } - } - - /** - * Sends the bit decision to the listener - */ - private void send( boolean decision ) - { - if( mListener != null ) - { - mListener.receive( mNormalOutput ? decision : !decision ); - } - } - - private void reset() - { - mBitSet.clear(); - mSampleCounter = 0; - } - - public void setListener( Listener listener) - { - mListener = listener; - } - - public void removeListener( Listener listener ) - { - mListener = null; - } - - /** - * Sends instrumentation tap event to all registered listeners - */ - private void sendTapEvent(BitSet bitset, SymbolEvent.Shift shift, boolean decision ) - { - if( mSymbolEventTap != null ) - { - SymbolEvent event = - new SymbolEvent( bitset.get( 0, mSymbolLength ), - mSymbolLength, - decision, - shift ); - - mSymbolEventTap.receive( event ); - } - } - - public void addTap( SymbolEventTap tap ) - { - mSymbolEventTap = tap; - } - - public void removeTap() - { - mSymbolEventTap = null; - } -} diff --git a/src/main/java/io/github/dsheirer/instrument/gui/viewer/DemodulatorViewerFX.java b/src/main/java/io/github/dsheirer/instrument/DemodulatorViewerFX.java similarity index 98% rename from src/main/java/io/github/dsheirer/instrument/gui/viewer/DemodulatorViewerFX.java rename to src/main/java/io/github/dsheirer/instrument/DemodulatorViewerFX.java index 5af21746..5c21f06a 100644 --- a/src/main/java/io/github/dsheirer/instrument/gui/viewer/DemodulatorViewerFX.java +++ b/src/main/java/io/github/dsheirer/instrument/DemodulatorViewerFX.java @@ -13,9 +13,9 @@ * If not, see * ******************************************************************************/ -package io.github.dsheirer.instrument.gui.viewer; +package io.github.dsheirer.instrument; -import io.github.dsheirer.instrument.gui.viewer.decoder.DecoderPaneFactory; +import io.github.dsheirer.instrument.decoder.DecoderPaneFactory; import io.github.dsheirer.module.decode.DecoderType; import io.github.dsheirer.module.decode.p25.P25Decoder; import javafx.application.Application; diff --git a/src/main/java/io/github/dsheirer/instrument/Instrumentable.java b/src/main/java/io/github/dsheirer/instrument/Instrumentable.java deleted file mode 100644 index c06db9ab..00000000 --- a/src/main/java/io/github/dsheirer/instrument/Instrumentable.java +++ /dev/null @@ -1,54 +0,0 @@ -/******************************************************************************* - * SDR Trunk - * Copyright (C) 2014 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 io.github.dsheirer.instrument; - -import io.github.dsheirer.instrument.tap.Tap; -import io.github.dsheirer.instrument.tap.TapGroup; - -import java.util.List; - -/** - * Interface for classes that want to expose internal data, streams or events - * to external listeners. - */ -public interface Instrumentable -{ - /** - * Returns the list of available tap point groups. Use the addTap() and - * removeTap() to register a tap on the instrumentable class and then add - * your TapListener(s) to the registered tap. - */ - public List getTapGroups(); - - /** - * Registers a tap on the instrumentable object. - * - * @param tap - one of the tap(s) obtained from getTaps() from the - * instrumentable source - */ - public void registerTap( Tap tap ); - - /** - * Unregisters a tap on the instrumentable object, if the tap is currently - * registered. - * - * @param tap - one of the tap(s) obtained from getTaps() from the - * instrumentable source - */ - public void unregisterTap( Tap tap ); -} diff --git a/src/main/java/io/github/dsheirer/instrument/InstrumentableProcessingChain.java b/src/main/java/io/github/dsheirer/instrument/InstrumentableProcessingChain.java deleted file mode 100644 index d2a99b2f..00000000 --- a/src/main/java/io/github/dsheirer/instrument/InstrumentableProcessingChain.java +++ /dev/null @@ -1,82 +0,0 @@ -/******************************************************************************* - * 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 io.github.dsheirer.instrument; - -import io.github.dsheirer.controller.channel.Channel.ChannelType; -import io.github.dsheirer.instrument.tap.Tap; -import io.github.dsheirer.instrument.tap.TapGroup; -import io.github.dsheirer.module.Module; -import io.github.dsheirer.module.ProcessingChain; -import io.github.dsheirer.source.Source; - -import java.util.ArrayList; -import java.util.List; - -public class InstrumentableProcessingChain extends ProcessingChain implements Instrumentable -{ - public InstrumentableProcessingChain() - { - super(ChannelType.STANDARD); - } - - public void setSource(Source source) throws IllegalStateException - { - mSource = source; - } - - @Override - public List getTapGroups() - { - List groups = new ArrayList<>(); - - for(Module module : getModules()) - { - if(module instanceof Instrumentable) - { - groups.addAll(((Instrumentable)module).getTapGroups()); - } - } - - return groups; - } - - @Override - public void registerTap(Tap tap) - { - for(Module module : getModules()) - { - if(module instanceof Instrumentable) - { - ((Instrumentable)module).registerTap(tap); - } - } - } - - @Override - public void unregisterTap(Tap tap) - { - for(Module module : getModules()) - { - if(module instanceof Instrumentable) - { - ((Instrumentable)module).unregisterTap(tap); - } - } - } -} diff --git a/src/main/java/io/github/dsheirer/instrument/gui/viewer/PlaybackController.java b/src/main/java/io/github/dsheirer/instrument/PlaybackController.java similarity index 98% rename from src/main/java/io/github/dsheirer/instrument/gui/viewer/PlaybackController.java rename to src/main/java/io/github/dsheirer/instrument/PlaybackController.java index 25bee580..68029c1f 100644 --- a/src/main/java/io/github/dsheirer/instrument/gui/viewer/PlaybackController.java +++ b/src/main/java/io/github/dsheirer/instrument/PlaybackController.java @@ -13,9 +13,9 @@ * If not, see * ******************************************************************************/ -package io.github.dsheirer.instrument.gui.viewer; +package io.github.dsheirer.instrument; -import io.github.dsheirer.instrument.gui.viewer.decoder.AbstractDecoderPane; +import io.github.dsheirer.instrument.decoder.AbstractDecoderPane; import io.github.dsheirer.sample.Listener; import io.github.dsheirer.sample.buffer.ReusableBuffer; import io.github.dsheirer.sample.buffer.ReusableBufferBroadcaster; diff --git a/src/main/java/io/github/dsheirer/instrument/gui/viewer/RecentFilesMenu.java b/src/main/java/io/github/dsheirer/instrument/RecentFilesMenu.java similarity index 86% rename from src/main/java/io/github/dsheirer/instrument/gui/viewer/RecentFilesMenu.java rename to src/main/java/io/github/dsheirer/instrument/RecentFilesMenu.java index 873babf7..ea8948d3 100644 --- a/src/main/java/io/github/dsheirer/instrument/gui/viewer/RecentFilesMenu.java +++ b/src/main/java/io/github/dsheirer/instrument/RecentFilesMenu.java @@ -1,4 +1,19 @@ -package io.github.dsheirer.instrument.gui.viewer; +/******************************************************************************* + * 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 + * + ******************************************************************************/ +package io.github.dsheirer.instrument; import javafx.event.ActionEvent; import javafx.event.EventHandler; diff --git a/src/main/java/io/github/dsheirer/instrument/gui/viewer/ViewerDesktop.java b/src/main/java/io/github/dsheirer/instrument/ViewerDesktop.java similarity index 92% rename from src/main/java/io/github/dsheirer/instrument/gui/viewer/ViewerDesktop.java rename to src/main/java/io/github/dsheirer/instrument/ViewerDesktop.java index ece3faa5..ed372f6f 100644 --- a/src/main/java/io/github/dsheirer/instrument/gui/viewer/ViewerDesktop.java +++ b/src/main/java/io/github/dsheirer/instrument/ViewerDesktop.java @@ -13,10 +13,10 @@ * If not, see * ******************************************************************************/ -package io.github.dsheirer.instrument.gui.viewer; +package io.github.dsheirer.instrument; -import io.github.dsheirer.instrument.gui.viewer.decoder.AbstractDecoderPane; -import io.github.dsheirer.instrument.gui.viewer.decoder.DecoderPaneFactory; +import io.github.dsheirer.instrument.decoder.AbstractDecoderPane; +import io.github.dsheirer.instrument.decoder.DecoderPaneFactory; import io.github.dsheirer.module.decode.DecoderType; import io.github.dsheirer.module.decode.p25.P25Decoder; import javafx.scene.layout.BorderPane; diff --git a/src/main/java/io/github/dsheirer/instrument/gui/viewer/chart/AFSK1200SampleBufferChart.java b/src/main/java/io/github/dsheirer/instrument/chart/AFSK1200SampleBufferChart.java similarity index 99% rename from src/main/java/io/github/dsheirer/instrument/gui/viewer/chart/AFSK1200SampleBufferChart.java rename to src/main/java/io/github/dsheirer/instrument/chart/AFSK1200SampleBufferChart.java index 9014ac5e..49e93240 100644 --- a/src/main/java/io/github/dsheirer/instrument/gui/viewer/chart/AFSK1200SampleBufferChart.java +++ b/src/main/java/io/github/dsheirer/instrument/chart/AFSK1200SampleBufferChart.java @@ -13,7 +13,7 @@ * If not, see * ******************************************************************************/ -package io.github.dsheirer.instrument.gui.viewer.chart; +package io.github.dsheirer.instrument.chart; import javafx.beans.value.ChangeListener; import javafx.beans.value.ObservableValue; diff --git a/src/main/java/io/github/dsheirer/instrument/gui/viewer/chart/AFSK1200ZeroCrossingErrorDetectorChart.java b/src/main/java/io/github/dsheirer/instrument/chart/AFSK1200ZeroCrossingErrorDetectorChart.java similarity index 98% rename from src/main/java/io/github/dsheirer/instrument/gui/viewer/chart/AFSK1200ZeroCrossingErrorDetectorChart.java rename to src/main/java/io/github/dsheirer/instrument/chart/AFSK1200ZeroCrossingErrorDetectorChart.java index 202cb607..e90b00d9 100644 --- a/src/main/java/io/github/dsheirer/instrument/gui/viewer/chart/AFSK1200ZeroCrossingErrorDetectorChart.java +++ b/src/main/java/io/github/dsheirer/instrument/chart/AFSK1200ZeroCrossingErrorDetectorChart.java @@ -13,7 +13,7 @@ * If not, see * ******************************************************************************/ -package io.github.dsheirer.instrument.gui.viewer.chart; +package io.github.dsheirer.instrument.chart; import javafx.beans.value.ChangeListener; import javafx.beans.value.ObservableValue; diff --git a/src/main/java/io/github/dsheirer/instrument/gui/viewer/chart/ComplexPhaseLineChart.java b/src/main/java/io/github/dsheirer/instrument/chart/ComplexPhaseLineChart.java similarity index 98% rename from src/main/java/io/github/dsheirer/instrument/gui/viewer/chart/ComplexPhaseLineChart.java rename to src/main/java/io/github/dsheirer/instrument/chart/ComplexPhaseLineChart.java index 000f3a50..97a238cc 100644 --- a/src/main/java/io/github/dsheirer/instrument/gui/viewer/chart/ComplexPhaseLineChart.java +++ b/src/main/java/io/github/dsheirer/instrument/chart/ComplexPhaseLineChart.java @@ -13,7 +13,7 @@ * If not, see * ******************************************************************************/ -package io.github.dsheirer.instrument.gui.viewer.chart; +package io.github.dsheirer.instrument.chart; import io.github.dsheirer.buffer.ComplexCircularBuffer; import io.github.dsheirer.dsp.gain.ComplexGain; diff --git a/src/main/java/io/github/dsheirer/instrument/gui/viewer/chart/ComplexSampleLineChart.java b/src/main/java/io/github/dsheirer/instrument/chart/ComplexSampleLineChart.java similarity index 98% rename from src/main/java/io/github/dsheirer/instrument/gui/viewer/chart/ComplexSampleLineChart.java rename to src/main/java/io/github/dsheirer/instrument/chart/ComplexSampleLineChart.java index d1b4b00c..e36214a0 100644 --- a/src/main/java/io/github/dsheirer/instrument/gui/viewer/chart/ComplexSampleLineChart.java +++ b/src/main/java/io/github/dsheirer/instrument/chart/ComplexSampleLineChart.java @@ -13,7 +13,7 @@ * If not, see * ******************************************************************************/ -package io.github.dsheirer.instrument.gui.viewer.chart; +package io.github.dsheirer.instrument.chart; import io.github.dsheirer.buffer.ComplexCircularBuffer; import io.github.dsheirer.dsp.gain.ComplexGain; diff --git a/src/main/java/io/github/dsheirer/instrument/gui/viewer/chart/DecodedSymbolChart.java b/src/main/java/io/github/dsheirer/instrument/chart/DecodedSymbolChart.java similarity index 97% rename from src/main/java/io/github/dsheirer/instrument/gui/viewer/chart/DecodedSymbolChart.java rename to src/main/java/io/github/dsheirer/instrument/chart/DecodedSymbolChart.java index f2b20124..073169fa 100644 --- a/src/main/java/io/github/dsheirer/instrument/gui/viewer/chart/DecodedSymbolChart.java +++ b/src/main/java/io/github/dsheirer/instrument/chart/DecodedSymbolChart.java @@ -13,7 +13,7 @@ * If not, see * ******************************************************************************/ -package io.github.dsheirer.instrument.gui.viewer.chart; +package io.github.dsheirer.instrument.chart; import io.github.dsheirer.sample.Listener; import javafx.collections.FXCollections; diff --git a/src/main/java/io/github/dsheirer/instrument/gui/viewer/chart/DoubleLineChart.java b/src/main/java/io/github/dsheirer/instrument/chart/DoubleLineChart.java similarity index 97% rename from src/main/java/io/github/dsheirer/instrument/gui/viewer/chart/DoubleLineChart.java rename to src/main/java/io/github/dsheirer/instrument/chart/DoubleLineChart.java index 54811999..12e0c898 100644 --- a/src/main/java/io/github/dsheirer/instrument/gui/viewer/chart/DoubleLineChart.java +++ b/src/main/java/io/github/dsheirer/instrument/chart/DoubleLineChart.java @@ -13,7 +13,7 @@ * If not, see * ******************************************************************************/ -package io.github.dsheirer.instrument.gui.viewer.chart; +package io.github.dsheirer.instrument.chart; import io.github.dsheirer.buffer.DoubleCircularBuffer; import io.github.dsheirer.sample.Listener; diff --git a/src/main/java/io/github/dsheirer/instrument/gui/viewer/chart/EyeDiagramChart.java b/src/main/java/io/github/dsheirer/instrument/chart/EyeDiagramChart.java similarity index 98% rename from src/main/java/io/github/dsheirer/instrument/gui/viewer/chart/EyeDiagramChart.java rename to src/main/java/io/github/dsheirer/instrument/chart/EyeDiagramChart.java index b266e99a..5cf731a6 100644 --- a/src/main/java/io/github/dsheirer/instrument/gui/viewer/chart/EyeDiagramChart.java +++ b/src/main/java/io/github/dsheirer/instrument/chart/EyeDiagramChart.java @@ -13,7 +13,7 @@ * If not, see * ******************************************************************************/ -package io.github.dsheirer.instrument.gui.viewer.chart; +package io.github.dsheirer.instrument.chart; import io.github.dsheirer.dsp.psk.SymbolDecisionData; import io.github.dsheirer.sample.Listener; diff --git a/src/main/java/io/github/dsheirer/instrument/gui/viewer/chart/FMDemodulatedLineChart.java b/src/main/java/io/github/dsheirer/instrument/chart/FMDemodulatedLineChart.java similarity index 98% rename from src/main/java/io/github/dsheirer/instrument/gui/viewer/chart/FMDemodulatedLineChart.java rename to src/main/java/io/github/dsheirer/instrument/chart/FMDemodulatedLineChart.java index 334db601..7f44efb8 100644 --- a/src/main/java/io/github/dsheirer/instrument/gui/viewer/chart/FMDemodulatedLineChart.java +++ b/src/main/java/io/github/dsheirer/instrument/chart/FMDemodulatedLineChart.java @@ -13,7 +13,7 @@ * If not, see * ******************************************************************************/ -package io.github.dsheirer.instrument.gui.viewer.chart; +package io.github.dsheirer.instrument.chart; import io.github.dsheirer.buffer.DoubleCircularBuffer; import io.github.dsheirer.dsp.fm.FMDemodulator; diff --git a/src/main/java/io/github/dsheirer/instrument/gui/viewer/chart/IInstrumentedAFSK1200Decoder.java b/src/main/java/io/github/dsheirer/instrument/chart/IInstrumentedAFSK1200Decoder.java similarity index 95% rename from src/main/java/io/github/dsheirer/instrument/gui/viewer/chart/IInstrumentedAFSK1200Decoder.java rename to src/main/java/io/github/dsheirer/instrument/chart/IInstrumentedAFSK1200Decoder.java index c8236d15..c0e10cdb 100644 --- a/src/main/java/io/github/dsheirer/instrument/gui/viewer/chart/IInstrumentedAFSK1200Decoder.java +++ b/src/main/java/io/github/dsheirer/instrument/chart/IInstrumentedAFSK1200Decoder.java @@ -13,7 +13,7 @@ * If not, see * ******************************************************************************/ -package io.github.dsheirer.instrument.gui.viewer.chart; +package io.github.dsheirer.instrument.chart; import io.github.dsheirer.dsp.afsk.AFSK1200DecoderInstrumented; import io.github.dsheirer.message.Message; diff --git a/src/main/java/io/github/dsheirer/instrument/gui/viewer/chart/LTRNetSampleBufferChart.java b/src/main/java/io/github/dsheirer/instrument/chart/LTRNetSampleBufferChart.java similarity index 99% rename from src/main/java/io/github/dsheirer/instrument/gui/viewer/chart/LTRNetSampleBufferChart.java rename to src/main/java/io/github/dsheirer/instrument/chart/LTRNetSampleBufferChart.java index ade62f19..7237649c 100644 --- a/src/main/java/io/github/dsheirer/instrument/gui/viewer/chart/LTRNetSampleBufferChart.java +++ b/src/main/java/io/github/dsheirer/instrument/chart/LTRNetSampleBufferChart.java @@ -13,7 +13,7 @@ * If not, see * ******************************************************************************/ -package io.github.dsheirer.instrument.gui.viewer.chart; +package io.github.dsheirer.instrument.chart; import io.github.dsheirer.module.decode.ltrnet.LTRNetDecoderInstrumented; import javafx.beans.value.ChangeListener; diff --git a/src/main/java/io/github/dsheirer/instrument/gui/viewer/chart/PhaseLineChart.java b/src/main/java/io/github/dsheirer/instrument/chart/PhaseLineChart.java similarity index 97% rename from src/main/java/io/github/dsheirer/instrument/gui/viewer/chart/PhaseLineChart.java rename to src/main/java/io/github/dsheirer/instrument/chart/PhaseLineChart.java index df7ff76a..1b505f89 100644 --- a/src/main/java/io/github/dsheirer/instrument/gui/viewer/chart/PhaseLineChart.java +++ b/src/main/java/io/github/dsheirer/instrument/chart/PhaseLineChart.java @@ -13,7 +13,7 @@ * If not, see * ******************************************************************************/ -package io.github.dsheirer.instrument.gui.viewer.chart; +package io.github.dsheirer.instrument.chart; import io.github.dsheirer.buffer.DoubleCircularBuffer; import io.github.dsheirer.sample.Listener; diff --git a/src/main/java/io/github/dsheirer/instrument/gui/viewer/chart/RealSampleLineChart.java b/src/main/java/io/github/dsheirer/instrument/chart/RealSampleLineChart.java similarity index 98% rename from src/main/java/io/github/dsheirer/instrument/gui/viewer/chart/RealSampleLineChart.java rename to src/main/java/io/github/dsheirer/instrument/chart/RealSampleLineChart.java index 94f02060..9ec5210b 100644 --- a/src/main/java/io/github/dsheirer/instrument/gui/viewer/chart/RealSampleLineChart.java +++ b/src/main/java/io/github/dsheirer/instrument/chart/RealSampleLineChart.java @@ -13,7 +13,7 @@ * If not, see * ******************************************************************************/ -package io.github.dsheirer.instrument.gui.viewer.chart; +package io.github.dsheirer.instrument.chart; import io.github.dsheirer.buffer.RealCircularBuffer; import io.github.dsheirer.sample.Listener; diff --git a/src/main/java/io/github/dsheirer/instrument/gui/viewer/chart/SymbolChart.java b/src/main/java/io/github/dsheirer/instrument/chart/SymbolChart.java similarity index 98% rename from src/main/java/io/github/dsheirer/instrument/gui/viewer/chart/SymbolChart.java rename to src/main/java/io/github/dsheirer/instrument/chart/SymbolChart.java index 6f99f042..070ca5a9 100644 --- a/src/main/java/io/github/dsheirer/instrument/gui/viewer/chart/SymbolChart.java +++ b/src/main/java/io/github/dsheirer/instrument/chart/SymbolChart.java @@ -13,7 +13,7 @@ * If not, see * ******************************************************************************/ -package io.github.dsheirer.instrument.gui.viewer.chart; +package io.github.dsheirer.instrument.chart; import io.github.dsheirer.sample.Listener; import io.github.dsheirer.sample.complex.Complex; diff --git a/src/main/java/io/github/dsheirer/instrument/gui/viewer/chart/ZeroCrossingErrorDetectorChart.java b/src/main/java/io/github/dsheirer/instrument/chart/ZeroCrossingErrorDetectorChart.java similarity index 99% rename from src/main/java/io/github/dsheirer/instrument/gui/viewer/chart/ZeroCrossingErrorDetectorChart.java rename to src/main/java/io/github/dsheirer/instrument/chart/ZeroCrossingErrorDetectorChart.java index 2d9b055b..4b0ebcb6 100644 --- a/src/main/java/io/github/dsheirer/instrument/gui/viewer/chart/ZeroCrossingErrorDetectorChart.java +++ b/src/main/java/io/github/dsheirer/instrument/chart/ZeroCrossingErrorDetectorChart.java @@ -13,7 +13,7 @@ * If not, see * ******************************************************************************/ -package io.github.dsheirer.instrument.gui.viewer.chart; +package io.github.dsheirer.instrument.chart; import io.github.dsheirer.module.decode.ltrnet.LTRNetDecoderInstrumented; import javafx.beans.value.ChangeListener; diff --git a/src/main/java/io/github/dsheirer/instrument/gui/viewer/decoder/AbstractAFSK1200Pane.java b/src/main/java/io/github/dsheirer/instrument/decoder/AbstractAFSK1200Pane.java similarity index 91% rename from src/main/java/io/github/dsheirer/instrument/gui/viewer/decoder/AbstractAFSK1200Pane.java rename to src/main/java/io/github/dsheirer/instrument/decoder/AbstractAFSK1200Pane.java index 1ad275b8..def2f899 100644 --- a/src/main/java/io/github/dsheirer/instrument/gui/viewer/decoder/AbstractAFSK1200Pane.java +++ b/src/main/java/io/github/dsheirer/instrument/decoder/AbstractAFSK1200Pane.java @@ -13,13 +13,13 @@ * If not, see * ******************************************************************************/ -package io.github.dsheirer.instrument.gui.viewer.decoder; +package io.github.dsheirer.instrument.decoder; -import io.github.dsheirer.instrument.gui.viewer.chart.AFSK1200SampleBufferChart; -import io.github.dsheirer.instrument.gui.viewer.chart.AFSK1200ZeroCrossingErrorDetectorChart; -import io.github.dsheirer.instrument.gui.viewer.chart.DecodedSymbolChart; -import io.github.dsheirer.instrument.gui.viewer.chart.IInstrumentedAFSK1200Decoder; -import io.github.dsheirer.instrument.gui.viewer.chart.RealSampleLineChart; +import io.github.dsheirer.instrument.chart.AFSK1200SampleBufferChart; +import io.github.dsheirer.instrument.chart.AFSK1200ZeroCrossingErrorDetectorChart; +import io.github.dsheirer.instrument.chart.DecodedSymbolChart; +import io.github.dsheirer.instrument.chart.IInstrumentedAFSK1200Decoder; +import io.github.dsheirer.instrument.chart.RealSampleLineChart; import io.github.dsheirer.message.Message; import io.github.dsheirer.module.decode.DecoderType; import io.github.dsheirer.sample.Listener; diff --git a/src/main/java/io/github/dsheirer/instrument/gui/viewer/decoder/AbstractDecoderPane.java b/src/main/java/io/github/dsheirer/instrument/decoder/AbstractDecoderPane.java similarity index 95% rename from src/main/java/io/github/dsheirer/instrument/gui/viewer/decoder/AbstractDecoderPane.java rename to src/main/java/io/github/dsheirer/instrument/decoder/AbstractDecoderPane.java index 1535ec43..3770c136 100644 --- a/src/main/java/io/github/dsheirer/instrument/gui/viewer/decoder/AbstractDecoderPane.java +++ b/src/main/java/io/github/dsheirer/instrument/decoder/AbstractDecoderPane.java @@ -13,7 +13,7 @@ * If not, see * ******************************************************************************/ -package io.github.dsheirer.instrument.gui.viewer.decoder; +package io.github.dsheirer.instrument.decoder; import io.github.dsheirer.sample.Listener; import io.github.dsheirer.sample.SampleType; diff --git a/src/main/java/io/github/dsheirer/instrument/gui/viewer/decoder/ComplexDecoderPane.java b/src/main/java/io/github/dsheirer/instrument/decoder/ComplexDecoderPane.java similarity index 97% rename from src/main/java/io/github/dsheirer/instrument/gui/viewer/decoder/ComplexDecoderPane.java rename to src/main/java/io/github/dsheirer/instrument/decoder/ComplexDecoderPane.java index dc67f4e3..edbdece8 100644 --- a/src/main/java/io/github/dsheirer/instrument/gui/viewer/decoder/ComplexDecoderPane.java +++ b/src/main/java/io/github/dsheirer/instrument/decoder/ComplexDecoderPane.java @@ -13,7 +13,7 @@ * If not, see * ******************************************************************************/ -package io.github.dsheirer.instrument.gui.viewer.decoder; +package io.github.dsheirer.instrument.decoder; import io.github.dsheirer.module.decode.DecoderType; import io.github.dsheirer.sample.Listener; diff --git a/src/main/java/io/github/dsheirer/instrument/gui/viewer/decoder/DecoderPaneFactory.java b/src/main/java/io/github/dsheirer/instrument/decoder/DecoderPaneFactory.java similarity index 98% rename from src/main/java/io/github/dsheirer/instrument/gui/viewer/decoder/DecoderPaneFactory.java rename to src/main/java/io/github/dsheirer/instrument/decoder/DecoderPaneFactory.java index a78202c8..25812155 100644 --- a/src/main/java/io/github/dsheirer/instrument/gui/viewer/decoder/DecoderPaneFactory.java +++ b/src/main/java/io/github/dsheirer/instrument/decoder/DecoderPaneFactory.java @@ -13,7 +13,7 @@ * If not, see * ******************************************************************************/ -package io.github.dsheirer.instrument.gui.viewer.decoder; +package io.github.dsheirer.instrument.decoder; import io.github.dsheirer.module.decode.DecoderType; import io.github.dsheirer.module.decode.p25.P25Decoder; diff --git a/src/main/java/io/github/dsheirer/instrument/gui/viewer/decoder/Fleetsync2Pane.java b/src/main/java/io/github/dsheirer/instrument/decoder/Fleetsync2Pane.java similarity index 96% rename from src/main/java/io/github/dsheirer/instrument/gui/viewer/decoder/Fleetsync2Pane.java rename to src/main/java/io/github/dsheirer/instrument/decoder/Fleetsync2Pane.java index a2f45b38..f7a8f5e6 100644 --- a/src/main/java/io/github/dsheirer/instrument/gui/viewer/decoder/Fleetsync2Pane.java +++ b/src/main/java/io/github/dsheirer/instrument/decoder/Fleetsync2Pane.java @@ -13,7 +13,7 @@ * If not, see * ******************************************************************************/ -package io.github.dsheirer.instrument.gui.viewer.decoder; +package io.github.dsheirer.instrument.decoder; import io.github.dsheirer.module.decode.DecoderType; import io.github.dsheirer.module.decode.fleetsync2.Fleetsync2DecoderInstrumented; diff --git a/src/main/java/io/github/dsheirer/instrument/gui/viewer/decoder/LJ1200Pane.java b/src/main/java/io/github/dsheirer/instrument/decoder/LJ1200Pane.java similarity index 97% rename from src/main/java/io/github/dsheirer/instrument/gui/viewer/decoder/LJ1200Pane.java rename to src/main/java/io/github/dsheirer/instrument/decoder/LJ1200Pane.java index 2b5fa55b..801331ba 100644 --- a/src/main/java/io/github/dsheirer/instrument/gui/viewer/decoder/LJ1200Pane.java +++ b/src/main/java/io/github/dsheirer/instrument/decoder/LJ1200Pane.java @@ -13,7 +13,7 @@ * If not, see * ******************************************************************************/ -package io.github.dsheirer.instrument.gui.viewer.decoder; +package io.github.dsheirer.instrument.decoder; import io.github.dsheirer.dsp.symbol.ISyncDetectListener; import io.github.dsheirer.module.decode.DecoderType; diff --git a/src/main/java/io/github/dsheirer/instrument/gui/viewer/decoder/LTRNetPane.java b/src/main/java/io/github/dsheirer/instrument/decoder/LTRNetPane.java similarity index 94% rename from src/main/java/io/github/dsheirer/instrument/gui/viewer/decoder/LTRNetPane.java rename to src/main/java/io/github/dsheirer/instrument/decoder/LTRNetPane.java index ee8162f2..e0a5f15a 100644 --- a/src/main/java/io/github/dsheirer/instrument/gui/viewer/decoder/LTRNetPane.java +++ b/src/main/java/io/github/dsheirer/instrument/decoder/LTRNetPane.java @@ -13,11 +13,11 @@ * If not, see * ******************************************************************************/ -package io.github.dsheirer.instrument.gui.viewer.decoder; +package io.github.dsheirer.instrument.decoder; -import io.github.dsheirer.instrument.gui.viewer.chart.LTRNetSampleBufferChart; -import io.github.dsheirer.instrument.gui.viewer.chart.RealSampleLineChart; -import io.github.dsheirer.instrument.gui.viewer.chart.ZeroCrossingErrorDetectorChart; +import io.github.dsheirer.instrument.chart.LTRNetSampleBufferChart; +import io.github.dsheirer.instrument.chart.RealSampleLineChart; +import io.github.dsheirer.instrument.chart.ZeroCrossingErrorDetectorChart; import io.github.dsheirer.message.Message; import io.github.dsheirer.message.MessageDirection; import io.github.dsheirer.module.decode.DecoderType; diff --git a/src/main/java/io/github/dsheirer/instrument/gui/viewer/decoder/MDC1200Pane.java b/src/main/java/io/github/dsheirer/instrument/decoder/MDC1200Pane.java similarity index 96% rename from src/main/java/io/github/dsheirer/instrument/gui/viewer/decoder/MDC1200Pane.java rename to src/main/java/io/github/dsheirer/instrument/decoder/MDC1200Pane.java index 80953de9..6027eb3e 100644 --- a/src/main/java/io/github/dsheirer/instrument/gui/viewer/decoder/MDC1200Pane.java +++ b/src/main/java/io/github/dsheirer/instrument/decoder/MDC1200Pane.java @@ -13,7 +13,7 @@ * If not, see * ******************************************************************************/ -package io.github.dsheirer.instrument.gui.viewer.decoder; +package io.github.dsheirer.instrument.decoder; import io.github.dsheirer.dsp.symbol.ISyncDetectListener; import io.github.dsheirer.module.decode.DecoderType; diff --git a/src/main/java/io/github/dsheirer/instrument/gui/viewer/decoder/MPT1327Pane.java b/src/main/java/io/github/dsheirer/instrument/decoder/MPT1327Pane.java similarity index 97% rename from src/main/java/io/github/dsheirer/instrument/gui/viewer/decoder/MPT1327Pane.java rename to src/main/java/io/github/dsheirer/instrument/decoder/MPT1327Pane.java index dca8662b..696f101b 100644 --- a/src/main/java/io/github/dsheirer/instrument/gui/viewer/decoder/MPT1327Pane.java +++ b/src/main/java/io/github/dsheirer/instrument/decoder/MPT1327Pane.java @@ -13,7 +13,7 @@ * If not, see * ******************************************************************************/ -package io.github.dsheirer.instrument.gui.viewer.decoder; +package io.github.dsheirer.instrument.decoder; import io.github.dsheirer.dsp.symbol.ISyncDetectListener; import io.github.dsheirer.module.decode.DecoderType; diff --git a/src/main/java/io/github/dsheirer/instrument/gui/viewer/decoder/P25Phase1C4FMPane.java b/src/main/java/io/github/dsheirer/instrument/decoder/P25Phase1C4FMPane.java similarity index 94% rename from src/main/java/io/github/dsheirer/instrument/gui/viewer/decoder/P25Phase1C4FMPane.java rename to src/main/java/io/github/dsheirer/instrument/decoder/P25Phase1C4FMPane.java index ec35b0de..223960cb 100644 --- a/src/main/java/io/github/dsheirer/instrument/gui/viewer/decoder/P25Phase1C4FMPane.java +++ b/src/main/java/io/github/dsheirer/instrument/decoder/P25Phase1C4FMPane.java @@ -13,13 +13,13 @@ * If not, see * ******************************************************************************/ -package io.github.dsheirer.instrument.gui.viewer.decoder; +package io.github.dsheirer.instrument.decoder; -import io.github.dsheirer.instrument.gui.viewer.chart.ComplexSampleLineChart; -import io.github.dsheirer.instrument.gui.viewer.chart.DoubleLineChart; -import io.github.dsheirer.instrument.gui.viewer.chart.EyeDiagramChart; -import io.github.dsheirer.instrument.gui.viewer.chart.PhaseLineChart; -import io.github.dsheirer.instrument.gui.viewer.chart.SymbolChart; +import io.github.dsheirer.instrument.chart.ComplexSampleLineChart; +import io.github.dsheirer.instrument.chart.DoubleLineChart; +import io.github.dsheirer.instrument.chart.EyeDiagramChart; +import io.github.dsheirer.instrument.chart.PhaseLineChart; +import io.github.dsheirer.instrument.chart.SymbolChart; import io.github.dsheirer.message.Message; import io.github.dsheirer.module.decode.DecoderType; import io.github.dsheirer.module.decode.p25.P25DecoderC4FMInstrumented; diff --git a/src/main/java/io/github/dsheirer/instrument/gui/viewer/decoder/P25Phase1LSMPane.java b/src/main/java/io/github/dsheirer/instrument/decoder/P25Phase1LSMPane.java similarity index 94% rename from src/main/java/io/github/dsheirer/instrument/gui/viewer/decoder/P25Phase1LSMPane.java rename to src/main/java/io/github/dsheirer/instrument/decoder/P25Phase1LSMPane.java index 185e26a4..53335842 100644 --- a/src/main/java/io/github/dsheirer/instrument/gui/viewer/decoder/P25Phase1LSMPane.java +++ b/src/main/java/io/github/dsheirer/instrument/decoder/P25Phase1LSMPane.java @@ -13,13 +13,13 @@ * If not, see * ******************************************************************************/ -package io.github.dsheirer.instrument.gui.viewer.decoder; +package io.github.dsheirer.instrument.decoder; -import io.github.dsheirer.instrument.gui.viewer.chart.ComplexSampleLineChart; -import io.github.dsheirer.instrument.gui.viewer.chart.DoubleLineChart; -import io.github.dsheirer.instrument.gui.viewer.chart.EyeDiagramChart; -import io.github.dsheirer.instrument.gui.viewer.chart.PhaseLineChart; -import io.github.dsheirer.instrument.gui.viewer.chart.SymbolChart; +import io.github.dsheirer.instrument.chart.ComplexSampleLineChart; +import io.github.dsheirer.instrument.chart.DoubleLineChart; +import io.github.dsheirer.instrument.chart.EyeDiagramChart; +import io.github.dsheirer.instrument.chart.PhaseLineChart; +import io.github.dsheirer.instrument.chart.SymbolChart; import io.github.dsheirer.message.Message; import io.github.dsheirer.module.decode.DecoderType; import io.github.dsheirer.module.decode.p25.P25DecoderLSMInstrumented; diff --git a/src/main/java/io/github/dsheirer/instrument/gui/viewer/decoder/RealDecoderPane.java b/src/main/java/io/github/dsheirer/instrument/decoder/RealDecoderPane.java similarity index 97% rename from src/main/java/io/github/dsheirer/instrument/gui/viewer/decoder/RealDecoderPane.java rename to src/main/java/io/github/dsheirer/instrument/decoder/RealDecoderPane.java index 4d9a4871..3853c36c 100644 --- a/src/main/java/io/github/dsheirer/instrument/gui/viewer/decoder/RealDecoderPane.java +++ b/src/main/java/io/github/dsheirer/instrument/decoder/RealDecoderPane.java @@ -13,7 +13,7 @@ * If not, see * ******************************************************************************/ -package io.github.dsheirer.instrument.gui.viewer.decoder; +package io.github.dsheirer.instrument.decoder; import io.github.dsheirer.module.decode.DecoderType; import io.github.dsheirer.sample.Listener; diff --git a/src/main/java/io/github/dsheirer/instrument/gui/viewer/decoder/Tait1200Pane.java b/src/main/java/io/github/dsheirer/instrument/decoder/Tait1200Pane.java similarity index 97% rename from src/main/java/io/github/dsheirer/instrument/gui/viewer/decoder/Tait1200Pane.java rename to src/main/java/io/github/dsheirer/instrument/decoder/Tait1200Pane.java index 07b877e3..75cfb35e 100644 --- a/src/main/java/io/github/dsheirer/instrument/gui/viewer/decoder/Tait1200Pane.java +++ b/src/main/java/io/github/dsheirer/instrument/decoder/Tait1200Pane.java @@ -13,7 +13,7 @@ * If not, see * ******************************************************************************/ -package io.github.dsheirer.instrument.gui.viewer.decoder; +package io.github.dsheirer.instrument.decoder; import io.github.dsheirer.dsp.symbol.ISyncDetectListener; import io.github.dsheirer.module.decode.DecoderType; diff --git a/src/main/java/io/github/dsheirer/instrument/gui/ConstellationPanel.java b/src/main/java/io/github/dsheirer/instrument/gui/ConstellationPanel.java deleted file mode 100644 index 77682fa5..00000000 --- a/src/main/java/io/github/dsheirer/instrument/gui/ConstellationPanel.java +++ /dev/null @@ -1,77 +0,0 @@ -/******************************************************************************* - * SDR Trunk - * Copyright (C) 2014 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 io.github.dsheirer.instrument.gui; - -import io.github.dsheirer.gui.control.ConstellationViewer; -import io.github.dsheirer.sample.Listener; -import io.github.dsheirer.sample.complex.Complex; -import io.github.dsheirer.source.IFrameLocationListener; -import net.miginfocom.swing.MigLayout; - -import javax.swing.*; -import java.awt.*; - -public class ConstellationPanel extends JInternalFrame - implements IFrameLocationListener, Listener -{ - private static final long serialVersionUID = 1L; - - private ConstellationViewer mConstellation = new ConstellationViewer( 48000, 4800 ); - - public ConstellationPanel() - { - initGui(); - } - - private void initGui() - { - setLayout( new MigLayout( "insets 0 0 0 0 ", "[grow,fill]", "[grow,fill]" ) ); - - setTitle( "Constellation View" ); - setPreferredSize( new Dimension( 300, 300 ) ); - setSize( 300, 300 ); - - setResizable( true ); - setClosable( true ); - setIconifiable( true ); - setMaximizable( false ); - - add( mConstellation ); - } - - - @Override - public void frameLocationUpdated( int location ) - { - // TODO Auto-generated method stub - - } - - @Override - public void frameLocationReset() - { - // TODO Auto-generated method stub - - } - - @Override - public void receive( Complex sample ) - { - mConstellation.receive( sample ); - } -} diff --git a/src/main/java/io/github/dsheirer/instrument/gui/DecoderSelectionFrame.java b/src/main/java/io/github/dsheirer/instrument/gui/DecoderSelectionFrame.java deleted file mode 100644 index bc3f28d4..00000000 --- a/src/main/java/io/github/dsheirer/instrument/gui/DecoderSelectionFrame.java +++ /dev/null @@ -1,102 +0,0 @@ -package io.github.dsheirer.instrument.gui; - -import io.github.dsheirer.alias.AliasModel; -import io.github.dsheirer.audio.broadcast.BroadcastModel; -import io.github.dsheirer.controller.channel.Channel; -import io.github.dsheirer.controller.channel.ChannelModel; -import io.github.dsheirer.controller.channel.map.ChannelMapModel; -import io.github.dsheirer.module.decode.DecodeConfigurationEditor; -import io.github.dsheirer.module.decode.config.DecodeConfiguration; -import io.github.dsheirer.playlist.PlaylistManager; -import io.github.dsheirer.source.IControllableFileSource; -import net.miginfocom.swing.MigLayout; - -import javax.swing.JButton; -import javax.swing.JDesktopPane; -import javax.swing.JInternalFrame; -import java.awt.Dimension; -import java.awt.event.ActionEvent; -import java.awt.event.ActionListener; - -public class DecoderSelectionFrame extends JInternalFrame -{ - private static final long serialVersionUID = 1L; - - private Channel mChannel = new Channel(); - private DecodeConfigurationEditor mDecodeEditor = new DecodeConfigurationEditor(null); - private PlaylistManager mPlaylistManager; - - private IControllableFileSource mSource; - private JDesktopPane mDesktop; - - public DecoderSelectionFrame(JDesktopPane desktop, IControllableFileSource source) - { - AliasModel aliasModel = new AliasModel(); - BroadcastModel broadcastModel = new BroadcastModel(null); - ChannelModel channelModel = new ChannelModel(); - ChannelMapModel channelMapModel = new ChannelMapModel(); - - mPlaylistManager = new PlaylistManager(aliasModel, broadcastModel, channelModel, channelMapModel); - - mDesktop = desktop; - mSource = source; - - mDecodeEditor.setItem(mChannel); - initGUI(); - } - - private void initGUI() - { - setLayout(new MigLayout("", "[grow,fill]", "[][][grow,fill]")); - - setTitle("Decoders"); - setPreferredSize(new Dimension(700, 450)); - setSize(700, 450); - - setResizable(true); - setClosable(true); - setIconifiable(true); - setMaximizable(false); - - add(mDecodeEditor, "wrap"); - - add(new AddDecoderButton(), "span"); - - } - - public class AddDecoderButton extends JButton - { - private static final long serialVersionUID = 1L; - - public AddDecoderButton() - { - super("Add"); - - addActionListener(new ActionListener() - { - @Override - public void actionPerformed(ActionEvent arg0) - { - mDecodeEditor.save(); - - Channel channel = mDecodeEditor.getItem(); - - if(channel != null) - { - DecodeConfiguration config = channel.getDecodeConfiguration(); - - if( config != null ) - { - DecoderViewFrame decoderFrame = new DecoderViewFrame(mPlaylistManager, channel, mSource ); - - decoderFrame.setVisible( true ); - - mDesktop.add( decoderFrame ); - } - } - } - }); - } - } - -} diff --git a/src/main/java/io/github/dsheirer/instrument/gui/DecoderViewFrame.java b/src/main/java/io/github/dsheirer/instrument/gui/DecoderViewFrame.java deleted file mode 100644 index 8c944c0c..00000000 --- a/src/main/java/io/github/dsheirer/instrument/gui/DecoderViewFrame.java +++ /dev/null @@ -1,355 +0,0 @@ -/******************************************************************************* - * 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 io.github.dsheirer.instrument.gui; - -import io.github.dsheirer.alias.AliasModel; -import io.github.dsheirer.controller.channel.Channel; -import io.github.dsheirer.controller.channel.ChannelModel; -import io.github.dsheirer.controller.channel.ChannelProcessingManager; -import io.github.dsheirer.controller.channel.map.ChannelMapModel; -import io.github.dsheirer.instrument.Instrumentable; -import io.github.dsheirer.instrument.InstrumentableProcessingChain; -import io.github.dsheirer.instrument.tap.Tap; -import io.github.dsheirer.instrument.tap.TapGroup; -import io.github.dsheirer.instrument.tap.TapViewPanel; -import io.github.dsheirer.instrument.tap.stream.BinaryTap; -import io.github.dsheirer.instrument.tap.stream.BinaryTapViewPanel; -import io.github.dsheirer.instrument.tap.stream.ComplexSampleTap; -import io.github.dsheirer.instrument.tap.stream.ComplexSampleTapViewPanel; -import io.github.dsheirer.instrument.tap.stream.ComplexTap; -import io.github.dsheirer.instrument.tap.stream.ComplexTapViewPanel; -import io.github.dsheirer.instrument.tap.stream.DibitTap; -import io.github.dsheirer.instrument.tap.stream.DibitTapViewPanel; -import io.github.dsheirer.instrument.tap.stream.EyeDiagramDataTap; -import io.github.dsheirer.instrument.tap.stream.EyeDiagramDataTapPanel; -import io.github.dsheirer.instrument.tap.stream.FloatTap; -import io.github.dsheirer.instrument.tap.stream.FloatTapViewPanel; -import io.github.dsheirer.instrument.tap.stream.QPSKTap; -import io.github.dsheirer.instrument.tap.stream.QPSKTapPanel; -import io.github.dsheirer.instrument.tap.stream.SymbolEventTap; -import io.github.dsheirer.instrument.tap.stream.SymbolEventTapViewPanel; -import io.github.dsheirer.message.Message; -import io.github.dsheirer.module.Module; -import io.github.dsheirer.module.decode.DecoderFactory; -import io.github.dsheirer.module.decode.config.DecodeConfiguration; -import io.github.dsheirer.module.log.EventLogManager; -import io.github.dsheirer.playlist.PlaylistManager; -import io.github.dsheirer.sample.Listener; -import io.github.dsheirer.source.IControllableFileSource; -import io.github.dsheirer.source.Source; -import net.miginfocom.swing.MigLayout; -import org.slf4j.Logger; -import org.slf4j.LoggerFactory; - -import javax.swing.*; -import java.awt.*; -import java.awt.event.ActionEvent; -import java.awt.event.ActionListener; -import java.awt.event.MouseEvent; -import java.awt.event.MouseListener; -import java.util.HashMap; -import java.util.List; - -public class DecoderViewFrame extends JInternalFrame implements Listener -{ - private static final long serialVersionUID = 1L; - private final static Logger mLog = LoggerFactory.getLogger(DecoderViewFrame.class); - - private DecodeConfiguration mDecodeConfig; - private InstrumentableProcessingChain mProcessingChain; - private IControllableFileSource mSource; - - private HashMap mPanelMap = new HashMap(); - - public DecoderViewFrame(PlaylistManager playlistManager, Channel channel, IControllableFileSource source) - { - mSource = source; - - mDecodeConfig = channel.getDecodeConfiguration(); - - mProcessingChain = new InstrumentableProcessingChain(); - - ChannelModel channelModel = new ChannelModel(); - ChannelMapModel channelMapModel = new ChannelMapModel(); - - ChannelProcessingManager channelProcessingManager = new ChannelProcessingManager(channelModel, channelMapModel, - new AliasModel(), new EventLogManager(), null, null); - - List modules = DecoderFactory.getModules(channelModel, channelMapModel, channelProcessingManager, - new AliasModel(), channel, mProcessingChain.getChannelState().getMutableMetadata()); - - mProcessingChain.addModules(modules); - - mProcessingChain.addMessageListener(this); - - mProcessingChain.setSource((Source) mSource); - - mProcessingChain.start(); - - initGui(); - } - - private void initGui() - { - setLayout(new MigLayout("insets 0 0 0 0 ", "[grow,fill]", "[grow,fill]")); - - setTitle("Decoder [" + mDecodeConfig.getDecoderType().getDisplayString() + "]"); - setPreferredSize(new Dimension(450, 250)); - setSize(450, 250); - - setResizable(true); - setClosable(true); - setIconifiable(true); - setMaximizable(false); - - addMouseListener(new MouseListener() - { - @Override - public void mouseClicked(MouseEvent me) - { - if(me.getButton() == MouseEvent.BUTTON3) - { - JPopupMenu popup = new JPopupMenu(); - - popup.add(getTapContextMenu()); - - if(!mPanelMap.values().isEmpty()) - { - popup.add(new JSeparator()); - - for(TapViewPanel panel : mPanelMap.values()) - { - popup.add(panel.getContextMenu()); - } - } - - popup.show(DecoderViewFrame.this, me.getX(), me.getY()); - } - } - - @Override - public void mouseEntered(MouseEvent arg0) - { - } - - @Override - public void mouseExited(MouseEvent arg0) - { - } - - @Override - public void mousePressed(MouseEvent arg0) - { - } - - @Override - public void mouseReleased(MouseEvent arg0) - { - } - - }); - } - - private JMenu getTapContextMenu() - { - JMenu menu = new JMenu("Modules"); - - if(mProcessingChain != null) - { - for(Module module : mProcessingChain.getModules()) - { - if(module instanceof Instrumentable) - { - List groups = ((Instrumentable) module).getTapGroups(); - - for(TapGroup group : groups) - { - menu.add(new TapGroupMenu(group)); - } - } - } - } - - return menu; - } - - public void add(Tap tap) - { - TapViewPanel panel = null; - - switch(tap.getType()) - { - case EVENT_SYNC_DETECT: - break; - case STREAM_BINARY: - panel = new BinaryTapViewPanel((BinaryTap) tap); - break; - case STREAM_COMPLEX: - panel = new ComplexTapViewPanel((ComplexTap) tap); - break; - case STREAM_COMPLEX_SAMPLE: - panel = new ComplexSampleTapViewPanel((ComplexSampleTap) tap); - break; - case STREAM_DIBIT: - panel = new DibitTapViewPanel((DibitTap) tap); - break; - case STREAM_EYE_DIAGRAM: - panel = new EyeDiagramDataTapPanel((EyeDiagramDataTap) tap); - break; - case STREAM_FLOAT: - panel = new FloatTapViewPanel((FloatTap) tap); - break; - case STREAM_QPSK: - panel = new QPSKTapPanel((QPSKTap) tap); - break; - case STREAM_SYMBOL: - panel = new SymbolEventTapViewPanel((SymbolEventTap) tap); - break; - } - - if(panel != null) - { - add(panel, "span"); - - validate(); - - mPanelMap.put(tap, panel); - - for(Module module : mProcessingChain.getModules()) - { - if(module instanceof Instrumentable) - { - ((Instrumentable) module).registerTap(tap); - } - } - } - else - { - mLog.info("Tap panel is null, couldn't add for tap " + - tap.getName() + "[" + tap.getType().toString() + "]"); - } - } - - public void remove(Tap tap) - { - for(Module module : mProcessingChain.getModules()) - { - if(module instanceof Instrumentable) - { - ((Instrumentable) module).unregisterTap(tap); - } - } - - TapViewPanel panel = mPanelMap.get(tap); - - remove(panel); - - validate(); - - mPanelMap.remove(tap); - } - - public class AddAllTapsItem extends JMenuItem - { - private static final long serialVersionUID = 1L; - - private final List mTaps; - - public AddAllTapsItem(final List taps) - { - super("All Taps"); - - mTaps = taps; - - addActionListener(new ActionListener() - { - @Override - public void actionPerformed(ActionEvent e) - { - for(Tap tap : mTaps) - { - DecoderViewFrame.this.add(tap); - } - } - }); - } - } - - public class TapSelectionItem extends JCheckBoxMenuItem - { - private static final long serialVersionUID = 1L; - private Tap mTap; - - public TapSelectionItem(Tap tap) - { - super(tap.getName()); - - mTap = tap; - - if(mPanelMap.keySet().contains(tap)) - { - setSelected(true); - } - - addActionListener(new ActionListener() - { - @Override - public void actionPerformed(ActionEvent arg0) - { - if(mPanelMap.keySet().contains(mTap)) - { - DecoderViewFrame.this.remove(mTap); - } - else - { - DecoderViewFrame.this.add(mTap); - } - } - }); - } - } - - @Override - public void receive(Message message) - { - mLog.info(message.toString()); - } - - public class TapGroupMenu extends JMenu - { - private static final long serialVersionUID = 1L; - private TapGroup mTapGroup; - - public TapGroupMenu(TapGroup tapGroup) - { - super(tapGroup.getName()); - - mTapGroup = tapGroup; - - add(new AddAllTapsItem(mTapGroup.getTaps())); - - add(new JSeparator()); - - for(Tap tap : mTapGroup.getTaps()) - { - add(new TapSelectionItem(tap)); - } - } - } -} diff --git a/src/main/java/io/github/dsheirer/instrument/gui/SampleModel.java b/src/main/java/io/github/dsheirer/instrument/gui/SampleModel.java deleted file mode 100644 index 0d7705ec..00000000 --- a/src/main/java/io/github/dsheirer/instrument/gui/SampleModel.java +++ /dev/null @@ -1,101 +0,0 @@ -/******************************************************************************* - * SDR Trunk - * Copyright (C) 2014 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 io.github.dsheirer.instrument.gui; - -import io.github.dsheirer.instrument.tap.TapListener; -import org.slf4j.Logger; -import org.slf4j.LoggerFactory; - -import java.util.ArrayList; -import java.util.Collections; -import java.util.List; -import java.util.Observable; - -public class SampleModel extends Observable implements TapListener -{ - private final static Logger mLog = - LoggerFactory.getLogger( SampleModel.class ); - - private int mSampleCount = 2000; - private int mDelay = 0; - private ArrayList mSamples = new ArrayList(); - - public SampleModel() - { - } - - public List getSamples() - { - return Collections.unmodifiableList( mSamples ); - } - - public void clearSamples() - { - mSamples.clear(); - - changed(); - } - - public int getSampleCount() - { - return mSampleCount; - } - - public void setSampleCount( int count ) - { - mSampleCount = count; - - changed(); - } - - public void setDelay( int delay ) - { - mDelay = delay; - } - - public int getDelay() - { - return mDelay; - } - - @Override - public void receive( T t ) - { - if( mDelay > 0 ) - { - mDelay--; - } - else - { - mSamples.add( t ); - - while( mSamples.size() > mSampleCount ) - { - mSamples.remove( 0 ); - } - } - - changed(); - } - - private void changed() - { - setChanged(); - notifyObservers(); - } -} diff --git a/src/main/java/io/github/dsheirer/instrument/gui/SourceControllerFrame.java b/src/main/java/io/github/dsheirer/instrument/gui/SourceControllerFrame.java deleted file mode 100644 index 6b112e13..00000000 --- a/src/main/java/io/github/dsheirer/instrument/gui/SourceControllerFrame.java +++ /dev/null @@ -1,215 +0,0 @@ -/******************************************************************************* - * SDR Trunk - * Copyright (C) 2014 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 io.github.dsheirer.instrument.gui; - -import io.github.dsheirer.source.IControllableFileSource; -import io.github.dsheirer.source.IFrameLocationListener; -import net.miginfocom.swing.MigLayout; -import org.slf4j.Logger; -import org.slf4j.LoggerFactory; - -import javax.swing.*; -import java.awt.*; -import java.awt.event.ActionEvent; -import java.awt.event.ActionListener; -import java.awt.event.FocusEvent; -import java.awt.event.FocusListener; -import java.io.IOException; - -public class SourceControllerFrame extends JInternalFrame - implements IFrameLocationListener -{ - private static final long serialVersionUID = 1L; - private final static Logger mLog = - LoggerFactory.getLogger( SourceControllerFrame.class ); - - private IControllableFileSource mSource; - private JDesktopPane mDesktop; - - private JLabel mCurrentPosition = new JLabel( "0" ); - - public SourceControllerFrame( IControllableFileSource source, - JDesktopPane desktop ) - { - mSource = source; - mSource.setListener( this ); - - mDesktop = desktop; - - initGui(); - } - - private void initGui() - { - setTitle( "Source [" + mSource.getFile().getAbsolutePath() + "]" ); - setPreferredSize( new Dimension( 670, 70 ) ); - setSize( 670, 70 ); - - setResizable( true ); - setClosable( true ); - setIconifiable( true ); - setMaximizable( false ); - - JPanel panel = new JPanel(); - panel.setLayout( new MigLayout() ); - - JButton decoderButton = new JButton( "Decoders" ); - decoderButton.addActionListener( new ActionListener() - { - @Override - public void actionPerformed( ActionEvent e ) - { - EventQueue.invokeLater( new Runnable() - { - @Override - public void run() - { - DecoderSelectionFrame frame = - new DecoderSelectionFrame( mDesktop, mSource ); - - frame.setVisible( true ); - - mDesktop.add( frame ); - } - } ); - } - } ); - - panel.add( decoderButton ); - - panel.add( new JLabel( "Skip:" ) ); - panel.add( new SkipFramesField( mSource ) ); - panel.add( new NextFrameButton( mSource, "> 1", 1 ) ); - panel.add( new NextFrameButton( mSource, "> 10", 10 ) ); - panel.add( new NextFrameButton( mSource, "> 100", 100 ) ); - panel.add( new NextFrameButton( mSource, "> 1000", 1000 ) ); - panel.add( new JLabel( "Posn:" ) ); - panel.add( mCurrentPosition, "wrap" ); - - add( panel ); - } - - public class NextFrameButton extends JButton - { - private static final long serialVersionUID = 1L; - - private IControllableFileSource mSource; - private int mFrames; - - public NextFrameButton( IControllableFileSource source, - String label, int frames ) - { - super( label ); - mSource = source; - mFrames = frames; - - addActionListener( new ActionListener() - { - @Override - public void actionPerformed( ActionEvent arg0 ) - { - EventQueue.invokeLater( new Runnable() - { - @Override - public void run() - { - try - { - mSource.next( mFrames ); - } - catch ( IOException e ) - { - mLog.error( "Viewer - reading frames [" + mFrames + - "] from source", e ); - - JOptionPane.showMessageDialog( SourceControllerFrame.this, - "Can't read " + mFrames + " more frames [" + - e.getLocalizedMessage() + "]", - "Wave File Error", - JOptionPane.ERROR_MESSAGE ); - } - } - } ); - } - } ); - } - } - - public class SkipFramesField extends JTextField - { - private static final long serialVersionUID = 1L; - - private IControllableFileSource mSource; - - public SkipFramesField( IControllableFileSource source ) - { - super( "0" ); - - mSource = source; - - setMinimumSize( new Dimension( 100, getHeight() ) ); - - addFocusListener( new FocusListener() - { - @Override - public void focusGained( FocusEvent arg0 ) {} - - @Override - public void focusLost( FocusEvent arg0 ) - { - try - { - int framesToSkip = Integer.parseInt( getText() ); - - mSource.next( framesToSkip, false ); - } - catch( Exception e ) - { - mLog.error( "WaveSourceFrame - error skipping frames", e ); - - JOptionPane.showMessageDialog( SourceControllerFrame.this, - "Can't skip [" + getText() + "] frames.", - "Error", - JOptionPane.ERROR_MESSAGE ); - } - } - } ); - } - } - - - @Override - public void frameLocationUpdated( final int location ) - { - EventQueue.invokeLater( new Runnable() - { - @Override - public void run() - { - mCurrentPosition.setText( String.valueOf( location ) ); - } - } ); - } - - @Override - public void frameLocationReset() - { - // TODO Auto-generated method stub - - } -} diff --git a/src/main/java/io/github/dsheirer/instrument/gui/Viewer.java b/src/main/java/io/github/dsheirer/instrument/gui/Viewer.java deleted file mode 100644 index 39748fe2..00000000 --- a/src/main/java/io/github/dsheirer/instrument/gui/Viewer.java +++ /dev/null @@ -1,293 +0,0 @@ -/******************************************************************************* - * SDR Trunk - * Copyright (C) 2014 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 io.github.dsheirer.instrument.gui; - -import io.github.dsheirer.properties.SystemProperties; -import io.github.dsheirer.source.IControllableFileSource; -import io.github.dsheirer.source.wave.ComplexWaveSource; -import io.github.dsheirer.source.wave.RealWaveSource; -import io.github.dsheirer.util.TimeStamp; -import org.slf4j.Logger; -import org.slf4j.LoggerFactory; - -import javax.imageio.ImageIO; -import javax.swing.*; -import javax.swing.filechooser.FileFilter; -import java.awt.*; -import java.awt.event.ActionEvent; -import java.awt.event.ActionListener; -import java.awt.image.BufferedImage; -import java.io.File; -import java.io.IOException; -import java.nio.file.Files; -import java.nio.file.Path; - -public class Viewer -{ - private final static Logger mLog = LoggerFactory.getLogger( Viewer.class ); - - private JFrame mFrame; - private JDesktopPane mDesktop; - - private IControllableFileSource mSource; - - public Viewer() - { - initGUI(); - } - - public void setVisible( boolean visible ) - { - mFrame.setVisible( visible ); - } - - /** - * Initialize the contents of the frame. - */ - private void initGUI() - { - /* Setup main JFrame window */ - mFrame = new JFrame(); - mFrame.setTitle( "SDRTrunk - Sample File Viewer" ); - mFrame.setBounds( 100, 100, 800, 600 ); - mFrame.setDefaultCloseOperation( JFrame.EXIT_ON_CLOSE ); - - /* Multi-window desktop */ - mDesktop = new JDesktopPane(); - mFrame.add( mDesktop ); - - /* Menu items */ - JMenuBar menuBar = new JMenuBar(); - mFrame.setJMenuBar( menuBar ); - - JMenu fileMenu = new JMenu( "File" ); - menuBar.add( fileMenu ); - - fileMenu.add( new SourceFileItem() ); - fileMenu.add( new JSeparator() ); - - JMenuItem exitMenu = new JMenuItem( "Exit" ); - exitMenu.addActionListener( new ActionListener() - { - public void actionPerformed( ActionEvent event ) - { - System.exit( 0 ); - } - } ); - - fileMenu.add( exitMenu ); - - menuBar.add( new ScreenCaptureItem() ); - } - - private void setSourceFile( File file ) - { - mLog.info( "File selected [" + file.getAbsolutePath() + "]"); - - IControllableFileSource source = null; - - /* Attempt to open file as a 1-channel float source */ - try - { - source = new RealWaveSource( file ); - source.open(); - - mLog.info( "File opened as float wave file" ); - - } - catch( Exception e ) - { - mLog.error( "Couldn't open file as float single-channel source" ); - } - - if( source == null ) - { - /* Attempt to open file as a 2-channel complex source */ - try - { - ComplexWaveSource complex = new ComplexWaveSource( file ); - complex.open(); - - source = complex; - - mLog.info( "File opened as complex wave file" ); - } - catch( Exception e ) - { - mLog.error( "Couldn't open file as float source", e); - } - } - - if( source != null ) - { - mSource = source; - - SourceControllerFrame sourcePanel = - new SourceControllerFrame( source, mDesktop ); - sourcePanel.setVisible( true ); - mDesktop.add( sourcePanel ); - } - } - - public static void main( String[] args ) - { - final Viewer viewer = new Viewer(); - - EventQueue.invokeLater( new Runnable() - { - public void run() - { - try - { - viewer.setVisible( true ); - } - catch( Exception e ) - { - e.printStackTrace(); - } - } - } ); - - } - - public class SourceFileItem extends JMenuItem - { - private static final long serialVersionUID = 1L; - - public SourceFileItem() - { - super( "Open" ); - - addActionListener( new ActionListener() - { - @Override - public void actionPerformed( ActionEvent arg0 ) - { - final JFileChooser fc = new JFileChooser(); - fc.setFileFilter( new FileFilter() - { - @Override - public boolean accept( File file ) - { - return file.isDirectory() || - file.getName().endsWith( ".wav" ); - } - - @Override - public String getDescription() - { - return "Float or Complex 16-bit Wave Files"; - } - } ); - - int val = fc.showOpenDialog( mFrame ); - - if( val == JFileChooser.APPROVE_OPTION ) - { - final File file = fc.getSelectedFile(); - - EventQueue.invokeLater( new Runnable() - { - @Override - public void run() - { - setSourceFile( file ); - } - } ); - } - } - } ); - } - } - - public class ScreenCaptureItem extends JMenuItem - { - private static final long serialVersionUID = 1L; - - public ScreenCaptureItem() - { - super( "Screen Capture" ); - - addActionListener( new ActionListener() - { - @Override - public void actionPerformed( ActionEvent arg0 ) - { - EventQueue.invokeLater( new Runnable() - { - @Override - public void run() - { - try - { - Robot robot = new Robot(); - - final BufferedImage image = - robot.createScreenCapture( mFrame.getBounds() ); - - SystemProperties props = SystemProperties.getInstance(); - - Path capturePath = props.getApplicationFolder( "screen_captures" ); - - if( !Files.exists( capturePath ) ) - { - try - { - Files.createDirectory( capturePath ); - } - catch ( IOException e ) - { - mLog.error( "Couldn't create 'screen_captures' " - + "subdirectory in the " + - "SDRTrunk application directory", e ); - } - } - - String filename = TimeStamp.getTimeStamp( "_" ) + - "_screen_capture.png"; - - final Path captureFile = capturePath.resolve( filename ); - - EventQueue.invokeLater( new Runnable() - { - @Override - public void run() - { - try - { - ImageIO.write( image, "png", - captureFile.toFile() ); - } - catch ( IOException e ) - { - mLog.error( "Couldn't write screen capture to " - + "file [" + captureFile.toString() + "]", e ); - } - }} ); - } - catch ( AWTException e ) - { - mLog.error( "Exception while taking screen capture", e ); - } - } - } ); - } - } ); - } - } -} diff --git a/src/main/java/io/github/dsheirer/instrument/tap/Tap.java b/src/main/java/io/github/dsheirer/instrument/tap/Tap.java deleted file mode 100644 index ff3cc97e..00000000 --- a/src/main/java/io/github/dsheirer/instrument/tap/Tap.java +++ /dev/null @@ -1,96 +0,0 @@ -/******************************************************************************* - * SDR Trunk - * Copyright (C) 2014 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 io.github.dsheirer.instrument.tap; - -import java.util.ArrayList; - -public abstract class Tap -{ - protected TapType mType; - protected String mName; - protected int mDelay; - - protected ArrayList mListeners = new ArrayList(); - - /** - * Instrumentation tap. Provides a tap into a data stream or event stream - * within a process, so that registered listeners can be notified every - * time a new data sample is availabe, or an event occurrs. - * - * @param name - displayable name to use for the tap - * - * @param delay - indicates the number of delay units from when the data - * begins the process until this event occurs. When this data or event is - * plotted on a flow graph, the delay value will be used to adjust placement - * on the display to correctly align events. - */ - public Tap( TapType type, String name, int delay ) - { - mType = type; - mName = name; - mDelay = delay; - } - - public TapType getType() - { - return mType; - } - - /** - * Display name for this tap - */ - public String getName() - { - return mName; - } - - /** - * Delay for this tap - */ - public int getDelay() - { - return mDelay; - } - - /** - * Registers a listener for data or events produced by this tap - */ - public void addListener( TapListener listener ) - { - if( !mListeners.contains( listener ) ) - { - mListeners.add( listener ); - } - } - - /** - * Removes the listener from receiving data or events from this tap - */ - public void removeListener( TapListener listener ) - { - mListeners.remove( listener ); - } - - /** - * Number of listeners currently registered on this tap - */ - public int getListenerCount() - { - return mListeners.size(); - } -} diff --git a/src/main/java/io/github/dsheirer/instrument/tap/TapGroup.java b/src/main/java/io/github/dsheirer/instrument/tap/TapGroup.java deleted file mode 100644 index 7cb5deaf..00000000 --- a/src/main/java/io/github/dsheirer/instrument/tap/TapGroup.java +++ /dev/null @@ -1,35 +0,0 @@ -package io.github.dsheirer.instrument.tap; - -import java.util.ArrayList; -import java.util.List; - -public class TapGroup -{ - private String mName; - private List mTaps = new ArrayList<>(); - - public TapGroup( String name ) - { - mName = name; - } - - public String getName() - { - return mName; - } - - public String toString() - { - return getName(); - } - - public List getTaps() - { - return mTaps; - } - - public void add( Tap tap ) - { - mTaps.add( tap ); - } -} diff --git a/src/main/java/io/github/dsheirer/instrument/tap/TapListener.java b/src/main/java/io/github/dsheirer/instrument/tap/TapListener.java deleted file mode 100644 index eda419a7..00000000 --- a/src/main/java/io/github/dsheirer/instrument/tap/TapListener.java +++ /dev/null @@ -1,23 +0,0 @@ -/******************************************************************************* - * SDR Trunk - * Copyright (C) 2014 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 io.github.dsheirer.instrument.tap; - -public interface TapListener -{ - public void receive( T t ); -} diff --git a/src/main/java/io/github/dsheirer/instrument/tap/TapType.java b/src/main/java/io/github/dsheirer/instrument/tap/TapType.java deleted file mode 100644 index 6013523f..00000000 --- a/src/main/java/io/github/dsheirer/instrument/tap/TapType.java +++ /dev/null @@ -1,31 +0,0 @@ -/******************************************************************************* - * SDR Trunk - * Copyright (C) 2014 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 io.github.dsheirer.instrument.tap; - -public enum TapType -{ - EVENT_SYNC_DETECT, - STREAM_BINARY, - STREAM_COMPLEX, - STREAM_COMPLEX_SAMPLE, - STREAM_DIBIT, - STREAM_EYE_DIAGRAM, - STREAM_FLOAT, - STREAM_QPSK, - STREAM_SYMBOL; -} diff --git a/src/main/java/io/github/dsheirer/instrument/tap/TapViewPanel.java b/src/main/java/io/github/dsheirer/instrument/tap/TapViewPanel.java deleted file mode 100644 index 2af797a0..00000000 --- a/src/main/java/io/github/dsheirer/instrument/tap/TapViewPanel.java +++ /dev/null @@ -1,131 +0,0 @@ -/******************************************************************************* - * SDR Trunk - * Copyright (C) 2014 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 io.github.dsheirer.instrument.tap; - -import io.github.dsheirer.instrument.gui.SampleModel; - -import javax.swing.*; -import java.awt.*; -import java.awt.event.ActionEvent; -import java.awt.event.ActionListener; -import java.util.Observer; - -public abstract class TapViewPanel extends JPanel implements Observer -{ - private static final long serialVersionUID = 1L; - - protected SampleModel mModel; - protected String mLabel; - - protected float mVerticalZoom = 1.0f; - - public TapViewPanel( SampleModel model, String label ) - { - mModel = model; - mModel.addObserver( this ); - - mLabel = label; - - initGui(); - } - - private void initGui() - { - setBackground( Color.WHITE ); - setForeground( Color.BLUE ); - - setSize( new Dimension( 400, 200 ) ); - } - - public void reset() - { - mModel.clearSamples(); - } - - public JMenu getContextMenu() - { - JMenu menu = new JMenu( mLabel ); - - JMenu verticalMenu = new JMenu( "Vertical Zoom" ); - menu.add( verticalMenu ); - - verticalMenu.add( new VerticalZoomItem( this, "8", 8.0f ) ); - verticalMenu.add( new VerticalZoomItem( this, "4", 4.0f ) ); - verticalMenu.add( new VerticalZoomItem( this, "2", 2.0f ) ); - verticalMenu.add( new VerticalZoomItem( this, "1", 1.0f ) ); - verticalMenu.add( new VerticalZoomItem( this, ".5", 0.5f ) ); - verticalMenu.add( new VerticalZoomItem( this, ".25", 0.25f ) ); - - return menu; - } - - public SampleModel getModel() - { - return mModel; - } - - public int getSampleCount() - { - return getModel().getSampleCount(); - } - - public void setSampleCount( int sampleCount ) - { - getModel().setSampleCount( sampleCount ); - } - - public float getVerticalZoom() - { - return mVerticalZoom; - } - - public void setVerticalZoom( float zoom ) - { - mVerticalZoom = zoom; - repaint(); - } - - public class VerticalZoomItem extends JCheckBoxMenuItem - { - private static final long serialVersionUID = 1L; - - private TapViewPanel mPanel; - private float mZoom; - - public VerticalZoomItem( TapViewPanel panel, String label, float zoom ) - { - super( label ); - mPanel = panel; - mZoom = zoom; - - if( mPanel.getVerticalZoom() == zoom ) - { - setSelected( true ); - } - - addActionListener( new ActionListener() - { - @Override - public void actionPerformed( ActionEvent arg0 ) - { - mPanel.setVerticalZoom( mZoom ); - } - } ); - } - } -} diff --git a/src/main/java/io/github/dsheirer/instrument/tap/event/SyncDetectTap.java b/src/main/java/io/github/dsheirer/instrument/tap/event/SyncDetectTap.java deleted file mode 100644 index 087393a8..00000000 --- a/src/main/java/io/github/dsheirer/instrument/tap/event/SyncDetectTap.java +++ /dev/null @@ -1,58 +0,0 @@ -/******************************************************************************* - * SDR Trunk - * Copyright (C) 2014 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 io.github.dsheirer.instrument.tap.event; - -import io.github.dsheirer.dsp.symbol.ISyncDetectListener; -import io.github.dsheirer.dsp.symbol.SyncDetectProvider; -import io.github.dsheirer.instrument.tap.Tap; -import io.github.dsheirer.instrument.tap.TapListener; -import io.github.dsheirer.instrument.tap.TapType; - -public class SyncDetectTap extends Tap implements ISyncDetectListener, SyncDetectProvider -{ - private ISyncDetectListener mListener; - - /** - * Sync detect event tap. Indicates when message sync word has been - * detected within a binary stream of data. - */ - public SyncDetectTap() - { - super(TapType.EVENT_SYNC_DETECT, "Sync Detected", 0); - } - - @Override - public void syncDetected() - { - if(mListener != null) - { - mListener.syncDetected(); - } - - for(TapListener listener : mListeners) - { - listener.receive(getName()); - } - } - - @Override - public void setSyncDetectListener(ISyncDetectListener listener) - { - mListener = listener; - } -} diff --git a/src/main/java/io/github/dsheirer/instrument/tap/stream/AdditiveFloatTap.java b/src/main/java/io/github/dsheirer/instrument/tap/stream/AdditiveFloatTap.java deleted file mode 100644 index 75d5582e..00000000 --- a/src/main/java/io/github/dsheirer/instrument/tap/stream/AdditiveFloatTap.java +++ /dev/null @@ -1,40 +0,0 @@ -/******************************************************************************* - * SDR Trunk - * Copyright (C) 2014 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 io.github.dsheirer.instrument.tap.stream; - - -public class AdditiveFloatTap extends FloatTap -{ - private float mValue; - - public AdditiveFloatTap( String name, - int delay, - float sampleRateRatio, - float addValue ) - { - super( name, delay, sampleRateRatio ); - - mValue = addValue; - } - - @Override - public void receive( float sample ) - { - super.receive( sample + mValue ); - } -} diff --git a/src/main/java/io/github/dsheirer/instrument/tap/stream/BinaryTap.java b/src/main/java/io/github/dsheirer/instrument/tap/stream/BinaryTap.java deleted file mode 100644 index 9b82a7a2..00000000 --- a/src/main/java/io/github/dsheirer/instrument/tap/stream/BinaryTap.java +++ /dev/null @@ -1,59 +0,0 @@ -/******************************************************************************* - * SDR Trunk - * Copyright (C) 2014 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 io.github.dsheirer.instrument.tap.stream; - -import io.github.dsheirer.instrument.tap.TapListener; -import io.github.dsheirer.instrument.tap.TapType; -import io.github.dsheirer.sample.Listener; - -public class BinaryTap extends StreamTap implements Listener - -{ - private Listener mListener; - - public BinaryTap( String name, - int delay, - float sampleRateRatio ) - { - super( TapType.STREAM_BINARY, name, delay, sampleRateRatio ); - } - - @Override - public void receive( Boolean bit ) - { - if( mListener != null ) - { - mListener.receive( bit ); - } - - for( TapListener listener: mListeners ) - { - listener.receive( bit ); - } - } - - public void setListener( Listener listener ) - { - mListener = listener; - } - - public void removeListener( Listener listener ) - { - mListener = null; - } -} diff --git a/src/main/java/io/github/dsheirer/instrument/tap/stream/BinaryTapViewPanel.java b/src/main/java/io/github/dsheirer/instrument/tap/stream/BinaryTapViewPanel.java deleted file mode 100644 index 76f6bab6..00000000 --- a/src/main/java/io/github/dsheirer/instrument/tap/stream/BinaryTapViewPanel.java +++ /dev/null @@ -1,156 +0,0 @@ -/******************************************************************************* - * SDR Trunk - * Copyright (C) 2014 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 io.github.dsheirer.instrument.tap.stream; - -import io.github.dsheirer.instrument.gui.SampleModel; -import io.github.dsheirer.instrument.tap.Tap; -import io.github.dsheirer.instrument.tap.TapViewPanel; - -import java.awt.Color; -import java.awt.FontMetrics; -import java.awt.Graphics; -import java.awt.Graphics2D; -import java.awt.geom.Path2D; -import java.awt.geom.Rectangle2D; -import java.util.List; -import java.util.Observable; - -public class BinaryTapViewPanel extends TapViewPanel -{ - private static final long serialVersionUID = 1L; - - private Tap mTap; - private List mSamples; - private int mSampleCount; - - public BinaryTapViewPanel( BinaryTap tap ) - { - super( new SampleModel(), tap.getName() ); - - mTap = tap; - mTap.addListener( getModel() ); - - getModel().setSampleCount( - (int)( getModel().getSampleCount() * tap.getSampleRateRatio() ) ); - - getModel().setDelay( tap.getDelay() ); - } - - @Override - public void update( Observable arg0, Object arg1 ) - { - mSamples = (List)getModel().getSamples(); - mSampleCount = getModel().getSampleCount(); - repaint(); - } - - public void paintComponent( Graphics g ) - { - super.paintComponent( g ); - - Graphics2D g2 = (Graphics2D)g; - - int height = getHeight(); - int width = getWidth(); - - g2.setColor( Color.DARK_GRAY ); - - float middle = (float)height / 2.0f; - - g2.drawLine( 0, (int)middle, width, (int)middle ); - - if( mSamples != null && mSamples.size() > 0 ) - { - g2.setColor( getForeground() ); - - g2.drawString( mLabel, 5 , 15 ); - - for( int x = 1; x < mSamples.size(); x++ ) - { - paintBit( g, x, mSamples.get( x ) ); - } - } - - g2.dispose(); - } - - /** - * Paints a polyline representing a boolean value with a 0 or 1 label in - * the middle of it - * - * @param graphics object - * @param index - index of the sample int the sample buffer - * @param value - bit value, 0 or 1 - */ - private void paintBit( Graphics graphics, int index, boolean value ) - { - Graphics2D g2 = (Graphics2D)graphics; - - Path2D.Float polyline = - new Path2D.Float( Path2D.Float.WIND_EVEN_ODD, mSamples.size() ); - - float middleY = ( (float)getHeight() / 2.0f ); - - float indexWidth = (float)( getWidth() ) / (float)mSampleCount; - - float startX = (float)index * indexWidth; - float stopX = (float)(index + 1 ) * indexWidth; - float fivePercent = indexWidth * .05f; - float leftX = startX + fivePercent; - float rightX = stopX - fivePercent; - - /* Start */ - polyline.moveTo( startX, middleY ); - - /* Left horizonal segment */ - polyline.lineTo( leftX, middleY ); - - float y = ( value ? ( middleY - ( middleY * 0.5f) ) : - ( middleY + ( middleY * 0.5f) ) ); - - /* Left vertical segment */ - polyline.lineTo( leftX, y ); - - /* Middle horizontal segment */ - polyline.lineTo( rightX, y ); - - /* Right vertical segment */ - polyline.lineTo( rightX, middleY ); - - /* Right horizontal segment */ - polyline.lineTo( stopX, middleY ); - - g2.draw( polyline ); - - /* Draw the bit value label */ - String bit = value ? "1" : "0"; - - FontMetrics fontMetrics = graphics.getFontMetrics( this.getFont() ); - - Rectangle2D label = fontMetrics.getStringBounds( bit, graphics ); - - float offsetX = (float)label.getWidth() / 2.0f; - float offsetY = (float)label.getWidth() / 2.0f; - - float middleX = startX + ( indexWidth / 2.0f ); - - graphics.drawString( bit, - (int)( middleX - offsetX ), - (int)( middleY - offsetY ) ); - } -} diff --git a/src/main/java/io/github/dsheirer/instrument/tap/stream/ComplexSampleTap.java b/src/main/java/io/github/dsheirer/instrument/tap/stream/ComplexSampleTap.java deleted file mode 100644 index 28451918..00000000 --- a/src/main/java/io/github/dsheirer/instrument/tap/stream/ComplexSampleTap.java +++ /dev/null @@ -1,64 +0,0 @@ -/******************************************************************************* - * SDR Trunk - * Copyright (C) 2014 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 io.github.dsheirer.instrument.tap.stream; - -import io.github.dsheirer.instrument.tap.TapListener; -import io.github.dsheirer.instrument.tap.TapType; -import io.github.dsheirer.sample.complex.Complex; -import io.github.dsheirer.sample.complex.ComplexSampleListener; - -public class ComplexSampleTap extends StreamTap implements ComplexSampleListener -{ - private ComplexSampleListener mListener; - - public ComplexSampleTap( String name, - int delay, - float sampleRateRatio ) - { - super( TapType.STREAM_COMPLEX_SAMPLE, name, delay, sampleRateRatio ); - } - - public ComplexSampleTap( TapType tapType, String name, int delay, float sampleRateRatio ) - { - super( tapType, name, delay, sampleRateRatio ); - } - - @Override - public void receive( float inphase, float quadrature ) - { - if( mListener != null ) - { - mListener.receive( inphase, quadrature ); - } - - for( TapListener listener: mListeners ) - { - listener.receive( new Complex( inphase, quadrature ) ); - } - } - - public void setListener( ComplexSampleListener listener ) - { - mListener = listener; - } - - public void removeListener( ComplexSampleListener listener ) - { - mListener = null; - } -} diff --git a/src/main/java/io/github/dsheirer/instrument/tap/stream/ComplexSampleTapViewPanel.java b/src/main/java/io/github/dsheirer/instrument/tap/stream/ComplexSampleTapViewPanel.java deleted file mode 100644 index 1ab7bac5..00000000 --- a/src/main/java/io/github/dsheirer/instrument/tap/stream/ComplexSampleTapViewPanel.java +++ /dev/null @@ -1,150 +0,0 @@ -/******************************************************************************* - * SDR Trunk - * Copyright (C) 2014 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 io.github.dsheirer.instrument.tap.stream; - -import io.github.dsheirer.gui.SDRTrunk; -import io.github.dsheirer.instrument.gui.SampleModel; -import io.github.dsheirer.instrument.tap.Tap; -import io.github.dsheirer.instrument.tap.TapViewPanel; -import io.github.dsheirer.sample.complex.Complex; -import org.slf4j.Logger; -import org.slf4j.LoggerFactory; - -import java.awt.*; -import java.awt.geom.Path2D; -import java.awt.geom.Point2D; -import java.util.List; -import java.util.Observable; - -public class ComplexSampleTapViewPanel extends TapViewPanel -{ - private static final long serialVersionUID = 1L; - private final static Logger mLog = LoggerFactory.getLogger( SDRTrunk.class ); - - private Tap mTap; - private List mSamples; - private int mSampleCount; - - public ComplexSampleTapViewPanel( ComplexSampleTap tap ) - { - super( new SampleModel(), tap.getName() ); - - setSize( new Dimension( 400, 400 ) ); - - mTap = tap; - mTap.addListener( getModel() ); - - mSampleCount = (int)( 2000 * tap.getSampleRateRatio() ); - - getModel().setSampleCount( mSampleCount ); - getModel().setDelay( tap.getDelay() ); - - mLog.info( "Complex Tap Panel [" + tap.getName() + "] count: " + - getModel().getSampleCount() + " delay:" + getModel().getDelay() ); - } - - @Override - public void update( Observable arg0, Object arg1 ) - { - mSamples = (List)getModel().getSamples(); - repaint(); - } - - public void paintComponent( Graphics g ) - { - super.paintComponent( g ); - - Graphics2D g2 = (Graphics2D)g; - - int height = getHeight(); - int width = getWidth(); - - g2.setColor( Color.DARK_GRAY ); - - float middle = (float)height / 4.0f; - - g2.drawLine( 0, (int)middle, width, (int)middle ); - g2.drawLine( 0, (int)( 3 * middle ), width, (int)( 3 * middle ) ); - - if( mSamples != null && - mSamples.size() > 0 ) - { - g2.setColor( getForeground() ); - - g2.drawString( mLabel, 5 , 15 ); - - Path2D.Float polylineReal = - new Path2D.Float( Path2D.Float.WIND_EVEN_ODD, mSamples.size() ); - Path2D.Float polylineImag = - new Path2D.Float( Path2D.Float.WIND_EVEN_ODD, mSamples.size() ); - - /* Move to the first point */ - Point2D.Float firstReal = resolve( 0, mSamples.get( 0 ), Type.REAL ); - polylineReal.moveTo( firstReal.x, firstReal.y ); - - Point2D.Float firstImag = resolve( 0, mSamples.get( 0 ), Type.IMAGINERY ); - polylineImag.moveTo( firstImag.x, firstImag.y ); - - for( int x = 1; x < mSamples.size(); x++ ) - { - Point2D.Float pointReal = resolve( x, mSamples.get( x ), Type.REAL ); - polylineReal.lineTo( pointReal.x, pointReal.y ); - - Point2D.Float pointImag = resolve( x, mSamples.get( x ), Type.IMAGINERY ); - polylineImag.lineTo( pointImag.x, pointImag.y ); - -// mLog.debug( "Imag y:" + pointImag.getY() + " height:" + getHeight() ); - } - - g2.draw( polylineReal ); - g2.draw( polylineImag ); - } - - g2.dispose(); - } - - - private Point2D.Float resolve( int index, Complex sample, Type type ) - { - int middle = (int)( getHeight() / 4 ); - - float y = middle - ( middle * mVerticalZoom * ( type == Type.REAL ? - sample.real() : sample.imaginary()) ); - - if( type == Type.IMAGINERY ) - { - y += ( middle * 2 ); - } - - /* Clip y to zero as necessary */ - y = ( y < 0 ) ? 0 : y; - - /* Clip y to max height as necessary */ - y = ( y > getHeight() ) ? getHeight() : y; - - float indexWidth = (float)getWidth() / (float)mSampleCount; - - float x = (float)index * indexWidth; - - Point2D.Float point = new Point2D.Float( x, y ); - - return new Point2D.Float( x , y ); - } - - public enum Type{ REAL,IMAGINERY }; -} diff --git a/src/main/java/io/github/dsheirer/instrument/tap/stream/ComplexTap.java b/src/main/java/io/github/dsheirer/instrument/tap/stream/ComplexTap.java deleted file mode 100644 index 34f798d3..00000000 --- a/src/main/java/io/github/dsheirer/instrument/tap/stream/ComplexTap.java +++ /dev/null @@ -1,64 +0,0 @@ -/******************************************************************************* - * SDR Trunk - * Copyright (C) 2014 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 io.github.dsheirer.instrument.tap.stream; - -import io.github.dsheirer.instrument.tap.TapListener; -import io.github.dsheirer.instrument.tap.TapType; -import io.github.dsheirer.sample.Listener; -import io.github.dsheirer.sample.complex.Complex; - -public class ComplexTap extends StreamTap implements Listener -{ - private Listener mListener; - - public ComplexTap( String name, - int delay, - float sampleRateRatio ) - { - super( TapType.STREAM_COMPLEX, name, delay, sampleRateRatio ); - } - - public ComplexTap( TapType tapType, String name, int delay, float sampleRateRatio ) - { - super( tapType, name, delay, sampleRateRatio ); - } - - @Override - public void receive( Complex sample ) - { - if( mListener != null ) - { - mListener.receive( sample ); - } - - for( TapListener listener: mListeners ) - { - listener.receive( sample ); - } - } - - public void setListener( Listener listener ) - { - mListener = listener; - } - - public void removeListener( Listener listener ) - { - mListener = null; - } -} diff --git a/src/main/java/io/github/dsheirer/instrument/tap/stream/ComplexTapViewPanel.java b/src/main/java/io/github/dsheirer/instrument/tap/stream/ComplexTapViewPanel.java deleted file mode 100644 index bf10ed3c..00000000 --- a/src/main/java/io/github/dsheirer/instrument/tap/stream/ComplexTapViewPanel.java +++ /dev/null @@ -1,150 +0,0 @@ -/******************************************************************************* - * SDR Trunk - * Copyright (C) 2014 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 io.github.dsheirer.instrument.tap.stream; - -import io.github.dsheirer.gui.SDRTrunk; -import io.github.dsheirer.instrument.gui.SampleModel; -import io.github.dsheirer.instrument.tap.Tap; -import io.github.dsheirer.instrument.tap.TapViewPanel; -import io.github.dsheirer.sample.complex.Complex; -import org.slf4j.Logger; -import org.slf4j.LoggerFactory; - -import java.awt.*; -import java.awt.geom.Path2D; -import java.awt.geom.Point2D; -import java.util.List; -import java.util.Observable; - -public class ComplexTapViewPanel extends TapViewPanel -{ - private static final long serialVersionUID = 1L; - private final static Logger mLog = LoggerFactory.getLogger( SDRTrunk.class ); - - private Tap mTap; - private List mSamples; - private int mSampleCount; - - public ComplexTapViewPanel( ComplexTap tap ) - { - super( new SampleModel(), tap.getName() ); - - setSize( new Dimension( 400, 400 ) ); - - mTap = tap; - mTap.addListener( getModel() ); - - mSampleCount = (int)( 2000 * tap.getSampleRateRatio() ); - - getModel().setSampleCount( mSampleCount ); - getModel().setDelay( tap.getDelay() ); - - mLog.info( "Complex Tap Panel [" + tap.getName() + "] count: " + - getModel().getSampleCount() + " delay:" + getModel().getDelay() ); - } - - @Override - public void update( Observable arg0, Object arg1 ) - { - mSamples = (List)getModel().getSamples(); - repaint(); - } - - public void paintComponent( Graphics g ) - { - super.paintComponent( g ); - - Graphics2D g2 = (Graphics2D)g; - - int height = getHeight(); - int width = getWidth(); - - g2.setColor( Color.DARK_GRAY ); - - float middle = (float)height / 4.0f; - - g2.drawLine( 0, (int)middle, width, (int)middle ); - g2.drawLine( 0, (int)( 3 * middle ), width, (int)( 3 * middle ) ); - - if( mSamples != null && - mSamples.size() > 0 ) - { - g2.setColor( getForeground() ); - - g2.drawString( mLabel, 5 , 15 ); - - Path2D.Float polylineReal = - new Path2D.Float( Path2D.Float.WIND_EVEN_ODD, mSamples.size() ); - Path2D.Float polylineImag = - new Path2D.Float( Path2D.Float.WIND_EVEN_ODD, mSamples.size() ); - - /* Move to the first point */ - Point2D.Float firstReal = resolve( 0, mSamples.get( 0 ), Type.REAL ); - polylineReal.moveTo( firstReal.x, firstReal.y ); - - Point2D.Float firstImag = resolve( 0, mSamples.get( 0 ), Type.IMAGINERY ); - polylineImag.moveTo( firstImag.x, firstImag.y ); - - for( int x = 1; x < mSamples.size(); x++ ) - { - Point2D.Float pointReal = resolve( x, mSamples.get( x ), Type.REAL ); - polylineReal.lineTo( pointReal.x, pointReal.y ); - - Point2D.Float pointImag = resolve( x, mSamples.get( x ), Type.IMAGINERY ); - polylineImag.lineTo( pointImag.x, pointImag.y ); - -// mLog.debug( "Imag y:" + pointImag.getY() + " height:" + getHeight() ); - } - - g2.draw( polylineReal ); - g2.draw( polylineImag ); - } - - g2.dispose(); - } - - - private Point2D.Float resolve( int index, Complex sample, Type type ) - { - int middle = (int)( getHeight() / 4 ); - - float y = middle - ( middle * mVerticalZoom * ( type == Type.REAL ? - sample.real() : sample.imaginary()) ); - - if( type == Type.IMAGINERY ) - { - y += ( middle * 2 ); - } - - /* Clip y to zero as necessary */ - y = ( y < 0 ) ? 0 : y; - - /* Clip y to max height as necessary */ - y = ( y > getHeight() ) ? getHeight() : y; - - float indexWidth = (float)getWidth() / (float)mSampleCount; - - float x = (float)index * indexWidth; - - Point2D.Float point = new Point2D.Float( x, y ); - - return new Point2D.Float( x , y ); - } - - public enum Type{ REAL,IMAGINERY }; -} diff --git a/src/main/java/io/github/dsheirer/instrument/tap/stream/DibitTap.java b/src/main/java/io/github/dsheirer/instrument/tap/stream/DibitTap.java deleted file mode 100644 index dcd30186..00000000 --- a/src/main/java/io/github/dsheirer/instrument/tap/stream/DibitTap.java +++ /dev/null @@ -1,60 +0,0 @@ -/******************************************************************************* - * SDR Trunk - * Copyright (C) 2014 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 io.github.dsheirer.instrument.tap.stream; - -import io.github.dsheirer.dsp.symbol.Dibit; -import io.github.dsheirer.instrument.tap.TapListener; -import io.github.dsheirer.instrument.tap.TapType; -import io.github.dsheirer.sample.Listener; - -public class DibitTap extends StreamTap implements Listener - -{ - private Listener mListener; - - public DibitTap( String name, - int delay, - float sampleRateRatio ) - { - super( TapType.STREAM_DIBIT, name, delay, sampleRateRatio ); - } - - @Override - public void receive( Dibit dibit ) - { - if( mListener != null ) - { - mListener.receive( dibit ); - } - - for( TapListener listener: mListeners ) - { - listener.receive( dibit ); - } - } - - public void setListener( Listener listener ) - { - mListener = listener; - } - - public void removeListener( Listener listener ) - { - mListener = null; - } -} diff --git a/src/main/java/io/github/dsheirer/instrument/tap/stream/DibitTapViewPanel.java b/src/main/java/io/github/dsheirer/instrument/tap/stream/DibitTapViewPanel.java deleted file mode 100644 index 7a53e622..00000000 --- a/src/main/java/io/github/dsheirer/instrument/tap/stream/DibitTapViewPanel.java +++ /dev/null @@ -1,186 +0,0 @@ -/******************************************************************************* - * SDR Trunk - * Copyright (C) 2014 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 io.github.dsheirer.instrument.tap.stream; - -import io.github.dsheirer.dsp.symbol.Dibit; -import io.github.dsheirer.instrument.gui.SampleModel; -import io.github.dsheirer.instrument.tap.Tap; -import io.github.dsheirer.instrument.tap.TapViewPanel; - -import java.awt.*; -import java.awt.geom.Path2D; -import java.awt.geom.Rectangle2D; -import java.util.List; -import java.util.Observable; - -public class DibitTapViewPanel extends TapViewPanel -{ - private static final long serialVersionUID = 1L; - - private Tap mTap; - private List mSamples; - private int mSampleCount; - private int mOffset = 0; - - public DibitTapViewPanel( DibitTap tap ) - { - super( new SampleModel(), tap.getName() ); - - mTap = tap; - mTap.addListener( getModel() ); - - getModel().setSampleCount( - (int)( getModel().getSampleCount() * tap.getSampleRateRatio() ) ); - - getModel().setDelay( tap.getDelay() ); - } - - @Override - public void update( Observable arg0, Object arg1 ) - { - mSamples = (List)getModel().getSamples(); - mSampleCount = getModel().getSampleCount(); - repaint(); - } - - public void paintComponent( Graphics g ) - { - super.paintComponent( g ); - - Graphics2D g2 = (Graphics2D)g; - - int height = getHeight(); - int width = getWidth(); - - g2.setColor( Color.DARK_GRAY ); - - float middle = (float)height / 2.0f; - - g2.drawLine( 0, (int)middle, width, (int)middle ); - - if( mSamples != null && mSamples.size() > 0 ) - { - g2.setColor( getForeground() ); - - g2.drawString( mLabel, 5 , 15 ); - - for( int x = 1; x < mSamples.size(); x++ ) - { - paintDibit( g, x, mSamples.get( x ) ); - } - } - - g2.dispose(); - } - - /** - * Paints a polyline representing a boolean value with a 0 or 1 label in - * the middle of it - * - * @param graphics - * @param index - index of the sample int the sample buffer - * @param value - bit value, 0 or 1 - */ - private void paintDibit( Graphics graphics, int index, Dibit value ) - { - Graphics2D g2 = (Graphics2D)graphics; - - Path2D.Float polyline = - new Path2D.Float( Path2D.Float.WIND_EVEN_ODD, mSamples.size() ); - - float middleY = ( (float)getHeight() / 2.0f ); - - float indexWidth = (float)( getWidth() ) / (float)mSampleCount; - - float startX = (float)index * indexWidth; - float stopX = (float)(index + 1 ) * indexWidth; - float fivePercent = indexWidth * .05f; - float leftX = startX + fivePercent; - float rightX = stopX - fivePercent; - - /* Start */ - polyline.moveTo( startX, middleY ); - - /* Left horizonal segment */ - polyline.lineTo( leftX, middleY ); - - float y = middleY; - - switch( value ) - { - case D01_PLUS_3: - y = middleY - ( middleY * 0.75f); - break; - case D00_PLUS_1: - y = middleY - ( middleY * 0.25f); - break; - case D10_MINUS_1: - y = middleY + ( middleY * 0.25f); - break; - case D11_MINUS_3: - y = middleY + ( middleY * 0.75f); - break; - } - - /* Left vertical segment */ - polyline.lineTo( leftX, y ); - - /* Middle horizontal segment */ - polyline.lineTo( rightX, y ); - - /* Right vertical segment */ - polyline.lineTo( rightX, middleY ); - - /* Right horizontal segment */ - polyline.lineTo( stopX, middleY ); - - g2.draw( polyline ); - - /* Draw the bit value label */ - String bit = null; - - switch( value ) - { - case D00_PLUS_1: - bit = "+1"; - break; - case D01_PLUS_3: - bit = "+3"; - break; - case D10_MINUS_1: - bit = "-1"; - break; - case D11_MINUS_3: - bit = "-3"; - break; - } - - FontMetrics fontMetrics = graphics.getFontMetrics( this.getFont() ); - - Rectangle2D label = fontMetrics.getStringBounds( bit, graphics ); - - float offsetX = (float)label.getWidth() / 2.0f; - float offsetY = (float)label.getWidth() / 2.0f; - - float middleX = startX + ( indexWidth / 2.0f ); - - graphics.drawString( bit, - (int)( middleX - offsetX ), - (int)( middleY - offsetY ) ); - } -} diff --git a/src/main/java/io/github/dsheirer/instrument/tap/stream/EyeDiagramData.java b/src/main/java/io/github/dsheirer/instrument/tap/stream/EyeDiagramData.java deleted file mode 100644 index 3006c011..00000000 --- a/src/main/java/io/github/dsheirer/instrument/tap/stream/EyeDiagramData.java +++ /dev/null @@ -1,46 +0,0 @@ -package io.github.dsheirer.instrument.tap.stream; - - -public class EyeDiagramData -{ - private float[] mSamplesInphase; - private float[] mSamplesQuadrature; - private float mLeftPoint; - private float mRightPoint; - private float mError; - - public EyeDiagramData( float[] samplesInphase, float[] samplesQuadrature, - float leftPoint, float rightPoint, float error ) - { - mSamplesInphase = samplesInphase; - mSamplesQuadrature = samplesQuadrature; - mLeftPoint = leftPoint; - mRightPoint = rightPoint; - mError = error; - } - - public float[] getInphaseSamples() - { - return mSamplesInphase; - } - - public float[] getQuadratureSamples() - { - return mSamplesQuadrature; - } - - public float getLeftPoint() - { - return mLeftPoint; - } - - public float getRightPoint() - { - return mRightPoint; - } - - public float getError() - { - return mError; - } -} diff --git a/src/main/java/io/github/dsheirer/instrument/tap/stream/EyeDiagramDataTap.java b/src/main/java/io/github/dsheirer/instrument/tap/stream/EyeDiagramDataTap.java deleted file mode 100644 index 4278e469..00000000 --- a/src/main/java/io/github/dsheirer/instrument/tap/stream/EyeDiagramDataTap.java +++ /dev/null @@ -1,60 +0,0 @@ -/******************************************************************************* - * 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 - ******************************************************************************/ -package io.github.dsheirer.instrument.tap.stream; - -import io.github.dsheirer.instrument.tap.TapListener; -import io.github.dsheirer.instrument.tap.TapType; -import io.github.dsheirer.sample.Listener; -import org.slf4j.Logger; -import org.slf4j.LoggerFactory; - -public class EyeDiagramDataTap extends StreamTap implements Listener -{ - private final static Logger mLog = LoggerFactory.getLogger( EyeDiagramDataTap.class ); - - private Listener mListener; - - public EyeDiagramDataTap( String name, int delay, float sampleRate ) - { - super( TapType.STREAM_EYE_DIAGRAM, name, delay, sampleRate ); - } - - @Override - public void receive( EyeDiagramData data ) - { - if( mListener != null ) - { - mListener.receive( data ); - } - - for( TapListener listener: mListeners ) - { - listener.receive( data ); - } - } - - public void setListener( Listener listener ) - { - mListener = listener; - } - - public void removeListener( Listener listener ) - { - mListener = null; - } -} diff --git a/src/main/java/io/github/dsheirer/instrument/tap/stream/EyeDiagramDataTapPanel.java b/src/main/java/io/github/dsheirer/instrument/tap/stream/EyeDiagramDataTapPanel.java deleted file mode 100644 index 530da620..00000000 --- a/src/main/java/io/github/dsheirer/instrument/tap/stream/EyeDiagramDataTapPanel.java +++ /dev/null @@ -1,221 +0,0 @@ -/******************************************************************************* - * SDR Trunk - * Copyright (C) 2014 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 io.github.dsheirer.instrument.tap.stream; - -import io.github.dsheirer.instrument.gui.SampleModel; -import io.github.dsheirer.instrument.tap.TapViewPanel; -import io.github.dsheirer.sample.complex.Complex; -import org.slf4j.Logger; -import org.slf4j.LoggerFactory; - -import java.awt.*; -import java.awt.geom.GeneralPath; -import java.awt.geom.Path2D; -import java.util.List; -import java.util.Observable; - -public class EyeDiagramDataTapPanel extends TapViewPanel -{ - private static final long serialVersionUID = 1L; - private final static Logger mLog = - LoggerFactory.getLogger( EyeDiagramDataTapPanel.class ); - - private EyeDiagramDataTap mTap; - private List mSamples; - - public EyeDiagramDataTapPanel( EyeDiagramDataTap tap ) - { - super( new SampleModel(), tap.getName() ); - - mTap = tap; - mTap.addListener( getModel() ); - - getModel().setSampleCount( 15 ); - getModel().setDelay( tap.getDelay() ); - } - - @Override - public void update( Observable arg0, Object arg1 ) - { - mSamples = (List)getModel().getSamples(); - - repaint(); - } - - public void paintComponent( Graphics g ) - { - super.paintComponent( g ); - - Graphics2D g2 = (Graphics2D)g; - - float third = getWidth() / 3.0f; - float two_thirds = 2.0f * third; - float halfHeight = getHeight() / 2.0f; - - float xAxisIncrement = third / 20.0f; - - if( mSamples != null && mSamples.size() > 0 ) - { - /* Draw each of the sample sets */ - for( int z = 0; z < mSamples.size(); z++ ) - { - if( z == mSamples.size() - 1 ) - { - g2.setColor( Color.BLUE ); - } - else if( z > ( mSamples.size() * .75 ) ) - { - g2.setColor( Color.DARK_GRAY ); - } - else - { - g2.setColor( Color.LIGHT_GRAY ); - } - - EyeDiagramData data = mSamples.get( z ); - - float[] inphaseSamples = data.getInphaseSamples(); - float[] quadratureSamples = data.getQuadratureSamples(); - - /* Draw the sample set containing two symbol periods of data */ - if( inphaseSamples.length >= 1 ) - { - Path2D.Float iLine = new Path2D.Float( GeneralPath.WIND_EVEN_ODD, 20 ); - Path2D.Float qLine = new Path2D.Float( GeneralPath.WIND_EVEN_ODD, 20 ); - Path2D.Float mLine = new Path2D.Float( GeneralPath.WIND_EVEN_ODD, 20 ); - - float xAxis = 0; - - iLine.moveTo( xAxis, getSampleHeight( halfHeight, inphaseSamples[ 0 ] ) ); - qLine.moveTo( xAxis + third, getSampleHeight( halfHeight, quadratureSamples[ 0 ] ) ); - mLine.moveTo( xAxis + ( two_thirds ), getMagnitudeHeight( Complex.magnitude( inphaseSamples[ 0 ], quadratureSamples[ 0 ] ) ) ); - - for( int x = 1; x < inphaseSamples.length; x++ ) - { - xAxis += xAxisIncrement; - - iLine.lineTo( xAxis, getSampleHeight( halfHeight, inphaseSamples[ x ] ) ); - qLine.lineTo( xAxis + third, getSampleHeight( halfHeight, quadratureSamples[ x ] ) ); - mLine.lineTo( xAxis + two_thirds, getMagnitudeHeight( Complex.magnitude( inphaseSamples[ x ], quadratureSamples[ x ] ) ) ); - } - - g2.draw( iLine ); - g2.draw( qLine ); - g2.draw( mLine ); - - /* Left Sample line */ - if( z == mSamples.size() - 1 ) - { - g2.setColor( Color.GREEN ); - } - else - { - g2.setColor( Color.LIGHT_GRAY ); - } - xAxis = xAxisIncrement * data.getLeftPoint(); - g2.drawLine( (int)xAxis, 5, (int)xAxis, getHeight() - 5 ); - g2.drawLine( (int)( xAxis + third ), 5, (int)( xAxis + third ), getHeight() - 5 ); - g2.drawLine( (int)( xAxis + two_thirds ), 5, (int)( xAxis + two_thirds ), getHeight() - 5 ); - - /* Right Sample line */ - if( z == mSamples.size() - 1 ) - { - g2.setColor( Color.GREEN ); - } - else - { - g2.setColor( Color.LIGHT_GRAY ); - } - xAxis = xAxisIncrement * data.getRightPoint(); - g2.drawLine( (int)xAxis, 5, (int)xAxis, getHeight() - 5 ); - g2.drawLine( (int)( xAxis + third ), 5, (int)( xAxis + third ), getHeight() - 5 ); - g2.drawLine( (int)( xAxis + two_thirds ), 5, (int)( xAxis + two_thirds ), getHeight() - 5 ); - - /* Error indicator */ - float error = data.getError(); - - if( z == mSamples.size() - 1 ) - { - g2.setColor( Color.BLACK ); - - float xFrom; - float xTo; - - if( error < 0.0f ) - { - xFrom = (int)( xAxisIncrement * data.getLeftPoint() ); - xTo = xFrom - 25.0f; - } - else - { - xFrom = (int)( xAxisIncrement * data.getRightPoint() ); - xTo = xFrom + 25.0f; - } - - g2.drawLine( (int)xFrom, (int)halfHeight - 15, (int)xTo, (int)halfHeight ); - g2.drawLine( (int)xFrom, (int)halfHeight + 15, (int)xTo, (int)halfHeight ); - g2.drawLine( (int)( xFrom + third ), (int)halfHeight - 15, (int)( xTo + third ), (int)halfHeight ); - g2.drawLine( (int)( xFrom + third ), (int)halfHeight + 15, (int)( xTo + third ), (int)halfHeight ); - g2.drawLine( (int)( xFrom + two_thirds ), (int)halfHeight - 15, (int)( xTo + two_thirds ), (int)halfHeight ); - g2.drawLine( (int)( xFrom + two_thirds ), (int)halfHeight + 15, (int)( xTo + two_thirds ), (int)halfHeight ); - } - - - - - } - } - } - - g2.setColor( Color.BLUE ); - g2.drawString( "Eye Diagram - I Leg", 5, 15 ); - g2.drawString( "Eye Diagram - Q Leg", third, 15 ); - g2.drawString( "Eye Diagram - Magnitude", two_thirds, 15 ); - - - /* Horizontal middle line */ - g2.setColor( Color.CYAN ); - g2.drawLine( 0, (int)halfHeight, getWidth(), (int)halfHeight ); - - g2.dispose(); - } - - private float getSampleHeight( float halfHeight, float value ) - { - if( value > 1.0 ) - { - value = 1.0f; - } - if( value < -1.0 ) - { - value = -1.0f; - } - - return halfHeight + ( .95f * ( halfHeight * value ) ); - } - - private float getMagnitudeHeight( float magnitude ) - { - if( magnitude > 1.0 ) - { - magnitude = 1.0f; - } - - return getHeight() - ( getHeight() * magnitude ); - } -} diff --git a/src/main/java/io/github/dsheirer/instrument/tap/stream/FloatBufferTap.java b/src/main/java/io/github/dsheirer/instrument/tap/stream/FloatBufferTap.java deleted file mode 100644 index 8e038c5a..00000000 --- a/src/main/java/io/github/dsheirer/instrument/tap/stream/FloatBufferTap.java +++ /dev/null @@ -1,43 +0,0 @@ -package io.github.dsheirer.instrument.tap.stream; - -import io.github.dsheirer.sample.Listener; -import io.github.dsheirer.sample.real.RealBuffer; -import org.slf4j.Logger; -import org.slf4j.LoggerFactory; - -public class FloatBufferTap extends FloatTap implements Listener -{ - private final static Logger mLog = - LoggerFactory.getLogger( FloatBufferTap.class ); - - private Listener mRealBufferListener; - - public FloatBufferTap( String name, int delay, float sampleRateRatio ) - { - super( name, delay, sampleRateRatio ); - } - - @Override - public void receive( RealBuffer buffer ) - { - if( mRealBufferListener != null ) - { - mRealBufferListener.receive( buffer ); - } - - for( float sample: buffer.getSamples() ) - { - super.receive( sample ); - } - } - - public void setListener( Listener listener ) - { - mRealBufferListener = listener; - } - - public void removeListener( Listener listener ) - { - mRealBufferListener = null; - } -} diff --git a/src/main/java/io/github/dsheirer/instrument/tap/stream/FloatTap.java b/src/main/java/io/github/dsheirer/instrument/tap/stream/FloatTap.java deleted file mode 100644 index 156ce76f..00000000 --- a/src/main/java/io/github/dsheirer/instrument/tap/stream/FloatTap.java +++ /dev/null @@ -1,58 +0,0 @@ -/******************************************************************************* - * SDR Trunk - * Copyright (C) 2014 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 io.github.dsheirer.instrument.tap.stream; - -import io.github.dsheirer.instrument.tap.TapListener; -import io.github.dsheirer.instrument.tap.TapType; -import io.github.dsheirer.sample.real.RealSampleListener; - -public class FloatTap extends StreamTap implements RealSampleListener -{ - private RealSampleListener mFloatTapListener; - - public FloatTap( String name, - int delay, - float sampleRateRatio ) - { - super( TapType.STREAM_FLOAT, name, delay, sampleRateRatio ); - } - - @Override - public void receive( float sample ) - { - if( mFloatTapListener != null ) - { - mFloatTapListener.receive( sample ); - } - - for( TapListener listener: mListeners ) - { - listener.receive( sample ); - } - } - - public void setListener( RealSampleListener listener ) - { - mFloatTapListener = listener; - } - - public void removeListener( RealSampleListener listener ) - { - mFloatTapListener = null; - } -} diff --git a/src/main/java/io/github/dsheirer/instrument/tap/stream/FloatTapPhaseViewPanel.java b/src/main/java/io/github/dsheirer/instrument/tap/stream/FloatTapPhaseViewPanel.java deleted file mode 100644 index 7810684e..00000000 --- a/src/main/java/io/github/dsheirer/instrument/tap/stream/FloatTapPhaseViewPanel.java +++ /dev/null @@ -1,129 +0,0 @@ -/******************************************************************************* - * SDR Trunk - * Copyright (C) 2014 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 io.github.dsheirer.instrument.tap.stream; - -import io.github.dsheirer.instrument.gui.SampleModel; -import io.github.dsheirer.instrument.tap.Tap; -import io.github.dsheirer.instrument.tap.TapViewPanel; -import org.slf4j.Logger; -import org.slf4j.LoggerFactory; - -import java.awt.*; -import java.awt.geom.Path2D; -import java.awt.geom.Point2D; -import java.util.List; -import java.util.Observable; - -public class FloatTapPhaseViewPanel extends TapViewPanel -{ - private final static Logger mLog = - LoggerFactory.getLogger( FloatTapPhaseViewPanel.class ); - - private static final long serialVersionUID = 1L; - private Tap mTap; - private List mSamples; - private int mSampleCount; - - public FloatTapPhaseViewPanel( FloatTap tap ) - { - super( new SampleModel(), tap.getName() ); - - mTap = tap; - mTap.addListener( getModel() ); - - mSampleCount = (int)( 2000 * tap.getSampleRateRatio() ); - - getModel().setSampleCount( mSampleCount ); - getModel().setDelay( tap.getDelay() ); - - mLog.info( "Float Tap Panel [" + tap.getName() + "] count: " + - getModel().getSampleCount() + " delay:" + getModel().getDelay() ); - } - - @Override - public void update( Observable arg0, Object arg1 ) - { - mSamples = (List)getModel().getSamples(); - repaint(); - } - - public void paintComponent( Graphics g ) - { - super.paintComponent( g ); - - Graphics2D g2 = (Graphics2D)g; - - int height = getHeight(); - int width = getWidth(); - - g2.setColor( Color.DARK_GRAY ); - - float middle = (float)height / 2.0f; - - g2.drawLine( 0, (int)middle, width, (int)middle ); - - if( mSamples != null && - mSamples.size() > 0 ) - { - g2.setColor( getForeground() ); - - g2.drawString( mLabel, 5 , 15 ); - - Path2D.Float polyline = - new Path2D.Float( Path2D.Float.WIND_EVEN_ODD, mSamples.size() ); - - /* Move to the first point */ - Point2D.Float first = resolve( 0, mSamples.get( 0 ) ); - polyline.moveTo( first.x, first.y ); - - for( int x = 1; x < mSamples.size(); x++ ) - { - Point2D.Float point = resolve( x, mSamples.get( x ) ); - - polyline.lineTo( point.x, point.y ); - } - - g2.draw(polyline); - } - - g2.dispose(); - } - - private Point2D.Float resolve( int index, float value ) - { - int middle = (int)( getHeight() / 2 ); - - double scaledValue = Math.asin( value ); - - float y = middle - (float)( middle * scaledValue * mVerticalZoom ); - - /* Clip y to zero as necessary */ - y = ( y < 0 ) ? 0 : y; - - /* Clip y to max height as necessary */ - y = ( y > getHeight() ) ? getHeight() : y; - - float indexWidth = (float)getWidth() / (float)mSampleCount; - - float x = (float)index * indexWidth; - - Point2D.Float point = new Point2D.Float( x, y ); - - return new Point2D.Float( x , y ); - } -} diff --git a/src/main/java/io/github/dsheirer/instrument/tap/stream/FloatTapViewPanel.java b/src/main/java/io/github/dsheirer/instrument/tap/stream/FloatTapViewPanel.java deleted file mode 100644 index b89eeafb..00000000 --- a/src/main/java/io/github/dsheirer/instrument/tap/stream/FloatTapViewPanel.java +++ /dev/null @@ -1,127 +0,0 @@ -/******************************************************************************* - * SDR Trunk - * Copyright (C) 2014 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 io.github.dsheirer.instrument.tap.stream; - -import io.github.dsheirer.gui.SDRTrunk; -import io.github.dsheirer.instrument.gui.SampleModel; -import io.github.dsheirer.instrument.tap.Tap; -import io.github.dsheirer.instrument.tap.TapViewPanel; -import org.slf4j.Logger; -import org.slf4j.LoggerFactory; - -import java.awt.*; -import java.awt.geom.Path2D; -import java.awt.geom.Point2D; -import java.util.List; -import java.util.Observable; - -public class FloatTapViewPanel extends TapViewPanel -{ - private static final long serialVersionUID = 1L; - private final static Logger mLog = LoggerFactory.getLogger( SDRTrunk.class ); - - private Tap mTap; - private List mSamples; - private int mSampleCount; - - public FloatTapViewPanel( FloatTap tap ) - { - super( new SampleModel(), tap.getName() ); - - mTap = tap; - mTap.addListener( getModel() ); - - mSampleCount = (int)( 2000 * tap.getSampleRateRatio() ); - - getModel().setSampleCount( mSampleCount ); - getModel().setDelay( tap.getDelay() ); - - mLog.info( "Float Tap Panel [" + tap.getName() + "] count: " + - getModel().getSampleCount() + " delay:" + getModel().getDelay() ); - } - - @Override - public void update( Observable arg0, Object arg1 ) - { - mSamples = (List)getModel().getSamples(); - repaint(); - } - - public void paintComponent( Graphics g ) - { - super.paintComponent( g ); - - Graphics2D g2 = (Graphics2D)g; - - int height = getHeight(); - int width = getWidth(); - - g2.setColor( Color.DARK_GRAY ); - - float middle = (float)height / 2.0f; - - g2.drawLine( 0, (int)middle, width, (int)middle ); - - if( mSamples != null && - mSamples.size() > 0 ) - { - g2.setColor( getForeground() ); - - g2.drawString( mLabel, 5 , 15 ); - - Path2D.Float polyline = - new Path2D.Float( Path2D.Float.WIND_EVEN_ODD, mSamples.size() ); - - /* Move to the first point */ - Point2D.Float first = resolve( 0, mSamples.get( 0 ) ); - polyline.moveTo( first.x, first.y ); - - for( int x = 1; x < mSamples.size(); x++ ) - { - Point2D.Float point = resolve( x, mSamples.get( x ) ); - - polyline.lineTo( point.x, point.y ); - } - - g2.draw(polyline); - } - - g2.dispose(); - } - - private Point2D.Float resolve( int index, float value ) - { - int middle = (int)( getHeight() / 2 ); - - float y = middle - ( middle * value * mVerticalZoom ); - - /* Clip y to zero as necessary */ - y = ( y < 0 ) ? 0 : y; - - /* Clip y to max height as necessary */ - y = ( y > getHeight() ) ? getHeight() : y; - - float indexWidth = (float)getWidth() / (float)mSampleCount; - - float x = (float)index * indexWidth; - - Point2D.Float point = new Point2D.Float( x, y ); - - return new Point2D.Float( x , y ); - } -} diff --git a/src/main/java/io/github/dsheirer/instrument/tap/stream/GainFloatTap.java b/src/main/java/io/github/dsheirer/instrument/tap/stream/GainFloatTap.java deleted file mode 100644 index cc75bc69..00000000 --- a/src/main/java/io/github/dsheirer/instrument/tap/stream/GainFloatTap.java +++ /dev/null @@ -1,40 +0,0 @@ -/******************************************************************************* - * SDR Trunk - * Copyright (C) 2014 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 io.github.dsheirer.instrument.tap.stream; - - -public class GainFloatTap extends FloatTap -{ - private float mGain; - - public GainFloatTap( String name, - int delay, - float sampleRateRatio, - float gain ) - { - super( name, delay, sampleRateRatio ); - - mGain = gain; - } - - @Override - public void receive( float sample ) - { - super.receive( sample * mGain ); - } -} diff --git a/src/main/java/io/github/dsheirer/instrument/tap/stream/QPSKTap.java b/src/main/java/io/github/dsheirer/instrument/tap/stream/QPSKTap.java deleted file mode 100644 index 7af8728b..00000000 --- a/src/main/java/io/github/dsheirer/instrument/tap/stream/QPSKTap.java +++ /dev/null @@ -1,11 +0,0 @@ -package io.github.dsheirer.instrument.tap.stream; - -import io.github.dsheirer.instrument.tap.TapType; - -public class QPSKTap extends ComplexTap -{ - public QPSKTap( String name, int delay, float sampleRateRatio ) - { - super( TapType.STREAM_QPSK, name, delay, sampleRateRatio ); - } -} diff --git a/src/main/java/io/github/dsheirer/instrument/tap/stream/QPSKTapPanel.java b/src/main/java/io/github/dsheirer/instrument/tap/stream/QPSKTapPanel.java deleted file mode 100644 index 6836ebdb..00000000 --- a/src/main/java/io/github/dsheirer/instrument/tap/stream/QPSKTapPanel.java +++ /dev/null @@ -1,157 +0,0 @@ -/******************************************************************************* - * SDR Trunk - * Copyright (C) 2014 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 io.github.dsheirer.instrument.tap.stream; - -import io.github.dsheirer.gui.SDRTrunk; -import io.github.dsheirer.instrument.gui.SampleModel; -import io.github.dsheirer.instrument.tap.Tap; -import io.github.dsheirer.instrument.tap.TapViewPanel; -import io.github.dsheirer.sample.complex.Complex; -import org.slf4j.Logger; -import org.slf4j.LoggerFactory; - -import java.awt.*; -import java.awt.geom.Ellipse2D; -import java.util.List; -import java.util.Observable; - -public class QPSKTapPanel extends TapViewPanel -{ - private static final long serialVersionUID = 1L; - private final static Logger mLog = LoggerFactory.getLogger( SDRTrunk.class ); - - private Tap mTap; - private List mSamples; - private int mSampleCount; - - public QPSKTapPanel( ComplexTap tap ) - { - super( new SampleModel(), tap.getName() ); - - mTap = tap; - mTap.addListener( getModel() ); - - mSampleCount = (int)( 60 * tap.getSampleRateRatio() ); - - getModel().setSampleCount( mSampleCount ); - getModel().setDelay( tap.getDelay() ); - - mLog.info( "Complex Tap Panel [" + tap.getName() + "] count: " + - getModel().getSampleCount() + " delay:" + getModel().getDelay() ); - } - - @Override - public void update( Observable arg0, Object arg1 ) - { - mSamples = (List)getModel().getSamples(); - repaint(); - } - - public void paintComponent( Graphics g ) - { - super.paintComponent( g ); - - Graphics2D g2 = (Graphics2D)g; - - int min = ( getHeight() < getWidth() ? getHeight() : getWidth() ); - - float middle = (float)min / 2.0f; - - g2.setColor( Color.DARK_GRAY ); - - /* Vertical Line */ - g2.drawLine( getWidth() / 2, 5, getWidth() / 2, getHeight() - 5 ); - - /* Horizontal Line */ - g2.drawLine( getWidth() / 2 - (int)( middle ), getHeight() / 2, - getWidth() / 2 + (int)( middle ), getHeight() / 2 ); - - g2.setColor( Color.CYAN ); - - /* Crossed Lines */ - g2.drawLine( getWidth() / 2 - (int)( middle ), 5, - getWidth() / 2 + (int)( middle ), getHeight() - 5 ); - g2.drawLine( getWidth() / 2 + (int)( middle ), 5, - getWidth() / 2 - (int)( middle ), getHeight() - 5 ); - - - - if( mSamples != null && - mSamples.size() > 0 ) - { - g2.drawString( mLabel, 5 , 15 ); - - /* Set color to light gray for oldest 90% of constellations */ - g2.setColor( Color.LIGHT_GRAY ); - - int counter = 0; - - int threshold = (int)( mSamples.size() * 0.9f ); - - for( Complex sample: mSamples ) - { - /* Set color to foreground for newest 10% of constellations */ - if( counter == threshold ) - { - g2.setColor( getForeground() ); - } - -// float scale; -// -// if( sample.real() != 0 && sample.imaginary() != 0 ) -// { -// scale = 1.0f / sample.magnitude(); -// } -// else if( sample.real() == 0 ) -// { -// scale = 1.0f / sample.imaginary(); -// } -// else -// { -// scale = 1.0f / sample.real(); -// } - -// float x = ( getWidth() / 2 ) + -// ( scale * sample.real() * middle * 0.75f * mVerticalZoom ); - float x = ( getWidth() / 2 ) + - ( sample.real() * middle * 0.75f * mVerticalZoom ); - - if( Float.isNaN( x ) ) - { - x = getWidth() / 2; - } - -// float y = ( getHeight() / 2 ) + -// ( scale * sample.imaginary() * middle * 0.75f * mVerticalZoom ); - float y = ( getHeight() / 2 ) + - ( sample.imaginary() * middle * 0.75f * mVerticalZoom ); - - if( Float.isNaN( y ) ) - { - y = getHeight() / 2; - } - - g2.draw( new Ellipse2D.Float( x, y, 4.0f, 4.0f ) ); - - counter++; - } - } - - g2.dispose(); - } -} diff --git a/src/main/java/io/github/dsheirer/instrument/tap/stream/StreamTap.java b/src/main/java/io/github/dsheirer/instrument/tap/stream/StreamTap.java deleted file mode 100644 index 607fc994..00000000 --- a/src/main/java/io/github/dsheirer/instrument/tap/stream/StreamTap.java +++ /dev/null @@ -1,41 +0,0 @@ -/******************************************************************************* - * SDR Trunk - * Copyright (C) 2014 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 io.github.dsheirer.instrument.tap.stream; - -import io.github.dsheirer.instrument.tap.Tap; -import io.github.dsheirer.instrument.tap.TapType; - -public abstract class StreamTap extends Tap -{ - protected float mSampleRateRatio; - - public StreamTap( TapType type, - String name, - int delay, - float sampleRate ) - { - super( type, name, delay ); - - mSampleRateRatio = sampleRate; - } - - public float getSampleRateRatio() - { - return mSampleRateRatio; - } -} diff --git a/src/main/java/io/github/dsheirer/instrument/tap/stream/SymbolEventTap.java b/src/main/java/io/github/dsheirer/instrument/tap/stream/SymbolEventTap.java deleted file mode 100644 index 8954be23..00000000 --- a/src/main/java/io/github/dsheirer/instrument/tap/stream/SymbolEventTap.java +++ /dev/null @@ -1,58 +0,0 @@ -/******************************************************************************* - * SDR Trunk - * Copyright (C) 2014 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 io.github.dsheirer.instrument.tap.stream; - -import io.github.dsheirer.dsp.symbol.SymbolEvent; -import io.github.dsheirer.instrument.tap.TapListener; -import io.github.dsheirer.instrument.tap.TapType; -import io.github.dsheirer.sample.Listener; - -public class SymbolEventTap extends StreamTap implements Listener -{ - private Listener mListener; - - public SymbolEventTap( String name, int delay, float sampleRate ) - { - super( TapType.STREAM_SYMBOL, name, delay, sampleRate ); - } - - @Override - public void receive( SymbolEvent symbolEvent ) - { - if( mListener != null ) - { - mListener.receive( symbolEvent ); - } - - for( TapListener listener: mListeners ) - { - listener.receive( symbolEvent ); - } - } - - public void setListener( Listener listener ) - { - mListener = listener; - } - - public void removeListener( Listener listener ) - { - mListener = null; - } - -} diff --git a/src/main/java/io/github/dsheirer/instrument/tap/stream/SymbolEventTapViewPanel.java b/src/main/java/io/github/dsheirer/instrument/tap/stream/SymbolEventTapViewPanel.java deleted file mode 100644 index fb271bcb..00000000 --- a/src/main/java/io/github/dsheirer/instrument/tap/stream/SymbolEventTapViewPanel.java +++ /dev/null @@ -1,187 +0,0 @@ -/******************************************************************************* - * SDR Trunk - * Copyright (C) 2014 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 io.github.dsheirer.instrument.tap.stream; - -import io.github.dsheirer.dsp.symbol.SymbolEvent; -import io.github.dsheirer.instrument.gui.SampleModel; -import io.github.dsheirer.instrument.tap.TapViewPanel; - -import java.awt.*; -import java.awt.geom.Path2D; -import java.awt.geom.Rectangle2D; -import java.util.List; -import java.util.Observable; - -public class SymbolEventTapViewPanel extends TapViewPanel -{ - private static final long serialVersionUID = 1L; - - private SymbolEventTap mTap; - private List mSamples; - private int mSampleCount; - - public SymbolEventTapViewPanel( SymbolEventTap tap ) - { - super( new SampleModel(), tap.getName() ); - - mTap = tap; - mTap.addListener( getModel() ); - - mSampleCount = (int)( getModel().getSampleCount() * tap.getSampleRateRatio() ); - - getModel().setSampleCount( mSampleCount ); - - getModel().setDelay( tap.getDelay() ); - } - - @Override - public void update( Observable arg0, Object arg1 ) - { - mSamples = (List)getModel().getSamples(); - - repaint(); - } - - public void paintComponent( Graphics g ) - { - super.paintComponent( g ); - - Graphics2D g2 = (Graphics2D)g; - - int height = getHeight(); - int width = getWidth(); - - g2.setColor( Color.DARK_GRAY ); - - float middle = (float)height / 2.0f; - - g2.drawLine( 0, (int)middle, width, (int)middle ); - - if( mSamples != null && mSamples.size() > 0 ) - { - g2.setColor( getForeground() ); - - g2.drawString( mLabel, 5 , 15 ); - - for( int x = 1; x < mSamples.size(); x++ ) - { - paint( g, x, mSamples.get( x ) ); - } - } - - g2.dispose(); - } - - /** - * Paints a polyline representing a boolean value with a 0 or 1 label in - * the middle of it - * - * @param graphics - * @param index - index of the sample int the sample buffer - * @param value - bit value, 0 or 1 - */ - private void paint( Graphics graphics, int index, SymbolEvent symbol ) - { - Graphics2D g2 = (Graphics2D)graphics; - - g2.setColor( getForeground() ); - - Path2D.Float polyline = - new Path2D.Float( Path2D.Float.WIND_EVEN_ODD, mSamples.size() ); - - float middleY = ( (float)getHeight() / 2.0f ); - - float indexWidth = (float)( getWidth() ) / (float)mSampleCount; - - float startX = (float)index * indexWidth; - float stopX = (float)(index + 1 ) * indexWidth; - float fivePercent = indexWidth * .05f; - float leftX = startX + fivePercent; - float rightX = stopX - fivePercent; - - /* Start */ - polyline.moveTo( startX, middleY ); - - /* Left horizonal segment */ - polyline.lineTo( leftX, middleY ); - - float y = ( symbol.getDecision() ? ( middleY - ( middleY * 0.5f) ) : - ( middleY + ( middleY * 0.5f) ) ); - - /* Left vertical segment */ - polyline.lineTo( leftX, y ); - - /* Middle horizontal segment */ - polyline.lineTo( rightX, y ); - - /* Right vertical segment */ - polyline.lineTo( rightX, middleY ); - - /* Right horizontal segment */ - polyline.lineTo( stopX, middleY ); - - g2.draw( polyline ); - - /* Draw the bit value label */ - String bit = symbol.getDecision() ? "1" : "0"; - - FontMetrics fontMetrics = graphics.getFontMetrics( this.getFont() ); - - Rectangle2D label = fontMetrics.getStringBounds( bit, graphics ); - - float offsetX = (float)label.getWidth() / 2.0f; - float offsetY = (float)label.getHeight() / 2.0f; - - float middleX = startX + ( indexWidth / 2.0f ); - - graphics.drawString( bit, - (int)( middleX - offsetX ), - (int)( middleY - ( ( middleY < y ) ? offsetY : -( 2.0f * offsetY ) ) ) ); - - /* Draw samples */ - float sampleWidth = ( rightX - leftX ) / (float)( symbol.getSamplesPerSymbol() + 1 ); - - g2.setColor( Color.GREEN ); - - for( int x = 0; x < symbol.getSamplesPerSymbol(); x++ ) - { - Path2D.Float line = - new Path2D.Float( Path2D.Float.WIND_EVEN_ODD, 2 ); - - float sampleX = leftX + ( ( x + 1 ) * sampleWidth ); - -// line.moveTo( sampleX, middleY ); - line.moveTo( sampleX, ( symbol.getBitSet().get( x ) ? - middleY - ( middleY * .05f ) : - middleY + ( middleY * .05f ) ) ); - - float sampleY = ( symbol.getBitSet().get( x ) ? - ( middleY - ( middleY * 0.45f ) ) : - ( middleY + ( middleY * 0.45f ) ) ); - - line.lineTo( sampleX, sampleY ); - - g2.draw( line ); - - graphics.drawString( symbol.getShift().getLabel(), - (int)( middleX - offsetX ), - (int)( y - ( ( y < middleY ) ? offsetY : -( 2.0f * offsetY ) ) ) ); - - } - } -} diff --git a/src/main/java/io/github/dsheirer/module/decode/fleetsync2/Fleetsync2DecoderInstrumented.java b/src/main/java/io/github/dsheirer/module/decode/fleetsync2/Fleetsync2DecoderInstrumented.java index 58a31ca8..88abbfeb 100644 --- a/src/main/java/io/github/dsheirer/module/decode/fleetsync2/Fleetsync2DecoderInstrumented.java +++ b/src/main/java/io/github/dsheirer/module/decode/fleetsync2/Fleetsync2DecoderInstrumented.java @@ -16,7 +16,7 @@ package io.github.dsheirer.module.decode.fleetsync2; import io.github.dsheirer.dsp.afsk.AFSK1200DecoderInstrumented; -import io.github.dsheirer.instrument.gui.viewer.chart.IInstrumentedAFSK1200Decoder; +import io.github.dsheirer.instrument.chart.IInstrumentedAFSK1200Decoder; import io.github.dsheirer.sample.buffer.ReusableBuffer; import javafx.beans.property.SimpleIntegerProperty; diff --git a/src/main/java/io/github/dsheirer/module/decode/lj1200/LJ1200DecoderInstrumented.java b/src/main/java/io/github/dsheirer/module/decode/lj1200/LJ1200DecoderInstrumented.java index 468b117b..80648fb2 100644 --- a/src/main/java/io/github/dsheirer/module/decode/lj1200/LJ1200DecoderInstrumented.java +++ b/src/main/java/io/github/dsheirer/module/decode/lj1200/LJ1200DecoderInstrumented.java @@ -16,7 +16,7 @@ package io.github.dsheirer.module.decode.lj1200; import io.github.dsheirer.dsp.afsk.AFSK1200DecoderInstrumented; -import io.github.dsheirer.instrument.gui.viewer.chart.IInstrumentedAFSK1200Decoder; +import io.github.dsheirer.instrument.chart.IInstrumentedAFSK1200Decoder; import io.github.dsheirer.sample.buffer.ReusableBuffer; import javafx.beans.property.SimpleIntegerProperty; diff --git a/src/main/java/io/github/dsheirer/module/decode/mdc1200/MDCDecoderInstrumented.java b/src/main/java/io/github/dsheirer/module/decode/mdc1200/MDCDecoderInstrumented.java index 1e2bdafd..c430b999 100644 --- a/src/main/java/io/github/dsheirer/module/decode/mdc1200/MDCDecoderInstrumented.java +++ b/src/main/java/io/github/dsheirer/module/decode/mdc1200/MDCDecoderInstrumented.java @@ -16,7 +16,7 @@ package io.github.dsheirer.module.decode.mdc1200; import io.github.dsheirer.dsp.afsk.AFSK1200DecoderInstrumented; -import io.github.dsheirer.instrument.gui.viewer.chart.IInstrumentedAFSK1200Decoder; +import io.github.dsheirer.instrument.chart.IInstrumentedAFSK1200Decoder; import io.github.dsheirer.sample.buffer.ReusableBuffer; import javafx.beans.property.SimpleIntegerProperty; diff --git a/src/main/java/io/github/dsheirer/module/decode/mpt1327/MPT1327DecoderInstrumented.java b/src/main/java/io/github/dsheirer/module/decode/mpt1327/MPT1327DecoderInstrumented.java index 33413a78..365ff66b 100644 --- a/src/main/java/io/github/dsheirer/module/decode/mpt1327/MPT1327DecoderInstrumented.java +++ b/src/main/java/io/github/dsheirer/module/decode/mpt1327/MPT1327DecoderInstrumented.java @@ -16,7 +16,7 @@ package io.github.dsheirer.module.decode.mpt1327; import io.github.dsheirer.dsp.afsk.AFSK1200DecoderInstrumented; -import io.github.dsheirer.instrument.gui.viewer.chart.IInstrumentedAFSK1200Decoder; +import io.github.dsheirer.instrument.chart.IInstrumentedAFSK1200Decoder; import io.github.dsheirer.sample.buffer.ReusableBuffer; import javafx.beans.property.SimpleIntegerProperty; diff --git a/src/main/java/io/github/dsheirer/module/decode/mpt1327/MPT1327DecoderLegacy.java b/src/main/java/io/github/dsheirer/module/decode/mpt1327/MPT1327DecoderLegacy.java deleted file mode 100644 index be8fef25..00000000 --- a/src/main/java/io/github/dsheirer/module/decode/mpt1327/MPT1327DecoderLegacy.java +++ /dev/null @@ -1,253 +0,0 @@ -/******************************************************************************* - * 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 io.github.dsheirer.module.decode.mpt1327; - -import io.github.dsheirer.alias.AliasList; -import io.github.dsheirer.bits.MessageFramer; -import io.github.dsheirer.dsp.filter.FilterFactory; -import io.github.dsheirer.dsp.filter.Filters; -import io.github.dsheirer.dsp.filter.fir.FIRFilterSpecification; -import io.github.dsheirer.dsp.filter.fir.real.RealFIRFilter_RB_RB; -import io.github.dsheirer.dsp.filter.halfband.real.HalfBandFilter_RB_RB; -import io.github.dsheirer.dsp.fsk.FSK2Decoder; -import io.github.dsheirer.instrument.Instrumentable; -import io.github.dsheirer.instrument.tap.Tap; -import io.github.dsheirer.instrument.tap.TapGroup; -import io.github.dsheirer.instrument.tap.stream.BinaryTap; -import io.github.dsheirer.instrument.tap.stream.FloatBufferTap; -import io.github.dsheirer.instrument.tap.stream.FloatTap; -import io.github.dsheirer.module.decode.Decoder; -import io.github.dsheirer.module.decode.DecoderType; -import io.github.dsheirer.sample.Broadcaster; -import io.github.dsheirer.sample.Listener; -import io.github.dsheirer.sample.real.IFilteredRealBufferListener; -import io.github.dsheirer.sample.real.RealBuffer; -import org.slf4j.Logger; -import org.slf4j.LoggerFactory; - -import java.util.ArrayList; -import java.util.List; - -/** - * MPT1327 Decoder - 1200 baud 2FSK decoder that can process 48k sample rate - * complex or floating point samples and output fully framed MPT1327 control and - * traffic messages. - */ -public class MPT1327DecoderLegacy extends Decoder implements IFilteredRealBufferListener, Instrumentable -{ - private final static Logger mLog = LoggerFactory.getLogger(MPT1327DecoderLegacy.class); - - private static final FIRFilterSpecification HIGH_PASS_SPECIFICATION = FIRFilterSpecification.highPassBuilder() - .sampleRate( 24000 ) - .stopBandCutoff( 800 ) - .stopBandAmplitude( 0.0 ) - .stopBandRipple( 0.03 ) - .passBandStart( 1000 ) - .passBandAmplitude( 1.0 ) - .passBandRipple( 0.08 ) - .build(); - - private static float[] HIGHPASS_FILTER; - - static - { - try - { - HIGHPASS_FILTER = FilterFactory.getTaps(HIGH_PASS_SPECIFICATION); - } - catch(Exception e) - { - mLog.error("Couldn't design MPT1327 highpass filter"); - } - } - - /* Decimated sample rate ( 48,000 / 2 = 24,000 ) feeding the decoder */ - private static final int sDECIMATED_SAMPLE_RATE = 24000; - - /* Baud or Symbol Rate */ - private static final int sSYMBOL_RATE = 1200; - - /* Message length -- longest possible message is: - * 4xREVS + 16xSYNC + 64xADD1 + 64xDCW1 + 64xDCW2 + 64xDCW3 + 64xDCW4 */ - private static final int sMESSAGE_LENGTH = 350; - - /* Instrumentation Taps */ - private static final String INSTRUMENT_HB1_FILTER_TO_HIGH_PASS = - "Tap Point: Half-band Decimation Filter > < Low Pass Filter"; - private static final String INSTRUMENT_HIGH_PASS_TO_DECODER = - "Tap Point: Low Pass Filter > < Decoder"; - private static final String INSTRUMENT_DECODER_TO_FRAMER = - "Tap Point: Decoder > < Sync Detect/Message Framer"; - private List mAvailableTaps; - - private HalfBandFilter_RB_RB mDecimationFilter; - private RealFIRFilter_RB_RB mHighPassFilter; - private FSK2Decoder mFSKDecoder; - private Broadcaster mSymbolBroadcaster; - private MessageFramer mControlMessageFramer; - private MessageFramer mTrafficMessageFramer; - private MPT1327MessageProcessor mMessageProcessor; - - public MPT1327DecoderLegacy(AliasList aliasList, Sync sync) - { - /** - * Normal: 2FSK Decoder with inverted output - * French: 2FSK Decoder with normal output - */ - if(sync == Sync.NORMAL) - { - mFSKDecoder = new FSK2Decoder(24000, 1200, FSK2Decoder.Output.INVERTED); - } - else if(sync == Sync.FRENCH) - { - mFSKDecoder = new FSK2Decoder(24000, 1200, FSK2Decoder.Output.NORMAL); - } - else - { - throw new IllegalArgumentException("MPT1327 Decoder - unrecognized Sync type"); - } - - /* Decimation filter - 48000 / 2 = 24000 output */ - mDecimationFilter = new HalfBandFilter_RB_RB(Filters.FIR_HALF_BAND_31T_ONE_EIGHTH_FCO.getCoefficients(), 1.0002f, true); - - //High-pass filter the audio to remove any DC component or CTCSS tones. The audio has already been low-pass - //filtered by the AudioDemodulation filter cutoff of 3400 Hz. - mHighPassFilter = new RealFIRFilter_RB_RB(HIGHPASS_FILTER, 1.0f); - mDecimationFilter.setListener(mHighPassFilter); - - mHighPassFilter.setListener(mFSKDecoder); - - mSymbolBroadcaster = new Broadcaster(); - mFSKDecoder.setListener(mSymbolBroadcaster); - - /* Message framer for control channel messages */ - mControlMessageFramer = new MessageFramer(sync.getControlSyncPattern().getPattern(), sMESSAGE_LENGTH); - mSymbolBroadcaster.addListener(mControlMessageFramer); - - /* Message framer for traffic channel massages */ - mTrafficMessageFramer = new MessageFramer(sync.getTrafficSyncPattern().getPattern(), sMESSAGE_LENGTH); - mSymbolBroadcaster.addListener(mTrafficMessageFramer); - - /* Fully decoded and framed messages processor */ - mMessageProcessor = new MPT1327MessageProcessor(aliasList); - mMessageProcessor.setMessageListener(getMessageListener()); - mControlMessageFramer.addMessageListener(mMessageProcessor); - mTrafficMessageFramer.addMessageListener(mMessageProcessor); - } - - @Override - public Listener getFilteredRealBufferListener() - { - return mDecimationFilter; - } - - @Override - public DecoderType getDecoderType() - { - return DecoderType.MPT1327; - } - - /** - * Cleanup method - */ - public void dispose() - { - super.dispose(); - - mSymbolBroadcaster.dispose(); - - mControlMessageFramer.dispose(); - - mFSKDecoder.dispose(); - - mDecimationFilter.dispose(); - - mMessageProcessor.dispose(); - - mTrafficMessageFramer.dispose(); - } - - /* Instrumentation Taps */ - @Override - public List getTapGroups() - { - if(mAvailableTaps == null) - { - mAvailableTaps = new ArrayList(); - - TapGroup group = new TapGroup("MPT-1327 Decoder"); - - group.add(new FloatTap(INSTRUMENT_HB1_FILTER_TO_HIGH_PASS, 0, 0.5f)); - group.add(new FloatTap(INSTRUMENT_HIGH_PASS_TO_DECODER, 0, 0.5f)); - group.add(new BinaryTap(INSTRUMENT_DECODER_TO_FRAMER, 0, 0.025f)); - - mAvailableTaps.add(group); - - /* Add the taps from the FSK decoder */ - mAvailableTaps.addAll(mFSKDecoder.getTapGroups()); - } - - return mAvailableTaps; - } - - @Override - public void registerTap(Tap tap) - { - /* Send request to decoder */ - mFSKDecoder.registerTap(tap); - - switch(tap.getName()) - { - case INSTRUMENT_HB1_FILTER_TO_HIGH_PASS: - FloatBufferTap hb1Tap = (FloatBufferTap) tap; - mDecimationFilter.setListener(hb1Tap); - hb1Tap.setListener(mHighPassFilter); - break; - case INSTRUMENT_HIGH_PASS_TO_DECODER: - FloatBufferTap lowTap = (FloatBufferTap) tap; - mHighPassFilter.setListener(lowTap); - lowTap.setListener(mFSKDecoder); - break; - case INSTRUMENT_DECODER_TO_FRAMER: - BinaryTap decoderTap = (BinaryTap) tap; - mFSKDecoder.setListener(decoderTap); - decoderTap.setListener(mSymbolBroadcaster); - break; - } - } - - @Override - public void unregisterTap(Tap tap) - { - mFSKDecoder.unregisterTap(tap); - - switch(tap.getName()) - { - case INSTRUMENT_HB1_FILTER_TO_HIGH_PASS: - mDecimationFilter.setListener(mHighPassFilter); - break; - case INSTRUMENT_HIGH_PASS_TO_DECODER: - mHighPassFilter.setListener(mFSKDecoder); - break; - case INSTRUMENT_DECODER_TO_FRAMER: - mFSKDecoder.setListener(mSymbolBroadcaster); - break; - } - } -} diff --git a/src/main/java/io/github/dsheirer/module/decode/p25/C4FMSymbolFilter.java b/src/main/java/io/github/dsheirer/module/decode/p25/C4FMSymbolFilter.java index bca6a3a2..5f3d4215 100644 --- a/src/main/java/io/github/dsheirer/module/decode/p25/C4FMSymbolFilter.java +++ b/src/main/java/io/github/dsheirer/module/decode/p25/C4FMSymbolFilter.java @@ -1,11 +1,6 @@ package io.github.dsheirer.module.decode.p25; import io.github.dsheirer.dsp.gain.DirectGainControl; -import io.github.dsheirer.instrument.Instrumentable; -import io.github.dsheirer.instrument.tap.Tap; -import io.github.dsheirer.instrument.tap.TapGroup; -import io.github.dsheirer.instrument.tap.stream.AdditiveFloatTap; -import io.github.dsheirer.instrument.tap.stream.FloatTap; import io.github.dsheirer.sample.Listener; import io.github.dsheirer.sample.real.RealBuffer; import io.github.dsheirer.sample.real.RealSampleListener; @@ -13,10 +8,7 @@ import io.github.dsheirer.sample.real.RealSampleProvider; import io.github.dsheirer.source.ISourceEventListener; import io.github.dsheirer.source.SourceEvent; -import java.util.ArrayList; -import java.util.List; - -public class C4FMSymbolFilter implements Listener, ISourceEventListener, RealSampleProvider, Instrumentable +public class C4FMSymbolFilter implements Listener, ISourceEventListener, RealSampleProvider { private static final float TAPS[][] = { @@ -151,11 +143,6 @@ public class C4FMSymbolFilter implements Listener, ISourceEventListe {0.00000e+00f, 0.00000e+00f, 0.00000e+00f, 1.00000e+00f, 0.00000e+00f, 0.00000e+00f, 0.00000e+00f, 0.00000e+00f}, // 128/128 }; - /* Instrumentation Taps */ - private static final String INSTRUMENT_SYMBOL_SPREAD = "Tap Point: Symbol Spread (Goal=2.0)"; - private FloatTap mSymbolSpreadTap; - private List mAvailableTaps; - private static final int NUMBER_FILTER_TAPS = 8; private static final int NUMBER_FILTER_STEPS = 128; @@ -379,11 +366,6 @@ public class C4FMSymbolFilter implements Listener, ISourceEventListe 500 * (mCoarseFrequencyCorrection > 0 ? 1 : -1); } - if(mSymbolSpreadTap != null) - { - mSymbolSpreadTap.receive(mSymbolSpread); - } - /* dispatch the interpolated value to the listener */ if(mListener != null) { @@ -404,46 +386,6 @@ public class C4FMSymbolFilter implements Listener, ISourceEventListe mListener = null; } - @Override - public List getTapGroups() - { - if(mAvailableTaps == null) - { - mAvailableTaps = new ArrayList<>(); - - /* - * Since the target is 2.0, we subtract 2.0 from the value so that - * a good value is plotted right on the zero center line - */ - TapGroup group = new TapGroup("C4FM Symbol Filter"); - - group.add( - new AdditiveFloatTap(INSTRUMENT_SYMBOL_SPREAD, 0, 0.1f, -2.0f)); - - mAvailableTaps.add(group); - } - - return mAvailableTaps; - } - - @Override - public void registerTap(Tap tap) - { - if(tap.getName().contentEquals(INSTRUMENT_SYMBOL_SPREAD)) - { - mSymbolSpreadTap = (FloatTap)tap; - } - } - - @Override - public void unregisterTap(Tap tap) - { - if(tap.getName().contentEquals(INSTRUMENT_SYMBOL_SPREAD)) - { - mSymbolSpreadTap = null; - } - } - /** * Broadcasts a frequency change event to the registered listener * diff --git a/src/main/java/io/github/dsheirer/module/decode/tait/Tait1200DecoderInstrumented.java b/src/main/java/io/github/dsheirer/module/decode/tait/Tait1200DecoderInstrumented.java index c6ce15f9..93150c67 100644 --- a/src/main/java/io/github/dsheirer/module/decode/tait/Tait1200DecoderInstrumented.java +++ b/src/main/java/io/github/dsheirer/module/decode/tait/Tait1200DecoderInstrumented.java @@ -16,7 +16,7 @@ package io.github.dsheirer.module.decode.tait; import io.github.dsheirer.dsp.afsk.AFSK1200DecoderInstrumented; -import io.github.dsheirer.instrument.gui.viewer.chart.IInstrumentedAFSK1200Decoder; +import io.github.dsheirer.instrument.chart.IInstrumentedAFSK1200Decoder; import io.github.dsheirer.sample.buffer.ReusableBuffer; import javafx.beans.property.SimpleIntegerProperty; From 7a830077bd35901356cda2db130be5f9e70ced8d Mon Sep 17 00:00:00 2001 From: Denny Date: Fri, 15 Jun 2018 07:29:27 -0400 Subject: [PATCH 77/87] [WIP] Cleanup. Refactors DecoderFX application to the gui package. --- .../{ => gui}/instrument/DemodulatorViewerFX.java | 4 ++-- .../{ => gui}/instrument/PlaybackController.java | 4 ++-- .../{ => gui}/instrument/RecentFilesMenu.java | 2 +- .../dsheirer/{ => gui}/instrument/ViewerDesktop.java | 6 +++--- .../instrument/chart/AFSK1200SampleBufferChart.java | 2 +- .../AFSK1200ZeroCrossingErrorDetectorChart.java | 2 +- .../instrument/chart/ComplexPhaseLineChart.java | 2 +- .../instrument/chart/ComplexSampleLineChart.java | 2 +- .../instrument/chart/DecodedSymbolChart.java | 2 +- .../{ => gui}/instrument/chart/DoubleLineChart.java | 2 +- .../{ => gui}/instrument/chart/EyeDiagramChart.java | 2 +- .../instrument/chart/FMDemodulatedLineChart.java | 2 +- .../chart/IInstrumentedAFSK1200Decoder.java | 2 +- .../instrument/chart/LTRNetSampleBufferChart.java | 2 +- .../{ => gui}/instrument/chart/PhaseLineChart.java | 2 +- .../instrument/chart/RealSampleLineChart.java | 2 +- .../{ => gui}/instrument/chart/SymbolChart.java | 2 +- .../chart/ZeroCrossingErrorDetectorChart.java | 2 +- .../instrument/decoder/AbstractAFSK1200Pane.java | 12 ++++++------ .../instrument/decoder/AbstractDecoderPane.java | 2 +- .../instrument/decoder/ComplexDecoderPane.java | 2 +- .../instrument/decoder/DecoderPaneFactory.java | 2 +- .../{ => gui}/instrument/decoder/Fleetsync2Pane.java | 2 +- .../{ => gui}/instrument/decoder/LJ1200Pane.java | 2 +- .../{ => gui}/instrument/decoder/LTRNetPane.java | 8 ++++---- .../{ => gui}/instrument/decoder/MDC1200Pane.java | 2 +- .../{ => gui}/instrument/decoder/MPT1327Pane.java | 2 +- .../instrument/decoder/P25Phase1C4FMPane.java | 12 ++++++------ .../instrument/decoder/P25Phase1LSMPane.java | 12 ++++++------ .../instrument/decoder/RealDecoderPane.java | 2 +- .../{ => gui}/instrument/decoder/Tait1200Pane.java | 2 +- .../fleetsync2/Fleetsync2DecoderInstrumented.java | 2 +- .../decode/lj1200/LJ1200DecoderInstrumented.java | 2 +- .../decode/mdc1200/MDCDecoderInstrumented.java | 2 +- .../decode/mpt1327/MPT1327DecoderInstrumented.java | 2 +- .../decode/tait/Tait1200DecoderInstrumented.java | 2 +- 36 files changed, 58 insertions(+), 58 deletions(-) rename src/main/java/io/github/dsheirer/{ => gui}/instrument/DemodulatorViewerFX.java (98%) rename src/main/java/io/github/dsheirer/{ => gui}/instrument/PlaybackController.java (99%) rename src/main/java/io/github/dsheirer/{ => gui}/instrument/RecentFilesMenu.java (99%) rename src/main/java/io/github/dsheirer/{ => gui}/instrument/ViewerDesktop.java (93%) rename src/main/java/io/github/dsheirer/{ => gui}/instrument/chart/AFSK1200SampleBufferChart.java (99%) rename src/main/java/io/github/dsheirer/{ => gui}/instrument/chart/AFSK1200ZeroCrossingErrorDetectorChart.java (99%) rename src/main/java/io/github/dsheirer/{ => gui}/instrument/chart/ComplexPhaseLineChart.java (98%) rename src/main/java/io/github/dsheirer/{ => gui}/instrument/chart/ComplexSampleLineChart.java (99%) rename src/main/java/io/github/dsheirer/{ => gui}/instrument/chart/DecodedSymbolChart.java (98%) rename src/main/java/io/github/dsheirer/{ => gui}/instrument/chart/DoubleLineChart.java (98%) rename src/main/java/io/github/dsheirer/{ => gui}/instrument/chart/EyeDiagramChart.java (98%) rename src/main/java/io/github/dsheirer/{ => gui}/instrument/chart/FMDemodulatedLineChart.java (98%) rename src/main/java/io/github/dsheirer/{ => gui}/instrument/chart/IInstrumentedAFSK1200Decoder.java (96%) rename src/main/java/io/github/dsheirer/{ => gui}/instrument/chart/LTRNetSampleBufferChart.java (99%) rename src/main/java/io/github/dsheirer/{ => gui}/instrument/chart/PhaseLineChart.java (98%) rename src/main/java/io/github/dsheirer/{ => gui}/instrument/chart/RealSampleLineChart.java (98%) rename src/main/java/io/github/dsheirer/{ => gui}/instrument/chart/SymbolChart.java (98%) rename src/main/java/io/github/dsheirer/{ => gui}/instrument/chart/ZeroCrossingErrorDetectorChart.java (99%) rename src/main/java/io/github/dsheirer/{ => gui}/instrument/decoder/AbstractAFSK1200Pane.java (92%) rename src/main/java/io/github/dsheirer/{ => gui}/instrument/decoder/AbstractDecoderPane.java (96%) rename src/main/java/io/github/dsheirer/{ => gui}/instrument/decoder/ComplexDecoderPane.java (97%) rename src/main/java/io/github/dsheirer/{ => gui}/instrument/decoder/DecoderPaneFactory.java (98%) rename src/main/java/io/github/dsheirer/{ => gui}/instrument/decoder/Fleetsync2Pane.java (96%) rename src/main/java/io/github/dsheirer/{ => gui}/instrument/decoder/LJ1200Pane.java (97%) rename src/main/java/io/github/dsheirer/{ => gui}/instrument/decoder/LTRNetPane.java (94%) rename src/main/java/io/github/dsheirer/{ => gui}/instrument/decoder/MDC1200Pane.java (97%) rename src/main/java/io/github/dsheirer/{ => gui}/instrument/decoder/MPT1327Pane.java (97%) rename src/main/java/io/github/dsheirer/{ => gui}/instrument/decoder/P25Phase1C4FMPane.java (94%) rename src/main/java/io/github/dsheirer/{ => gui}/instrument/decoder/P25Phase1LSMPane.java (94%) rename src/main/java/io/github/dsheirer/{ => gui}/instrument/decoder/RealDecoderPane.java (97%) rename src/main/java/io/github/dsheirer/{ => gui}/instrument/decoder/Tait1200Pane.java (97%) diff --git a/src/main/java/io/github/dsheirer/instrument/DemodulatorViewerFX.java b/src/main/java/io/github/dsheirer/gui/instrument/DemodulatorViewerFX.java similarity index 98% rename from src/main/java/io/github/dsheirer/instrument/DemodulatorViewerFX.java rename to src/main/java/io/github/dsheirer/gui/instrument/DemodulatorViewerFX.java index 5c21f06a..6006e65f 100644 --- a/src/main/java/io/github/dsheirer/instrument/DemodulatorViewerFX.java +++ b/src/main/java/io/github/dsheirer/gui/instrument/DemodulatorViewerFX.java @@ -13,9 +13,9 @@ * If not, see * ******************************************************************************/ -package io.github.dsheirer.instrument; +package io.github.dsheirer.gui.instrument; -import io.github.dsheirer.instrument.decoder.DecoderPaneFactory; +import io.github.dsheirer.gui.instrument.decoder.DecoderPaneFactory; import io.github.dsheirer.module.decode.DecoderType; import io.github.dsheirer.module.decode.p25.P25Decoder; import javafx.application.Application; diff --git a/src/main/java/io/github/dsheirer/instrument/PlaybackController.java b/src/main/java/io/github/dsheirer/gui/instrument/PlaybackController.java similarity index 99% rename from src/main/java/io/github/dsheirer/instrument/PlaybackController.java rename to src/main/java/io/github/dsheirer/gui/instrument/PlaybackController.java index 68029c1f..a679fafc 100644 --- a/src/main/java/io/github/dsheirer/instrument/PlaybackController.java +++ b/src/main/java/io/github/dsheirer/gui/instrument/PlaybackController.java @@ -13,9 +13,9 @@ * If not, see * ******************************************************************************/ -package io.github.dsheirer.instrument; +package io.github.dsheirer.gui.instrument; -import io.github.dsheirer.instrument.decoder.AbstractDecoderPane; +import io.github.dsheirer.gui.instrument.decoder.AbstractDecoderPane; import io.github.dsheirer.sample.Listener; import io.github.dsheirer.sample.buffer.ReusableBuffer; import io.github.dsheirer.sample.buffer.ReusableBufferBroadcaster; diff --git a/src/main/java/io/github/dsheirer/instrument/RecentFilesMenu.java b/src/main/java/io/github/dsheirer/gui/instrument/RecentFilesMenu.java similarity index 99% rename from src/main/java/io/github/dsheirer/instrument/RecentFilesMenu.java rename to src/main/java/io/github/dsheirer/gui/instrument/RecentFilesMenu.java index ea8948d3..27b7ece3 100644 --- a/src/main/java/io/github/dsheirer/instrument/RecentFilesMenu.java +++ b/src/main/java/io/github/dsheirer/gui/instrument/RecentFilesMenu.java @@ -13,7 +13,7 @@ * If not, see * ******************************************************************************/ -package io.github.dsheirer.instrument; +package io.github.dsheirer.gui.instrument; import javafx.event.ActionEvent; import javafx.event.EventHandler; diff --git a/src/main/java/io/github/dsheirer/instrument/ViewerDesktop.java b/src/main/java/io/github/dsheirer/gui/instrument/ViewerDesktop.java similarity index 93% rename from src/main/java/io/github/dsheirer/instrument/ViewerDesktop.java rename to src/main/java/io/github/dsheirer/gui/instrument/ViewerDesktop.java index ed372f6f..293ee6ef 100644 --- a/src/main/java/io/github/dsheirer/instrument/ViewerDesktop.java +++ b/src/main/java/io/github/dsheirer/gui/instrument/ViewerDesktop.java @@ -13,10 +13,10 @@ * If not, see * ******************************************************************************/ -package io.github.dsheirer.instrument; +package io.github.dsheirer.gui.instrument; -import io.github.dsheirer.instrument.decoder.AbstractDecoderPane; -import io.github.dsheirer.instrument.decoder.DecoderPaneFactory; +import io.github.dsheirer.gui.instrument.decoder.AbstractDecoderPane; +import io.github.dsheirer.gui.instrument.decoder.DecoderPaneFactory; import io.github.dsheirer.module.decode.DecoderType; import io.github.dsheirer.module.decode.p25.P25Decoder; import javafx.scene.layout.BorderPane; diff --git a/src/main/java/io/github/dsheirer/instrument/chart/AFSK1200SampleBufferChart.java b/src/main/java/io/github/dsheirer/gui/instrument/chart/AFSK1200SampleBufferChart.java similarity index 99% rename from src/main/java/io/github/dsheirer/instrument/chart/AFSK1200SampleBufferChart.java rename to src/main/java/io/github/dsheirer/gui/instrument/chart/AFSK1200SampleBufferChart.java index 49e93240..9c861dc0 100644 --- a/src/main/java/io/github/dsheirer/instrument/chart/AFSK1200SampleBufferChart.java +++ b/src/main/java/io/github/dsheirer/gui/instrument/chart/AFSK1200SampleBufferChart.java @@ -13,7 +13,7 @@ * If not, see * ******************************************************************************/ -package io.github.dsheirer.instrument.chart; +package io.github.dsheirer.gui.instrument.chart; import javafx.beans.value.ChangeListener; import javafx.beans.value.ObservableValue; diff --git a/src/main/java/io/github/dsheirer/instrument/chart/AFSK1200ZeroCrossingErrorDetectorChart.java b/src/main/java/io/github/dsheirer/gui/instrument/chart/AFSK1200ZeroCrossingErrorDetectorChart.java similarity index 99% rename from src/main/java/io/github/dsheirer/instrument/chart/AFSK1200ZeroCrossingErrorDetectorChart.java rename to src/main/java/io/github/dsheirer/gui/instrument/chart/AFSK1200ZeroCrossingErrorDetectorChart.java index e90b00d9..0d2b2449 100644 --- a/src/main/java/io/github/dsheirer/instrument/chart/AFSK1200ZeroCrossingErrorDetectorChart.java +++ b/src/main/java/io/github/dsheirer/gui/instrument/chart/AFSK1200ZeroCrossingErrorDetectorChart.java @@ -13,7 +13,7 @@ * If not, see * ******************************************************************************/ -package io.github.dsheirer.instrument.chart; +package io.github.dsheirer.gui.instrument.chart; import javafx.beans.value.ChangeListener; import javafx.beans.value.ObservableValue; diff --git a/src/main/java/io/github/dsheirer/instrument/chart/ComplexPhaseLineChart.java b/src/main/java/io/github/dsheirer/gui/instrument/chart/ComplexPhaseLineChart.java similarity index 98% rename from src/main/java/io/github/dsheirer/instrument/chart/ComplexPhaseLineChart.java rename to src/main/java/io/github/dsheirer/gui/instrument/chart/ComplexPhaseLineChart.java index 97a238cc..fb03391b 100644 --- a/src/main/java/io/github/dsheirer/instrument/chart/ComplexPhaseLineChart.java +++ b/src/main/java/io/github/dsheirer/gui/instrument/chart/ComplexPhaseLineChart.java @@ -13,7 +13,7 @@ * If not, see * ******************************************************************************/ -package io.github.dsheirer.instrument.chart; +package io.github.dsheirer.gui.instrument.chart; import io.github.dsheirer.buffer.ComplexCircularBuffer; import io.github.dsheirer.dsp.gain.ComplexGain; diff --git a/src/main/java/io/github/dsheirer/instrument/chart/ComplexSampleLineChart.java b/src/main/java/io/github/dsheirer/gui/instrument/chart/ComplexSampleLineChart.java similarity index 99% rename from src/main/java/io/github/dsheirer/instrument/chart/ComplexSampleLineChart.java rename to src/main/java/io/github/dsheirer/gui/instrument/chart/ComplexSampleLineChart.java index e36214a0..f567e0a6 100644 --- a/src/main/java/io/github/dsheirer/instrument/chart/ComplexSampleLineChart.java +++ b/src/main/java/io/github/dsheirer/gui/instrument/chart/ComplexSampleLineChart.java @@ -13,7 +13,7 @@ * If not, see * ******************************************************************************/ -package io.github.dsheirer.instrument.chart; +package io.github.dsheirer.gui.instrument.chart; import io.github.dsheirer.buffer.ComplexCircularBuffer; import io.github.dsheirer.dsp.gain.ComplexGain; diff --git a/src/main/java/io/github/dsheirer/instrument/chart/DecodedSymbolChart.java b/src/main/java/io/github/dsheirer/gui/instrument/chart/DecodedSymbolChart.java similarity index 98% rename from src/main/java/io/github/dsheirer/instrument/chart/DecodedSymbolChart.java rename to src/main/java/io/github/dsheirer/gui/instrument/chart/DecodedSymbolChart.java index 073169fa..3a416dfc 100644 --- a/src/main/java/io/github/dsheirer/instrument/chart/DecodedSymbolChart.java +++ b/src/main/java/io/github/dsheirer/gui/instrument/chart/DecodedSymbolChart.java @@ -13,7 +13,7 @@ * If not, see * ******************************************************************************/ -package io.github.dsheirer.instrument.chart; +package io.github.dsheirer.gui.instrument.chart; import io.github.dsheirer.sample.Listener; import javafx.collections.FXCollections; diff --git a/src/main/java/io/github/dsheirer/instrument/chart/DoubleLineChart.java b/src/main/java/io/github/dsheirer/gui/instrument/chart/DoubleLineChart.java similarity index 98% rename from src/main/java/io/github/dsheirer/instrument/chart/DoubleLineChart.java rename to src/main/java/io/github/dsheirer/gui/instrument/chart/DoubleLineChart.java index 12e0c898..cc278d8e 100644 --- a/src/main/java/io/github/dsheirer/instrument/chart/DoubleLineChart.java +++ b/src/main/java/io/github/dsheirer/gui/instrument/chart/DoubleLineChart.java @@ -13,7 +13,7 @@ * If not, see * ******************************************************************************/ -package io.github.dsheirer.instrument.chart; +package io.github.dsheirer.gui.instrument.chart; import io.github.dsheirer.buffer.DoubleCircularBuffer; import io.github.dsheirer.sample.Listener; diff --git a/src/main/java/io/github/dsheirer/instrument/chart/EyeDiagramChart.java b/src/main/java/io/github/dsheirer/gui/instrument/chart/EyeDiagramChart.java similarity index 98% rename from src/main/java/io/github/dsheirer/instrument/chart/EyeDiagramChart.java rename to src/main/java/io/github/dsheirer/gui/instrument/chart/EyeDiagramChart.java index 5cf731a6..a00db278 100644 --- a/src/main/java/io/github/dsheirer/instrument/chart/EyeDiagramChart.java +++ b/src/main/java/io/github/dsheirer/gui/instrument/chart/EyeDiagramChart.java @@ -13,7 +13,7 @@ * If not, see * ******************************************************************************/ -package io.github.dsheirer.instrument.chart; +package io.github.dsheirer.gui.instrument.chart; import io.github.dsheirer.dsp.psk.SymbolDecisionData; import io.github.dsheirer.sample.Listener; diff --git a/src/main/java/io/github/dsheirer/instrument/chart/FMDemodulatedLineChart.java b/src/main/java/io/github/dsheirer/gui/instrument/chart/FMDemodulatedLineChart.java similarity index 98% rename from src/main/java/io/github/dsheirer/instrument/chart/FMDemodulatedLineChart.java rename to src/main/java/io/github/dsheirer/gui/instrument/chart/FMDemodulatedLineChart.java index 7f44efb8..82980d9a 100644 --- a/src/main/java/io/github/dsheirer/instrument/chart/FMDemodulatedLineChart.java +++ b/src/main/java/io/github/dsheirer/gui/instrument/chart/FMDemodulatedLineChart.java @@ -13,7 +13,7 @@ * If not, see * ******************************************************************************/ -package io.github.dsheirer.instrument.chart; +package io.github.dsheirer.gui.instrument.chart; import io.github.dsheirer.buffer.DoubleCircularBuffer; import io.github.dsheirer.dsp.fm.FMDemodulator; diff --git a/src/main/java/io/github/dsheirer/instrument/chart/IInstrumentedAFSK1200Decoder.java b/src/main/java/io/github/dsheirer/gui/instrument/chart/IInstrumentedAFSK1200Decoder.java similarity index 96% rename from src/main/java/io/github/dsheirer/instrument/chart/IInstrumentedAFSK1200Decoder.java rename to src/main/java/io/github/dsheirer/gui/instrument/chart/IInstrumentedAFSK1200Decoder.java index c0e10cdb..446c6c64 100644 --- a/src/main/java/io/github/dsheirer/instrument/chart/IInstrumentedAFSK1200Decoder.java +++ b/src/main/java/io/github/dsheirer/gui/instrument/chart/IInstrumentedAFSK1200Decoder.java @@ -13,7 +13,7 @@ * If not, see * ******************************************************************************/ -package io.github.dsheirer.instrument.chart; +package io.github.dsheirer.gui.instrument.chart; import io.github.dsheirer.dsp.afsk.AFSK1200DecoderInstrumented; import io.github.dsheirer.message.Message; diff --git a/src/main/java/io/github/dsheirer/instrument/chart/LTRNetSampleBufferChart.java b/src/main/java/io/github/dsheirer/gui/instrument/chart/LTRNetSampleBufferChart.java similarity index 99% rename from src/main/java/io/github/dsheirer/instrument/chart/LTRNetSampleBufferChart.java rename to src/main/java/io/github/dsheirer/gui/instrument/chart/LTRNetSampleBufferChart.java index 7237649c..008b1824 100644 --- a/src/main/java/io/github/dsheirer/instrument/chart/LTRNetSampleBufferChart.java +++ b/src/main/java/io/github/dsheirer/gui/instrument/chart/LTRNetSampleBufferChart.java @@ -13,7 +13,7 @@ * If not, see * ******************************************************************************/ -package io.github.dsheirer.instrument.chart; +package io.github.dsheirer.gui.instrument.chart; import io.github.dsheirer.module.decode.ltrnet.LTRNetDecoderInstrumented; import javafx.beans.value.ChangeListener; diff --git a/src/main/java/io/github/dsheirer/instrument/chart/PhaseLineChart.java b/src/main/java/io/github/dsheirer/gui/instrument/chart/PhaseLineChart.java similarity index 98% rename from src/main/java/io/github/dsheirer/instrument/chart/PhaseLineChart.java rename to src/main/java/io/github/dsheirer/gui/instrument/chart/PhaseLineChart.java index 1b505f89..cdcaf2ad 100644 --- a/src/main/java/io/github/dsheirer/instrument/chart/PhaseLineChart.java +++ b/src/main/java/io/github/dsheirer/gui/instrument/chart/PhaseLineChart.java @@ -13,7 +13,7 @@ * If not, see * ******************************************************************************/ -package io.github.dsheirer.instrument.chart; +package io.github.dsheirer.gui.instrument.chart; import io.github.dsheirer.buffer.DoubleCircularBuffer; import io.github.dsheirer.sample.Listener; diff --git a/src/main/java/io/github/dsheirer/instrument/chart/RealSampleLineChart.java b/src/main/java/io/github/dsheirer/gui/instrument/chart/RealSampleLineChart.java similarity index 98% rename from src/main/java/io/github/dsheirer/instrument/chart/RealSampleLineChart.java rename to src/main/java/io/github/dsheirer/gui/instrument/chart/RealSampleLineChart.java index 9ec5210b..a914bc2a 100644 --- a/src/main/java/io/github/dsheirer/instrument/chart/RealSampleLineChart.java +++ b/src/main/java/io/github/dsheirer/gui/instrument/chart/RealSampleLineChart.java @@ -13,7 +13,7 @@ * If not, see * ******************************************************************************/ -package io.github.dsheirer.instrument.chart; +package io.github.dsheirer.gui.instrument.chart; import io.github.dsheirer.buffer.RealCircularBuffer; import io.github.dsheirer.sample.Listener; diff --git a/src/main/java/io/github/dsheirer/instrument/chart/SymbolChart.java b/src/main/java/io/github/dsheirer/gui/instrument/chart/SymbolChart.java similarity index 98% rename from src/main/java/io/github/dsheirer/instrument/chart/SymbolChart.java rename to src/main/java/io/github/dsheirer/gui/instrument/chart/SymbolChart.java index 070ca5a9..d5ea1567 100644 --- a/src/main/java/io/github/dsheirer/instrument/chart/SymbolChart.java +++ b/src/main/java/io/github/dsheirer/gui/instrument/chart/SymbolChart.java @@ -13,7 +13,7 @@ * If not, see * ******************************************************************************/ -package io.github.dsheirer.instrument.chart; +package io.github.dsheirer.gui.instrument.chart; import io.github.dsheirer.sample.Listener; import io.github.dsheirer.sample.complex.Complex; diff --git a/src/main/java/io/github/dsheirer/instrument/chart/ZeroCrossingErrorDetectorChart.java b/src/main/java/io/github/dsheirer/gui/instrument/chart/ZeroCrossingErrorDetectorChart.java similarity index 99% rename from src/main/java/io/github/dsheirer/instrument/chart/ZeroCrossingErrorDetectorChart.java rename to src/main/java/io/github/dsheirer/gui/instrument/chart/ZeroCrossingErrorDetectorChart.java index 4b0ebcb6..e10147c3 100644 --- a/src/main/java/io/github/dsheirer/instrument/chart/ZeroCrossingErrorDetectorChart.java +++ b/src/main/java/io/github/dsheirer/gui/instrument/chart/ZeroCrossingErrorDetectorChart.java @@ -13,7 +13,7 @@ * If not, see * ******************************************************************************/ -package io.github.dsheirer.instrument.chart; +package io.github.dsheirer.gui.instrument.chart; import io.github.dsheirer.module.decode.ltrnet.LTRNetDecoderInstrumented; import javafx.beans.value.ChangeListener; diff --git a/src/main/java/io/github/dsheirer/instrument/decoder/AbstractAFSK1200Pane.java b/src/main/java/io/github/dsheirer/gui/instrument/decoder/AbstractAFSK1200Pane.java similarity index 92% rename from src/main/java/io/github/dsheirer/instrument/decoder/AbstractAFSK1200Pane.java rename to src/main/java/io/github/dsheirer/gui/instrument/decoder/AbstractAFSK1200Pane.java index def2f899..5c9c092f 100644 --- a/src/main/java/io/github/dsheirer/instrument/decoder/AbstractAFSK1200Pane.java +++ b/src/main/java/io/github/dsheirer/gui/instrument/decoder/AbstractAFSK1200Pane.java @@ -13,13 +13,13 @@ * If not, see * ******************************************************************************/ -package io.github.dsheirer.instrument.decoder; +package io.github.dsheirer.gui.instrument.decoder; -import io.github.dsheirer.instrument.chart.AFSK1200SampleBufferChart; -import io.github.dsheirer.instrument.chart.AFSK1200ZeroCrossingErrorDetectorChart; -import io.github.dsheirer.instrument.chart.DecodedSymbolChart; -import io.github.dsheirer.instrument.chart.IInstrumentedAFSK1200Decoder; -import io.github.dsheirer.instrument.chart.RealSampleLineChart; +import io.github.dsheirer.gui.instrument.chart.AFSK1200SampleBufferChart; +import io.github.dsheirer.gui.instrument.chart.AFSK1200ZeroCrossingErrorDetectorChart; +import io.github.dsheirer.gui.instrument.chart.DecodedSymbolChart; +import io.github.dsheirer.gui.instrument.chart.IInstrumentedAFSK1200Decoder; +import io.github.dsheirer.gui.instrument.chart.RealSampleLineChart; import io.github.dsheirer.message.Message; import io.github.dsheirer.module.decode.DecoderType; import io.github.dsheirer.sample.Listener; diff --git a/src/main/java/io/github/dsheirer/instrument/decoder/AbstractDecoderPane.java b/src/main/java/io/github/dsheirer/gui/instrument/decoder/AbstractDecoderPane.java similarity index 96% rename from src/main/java/io/github/dsheirer/instrument/decoder/AbstractDecoderPane.java rename to src/main/java/io/github/dsheirer/gui/instrument/decoder/AbstractDecoderPane.java index 3770c136..64324438 100644 --- a/src/main/java/io/github/dsheirer/instrument/decoder/AbstractDecoderPane.java +++ b/src/main/java/io/github/dsheirer/gui/instrument/decoder/AbstractDecoderPane.java @@ -13,7 +13,7 @@ * If not, see * ******************************************************************************/ -package io.github.dsheirer.instrument.decoder; +package io.github.dsheirer.gui.instrument.decoder; import io.github.dsheirer.sample.Listener; import io.github.dsheirer.sample.SampleType; diff --git a/src/main/java/io/github/dsheirer/instrument/decoder/ComplexDecoderPane.java b/src/main/java/io/github/dsheirer/gui/instrument/decoder/ComplexDecoderPane.java similarity index 97% rename from src/main/java/io/github/dsheirer/instrument/decoder/ComplexDecoderPane.java rename to src/main/java/io/github/dsheirer/gui/instrument/decoder/ComplexDecoderPane.java index edbdece8..0bd39097 100644 --- a/src/main/java/io/github/dsheirer/instrument/decoder/ComplexDecoderPane.java +++ b/src/main/java/io/github/dsheirer/gui/instrument/decoder/ComplexDecoderPane.java @@ -13,7 +13,7 @@ * If not, see * ******************************************************************************/ -package io.github.dsheirer.instrument.decoder; +package io.github.dsheirer.gui.instrument.decoder; import io.github.dsheirer.module.decode.DecoderType; import io.github.dsheirer.sample.Listener; diff --git a/src/main/java/io/github/dsheirer/instrument/decoder/DecoderPaneFactory.java b/src/main/java/io/github/dsheirer/gui/instrument/decoder/DecoderPaneFactory.java similarity index 98% rename from src/main/java/io/github/dsheirer/instrument/decoder/DecoderPaneFactory.java rename to src/main/java/io/github/dsheirer/gui/instrument/decoder/DecoderPaneFactory.java index 25812155..3b2c8ff0 100644 --- a/src/main/java/io/github/dsheirer/instrument/decoder/DecoderPaneFactory.java +++ b/src/main/java/io/github/dsheirer/gui/instrument/decoder/DecoderPaneFactory.java @@ -13,7 +13,7 @@ * If not, see * ******************************************************************************/ -package io.github.dsheirer.instrument.decoder; +package io.github.dsheirer.gui.instrument.decoder; import io.github.dsheirer.module.decode.DecoderType; import io.github.dsheirer.module.decode.p25.P25Decoder; diff --git a/src/main/java/io/github/dsheirer/instrument/decoder/Fleetsync2Pane.java b/src/main/java/io/github/dsheirer/gui/instrument/decoder/Fleetsync2Pane.java similarity index 96% rename from src/main/java/io/github/dsheirer/instrument/decoder/Fleetsync2Pane.java rename to src/main/java/io/github/dsheirer/gui/instrument/decoder/Fleetsync2Pane.java index f7a8f5e6..d37f97fb 100644 --- a/src/main/java/io/github/dsheirer/instrument/decoder/Fleetsync2Pane.java +++ b/src/main/java/io/github/dsheirer/gui/instrument/decoder/Fleetsync2Pane.java @@ -13,7 +13,7 @@ * If not, see * ******************************************************************************/ -package io.github.dsheirer.instrument.decoder; +package io.github.dsheirer.gui.instrument.decoder; import io.github.dsheirer.module.decode.DecoderType; import io.github.dsheirer.module.decode.fleetsync2.Fleetsync2DecoderInstrumented; diff --git a/src/main/java/io/github/dsheirer/instrument/decoder/LJ1200Pane.java b/src/main/java/io/github/dsheirer/gui/instrument/decoder/LJ1200Pane.java similarity index 97% rename from src/main/java/io/github/dsheirer/instrument/decoder/LJ1200Pane.java rename to src/main/java/io/github/dsheirer/gui/instrument/decoder/LJ1200Pane.java index 801331ba..b762fab1 100644 --- a/src/main/java/io/github/dsheirer/instrument/decoder/LJ1200Pane.java +++ b/src/main/java/io/github/dsheirer/gui/instrument/decoder/LJ1200Pane.java @@ -13,7 +13,7 @@ * If not, see * ******************************************************************************/ -package io.github.dsheirer.instrument.decoder; +package io.github.dsheirer.gui.instrument.decoder; import io.github.dsheirer.dsp.symbol.ISyncDetectListener; import io.github.dsheirer.module.decode.DecoderType; diff --git a/src/main/java/io/github/dsheirer/instrument/decoder/LTRNetPane.java b/src/main/java/io/github/dsheirer/gui/instrument/decoder/LTRNetPane.java similarity index 94% rename from src/main/java/io/github/dsheirer/instrument/decoder/LTRNetPane.java rename to src/main/java/io/github/dsheirer/gui/instrument/decoder/LTRNetPane.java index e0a5f15a..bf09bb33 100644 --- a/src/main/java/io/github/dsheirer/instrument/decoder/LTRNetPane.java +++ b/src/main/java/io/github/dsheirer/gui/instrument/decoder/LTRNetPane.java @@ -13,11 +13,11 @@ * If not, see * ******************************************************************************/ -package io.github.dsheirer.instrument.decoder; +package io.github.dsheirer.gui.instrument.decoder; -import io.github.dsheirer.instrument.chart.LTRNetSampleBufferChart; -import io.github.dsheirer.instrument.chart.RealSampleLineChart; -import io.github.dsheirer.instrument.chart.ZeroCrossingErrorDetectorChart; +import io.github.dsheirer.gui.instrument.chart.LTRNetSampleBufferChart; +import io.github.dsheirer.gui.instrument.chart.RealSampleLineChart; +import io.github.dsheirer.gui.instrument.chart.ZeroCrossingErrorDetectorChart; import io.github.dsheirer.message.Message; import io.github.dsheirer.message.MessageDirection; import io.github.dsheirer.module.decode.DecoderType; diff --git a/src/main/java/io/github/dsheirer/instrument/decoder/MDC1200Pane.java b/src/main/java/io/github/dsheirer/gui/instrument/decoder/MDC1200Pane.java similarity index 97% rename from src/main/java/io/github/dsheirer/instrument/decoder/MDC1200Pane.java rename to src/main/java/io/github/dsheirer/gui/instrument/decoder/MDC1200Pane.java index 6027eb3e..98807bfa 100644 --- a/src/main/java/io/github/dsheirer/instrument/decoder/MDC1200Pane.java +++ b/src/main/java/io/github/dsheirer/gui/instrument/decoder/MDC1200Pane.java @@ -13,7 +13,7 @@ * If not, see * ******************************************************************************/ -package io.github.dsheirer.instrument.decoder; +package io.github.dsheirer.gui.instrument.decoder; import io.github.dsheirer.dsp.symbol.ISyncDetectListener; import io.github.dsheirer.module.decode.DecoderType; diff --git a/src/main/java/io/github/dsheirer/instrument/decoder/MPT1327Pane.java b/src/main/java/io/github/dsheirer/gui/instrument/decoder/MPT1327Pane.java similarity index 97% rename from src/main/java/io/github/dsheirer/instrument/decoder/MPT1327Pane.java rename to src/main/java/io/github/dsheirer/gui/instrument/decoder/MPT1327Pane.java index 696f101b..83910f71 100644 --- a/src/main/java/io/github/dsheirer/instrument/decoder/MPT1327Pane.java +++ b/src/main/java/io/github/dsheirer/gui/instrument/decoder/MPT1327Pane.java @@ -13,7 +13,7 @@ * If not, see * ******************************************************************************/ -package io.github.dsheirer.instrument.decoder; +package io.github.dsheirer.gui.instrument.decoder; import io.github.dsheirer.dsp.symbol.ISyncDetectListener; import io.github.dsheirer.module.decode.DecoderType; diff --git a/src/main/java/io/github/dsheirer/instrument/decoder/P25Phase1C4FMPane.java b/src/main/java/io/github/dsheirer/gui/instrument/decoder/P25Phase1C4FMPane.java similarity index 94% rename from src/main/java/io/github/dsheirer/instrument/decoder/P25Phase1C4FMPane.java rename to src/main/java/io/github/dsheirer/gui/instrument/decoder/P25Phase1C4FMPane.java index 223960cb..34755d18 100644 --- a/src/main/java/io/github/dsheirer/instrument/decoder/P25Phase1C4FMPane.java +++ b/src/main/java/io/github/dsheirer/gui/instrument/decoder/P25Phase1C4FMPane.java @@ -13,13 +13,13 @@ * If not, see * ******************************************************************************/ -package io.github.dsheirer.instrument.decoder; +package io.github.dsheirer.gui.instrument.decoder; -import io.github.dsheirer.instrument.chart.ComplexSampleLineChart; -import io.github.dsheirer.instrument.chart.DoubleLineChart; -import io.github.dsheirer.instrument.chart.EyeDiagramChart; -import io.github.dsheirer.instrument.chart.PhaseLineChart; -import io.github.dsheirer.instrument.chart.SymbolChart; +import io.github.dsheirer.gui.instrument.chart.ComplexSampleLineChart; +import io.github.dsheirer.gui.instrument.chart.DoubleLineChart; +import io.github.dsheirer.gui.instrument.chart.EyeDiagramChart; +import io.github.dsheirer.gui.instrument.chart.PhaseLineChart; +import io.github.dsheirer.gui.instrument.chart.SymbolChart; import io.github.dsheirer.message.Message; import io.github.dsheirer.module.decode.DecoderType; import io.github.dsheirer.module.decode.p25.P25DecoderC4FMInstrumented; diff --git a/src/main/java/io/github/dsheirer/instrument/decoder/P25Phase1LSMPane.java b/src/main/java/io/github/dsheirer/gui/instrument/decoder/P25Phase1LSMPane.java similarity index 94% rename from src/main/java/io/github/dsheirer/instrument/decoder/P25Phase1LSMPane.java rename to src/main/java/io/github/dsheirer/gui/instrument/decoder/P25Phase1LSMPane.java index 53335842..1f96e25e 100644 --- a/src/main/java/io/github/dsheirer/instrument/decoder/P25Phase1LSMPane.java +++ b/src/main/java/io/github/dsheirer/gui/instrument/decoder/P25Phase1LSMPane.java @@ -13,13 +13,13 @@ * If not, see * ******************************************************************************/ -package io.github.dsheirer.instrument.decoder; +package io.github.dsheirer.gui.instrument.decoder; -import io.github.dsheirer.instrument.chart.ComplexSampleLineChart; -import io.github.dsheirer.instrument.chart.DoubleLineChart; -import io.github.dsheirer.instrument.chart.EyeDiagramChart; -import io.github.dsheirer.instrument.chart.PhaseLineChart; -import io.github.dsheirer.instrument.chart.SymbolChart; +import io.github.dsheirer.gui.instrument.chart.ComplexSampleLineChart; +import io.github.dsheirer.gui.instrument.chart.DoubleLineChart; +import io.github.dsheirer.gui.instrument.chart.EyeDiagramChart; +import io.github.dsheirer.gui.instrument.chart.PhaseLineChart; +import io.github.dsheirer.gui.instrument.chart.SymbolChart; import io.github.dsheirer.message.Message; import io.github.dsheirer.module.decode.DecoderType; import io.github.dsheirer.module.decode.p25.P25DecoderLSMInstrumented; diff --git a/src/main/java/io/github/dsheirer/instrument/decoder/RealDecoderPane.java b/src/main/java/io/github/dsheirer/gui/instrument/decoder/RealDecoderPane.java similarity index 97% rename from src/main/java/io/github/dsheirer/instrument/decoder/RealDecoderPane.java rename to src/main/java/io/github/dsheirer/gui/instrument/decoder/RealDecoderPane.java index 3853c36c..afc05786 100644 --- a/src/main/java/io/github/dsheirer/instrument/decoder/RealDecoderPane.java +++ b/src/main/java/io/github/dsheirer/gui/instrument/decoder/RealDecoderPane.java @@ -13,7 +13,7 @@ * If not, see * ******************************************************************************/ -package io.github.dsheirer.instrument.decoder; +package io.github.dsheirer.gui.instrument.decoder; import io.github.dsheirer.module.decode.DecoderType; import io.github.dsheirer.sample.Listener; diff --git a/src/main/java/io/github/dsheirer/instrument/decoder/Tait1200Pane.java b/src/main/java/io/github/dsheirer/gui/instrument/decoder/Tait1200Pane.java similarity index 97% rename from src/main/java/io/github/dsheirer/instrument/decoder/Tait1200Pane.java rename to src/main/java/io/github/dsheirer/gui/instrument/decoder/Tait1200Pane.java index 75cfb35e..9b8b997e 100644 --- a/src/main/java/io/github/dsheirer/instrument/decoder/Tait1200Pane.java +++ b/src/main/java/io/github/dsheirer/gui/instrument/decoder/Tait1200Pane.java @@ -13,7 +13,7 @@ * If not, see * ******************************************************************************/ -package io.github.dsheirer.instrument.decoder; +package io.github.dsheirer.gui.instrument.decoder; import io.github.dsheirer.dsp.symbol.ISyncDetectListener; import io.github.dsheirer.module.decode.DecoderType; diff --git a/src/main/java/io/github/dsheirer/module/decode/fleetsync2/Fleetsync2DecoderInstrumented.java b/src/main/java/io/github/dsheirer/module/decode/fleetsync2/Fleetsync2DecoderInstrumented.java index 88abbfeb..481c2f26 100644 --- a/src/main/java/io/github/dsheirer/module/decode/fleetsync2/Fleetsync2DecoderInstrumented.java +++ b/src/main/java/io/github/dsheirer/module/decode/fleetsync2/Fleetsync2DecoderInstrumented.java @@ -16,7 +16,7 @@ package io.github.dsheirer.module.decode.fleetsync2; import io.github.dsheirer.dsp.afsk.AFSK1200DecoderInstrumented; -import io.github.dsheirer.instrument.chart.IInstrumentedAFSK1200Decoder; +import io.github.dsheirer.gui.instrument.chart.IInstrumentedAFSK1200Decoder; import io.github.dsheirer.sample.buffer.ReusableBuffer; import javafx.beans.property.SimpleIntegerProperty; diff --git a/src/main/java/io/github/dsheirer/module/decode/lj1200/LJ1200DecoderInstrumented.java b/src/main/java/io/github/dsheirer/module/decode/lj1200/LJ1200DecoderInstrumented.java index 80648fb2..b86661ee 100644 --- a/src/main/java/io/github/dsheirer/module/decode/lj1200/LJ1200DecoderInstrumented.java +++ b/src/main/java/io/github/dsheirer/module/decode/lj1200/LJ1200DecoderInstrumented.java @@ -16,7 +16,7 @@ package io.github.dsheirer.module.decode.lj1200; import io.github.dsheirer.dsp.afsk.AFSK1200DecoderInstrumented; -import io.github.dsheirer.instrument.chart.IInstrumentedAFSK1200Decoder; +import io.github.dsheirer.gui.instrument.chart.IInstrumentedAFSK1200Decoder; import io.github.dsheirer.sample.buffer.ReusableBuffer; import javafx.beans.property.SimpleIntegerProperty; diff --git a/src/main/java/io/github/dsheirer/module/decode/mdc1200/MDCDecoderInstrumented.java b/src/main/java/io/github/dsheirer/module/decode/mdc1200/MDCDecoderInstrumented.java index c430b999..a3b90632 100644 --- a/src/main/java/io/github/dsheirer/module/decode/mdc1200/MDCDecoderInstrumented.java +++ b/src/main/java/io/github/dsheirer/module/decode/mdc1200/MDCDecoderInstrumented.java @@ -16,7 +16,7 @@ package io.github.dsheirer.module.decode.mdc1200; import io.github.dsheirer.dsp.afsk.AFSK1200DecoderInstrumented; -import io.github.dsheirer.instrument.chart.IInstrumentedAFSK1200Decoder; +import io.github.dsheirer.gui.instrument.chart.IInstrumentedAFSK1200Decoder; import io.github.dsheirer.sample.buffer.ReusableBuffer; import javafx.beans.property.SimpleIntegerProperty; diff --git a/src/main/java/io/github/dsheirer/module/decode/mpt1327/MPT1327DecoderInstrumented.java b/src/main/java/io/github/dsheirer/module/decode/mpt1327/MPT1327DecoderInstrumented.java index 365ff66b..8ef42fce 100644 --- a/src/main/java/io/github/dsheirer/module/decode/mpt1327/MPT1327DecoderInstrumented.java +++ b/src/main/java/io/github/dsheirer/module/decode/mpt1327/MPT1327DecoderInstrumented.java @@ -16,7 +16,7 @@ package io.github.dsheirer.module.decode.mpt1327; import io.github.dsheirer.dsp.afsk.AFSK1200DecoderInstrumented; -import io.github.dsheirer.instrument.chart.IInstrumentedAFSK1200Decoder; +import io.github.dsheirer.gui.instrument.chart.IInstrumentedAFSK1200Decoder; import io.github.dsheirer.sample.buffer.ReusableBuffer; import javafx.beans.property.SimpleIntegerProperty; diff --git a/src/main/java/io/github/dsheirer/module/decode/tait/Tait1200DecoderInstrumented.java b/src/main/java/io/github/dsheirer/module/decode/tait/Tait1200DecoderInstrumented.java index 93150c67..b4f7eab4 100644 --- a/src/main/java/io/github/dsheirer/module/decode/tait/Tait1200DecoderInstrumented.java +++ b/src/main/java/io/github/dsheirer/module/decode/tait/Tait1200DecoderInstrumented.java @@ -16,7 +16,7 @@ package io.github.dsheirer.module.decode.tait; import io.github.dsheirer.dsp.afsk.AFSK1200DecoderInstrumented; -import io.github.dsheirer.instrument.chart.IInstrumentedAFSK1200Decoder; +import io.github.dsheirer.gui.instrument.chart.IInstrumentedAFSK1200Decoder; import io.github.dsheirer.sample.buffer.ReusableBuffer; import javafx.beans.property.SimpleIntegerProperty; From 19fa256c608b55d81c5e421bdd6c0b7bca088fcd Mon Sep 17 00:00:00 2001 From: Denny Date: Sat, 23 Jun 2018 09:49:40 -0400 Subject: [PATCH 78/87] [WIP]Updates mixer manager and mixer source to use 8 kHz audio for sound card inputs. Updated DecoderFactory to only include an AM/FM demodulator when we're using a tuner input. --- .../module/decode/DecoderFactory.java | 40 +- .../dsheirer/source/mixer/MixerManager.java | 630 +++++++++--------- .../source/mixer/RealMixerSource.java | 26 +- 3 files changed, 341 insertions(+), 355 deletions(-) diff --git a/src/main/java/io/github/dsheirer/module/decode/DecoderFactory.java b/src/main/java/io/github/dsheirer/module/decode/DecoderFactory.java index f0cfe14e..c5d6a81b 100644 --- a/src/main/java/io/github/dsheirer/module/decode/DecoderFactory.java +++ b/src/main/java/io/github/dsheirer/module/decode/DecoderFactory.java @@ -89,6 +89,7 @@ import io.github.dsheirer.module.decode.tait.Tait1200DecoderState; import io.github.dsheirer.module.demodulate.am.AMDemodulatorModule; import io.github.dsheirer.module.demodulate.audio.DemodulatedAudioFilterModule; import io.github.dsheirer.module.demodulate.fm.FMDemodulatorModule; +import io.github.dsheirer.source.SourceType; import org.slf4j.Logger; import org.slf4j.LoggerFactory; @@ -147,31 +148,44 @@ public class DecoderFactory switch(decodeConfig.getDecoderType()) { case AM: + if(channel.getSourceConfiguration().getSourceType() == SourceType.TUNER) + { + modules.add(new AMDemodulatorModule()); + //TODO: update AM demodulator to produce demodulated 8 kHz audio and delete the following filter module + modules.add(new DemodulatedAudioFilterModule(4000, 6000)); + } + modules.add(new AMDecoder(decodeConfig)); modules.add(new AlwaysUnsquelchedDecoderState(DecoderType.AM, channel.getName())); - modules.add(new AMDemodulatorModule()); -//TODO: update AM demodulator to produce demodulated 8 kHz audio and delete the following filter module - modules.add(new DemodulatedAudioFilterModule(4000, 6000)); modules.add(new AudioModule(metadata)); break; case NBFM: modules.add(new NBFMDecoder(decodeConfig)); modules.add(new AlwaysUnsquelchedDecoderState(DecoderType.NBFM, channel.getName())); - modules.add(new FMDemodulatorModule(CHANNEL_BANDWIDTH, DEMODULATED_AUDIO_SAMPLE_RATE)); modules.add(new AudioModule(metadata)); + if(channel.getSourceConfiguration().getSourceType() == SourceType.TUNER) + { + modules.add(new FMDemodulatorModule(CHANNEL_BANDWIDTH, DEMODULATED_AUDIO_SAMPLE_RATE)); + } break; case LTR_STANDARD: MessageDirection direction = ((DecodeConfigLTRStandard) decodeConfig).getMessageDirection(); modules.add(new LTRStandardDecoder(aliasList, direction)); modules.add(new LTRStandardDecoderState(aliasList)); - modules.add(new FMDemodulatorModule(CHANNEL_BANDWIDTH, DEMODULATED_AUDIO_SAMPLE_RATE)); modules.add(new AudioModule(metadata)); + if(channel.getSourceConfiguration().getSourceType() == SourceType.TUNER) + { + modules.add(new FMDemodulatorModule(CHANNEL_BANDWIDTH, DEMODULATED_AUDIO_SAMPLE_RATE)); + } break; case LTR_NET: modules.add(new LTRNetDecoder((DecodeConfigLTRNet) decodeConfig, aliasList)); modules.add(new LTRNetDecoderState(aliasList)); - modules.add(new FMDemodulatorModule(CHANNEL_BANDWIDTH, DEMODULATED_AUDIO_SAMPLE_RATE)); modules.add(new AudioModule(metadata)); + if(channel.getSourceConfiguration().getSourceType() == SourceType.TUNER) + { + modules.add(new FMDemodulatorModule(CHANNEL_BANDWIDTH, DEMODULATED_AUDIO_SAMPLE_RATE)); + } break; case MPT1327: DecodeConfigMPT1327 mptConfig = (DecodeConfigMPT1327) decodeConfig; @@ -179,8 +193,11 @@ public class DecoderFactory Sync sync = mptConfig.getSync(); modules.add(new MPT1327Decoder(aliasList, sync)); modules.add(new MPT1327DecoderState(aliasList, channelMap, channelType, mptConfig.getCallTimeout() * 1000)); - modules.add(new FMDemodulatorModule(CHANNEL_BANDWIDTH, DEMODULATED_AUDIO_SAMPLE_RATE)); modules.add(new AudioModule(metadata)); + if(channel.getSourceConfiguration().getSourceType() == SourceType.TUNER) + { + modules.add(new FMDemodulatorModule(CHANNEL_BANDWIDTH, DEMODULATED_AUDIO_SAMPLE_RATE)); + } if(channelType == ChannelType.STANDARD) { @@ -192,15 +209,17 @@ public class DecoderFactory case PASSPORT: modules.add(new PassportDecoder(decodeConfig, aliasList)); modules.add(new PassportDecoderState(aliasList)); - modules.add(new FMDemodulatorModule(CHANNEL_BANDWIDTH, DEMODULATED_AUDIO_SAMPLE_RATE)); modules.add(new AudioModule(metadata)); + if(channel.getSourceConfiguration().getSourceType() == SourceType.TUNER) + { + modules.add(new FMDemodulatorModule(CHANNEL_BANDWIDTH, DEMODULATED_AUDIO_SAMPLE_RATE)); + } break; case P25_PHASE1: DecodeConfigP25Phase1 p25Config = (DecodeConfigP25Phase1) decodeConfig; Modulation modulation = p25Config.getModulation(); - switch(modulation) { case C4FM: @@ -288,8 +307,7 @@ public class DecoderFactory { if(module instanceof Decoder) { - filterSet.addFilters(getMessageFilter( - ((Decoder) module).getDecoderType())); + filterSet.addFilters(getMessageFilter(((Decoder)module).getDecoderType())); } } diff --git a/src/main/java/io/github/dsheirer/source/mixer/MixerManager.java b/src/main/java/io/github/dsheirer/source/mixer/MixerManager.java index 7fa74a20..fad3a366 100644 --- a/src/main/java/io/github/dsheirer/source/mixer/MixerManager.java +++ b/src/main/java/io/github/dsheirer/source/mixer/MixerManager.java @@ -1,19 +1,17 @@ /******************************************************************************* - * SDR Trunk - * Copyright (C) 2014 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 + * 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 + * ******************************************************************************/ package io.github.dsheirer.source.mixer; @@ -42,395 +40,369 @@ import java.util.Collections; import java.util.EnumSet; import java.util.HashMap; import java.util.List; +import java.util.Map; public class MixerManager { - private final static Logger mLog = - LoggerFactory.getLogger( MixerManager.class ); + private final static Logger mLog = LoggerFactory.getLogger(MixerManager.class); - private List mInputMixers = new ArrayList<>(); - private List mOutputMixers = new ArrayList<>(); - private HashMap mMixerTuners = new HashMap<>(); + private List mInputMixers = new ArrayList<>(); + private List mOutputMixers = new ArrayList<>(); + private Map mMixerTuners = new HashMap<>(); - public MixerManager() - { - loadMixers(); - } - - public RealMixerSource getSource( SourceConfiguration config ) + public MixerManager() { - RealMixerSource retVal = null; - - if( config instanceof SourceConfigMixer) - { - SourceConfigMixer mixerConfig = (SourceConfigMixer)config; + loadMixers(); + } - String mixerName = mixerConfig.getMixer(); + public RealMixerSource getSource(SourceConfiguration config) + { + RealMixerSource retVal = null; - if( mixerName != null ) - { - InputMixerConfiguration mixer = getInputMixer( mixerName ); + if(config instanceof SourceConfigMixer) + { + SourceConfigMixer mixerConfig = (SourceConfigMixer)config; - if( mixer != null ) - { - MixerChannel channel = mixerConfig.getChannel(); - - if( mixer.supportsChannel( channel ) ) - { - - if( channel == MixerChannel.MONO ) - { - DataLine.Info info = - new DataLine.Info(TargetDataLine.class, - AudioFormats.PCM_SIGNED_48KHZ_16BITS_MONO ); + String mixerName = mixerConfig.getMixer(); + + if(mixerName != null) + { + InputMixerConfiguration mixer = getInputMixer(mixerName); + + if(mixer != null) + { + MixerChannel channel = mixerConfig.getChannel(); + + if(mixer.supportsChannel(channel)) + { + + if(channel == MixerChannel.MONO) + { + DataLine.Info info = new DataLine.Info(TargetDataLine.class, + AudioFormats.PCM_SIGNED_8KHZ_16BITS_MONO); + + TargetDataLine dataLine; - TargetDataLine dataLine; - try { - dataLine = (TargetDataLine)mixer - .getMixer().getLine( info ); + dataLine = (TargetDataLine)mixer.getMixer().getLine(info); - if( dataLine != null ) - { - return new RealMixerSource( dataLine, - AudioFormats.PCM_SIGNED_48KHZ_16BITS_MONO, - new ShortAdapter() ); - } + if(dataLine != null) + { + return new RealMixerSource(dataLine, AudioFormats.PCM_SIGNED_8KHZ_16BITS_MONO, + new ShortAdapter()); + } } - catch ( LineUnavailableException e ) + catch(LineUnavailableException e) { - mLog.error( "couldn't get mixer data line " + - "for [" + mixerName + "] for channel [" + - channel.name() + "]", e ); + mLog.error("couldn't get mixer data line for [" + mixerName + "] for channel [" + + channel.name() + "]", e); } - - } - else - { - DataLine.Info info = - new DataLine.Info(TargetDataLine.class, - AudioFormats.PCM_SIGNED_48KHZ_16BITS_STEREO ); - TargetDataLine dataLine; + } + else + { + DataLine.Info info = new DataLine.Info(TargetDataLine.class, + AudioFormats.PCM_SIGNED_8KHZ_16BITS_STEREO); - try + TargetDataLine dataLine; + + try { - dataLine = (TargetDataLine)mixer - .getMixer().getLine( info ); + dataLine = (TargetDataLine)mixer.getMixer().getLine(info); - if( dataLine != null ) - { - return new RealMixerSource( dataLine, - AudioFormats.PCM_SIGNED_48KHZ_16BITS_STEREO, - new ChannelShortAdapter( - mixerConfig.getChannel() ) ); - } + if(dataLine != null) + { + return new RealMixerSource(dataLine, AudioFormats.PCM_SIGNED_8KHZ_16BITS_STEREO, + new ChannelShortAdapter(mixerConfig.getChannel())); + } } - catch ( LineUnavailableException e ) + catch(LineUnavailableException e) { - mLog.error( "couldn't get mixer data line " + - "for [" + mixerName + "] for channel [" + - channel.name() + "]", e ); + mLog.error("couldn't get mixer data line for [" + mixerName + "] for channel [" + + channel.name() + "]", e); } - } - } - } - } - } + } + } + } + } + } - return null; + return null; } - + public InputMixerConfiguration[] getInputMixers() { - return mInputMixers.toArray( new InputMixerConfiguration[ mInputMixers.size() ] ); + return mInputMixers.toArray(new InputMixerConfiguration[mInputMixers.size()]); } - - public InputMixerConfiguration getInputMixer( String name ) + + public InputMixerConfiguration getInputMixer(String name) { - for( InputMixerConfiguration mixer: mInputMixers ) - { - if( mixer.getMixerName().contentEquals( name ) ) - { - return mixer; - } - } - - return null; + for(InputMixerConfiguration mixer : mInputMixers) + { + if(mixer.getMixerName().contentEquals(name)) + { + return mixer; + } + } + + return null; } public MixerChannelConfiguration[] getOutputMixers() { - return mOutputMixers.toArray( new MixerChannelConfiguration[ mOutputMixers.size() ] ); + return mOutputMixers.toArray(new MixerChannelConfiguration[mOutputMixers.size()]); } - + public Collection getMixerTunerDataLines() { - if( mMixerTuners.isEmpty() ) - { - return Collections.emptyList(); - } - else - { - return mMixerTuners.values(); - } - } - - private void loadMixers() - { - StringBuilder sb = new StringBuilder(); - - sb.append( "loading system mixer devices\n" ); - - for( Mixer.Info mixerInfo: AudioSystem.getMixerInfo() ) + if(mMixerTuners.isEmpty()) { - //Sort between the mixers and the tuner mixers, and load each - MixerTunerType mixerTunerType = MixerTunerType.getMixerTunerType( mixerInfo ); - - if( mixerTunerType == MixerTunerType.UNKNOWN ) - { - Mixer mixer = AudioSystem.getMixer( mixerInfo ); + return Collections.emptyList(); + } + else + { + return mMixerTuners.values(); + } + } - if( mixer != null ) + private void loadMixers() + { + StringBuilder sb = new StringBuilder(); + + sb.append("loading system mixer devices\n"); + + for(Mixer.Info mixerInfo : AudioSystem.getMixerInfo()) + { + //Sort between the mixers and the tuner mixers, and load each + MixerTunerType mixerTunerType = MixerTunerType.getMixerTunerType(mixerInfo); + + if(mixerTunerType == MixerTunerType.UNKNOWN) + { + Mixer mixer = AudioSystem.getMixer(mixerInfo); + + if(mixer != null) { - EnumSet inputChannels = getSupportedTargetChannels( mixer ); - - if( inputChannels != null ) - { - mInputMixers.add( - new InputMixerConfiguration( mixer, inputChannels ) ); - - sb.append( "\t[LOADED] Input: " + mixerInfo.getName() + - " CHANNELS: " + inputChannels + "\n" ); - } - - EnumSet outputChannels = getSupportedSourceChannels( mixer ); - - if( outputChannels != null ) - { - for( MixerChannel channel: outputChannels ) - { - mOutputMixers.add( new MixerChannelConfiguration( mixer, channel ) ); - } + EnumSet inputChannels = getSupportedTargetChannels(mixer); - sb.append( "\t[LOADED] Output: " + mixerInfo.getName() + - " CHANNELS: " + outputChannels + "\n" ); - } + if(inputChannels != null) + { + mInputMixers.add(new InputMixerConfiguration(mixer, inputChannels)); + + sb.append("\t[LOADED] Input: " + mixerInfo.getName() + " CHANNELS: " + inputChannels + "\n"); + } + + EnumSet outputChannels = getSupportedSourceChannels(mixer); + + if(outputChannels != null) + { + for(MixerChannel channel : outputChannels) + { + mOutputMixers.add(new MixerChannelConfiguration(mixer, channel)); + } + + sb.append("\t[LOADED] Output: " + mixerInfo.getName() + " CHANNELS: " + outputChannels + "\n"); + } } else { - sb.append( "\t[NOT LOADED] Mixer:" + mixerInfo.getName() + - " - couldn't get mixer\n" ); + sb.append("\t[NOT LOADED] Mixer:" + mixerInfo.getName() + " - couldn't get mixer\n"); } - } - else //Process as a Mixer-based tuner data line - { - TargetDataLine tdl = - getTargetDataLine( mixerInfo, - mixerTunerType.getAudioFormat() ); + } + else + { + TargetDataLine tdl = getTargetDataLine(mixerInfo, mixerTunerType.getAudioFormat()); - if( tdl != null ) - { - switch( mixerTunerType ) - { - case FUNCUBE_DONGLE_PRO: - case FUNCUBE_DONGLE_PRO_PLUS: - mMixerTuners.put( mixerInfo.getName(), - new MixerTunerDataLine( tdl, mixerTunerType ) ); - sb.append( "\t[LOADED] FunCube Dongle Mixer Tuner:" + mixerInfo.getName() + "[" + mixerTunerType.getDisplayString() + "]\n" ); - break; - case UNKNOWN: - default: - sb.append( "\t[NOT LOADED] Tuner:" + mixerInfo.getName() + " - unrecognized tuner type\n" ); - break; - } - } - } + if(tdl != null) + { + switch(mixerTunerType) + { + case FUNCUBE_DONGLE_PRO: + case FUNCUBE_DONGLE_PRO_PLUS: + mMixerTuners.put(mixerInfo.getName(), + new MixerTunerDataLine(tdl, mixerTunerType)); + sb.append("\t[LOADED] FunCube Dongle Mixer Tuner:" + mixerInfo.getName() + "[" + mixerTunerType.getDisplayString() + "]\n"); + break; + case UNKNOWN: + default: + sb.append("\t[NOT LOADED] Tuner:" + mixerInfo.getName() + " - unrecognized tuner type\n"); + break; + } + } + } } - - mLog.info( sb.toString() ); - } - - public HashMap getMixerTuners() - { - return mMixerTuners; - } - - private TargetDataLine getTargetDataLine( Mixer.Info mixerInfo, AudioFormat format ) - { - TargetDataLine retVal = null; - - Mixer mixer = AudioSystem.getMixer( mixerInfo ); - if( mixer != null ) + mLog.info(sb.toString()); + } + + public Map getMixerTuners() + { + return mMixerTuners; + } + + private TargetDataLine getTargetDataLine(Mixer.Info mixerInfo, AudioFormat format) + { + TargetDataLine retVal = null; + + Mixer mixer = AudioSystem.getMixer(mixerInfo); + + if(mixer != null) { try { - DataLine.Info datalineInfo= - new DataLine.Info(TargetDataLine.class, format ); + DataLine.Info datalineInfo = new DataLine.Info(TargetDataLine.class, format); - retVal = (TargetDataLine) mixer.getLine( datalineInfo ); + retVal = (TargetDataLine)mixer.getLine(datalineInfo); } - catch( Exception e ) + catch(Exception e) { - //Do nothing ... we couldn't get the TDL + //Do nothing ... we couldn't get the TDL } } - + return retVal; - } - - private EnumSet getSupportedTargetChannels( Mixer mixer ) - { - DataLine.Info stereoInfo= - new DataLine.Info(TargetDataLine.class, - AudioFormats.PCM_SIGNED_48KHZ_16BITS_STEREO ); + } - boolean stereoSupported = mixer.isLineSupported( stereoInfo ); - - DataLine.Info monoInfo= - new DataLine.Info(TargetDataLine.class, - AudioFormats.PCM_SIGNED_48KHZ_16BITS_MONO ); + private EnumSet getSupportedTargetChannels(Mixer mixer) + { + DataLine.Info stereoInfo = new DataLine.Info(TargetDataLine.class, + AudioFormats.PCM_SIGNED_8KHZ_16BITS_STEREO); - boolean monoSupported = mixer.isLineSupported( monoInfo ); - - if( stereoSupported && monoSupported ) + boolean stereoSupported = mixer.isLineSupported(stereoInfo); + + DataLine.Info monoInfo = new DataLine.Info(TargetDataLine.class, + AudioFormats.PCM_SIGNED_8KHZ_16BITS_MONO); + + boolean monoSupported = mixer.isLineSupported(monoInfo); + + if(stereoSupported && monoSupported) { - return EnumSet.of( MixerChannel.LEFT, - MixerChannel.RIGHT, - MixerChannel.MONO ); + return EnumSet.of(MixerChannel.LEFT, MixerChannel.RIGHT, MixerChannel.MONO); } - else if( stereoSupported ) + else if(stereoSupported) { - return EnumSet.of( MixerChannel.LEFT, - MixerChannel.RIGHT, - MixerChannel.MONO ); + return EnumSet.of(MixerChannel.LEFT, MixerChannel.RIGHT, MixerChannel.MONO); } - else if( monoSupported ) + else if(monoSupported) { - return EnumSet.of( MixerChannel.MONO ); + return EnumSet.of(MixerChannel.MONO); } - + return null; - } + } - /** - * Returns enumset of SourceDataLine (audio output) channels - * (MONO and/or STEREO) supported by the mixer, or null if the mixer doesn't - * have any source data lines. - */ - private EnumSet getSupportedSourceChannels( Mixer mixer ) - { - DataLine.Info stereoInfo= - new DataLine.Info(SourceDataLine.class, - AudioFormats.PCM_SIGNED_48KHZ_16BITS_STEREO ); + /** + * Returns enumset of SourceDataLine (audio output) channels + * (MONO and/or STEREO) supported by the mixer, or null if the mixer doesn't + * have any source data lines. + */ + private EnumSet getSupportedSourceChannels(Mixer mixer) + { + DataLine.Info stereoInfo = new DataLine.Info(SourceDataLine.class, + AudioFormats.PCM_SIGNED_8KHZ_16BITS_STEREO); - boolean stereoSupported = mixer.isLineSupported( stereoInfo ); - - DataLine.Info monoInfo= - new DataLine.Info(SourceDataLine.class, - AudioFormats.PCM_SIGNED_48KHZ_16BITS_MONO ); + boolean stereoSupported = mixer.isLineSupported(stereoInfo); - boolean monoSupported = mixer.isLineSupported( monoInfo ); - - if( stereoSupported && monoSupported ) + DataLine.Info monoInfo = new DataLine.Info(SourceDataLine.class, AudioFormats.PCM_SIGNED_8KHZ_16BITS_MONO); + + boolean monoSupported = mixer.isLineSupported(monoInfo); + + if(stereoSupported && monoSupported) { - return EnumSet.of( MixerChannel.MONO, - MixerChannel.STEREO ); + return EnumSet.of(MixerChannel.MONO, MixerChannel.STEREO); } - else if( stereoSupported ) + else if(stereoSupported) { - return EnumSet.of( MixerChannel.STEREO ); + return EnumSet.of(MixerChannel.STEREO); } - else if( monoSupported ) + else if(monoSupported) { - return EnumSet.of( MixerChannel.MONO ); + return EnumSet.of(MixerChannel.MONO); } - + return null; - } - - public static String getMixerDevices() - { - StringBuilder sb = new StringBuilder(); - - for( Mixer.Info mixerInfo: AudioSystem.getMixerInfo() ) - { - sb.append( "\n--------------------------------------------------" ); - sb.append( "\nMIXER name:" + mixerInfo.getName() + - "\n desc:" + mixerInfo.getDescription() + - "\n vendor:" + mixerInfo.getVendor() + - "\n version:" + mixerInfo.getVersion() + - "\n" ); - - Mixer mixer = AudioSystem.getMixer( mixerInfo ); + } - Line.Info[] sourceLines = mixer.getSourceLineInfo(); - - for( Line.Info lineInfo: sourceLines ) - { - sb.append( " SOURCE LINE desc:" + lineInfo.toString() + - "\n class:" + lineInfo.getClass() + - "\n lineclass:" + lineInfo.getLineClass() + - "\n" ); - } + public static String getMixerDevices() + { + StringBuilder sb = new StringBuilder(); - Line.Info[] targetLines = mixer.getTargetLineInfo(); - - for( Line.Info lineInfo: targetLines ) - { - sb.append( " TARGET LINE desc:" + lineInfo.toString() + - "\n class:" + lineInfo.getClass() + - "\n lineclass:" + lineInfo.getLineClass() + - "\n" ); - } - - Line.Info portInfo = new Line.Info( Port.class ); - - if( mixer.isLineSupported( portInfo ) ) - { - sb.append( "**PORT LINE IS SUPPORTED BY THIS MIXER***\n" ); - } - } - - return sb.toString(); - } - - public class InputMixerConfiguration - { - private Mixer mMixer; - private EnumSet mChannels; - - public InputMixerConfiguration( Mixer mixer, EnumSet channels ) - { - mMixer = mixer; - mChannels = channels; - } - - public Mixer getMixer() - { - return mMixer; - } - - public String getMixerName() - { - return mMixer.getMixerInfo().getName(); - } - - public EnumSet getChannels() - { - return mChannels; - } - - public boolean supportsChannel( MixerChannel channel ) - { - return mChannels.contains( channel ); - } - - public String toString() - { - return mMixer.getMixerInfo().getName(); - } - } + for(Mixer.Info mixerInfo : AudioSystem.getMixerInfo()) + { + sb.append("\n--------------------------------------------------"); + sb.append("\nMIXER name:" + mixerInfo.getName() + + "\n desc:" + mixerInfo.getDescription() + + "\n vendor:" + mixerInfo.getVendor() + + "\n version:" + mixerInfo.getVersion() + + "\n"); + + Mixer mixer = AudioSystem.getMixer(mixerInfo); + + Line.Info[] sourceLines = mixer.getSourceLineInfo(); + + for(Line.Info lineInfo : sourceLines) + { + sb.append(" SOURCE LINE desc:" + lineInfo.toString() + + "\n class:" + lineInfo.getClass() + + "\n lineclass:" + lineInfo.getLineClass() + + "\n"); + } + + Line.Info[] targetLines = mixer.getTargetLineInfo(); + + for(Line.Info lineInfo : targetLines) + { + sb.append(" TARGET LINE desc:" + lineInfo.toString() + + "\n class:" + lineInfo.getClass() + + "\n lineclass:" + lineInfo.getLineClass() + + "\n"); + } + + Line.Info portInfo = new Line.Info(Port.class); + + if(mixer.isLineSupported(portInfo)) + { + sb.append("**PORT LINE IS SUPPORTED BY THIS MIXER***\n"); + } + } + + return sb.toString(); + } + + public class InputMixerConfiguration + { + private Mixer mMixer; + private EnumSet mChannels; + + public InputMixerConfiguration(Mixer mixer, EnumSet channels) + { + mMixer = mixer; + mChannels = channels; + } + + public Mixer getMixer() + { + return mMixer; + } + + public String getMixerName() + { + return mMixer.getMixerInfo().getName(); + } + + public EnumSet getChannels() + { + return mChannels; + } + + public boolean supportsChannel(MixerChannel channel) + { + return mChannels.contains(channel); + } + + public String toString() + { + return mMixer.getMixerInfo().getName(); + } + } } diff --git a/src/main/java/io/github/dsheirer/source/mixer/RealMixerSource.java b/src/main/java/io/github/dsheirer/source/mixer/RealMixerSource.java index ba4391d7..70b996c9 100644 --- a/src/main/java/io/github/dsheirer/source/mixer/RealMixerSource.java +++ b/src/main/java/io/github/dsheirer/source/mixer/RealMixerSource.java @@ -1,19 +1,17 @@ /******************************************************************************* - * SDR Trunk - * Copyright (C) 2014 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 * - * You should have received a copy of the GNU General Public License - * along with this program. If not, see ******************************************************************************/ package io.github.dsheirer.source.mixer; @@ -63,9 +61,7 @@ public class RealMixerSource extends RealSource * mixer into float array data. Can optionally invert the channel data if * the left/right stereo channels are inverted. */ - public RealMixerSource(TargetDataLine targetDataLine, - AudioFormat format, - ISampleAdapter sampleAdapter) + public RealMixerSource(TargetDataLine targetDataLine, AudioFormat format, ISampleAdapter sampleAdapter) { mTargetDataLine = targetDataLine; mAudioFormat = format; From a621a31aa9852fbae82473b1874a24758a10af9d Mon Sep 17 00:00:00 2001 From: Denny Date: Sun, 24 Jun 2018 09:50:36 -0400 Subject: [PATCH 79/87] [WIP] Updates the sample adapters to natively use ReusableBuffers. Refactors a MixerReader instance from the RealMixerSource so that it can be reused for both real and complex data outputs. Updates RealMixerSource to use refactored MixerReader and use a default sample rate of 8 kHz. Updates the ComplexMixer to use the refactored MixerReader. Updates the FCD 1/2 tuners for compatibility with the new ComplexMixer. Removes several unused classes. --- ...dapter.java => AbstractSampleAdapter.java} | 11 +- .../sample/adapter/ByteSampleAdapter.java | 52 ---- .../sample/adapter/ChannelShortAdapter.java | 87 ------ .../sample/adapter/ComplexSampleAdapter.java | 39 +++ .../sample/adapter/ComplexShortAdapter.java | 54 ++-- .../ComplexToFloatSampleConverter.java | 57 ---- .../adapter/FloatArrayToComplexAdapter.java | 61 ---- .../sample/adapter/ISampleAdapter.java | 27 -- .../adapter/RealChannelShortAdapter.java | 91 ++++++ .../sample/adapter/RealSampleAdapter.java | 39 +++ .../sample/adapter/RealShortAdapter.java | 73 +++++ .../dsheirer/sample/adapter/ShortAdapter.java | 62 ---- .../github/dsheirer/sample/adapter/Test.java | 25 -- .../dsheirer/source/mixer/ComplexMixer.java | 207 +++---------- .../dsheirer/source/mixer/MixerManager.java | 10 +- .../dsheirer/source/mixer/MixerReader.java | 283 ++++++++++++++++++ .../source/mixer/RealMixerSource.java | 242 ++++----------- .../source/tuner/fcd/FCDTunerController.java | 8 +- .../fcd/proplusV2/FCD2TunerController.java | 4 +- 19 files changed, 672 insertions(+), 760 deletions(-) rename src/main/java/io/github/dsheirer/sample/adapter/{IComplexSampleAdapter.java => AbstractSampleAdapter.java} (70%) delete mode 100644 src/main/java/io/github/dsheirer/sample/adapter/ByteSampleAdapter.java delete mode 100644 src/main/java/io/github/dsheirer/sample/adapter/ChannelShortAdapter.java create mode 100644 src/main/java/io/github/dsheirer/sample/adapter/ComplexSampleAdapter.java delete mode 100644 src/main/java/io/github/dsheirer/sample/adapter/ComplexToFloatSampleConverter.java delete mode 100644 src/main/java/io/github/dsheirer/sample/adapter/FloatArrayToComplexAdapter.java delete mode 100644 src/main/java/io/github/dsheirer/sample/adapter/ISampleAdapter.java create mode 100644 src/main/java/io/github/dsheirer/sample/adapter/RealChannelShortAdapter.java create mode 100644 src/main/java/io/github/dsheirer/sample/adapter/RealSampleAdapter.java create mode 100644 src/main/java/io/github/dsheirer/sample/adapter/RealShortAdapter.java delete mode 100644 src/main/java/io/github/dsheirer/sample/adapter/ShortAdapter.java delete mode 100644 src/main/java/io/github/dsheirer/sample/adapter/Test.java create mode 100644 src/main/java/io/github/dsheirer/source/mixer/MixerReader.java diff --git a/src/main/java/io/github/dsheirer/sample/adapter/IComplexSampleAdapter.java b/src/main/java/io/github/dsheirer/sample/adapter/AbstractSampleAdapter.java similarity index 70% rename from src/main/java/io/github/dsheirer/sample/adapter/IComplexSampleAdapter.java rename to src/main/java/io/github/dsheirer/sample/adapter/AbstractSampleAdapter.java index 99d07eeb..b2db661c 100644 --- a/src/main/java/io/github/dsheirer/sample/adapter/IComplexSampleAdapter.java +++ b/src/main/java/io/github/dsheirer/sample/adapter/AbstractSampleAdapter.java @@ -15,14 +15,9 @@ ******************************************************************************/ package io.github.dsheirer.sample.adapter; -import io.github.dsheirer.sample.buffer.ReusableComplexBuffer; +import io.github.dsheirer.sample.buffer.ReusableBuffer; -public interface IComplexSampleAdapter +public abstract class AbstractSampleAdapter { - /** - * Converts the sample byte array into complex float samples, loading the converted float samples into the buffer. - * @param samples - * @param reusableComplexBuffer - */ - void convertAndLoad(byte[] samples, ReusableComplexBuffer reusableComplexBuffer); + public abstract T convert(byte[] sampleBytes); } diff --git a/src/main/java/io/github/dsheirer/sample/adapter/ByteSampleAdapter.java b/src/main/java/io/github/dsheirer/sample/adapter/ByteSampleAdapter.java deleted file mode 100644 index 9955c1a2..00000000 --- a/src/main/java/io/github/dsheirer/sample/adapter/ByteSampleAdapter.java +++ /dev/null @@ -1,52 +0,0 @@ -/******************************************************************************* - * SDR Trunk - * Copyright (C) 2014 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 io.github.dsheirer.sample.adapter; - -public class ByteSampleAdapter implements ISampleAdapter -{ - private final static float[] LOOKUP_VALUES; - - /** - * Creates a static lookup table that converts the 8-bit valued range - * from 0 - 255 into scaled float values of -1.0 to 0 to 1.0 - */ - static - { - LOOKUP_VALUES = new float[ 256 ]; - - for( int x = 0; x < 256; x++ ) - { - LOOKUP_VALUES[ x ] = (float)( x - 127 ) / 128.0f; - } - } - - @Override - public float[] convert( byte[] samples ) - { - float[] convertedSamples = new float[ samples.length ]; - int pointer = 0; - - for( byte sample: samples ) - { - /* Convert byte value into float from the lookup table */ - convertedSamples[ pointer++ ] = LOOKUP_VALUES[ ( sample & 0xFF ) ]; - } - - return convertedSamples; - } -} diff --git a/src/main/java/io/github/dsheirer/sample/adapter/ChannelShortAdapter.java b/src/main/java/io/github/dsheirer/sample/adapter/ChannelShortAdapter.java deleted file mode 100644 index 379087b4..00000000 --- a/src/main/java/io/github/dsheirer/sample/adapter/ChannelShortAdapter.java +++ /dev/null @@ -1,87 +0,0 @@ -/******************************************************************************* - * SDR Trunk - * Copyright (C) 2014 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 io.github.dsheirer.sample.adapter; - -import io.github.dsheirer.source.mixer.MixerChannel; -import org.apache.commons.lang3.Validate; - -import java.nio.ByteBuffer; -import java.nio.ByteOrder; - -/** - * Converts 16-bit little endian byte data into an array of floats - */ -public class ChannelShortAdapter implements ISampleAdapter -{ - private ShortToFloatMap mMap = new ShortToFloatMap(); - private ByteOrder mByteOrder = ByteOrder.LITTLE_ENDIAN; - private MixerChannel mMixerChannel; - - public ChannelShortAdapter( MixerChannel channel ) - { - /* Only use with LEFT/RIGHT channels */ - Validate.isTrue(channel != MixerChannel.MONO); - - mMixerChannel = channel; - } - - @Override - public float[] convert( byte[] samples ) - { - float[] processed = new float[ samples.length / 4 ]; - - int pointer = 0; - - /* Wrap byte array in a byte buffer so we can process them as shorts */ - ByteBuffer buffer = ByteBuffer.wrap( samples ); - - /* Set endian to correct byte ordering */ - buffer.order( mByteOrder ); - - while( buffer.hasRemaining() ) - { - if( mMixerChannel == MixerChannel.LEFT ) - { - processed[ pointer ] = mMap.get( buffer.getShort() ); - - /* Throw away the right channel */ - buffer.getShort(); - } - else - { - /* Throw away the left channel */ - buffer.getShort(); - - processed[ pointer ] = mMap.get( buffer.getShort() ); - } - - pointer++; - } - - return processed; - } - - /** - * Set byte interpretation to little or big endian. Defaults to LITTLE - * endian - */ - public void setByteOrder( ByteOrder order ) - { - mByteOrder = order; - } -} diff --git a/src/main/java/io/github/dsheirer/sample/adapter/ComplexSampleAdapter.java b/src/main/java/io/github/dsheirer/sample/adapter/ComplexSampleAdapter.java new file mode 100644 index 00000000..21846439 --- /dev/null +++ b/src/main/java/io/github/dsheirer/sample/adapter/ComplexSampleAdapter.java @@ -0,0 +1,39 @@ +/******************************************************************************* + * 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 + * + ******************************************************************************/ +package io.github.dsheirer.sample.adapter; + +import io.github.dsheirer.sample.buffer.ReusableComplexBuffer; +import io.github.dsheirer.sample.buffer.ReusableComplexBufferQueue; + +public abstract class ComplexSampleAdapter extends AbstractSampleAdapter +{ + private ReusableComplexBufferQueue mReusableBufferQueue; + + /** + * Constructs a real sample adapter + * + * @param debugName to use for debug logging + */ + public ComplexSampleAdapter(String debugName) + { + mReusableBufferQueue = new ReusableComplexBufferQueue(debugName); + } + + protected ReusableComplexBuffer getBuffer(int size) + { + return mReusableBufferQueue.getBuffer(size); + } +} diff --git a/src/main/java/io/github/dsheirer/sample/adapter/ComplexShortAdapter.java b/src/main/java/io/github/dsheirer/sample/adapter/ComplexShortAdapter.java index 6a3c80ac..254af929 100644 --- a/src/main/java/io/github/dsheirer/sample/adapter/ComplexShortAdapter.java +++ b/src/main/java/io/github/dsheirer/sample/adapter/ComplexShortAdapter.java @@ -16,43 +16,61 @@ package io.github.dsheirer.sample.adapter; import io.github.dsheirer.sample.buffer.ReusableComplexBuffer; +import org.slf4j.Logger; +import org.slf4j.LoggerFactory; import java.nio.ByteBuffer; import java.nio.ByteOrder; -import java.nio.FloatBuffer; /** - * Converts 16-bit little endian byte data into an array of complex floats + * Converts 16-bit/2-byte little endian byte data into a reusable buffer of complex float sample data */ -public class ComplexShortAdapter implements IComplexSampleAdapter +public class ComplexShortAdapter extends ComplexSampleAdapter { + private final static Logger mLog = LoggerFactory.getLogger(ComplexShortAdapter.class); private ShortToFloatMap mMap = new ShortToFloatMap(); - private FloatBuffer mFloatBuffer; private ByteOrder mByteOrder = ByteOrder.LITTLE_ENDIAN; + private ByteBuffer mByteBuffer; + + /** + * Constructs a real sample adapter + * + * @param debugName to use for debug logging + */ + public ComplexShortAdapter(String debugName) + { + super(debugName); + } @Override - public void convertAndLoad(byte[] samples, ReusableComplexBuffer reusableComplexBuffer) + public ReusableComplexBuffer convert(byte[] samples) { - int sampleCount = samples.length / 2; + ReusableComplexBuffer reusableBuffer = getBuffer(samples.length / 2); + float[] convertedSamples = reusableBuffer.getSamples(); - if(mFloatBuffer == null || mFloatBuffer.capacity() != sampleCount) - { - mFloatBuffer = FloatBuffer.allocate(sampleCount); - } + int pointer = 0; - mFloatBuffer.rewind(); - - /* Wrap byte array in a byte buffer so we can process them as shorts */ - ByteBuffer buffer = ByteBuffer.wrap(samples); + mByteBuffer = ByteBuffer.wrap(samples); /* Set endian to correct byte ordering */ - buffer.order(mByteOrder); + mByteBuffer.order(mByteOrder); - while(buffer.hasRemaining()) + while(mByteBuffer.hasRemaining()) { - mFloatBuffer.put(mMap.get(buffer.getShort())); + convertedSamples[pointer] = mMap.get(mByteBuffer.getShort()); + pointer++; } - reusableComplexBuffer.reloadFrom(mFloatBuffer, System.currentTimeMillis()); + reusableBuffer.incrementUserCount(); + return reusableBuffer; + } + + /** + * Set byte interpretation to little or big endian. Defaults to LITTLE + * endian + */ + public void setByteOrder(ByteOrder order) + { + mByteOrder = order; } } diff --git a/src/main/java/io/github/dsheirer/sample/adapter/ComplexToFloatSampleConverter.java b/src/main/java/io/github/dsheirer/sample/adapter/ComplexToFloatSampleConverter.java deleted file mode 100644 index 6c013b44..00000000 --- a/src/main/java/io/github/dsheirer/sample/adapter/ComplexToFloatSampleConverter.java +++ /dev/null @@ -1,57 +0,0 @@ -/******************************************************************************* - * SDR Trunk - * Copyright (C) 2014 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 io.github.dsheirer.sample.adapter; - -import io.github.dsheirer.sample.Listener; -import io.github.dsheirer.sample.complex.Complex; -import io.github.dsheirer.source.mixer.MixerChannel; - -import java.util.List; - -/** - * Receives complex samples and send either the left channel or the right - * channel to the short sample listener - */ -public class ComplexToFloatSampleConverter implements Listener> -{ - private MixerChannel mChannel; - private Listener mListener; - - public ComplexToFloatSampleConverter( Listener listener, - MixerChannel channel ) - { - mListener = listener; - mChannel = channel; - } - - @Override - public void receive( List samples ) - { - for( Complex sample: samples ) - { - mListener.receive( mChannel == MixerChannel.LEFT ? - sample.left() : - sample.right() ); - } - } - - public Listener getListener() - { - return mListener; - } -} diff --git a/src/main/java/io/github/dsheirer/sample/adapter/FloatArrayToComplexAdapter.java b/src/main/java/io/github/dsheirer/sample/adapter/FloatArrayToComplexAdapter.java deleted file mode 100644 index 70af8237..00000000 --- a/src/main/java/io/github/dsheirer/sample/adapter/FloatArrayToComplexAdapter.java +++ /dev/null @@ -1,61 +0,0 @@ -/******************************************************************************* - * SDR Trunk - * Copyright (C) 2014 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 io.github.dsheirer.sample.adapter; - -import io.github.dsheirer.sample.Listener; -import io.github.dsheirer.sample.complex.Complex; - -public class FloatArrayToComplexAdapter implements Listener -{ - private Listener mListener; - - @Override - public void receive( Float[] samples ) - { - if( samples != null ) - { - for( int x = 0; x < samples.length; x += 2 ) - { - if( mListener != null && - samples[ x ] != null && - samples[ x + 1 ] != null ) - { - mListener.receive( new Complex( samples[ x ], - samples[ x + 1 ] ) ); - } - } - - samples = null; - } - } - - public void setListener( Listener listener ) - { - mListener = listener; - } - - public void removeListener( Listener listener ) - { - mListener = null; - } - - public void dispose() - { - mListener = null; - } -} diff --git a/src/main/java/io/github/dsheirer/sample/adapter/ISampleAdapter.java b/src/main/java/io/github/dsheirer/sample/adapter/ISampleAdapter.java deleted file mode 100644 index ec80a8ae..00000000 --- a/src/main/java/io/github/dsheirer/sample/adapter/ISampleAdapter.java +++ /dev/null @@ -1,27 +0,0 @@ -/******************************************************************************* - * SDR Trunk - * Copyright (C) 2014,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 - ******************************************************************************/ -package io.github.dsheirer.sample.adapter; - -/* - * Adapter to convert byte data received from a data stream into float[] - * samples - */ -public interface ISampleAdapter -{ - public abstract float[] convert( byte[] samples ); -} diff --git a/src/main/java/io/github/dsheirer/sample/adapter/RealChannelShortAdapter.java b/src/main/java/io/github/dsheirer/sample/adapter/RealChannelShortAdapter.java new file mode 100644 index 00000000..77b528e3 --- /dev/null +++ b/src/main/java/io/github/dsheirer/sample/adapter/RealChannelShortAdapter.java @@ -0,0 +1,91 @@ +/******************************************************************************* + * 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 + * + ******************************************************************************/ +package io.github.dsheirer.sample.adapter; + +import io.github.dsheirer.sample.buffer.ReusableBuffer; +import io.github.dsheirer.source.mixer.MixerChannel; +import org.apache.commons.lang3.Validate; +import org.slf4j.Logger; +import org.slf4j.LoggerFactory; + +import java.nio.ByteBuffer; +import java.nio.ByteOrder; + +/** + * Adapter for converting stereo/2-channel short sample data to single channel float samples. + */ +public class RealChannelShortAdapter extends RealSampleAdapter +{ + private final static Logger mLog = LoggerFactory.getLogger(RealChannelShortAdapter.class); + private ShortToFloatMap mMap = new ShortToFloatMap(); + private ByteOrder mByteOrder = ByteOrder.LITTLE_ENDIAN; + private MixerChannel mMixerChannel; + private ByteBuffer mByteBuffer; + + public RealChannelShortAdapter(MixerChannel channel, String debugName) + { + super(debugName); + + /* Only use with LEFT/RIGHT channels */ + Validate.isTrue(channel != MixerChannel.MONO); + + mMixerChannel = channel; + } + + @Override + public ReusableBuffer convert(byte[] samples) + { + ReusableBuffer reusableBuffer = getBuffer(samples.length / 4); + float[] convertedSamples = reusableBuffer.getSamples(); + + mByteBuffer = ByteBuffer.wrap(samples); + + /* Set endian to correct byte ordering */ + mByteBuffer.order(mByteOrder); + + int pointer = 0; + + while(mByteBuffer.hasRemaining()) + { + if(mMixerChannel == MixerChannel.LEFT) + { + convertedSamples[pointer++] = mMap.get(mByteBuffer.getShort()); + + //Throw away the right channel + mByteBuffer.getShort(); + } + else + { + //Throw away the left channel + mByteBuffer.getShort(); + + convertedSamples[pointer++] = mMap.get(mByteBuffer.getShort()); + } + } + + reusableBuffer.incrementUserCount(); + return reusableBuffer; + } + + /** + * Set byte interpretation to little or big endian. Defaults to LITTLE + * endian + */ + public void setByteOrder(ByteOrder order) + { + mByteOrder = order; + } +} diff --git a/src/main/java/io/github/dsheirer/sample/adapter/RealSampleAdapter.java b/src/main/java/io/github/dsheirer/sample/adapter/RealSampleAdapter.java new file mode 100644 index 00000000..afbe0c7f --- /dev/null +++ b/src/main/java/io/github/dsheirer/sample/adapter/RealSampleAdapter.java @@ -0,0 +1,39 @@ +/******************************************************************************* + * 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 + * + ******************************************************************************/ +package io.github.dsheirer.sample.adapter; + +import io.github.dsheirer.sample.buffer.ReusableBuffer; +import io.github.dsheirer.sample.buffer.ReusableBufferQueue; + +public abstract class RealSampleAdapter extends AbstractSampleAdapter +{ + private ReusableBufferQueue mReusableBufferQueue; + + /** + * Constructs a real sample adapter + * + * @param debugName to use for debug logging + */ + public RealSampleAdapter(String debugName) + { + mReusableBufferQueue = new ReusableBufferQueue(debugName); + } + + protected ReusableBuffer getBuffer(int size) + { + return mReusableBufferQueue.getBuffer(size); + } +} diff --git a/src/main/java/io/github/dsheirer/sample/adapter/RealShortAdapter.java b/src/main/java/io/github/dsheirer/sample/adapter/RealShortAdapter.java new file mode 100644 index 00000000..7d1da2aa --- /dev/null +++ b/src/main/java/io/github/dsheirer/sample/adapter/RealShortAdapter.java @@ -0,0 +1,73 @@ +/******************************************************************************* + * 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 + * + ******************************************************************************/ +package io.github.dsheirer.sample.adapter; + +import io.github.dsheirer.sample.buffer.ReusableBuffer; + +import java.nio.ByteBuffer; +import java.nio.ByteOrder; + +/** + * Converts 16-bit/2-byte little endian byte data into a reusable buffer of real float sample data + */ +public class RealShortAdapter extends RealSampleAdapter +{ + private ByteBuffer mByteBuffer; + private ShortToFloatMap mMap = new ShortToFloatMap(); + private ByteOrder mByteOrder = ByteOrder.LITTLE_ENDIAN; + + /** + * Constructs a real sample adapter + * + * @param debugName to use for debug logging + */ + public RealShortAdapter(String debugName) + { + super(debugName); + } + + @Override + public ReusableBuffer convert(byte[] samples) + { + ReusableBuffer reusableBuffer = getBuffer(samples.length / 2); + float[] convertedSamples = reusableBuffer.getSamples(); + + int pointer = 0; + + mByteBuffer = ByteBuffer.wrap(samples); + + /* Set endian to correct byte ordering */ + mByteBuffer.order(mByteOrder); + + while(mByteBuffer.hasRemaining()) + { + convertedSamples[pointer] = mMap.get(mByteBuffer.getShort()); + pointer++; + } + + reusableBuffer.incrementUserCount(); + return reusableBuffer; + } + + /** + * Set byte interpretation to little or big endian. Defaults to LITTLE + * endian + */ + public void setByteOrder(ByteOrder order) + { + mByteOrder = order; + } +} diff --git a/src/main/java/io/github/dsheirer/sample/adapter/ShortAdapter.java b/src/main/java/io/github/dsheirer/sample/adapter/ShortAdapter.java deleted file mode 100644 index 6faeb20a..00000000 --- a/src/main/java/io/github/dsheirer/sample/adapter/ShortAdapter.java +++ /dev/null @@ -1,62 +0,0 @@ -/******************************************************************************* - * SDR Trunk - * Copyright (C) 2014 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 io.github.dsheirer.sample.adapter; - -import java.nio.ByteBuffer; -import java.nio.ByteOrder; - -/** - * Converts 16-bit little endian byte data into an array of floats - */ -public class ShortAdapter implements ISampleAdapter -{ - private ShortToFloatMap mMap = new ShortToFloatMap(); - private ByteOrder mByteOrder = ByteOrder.LITTLE_ENDIAN; - - @Override - public float[] convert( byte[] samples ) - { - float[] processed = new float[ samples.length / 2 ]; - - int pointer = 0; - - /* Wrap byte array in a byte buffer so we can process them as shorts */ - ByteBuffer buffer = ByteBuffer.wrap( samples ); - - /* Set endian to correct byte ordering */ - buffer.order( mByteOrder ); - - while( buffer.hasRemaining() ) - { - processed[ pointer ] = mMap.get( buffer.getShort() ); - - pointer++; - } - - return processed; - } - - /** - * Set byte interpretation to little or big endian. Defaults to LITTLE - * endian - */ - public void setByteOrder( ByteOrder order ) - { - mByteOrder = order; - } -} diff --git a/src/main/java/io/github/dsheirer/sample/adapter/Test.java b/src/main/java/io/github/dsheirer/sample/adapter/Test.java deleted file mode 100644 index cec98e4a..00000000 --- a/src/main/java/io/github/dsheirer/sample/adapter/Test.java +++ /dev/null @@ -1,25 +0,0 @@ -package io.github.dsheirer.sample.adapter; - -import io.github.dsheirer.source.tuner.hackrf.HackRFTunerController; -import org.slf4j.Logger; -import org.slf4j.LoggerFactory; - -public class Test -{ - private final static Logger mLog = - LoggerFactory.getLogger( HackRFTunerController.class ); - - public static void main( String[] args ) - { - mLog.debug( "Starting" ); - - ShortToFloatMap map1 = new ShortToFloatMap(); - - for( short x = -32768; x < -32758; x++ ) - { - mLog.debug( "X:" + x + " value:" + map1.get( x ) ); - } - mLog.debug( "Finished" ); - } - -} diff --git a/src/main/java/io/github/dsheirer/source/mixer/ComplexMixer.java b/src/main/java/io/github/dsheirer/source/mixer/ComplexMixer.java index 9d83fdac..51c68e44 100644 --- a/src/main/java/io/github/dsheirer/source/mixer/ComplexMixer.java +++ b/src/main/java/io/github/dsheirer/source/mixer/ComplexMixer.java @@ -16,69 +16,74 @@ package io.github.dsheirer.source.mixer; import io.github.dsheirer.sample.Listener; -import io.github.dsheirer.sample.adapter.IComplexSampleAdapter; +import io.github.dsheirer.sample.adapter.ComplexSampleAdapter; import io.github.dsheirer.sample.buffer.ReusableComplexBuffer; -import io.github.dsheirer.sample.buffer.ReusableComplexBufferQueue; -import io.github.dsheirer.source.SourceException; +import io.github.dsheirer.source.SourceEvent; import org.slf4j.Logger; import org.slf4j.LoggerFactory; import javax.sound.sampled.AudioFormat; -import javax.sound.sampled.AudioSystem; -import javax.sound.sampled.LineUnavailableException; import javax.sound.sampled.TargetDataLine; -import java.util.concurrent.atomic.AtomicBoolean; public class ComplexMixer { private final static Logger mLog = LoggerFactory.getLogger(ComplexMixer.class); - - private long mFrequency = 0; - - private BufferReader mBufferReader = new BufferReader(); - private TargetDataLine mTargetDataLine; - private AudioFormat mAudioFormat; + private MixerReader mMixerReader; private String mName; - private int mBytesPerFrame = 0; - private IComplexSampleAdapter mSampleAdapter; - private Listener mListener; - private ReusableComplexBufferQueue mReusableComplexBufferQueue = new ReusableComplexBufferQueue("ComplexMixer"); /** * Complex Mixer - constructs a reader on the mixer/sound card target data line using the specified audio - * format (sample size, sample rate ) and broadcasts complex (I/Q) sample buffers to all registered listeners. - * Reads sample buffers sized to 10% of the sample rate specified in audio format. + * format (sample size, sample rate ) and broadcasts complex (I/Q) sample buffers to the registered listener. * * @param targetDataLine - mixer or sound card to be used * @param format - audio format * @param name - token name to use for this mixer * @param complexSampleAdapter - adapter to convert byte array data read from the mixer into ComplexBuffer. - * @param listener to receive generated complex buffers */ public ComplexMixer(TargetDataLine targetDataLine, AudioFormat format, String name, - IComplexSampleAdapter complexSampleAdapter, Listener listener) + ComplexSampleAdapter complexSampleAdapter) { - mTargetDataLine = targetDataLine; + mMixerReader = new MixerReader<>(format, targetDataLine, complexSampleAdapter); mName = name; - mAudioFormat = format; - mSampleAdapter = complexSampleAdapter; - mListener = listener; + } + + /** + * Sets the buffer size in bytes per buffer for each read interval. + * @param bufferSize in bytes + */ + public void setBufferSize(int bufferSize) + { + mMixerReader.setBufferSize(bufferSize); } public void start() { - if(!mBufferReader.isRunning()) - { - Thread thread = new Thread(mBufferReader); - thread.setDaemon(true); - thread.setName(mName + " Complex Sample Reader"); - thread.start(); - } + mMixerReader.start(); } public void stop() { - mBufferReader.stop(); + mMixerReader.stop(); + } + + public void setBufferListener(Listener listener) + { + mMixerReader.setBufferListener(listener); + } + + public void removeBufferListener() + { + mMixerReader.removeBufferListener(); + } + + public void setSourceEventListener(Listener listener) + { + mMixerReader.setSourceEventListener(listener); + } + + public void removeSourceEventListener() + { + mMixerReader.removeSourceEventListener(); } @Override @@ -87,144 +92,8 @@ public class ComplexMixer return mName; } - public int getSampleRate() - { - if(mTargetDataLine != null) - { - return (int)mTargetDataLine.getFormat().getSampleRate(); - } - else - { - return 0; - } - } - - /** - * Returns the frequency of this source. Default is 0. - */ - public long getFrequency() throws SourceException - { - return mFrequency; - } - - /** - * Specify the frequency that will be returned from this source. This may - * be useful if you are streaming an external audio source in through the - * sound card and you want to specify a frequency for that source - */ - public void setFrequency(long frequency) - { - mFrequency = frequency; - } - - /** - * Reader thread. Performs blocking read against the mixer target data - * line, converts the samples to an array of floats using the specified - * adapter. Dispatches float arrays to all registered listeners. - */ - public class BufferReader implements Runnable - { - private AtomicBoolean mRunning = new AtomicBoolean(); - - @Override - public void run() - { - if(mRunning.compareAndSet(false, true)) - { - if(mTargetDataLine == null) - { - mRunning.set(false); - mLog.error("ComplexMixerSource - mixer target data line is null"); - } - else - { - mBytesPerFrame = mAudioFormat.getSampleSizeInBits() / 8; - - if(mBytesPerFrame == AudioSystem.NOT_SPECIFIED) - { - mBytesPerFrame = 2; - } - - //Set buffer size to 1/10 second of samples - int sampleCount = (int)(mAudioFormat.getSampleRate() * 0.10) * 2; - int bufferSize = sampleCount * mBytesPerFrame; - - //We'll reuse the same buffer for each read - byte[] buffer = new byte[bufferSize]; - - try - { - mTargetDataLine.open(mAudioFormat); - mTargetDataLine.start(); - } - catch(LineUnavailableException e) - { - mLog.error("ComplexMixerSource - mixer target data line not available to read data from", e); - mRunning.set(false); - } - - while(mRunning.get()) - { - try - { - //Blocking read - waits until the buffer fills - mTargetDataLine.read(buffer, 0, buffer.length); - - /* Dispatch samples to registered listeners */ - if(mListener != null) - { - ReusableComplexBuffer reusableComplexBuffer = mReusableComplexBufferQueue.getBuffer(sampleCount); - - //Convert samples and load into reusable buffer - mSampleAdapter.convertAndLoad(buffer, reusableComplexBuffer); - - mListener.receive(reusableComplexBuffer); - } - } - catch(Exception e) - { - mLog.error("error while reading from the mixer target data line", e); - mRunning.set(false); - } - } - } - - /* Close the data line if it is still open */ - if(mTargetDataLine != null && mTargetDataLine.isOpen()) - { - mTargetDataLine.close(); - } - } - } - - /** - * Stops the reader thread - */ - public void stop() - { - if(mRunning.compareAndSet(true, false)) - { - mTargetDataLine.stop(); - mTargetDataLine.close(); - } - } - - /** - * Indicates if the reader thread is running - */ - public boolean isRunning() - { - return mRunning.get(); - } - } - public void dispose() { - if(mBufferReader != null) - { - mBufferReader.stop(); - } - - mListener = null; + mMixerReader.dispose(); } } diff --git a/src/main/java/io/github/dsheirer/source/mixer/MixerManager.java b/src/main/java/io/github/dsheirer/source/mixer/MixerManager.java index fad3a366..508269db 100644 --- a/src/main/java/io/github/dsheirer/source/mixer/MixerManager.java +++ b/src/main/java/io/github/dsheirer/source/mixer/MixerManager.java @@ -16,8 +16,8 @@ package io.github.dsheirer.source.mixer; import io.github.dsheirer.audio.AudioFormats; -import io.github.dsheirer.sample.adapter.ChannelShortAdapter; -import io.github.dsheirer.sample.adapter.ShortAdapter; +import io.github.dsheirer.sample.adapter.RealChannelShortAdapter; +import io.github.dsheirer.sample.adapter.RealShortAdapter; import io.github.dsheirer.source.config.SourceConfigMixer; import io.github.dsheirer.source.config.SourceConfiguration; import io.github.dsheirer.source.tuner.MixerTunerDataLine; @@ -90,7 +90,7 @@ public class MixerManager if(dataLine != null) { return new RealMixerSource(dataLine, AudioFormats.PCM_SIGNED_8KHZ_16BITS_MONO, - new ShortAdapter()); + new RealShortAdapter("RealMixerSource - Mono")); } } catch(LineUnavailableException e) @@ -100,7 +100,7 @@ public class MixerManager } } - else + else //STEREO { DataLine.Info info = new DataLine.Info(TargetDataLine.class, AudioFormats.PCM_SIGNED_8KHZ_16BITS_STEREO); @@ -114,7 +114,7 @@ public class MixerManager if(dataLine != null) { return new RealMixerSource(dataLine, AudioFormats.PCM_SIGNED_8KHZ_16BITS_STEREO, - new ChannelShortAdapter(mixerConfig.getChannel())); + new RealChannelShortAdapter(mixerConfig.getChannel(), "RealMixerSource-Stereo")); } } catch(LineUnavailableException e) diff --git a/src/main/java/io/github/dsheirer/source/mixer/MixerReader.java b/src/main/java/io/github/dsheirer/source/mixer/MixerReader.java new file mode 100644 index 00000000..87315c22 --- /dev/null +++ b/src/main/java/io/github/dsheirer/source/mixer/MixerReader.java @@ -0,0 +1,283 @@ +/******************************************************************************* + * 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 + * + ******************************************************************************/ +package io.github.dsheirer.source.mixer; + +import io.github.dsheirer.sample.Listener; +import io.github.dsheirer.sample.adapter.AbstractSampleAdapter; +import io.github.dsheirer.sample.buffer.ReusableBuffer; +import io.github.dsheirer.source.SourceEvent; +import io.github.dsheirer.source.heartbeat.HeartbeatManager; +import io.github.dsheirer.util.ThreadPool; +import org.slf4j.Logger; +import org.slf4j.LoggerFactory; + +import javax.sound.sampled.AudioFormat; +import javax.sound.sampled.LineUnavailableException; +import javax.sound.sampled.TargetDataLine; +import java.util.Arrays; +import java.util.concurrent.ScheduledFuture; +import java.util.concurrent.TimeUnit; +import java.util.concurrent.atomic.AtomicBoolean; + +/** + * Target Data Line sample data processor. Performs blocking read against the mixer target data line, converts + * the samples to an array of floats using the specified adapter. Dispatches float arrays to the registered + * buffer listener. + */ +public class MixerReader implements Runnable +{ + private final static Logger mLog = LoggerFactory.getLogger(MixerReader.class); + + private static final long BUFFER_PROCESSING_INTERVAL_MS = 50; //50 milliseconds, 20 reads per second + private TargetDataLine mTargetDataLine; + private byte[] mBuffer; + private int mBufferSize; + private AtomicBoolean mRunning = new AtomicBoolean(); + private ScheduledFuture mScheduledFuture; + private int mBytesRead; + private AbstractSampleAdapter mSampleAdapter; + private Listener mReusableBufferListener; + private Listener mSourceEventListener; + private AudioFormat mAudioFormat; + private HeartbeatManager mHeartbeatManager; + + public MixerReader(AudioFormat audioFormat, TargetDataLine targetDataLine, + AbstractSampleAdapter abstractSampleAdapter, HeartbeatManager heartbeatManager) + { + mTargetDataLine = targetDataLine; + mAudioFormat = audioFormat; + mSampleAdapter = abstractSampleAdapter; + mHeartbeatManager = heartbeatManager; + + /* Set buffer size to 1/10 second of samples */ + mBufferSize = (int)(mAudioFormat.getSampleRate() * 0.1) * mAudioFormat.getFrameSize(); + } + + public MixerReader(AudioFormat audioFormat, TargetDataLine targetDataLine, + AbstractSampleAdapter abstractSampleAdapter) + { + this(audioFormat, targetDataLine, abstractSampleAdapter, null); + } + + /** + * Sets the size of buffers to use. This reader will read from target data line 20 times per second. + * @param bytesPerBuffer to size for each buffer read. + */ + public void setBufferSize(int bytesPerBuffer) + { + mBufferSize = bytesPerBuffer; + mBuffer = new byte[mBufferSize]; + } + + /** + * Opens the source mixer target data line + * + * @throws LineUnavailableException if the target data line is null or unavailable (from the OS). + */ + private void openTargetDataLine() throws LineUnavailableException + { + if(mTargetDataLine == null) + { + throw new LineUnavailableException("Source Mixer TargetDataLine is null"); + } + + mBuffer = new byte[mBufferSize]; + + mTargetDataLine.open(mAudioFormat); + mTargetDataLine.start(); + } + + /** + * Closes the mixer source target data line + */ + private void closeTargetDataLine() + { + if(mTargetDataLine != null && mTargetDataLine.isOpen()) + { + mTargetDataLine.close(); + } + } + + /** + * Starts the sample processing reader thread + */ + public void start() + { + if(mRunning.compareAndSet(false, true)) + { + try + { + openTargetDataLine(); + } + catch(LineUnavailableException e) + { + mLog.error("Unable to open mixer source target data line", e); + mRunning.set(false); + return; + } + + //Cancel the scheduled thread if it wasn't cancelled previously ... this shouldn't happen + if(mScheduledFuture != null) + { + mScheduledFuture.cancel(true); + mScheduledFuture = null; + } + + mScheduledFuture = ThreadPool.SCHEDULED.scheduleAtFixedRate(this, + 0, BUFFER_PROCESSING_INTERVAL_MS, TimeUnit.MILLISECONDS); + } + else + { + mLog.warn("Attempt to start an already running BufferReader instance - this shouldn't happen"); + } + } + + /** + * Stops the sample processing reader thread + */ + public void stop() + { + if(mRunning.compareAndSet(true, false)) + { + if(mScheduledFuture != null) + { + mScheduledFuture.cancel(true); + mScheduledFuture = null; + } + + closeTargetDataLine(); + } + else + { + mLog.warn("Attempt to stop an already stopped BufferReader instance - this shouldn't happen"); + } + } + + /** + * Sample processing thread. This method runs each time the timer fires. + */ + @Override + public void run() + { + if(mRunning.get()) + { + if(mHeartbeatManager != null) + { + mHeartbeatManager.broadcast(); + } + + try + { + mBytesRead = 0; + + //Blocking read - waits until the buffer fills + mBytesRead = mTargetDataLine.read(mBuffer, 0, mBuffer.length); + + if(mBytesRead == mBuffer.length) + { + //Sample adapter automatically sets initial listener count to one. + T reusableBuffer = mSampleAdapter.convert(mBuffer); + + if(reusableBuffer != null && mReusableBufferListener != null) + { + mReusableBufferListener.receive(reusableBuffer); + } + } + else if(mBytesRead > 0) + { + //Sample adapter automatically sets initial listener count to one. + T reusableBuffer = mSampleAdapter.convert(Arrays.copyOf(mBuffer, mBytesRead)); + + if(reusableBuffer != null && mReusableBufferListener != null) + { + mReusableBufferListener.receive(reusableBuffer); + } + } + } + catch(Exception e) + { + mLog.error("MixerSource - error while reading from the mixer target data line", e); + stop(); + } + } + } + + /** + * Indicates if the reader thread is running + */ + public boolean isRunning() + { + return mRunning.get(); + } + + /** + * Sets the listener to receive sample data in reusable buffers. + */ + public void setBufferListener(Listener listener) + { + mReusableBufferListener = listener; + } + + /** + * Removes the listener from receiving sample data. + */ + public void removeBufferListener() + { + mReusableBufferListener = null; + } + + /** + * Registers a listener to receive source events + * + * @param listener + */ + public void setSourceEventListener(Listener listener) + { + mSourceEventListener = listener; + } + + /** + * Removes a listener from receiving source events + */ + public void removeSourceEventListener() + { + mSourceEventListener = null; + } + + /** + * Current source event listener + */ + public Listener getSourceEventListener() + { + return mSourceEventListener; + } + + /** + * Sample rate specified for the underlying target data line. + */ + public double getSampleRate() + { + return mAudioFormat.getSampleRate(); + } + + public void dispose() + { + stop(); + mHeartbeatManager = null; + mReusableBufferListener = null; + mSourceEventListener = null; + } +} diff --git a/src/main/java/io/github/dsheirer/source/mixer/RealMixerSource.java b/src/main/java/io/github/dsheirer/source/mixer/RealMixerSource.java index 70b996c9..5cb6a7fb 100644 --- a/src/main/java/io/github/dsheirer/source/mixer/RealMixerSource.java +++ b/src/main/java/io/github/dsheirer/source/mixer/RealMixerSource.java @@ -16,38 +16,18 @@ package io.github.dsheirer.source.mixer; import io.github.dsheirer.sample.Listener; -import io.github.dsheirer.sample.adapter.ISampleAdapter; +import io.github.dsheirer.sample.adapter.AbstractSampleAdapter; import io.github.dsheirer.sample.buffer.ReusableBuffer; -import io.github.dsheirer.sample.buffer.ReusableBufferBroadcaster; -import io.github.dsheirer.sample.buffer.ReusableBufferQueue; -import io.github.dsheirer.sample.real.RealBuffer; import io.github.dsheirer.source.RealSource; import io.github.dsheirer.source.SourceEvent; -import org.slf4j.Logger; -import org.slf4j.LoggerFactory; import javax.sound.sampled.AudioFormat; -import javax.sound.sampled.AudioSystem; -import javax.sound.sampled.LineUnavailableException; import javax.sound.sampled.TargetDataLine; -import java.util.concurrent.CopyOnWriteArrayList; -import java.util.concurrent.atomic.AtomicBoolean; public class RealMixerSource extends RealSource { - private final static Logger mLog = LoggerFactory.getLogger(RealMixerSource.class); - private ReusableBufferBroadcaster mBufferBroadcaster = new ReusableBufferBroadcaster<>(); - private ReusableBufferQueue mReusableBufferQueue = new ReusableBufferQueue("RealMixerSource"); - private long mFrequency = 0; - private int mBufferSize = 16384; - - private BufferReader mBufferReader = new BufferReader(); - private TargetDataLine mTargetDataLine; - private AudioFormat mAudioFormat; - private int mBytesPerFrame = 0; - private ISampleAdapter mSampleAdapter; - - private CopyOnWriteArrayList> mSampleListeners = new CopyOnWriteArrayList>(); + private long mFrequency; + private MixerReader mMixerReader; /** * Real Mixer Source - constructs a reader on the mixer/sound card target @@ -58,89 +38,94 @@ public class RealMixerSource extends RealSource * @param targetDataLine - mixer or sound card to be used * @param format - audio format * @param sampleAdapter - adapter to convert byte array data read from the - * mixer into float array data. Can optionally invert the channel data if - * the left/right stereo channels are inverted. + * mixer into float array data. */ - public RealMixerSource(TargetDataLine targetDataLine, AudioFormat format, ISampleAdapter sampleAdapter) + public RealMixerSource(TargetDataLine targetDataLine, AudioFormat format, AbstractSampleAdapter sampleAdapter) { - mTargetDataLine = targetDataLine; - mAudioFormat = format; - mSampleAdapter = sampleAdapter; + mMixerReader = new MixerReader(format, targetDataLine, sampleAdapter, getHeartbeatManager()); } + /** + * Registers a listener to receive source events + * + * @param listener + */ @Override public void setSourceEventListener(Listener listener) { - //Not implemented + mMixerReader.setSourceEventListener(listener); } + /** + * Removes a listener from receiving source events + */ @Override public void removeSourceEventListener() { - //Not implemented + mMixerReader.removeSourceEventListener(); } + /** + * Current source event listener + */ @Override public Listener getSourceEventListener() { - //Not implemented - return null; + return mMixerReader.getSourceEventListener(); } @Override public void reset() { + stop(); } @Override public void start() { + if(!mMixerReader.isRunning()) + { + mMixerReader.start(); + } } @Override public void stop() { - } - - public void setListener(Listener listener) - { - mBufferBroadcaster.addListener(listener); - - /* If this is the first listener, start the reader thread */ - if(!mBufferReader.isRunning()) + if(mMixerReader.isRunning()) { - Thread thread = new Thread(mBufferReader); - thread.setDaemon(true); - thread.setName("Sample Reader"); - thread.start(); - } - } - - public void removeListener(Listener listener) - { - mBufferBroadcaster.removeListener(listener); - - /* If this is the last listener, stop the reader thread */ - if(mSampleListeners.isEmpty()) - { - mBufferReader.stop(); - } - } - - public double getSampleRate() - { - if(mTargetDataLine != null) - { - return mTargetDataLine.getFormat().getSampleRate(); - } - else - { - return 0; + mMixerReader.stop(); } } /** - * Returns the frequency of this source. Default is 0. + * Sets the listener to receive sample data in reusable buffers. + */ + public void setListener(Listener listener) + { + mMixerReader.setBufferListener(listener); + } + + /** + * Removes the listener from receiving sample data. + * @param listener + */ + public void removeListener(Listener listener) + { + mMixerReader.removeBufferListener(); + } + + /** + * Sample rate specified for the underlying target data line. + */ + @Override + public double getSampleRate() + { + return mMixerReader.getSampleRate(); + } + + /** + * Returns the frequency of this source. Default is 0 if the frequency hasn't been set. */ public long getFrequency() { @@ -148,131 +133,18 @@ public class RealMixerSource extends RealSource } /** - * Specify the frequency that will be returned from this source. This may - * be useful if you are streaming an external audio source in through the - * sound card and you want to specify a frequency for that source + * Specify the frequency that will be returned from this source. This may be useful if you are streaming an + * external audio source in through the sound card and you want to specify a frequency for that source */ public void setFrequency(long frequency) { mFrequency = frequency; } - /** - * Reader thread. Performs blocking read against the mixer target data - * line, converts the samples to an array of floats using the specified - * adapter. Dispatches float arrays to all registered listeners. - */ - public class BufferReader implements Runnable - { - private AtomicBoolean mRunning = new AtomicBoolean(); - - @Override - public void run() - { - if(mRunning.compareAndSet(false, true)) - { - getHeartbeatManager().broadcast(); - - if(mTargetDataLine == null) - { - mRunning.set(false); - - mLog.error("ComplexMixerSource - mixer target data line" - + " is null"); - } - else - { - mBytesPerFrame = mAudioFormat.getSampleSizeInBits() / 8; - - if(mBytesPerFrame == AudioSystem.NOT_SPECIFIED) - { - mBytesPerFrame = 2; - } - - /* Set buffer size to 1/10 second of samples */ - mBufferSize = (int)(mAudioFormat.getSampleRate() - * 0.05) * mBytesPerFrame; - - /* We'll reuse the same buffer for each read */ - byte[] buffer = new byte[mBufferSize]; - - try - { - mTargetDataLine.open(mAudioFormat); - - mTargetDataLine.start(); - } - catch(LineUnavailableException e) - { - mLog.error("ComplexMixerSource - mixer target data line" - + "not available to read data from", e); - - mRunning.set(false); - } - - while(mRunning.get()) - { - try - { - /* Blocking read - waits until the buffer fills */ - mTargetDataLine.read(buffer, 0, buffer.length); - - /* Convert samples to float array */ -//TODO: change this sample adapter over to use reusable buffers and move the buffer queue into the adapter - float[] samples = mSampleAdapter.convert(buffer); - - /* Dispatch samples to registered listeners */ - ReusableBuffer reusableBuffer = mReusableBufferQueue.getBuffer(samples.length); - reusableBuffer.reloadFrom(samples, System.currentTimeMillis()); - reusableBuffer.incrementUserCount(); - mBufferBroadcaster.broadcast(reusableBuffer); - } - catch(Exception e) - { - mLog.error("ComplexMixerSource - error while reading" - + "from the mixer target data line", e); - - mRunning.set(false); - } - } - } - - /* Close the data line if it is still open */ - if(mTargetDataLine != null && mTargetDataLine.isOpen()) - { - mTargetDataLine.close(); - } - } - } - - /** - * Stops the reader thread - */ - public void stop() - { - if(mRunning.compareAndSet(true, false)) - { - mTargetDataLine.stop(); - mTargetDataLine.close(); - } - } - - /** - * Indicates if the reader thread is running - */ - public boolean isRunning() - { - return mRunning.get(); - } - } @Override public void dispose() { - if(mBufferReader != null) - { - mBufferReader.stop(); - mSampleListeners.clear(); - } + mMixerReader.dispose(); } } diff --git a/src/main/java/io/github/dsheirer/source/tuner/fcd/FCDTunerController.java b/src/main/java/io/github/dsheirer/source/tuner/fcd/FCDTunerController.java index 002bcc63..7693e5af 100644 --- a/src/main/java/io/github/dsheirer/source/tuner/fcd/FCDTunerController.java +++ b/src/main/java/io/github/dsheirer/source/tuner/fcd/FCDTunerController.java @@ -1,6 +1,6 @@ /******************************************************************************* * sdrtrunk - * 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 @@ -88,7 +88,11 @@ public abstract class FCDTunerController extends TunerController } mComplexMixer = new ComplexMixer( mixerTDL.getTargetDataLine(), audioFormat, tunerName, - new ComplexShortAdapter(), mReusableBufferBroadcaster); + new ComplexShortAdapter(mixerTDL.getMixerTunerType().getDisplayString())); + + mComplexMixer.setBufferSize(audioFormat.getFrameSize() * (int)(audioFormat.getSampleRate() * 0.1)); + + mComplexMixer.setBufferListener(mReusableBufferBroadcaster); } /** diff --git a/src/main/java/io/github/dsheirer/source/tuner/fcd/proplusV2/FCD2TunerController.java b/src/main/java/io/github/dsheirer/source/tuner/fcd/proplusV2/FCD2TunerController.java index c8e71140..dfb81b67 100644 --- a/src/main/java/io/github/dsheirer/source/tuner/fcd/proplusV2/FCD2TunerController.java +++ b/src/main/java/io/github/dsheirer/source/tuner/fcd/proplusV2/FCD2TunerController.java @@ -1,6 +1,6 @@ /******************************************************************************* * sdrtrunk - * 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 @@ -45,7 +45,7 @@ public class FCD2TunerController extends FCDTunerController public FCD2TunerController(MixerTunerDataLine mixerTDL, Device device, DeviceDescriptor descriptor) { super(mixerTDL, device, descriptor, MixerTunerType.FUNCUBE_DONGLE_PRO_PLUS.getDisplayString(), - MINIMUM_TUNABLE_FREQUENCY, MAXIMUM_TUNABLE_FREQUENCY, MixerTunerType.FUNCUBE_DONGLE_PRO.getAudioFormat()); + MINIMUM_TUNABLE_FREQUENCY, MAXIMUM_TUNABLE_FREQUENCY, MixerTunerType.FUNCUBE_DONGLE_PRO_PLUS.getAudioFormat()); } public void init() throws SourceException From df1bc1031e8bc8f6a77a16d697dc90df77d33f70 Mon Sep 17 00:00:00 2001 From: Denny Date: Fri, 29 Jun 2018 06:53:03 -0400 Subject: [PATCH 80/87] [WIP] Updates AudioPackets to be ReusableAudioPackets. --- .../github/dsheirer/audio/AudioManager.java | 37 +++-- .../io/github/dsheirer/audio/AudioModule.java | 27 +++- .../io/github/dsheirer/audio/AudioPacket.java | 78 --------- .../audio/AudioPacketRateCounter.java | 55 ------- .../io/github/dsheirer/audio/AudioUtils.java | 29 ++-- .../dsheirer/audio/IAudioPacketListener.java | 3 +- .../dsheirer/audio/IAudioPacketProvider.java | 3 +- .../audio/broadcast/BroadcastModel.java | 9 +- .../audio/broadcast/StreamManager.java | 16 +- .../audio/convert/IAudioConverter.java | 4 +- .../audio/convert/MP3AudioConverter.java | 4 +- .../audio/convert/MP3SilenceGenerator.java | 12 +- .../dsheirer/audio/output/AudioOutput.java | 30 ++-- .../audio/output/MonoAudioOutput.java | 18 ++- .../audio/output/StereoAudioOutput.java | 19 +-- .../channel/ChannelProcessingManager.java | 10 +- .../dsheirer/module/ProcessingChain.java | 8 +- .../decode/p25/audio/P25AudioModule.java | 20 ++- .../github/dsheirer/record/AudioRecorder.java | 29 ++-- .../dsheirer/record/RecorderManager.java | 37 +++-- .../dsheirer/record/mp3/MP3Recorder.java | 6 +- .../wave/AudioPacketMonoWaveReader.java | 69 -------- .../record/wave/AudioPacketWaveRecorder.java | 40 ++--- .../sample/buffer/ReusableAudioPacket.java | 151 ++++++++++++++++++ .../buffer/ReusableAudioPacketQueue.java | 76 +++++++++ .../buffer/ReusableBufferBroadcaster.java | 2 +- 26 files changed, 451 insertions(+), 341 deletions(-) delete mode 100644 src/main/java/io/github/dsheirer/audio/AudioPacket.java delete mode 100644 src/main/java/io/github/dsheirer/audio/AudioPacketRateCounter.java delete mode 100644 src/main/java/io/github/dsheirer/record/wave/AudioPacketMonoWaveReader.java create mode 100644 src/main/java/io/github/dsheirer/sample/buffer/ReusableAudioPacket.java create mode 100644 src/main/java/io/github/dsheirer/sample/buffer/ReusableAudioPacketQueue.java diff --git a/src/main/java/io/github/dsheirer/audio/AudioManager.java b/src/main/java/io/github/dsheirer/audio/AudioManager.java index c0f1d877..e48cf36a 100644 --- a/src/main/java/io/github/dsheirer/audio/AudioManager.java +++ b/src/main/java/io/github/dsheirer/audio/AudioManager.java @@ -24,6 +24,7 @@ import io.github.dsheirer.audio.output.StereoAudioOutput; import io.github.dsheirer.properties.SystemProperties; import io.github.dsheirer.sample.Broadcaster; import io.github.dsheirer.sample.Listener; +import io.github.dsheirer.sample.buffer.ReusableAudioPacket; import io.github.dsheirer.source.mixer.MixerChannel; import io.github.dsheirer.source.mixer.MixerChannelConfiguration; import io.github.dsheirer.source.mixer.MixerManager; @@ -43,7 +44,7 @@ import java.util.concurrent.LinkedTransferQueue; import java.util.concurrent.ScheduledFuture; import java.util.concurrent.TimeUnit; -public class AudioManager implements Listener, IAudioController +public class AudioManager implements Listener, IAudioController { private static final Logger mLog = LoggerFactory.getLogger(AudioManager.class); @@ -58,7 +59,7 @@ public class AudioManager implements Listener, IAudioController public static final AudioEvent CONFIGURATION_CHANGE_COMPLETE = new AudioEvent(AudioEvent.Type.AUDIO_CONFIGURATION_CHANGE_COMPLETE, null); - private LinkedTransferQueue mAudioPacketQueue = new LinkedTransferQueue<>(); + private LinkedTransferQueue mAudioPacketQueue = new LinkedTransferQueue<>(); private Map mChannelConnectionMap = new HashMap<>(); private List mAudioOutputConnections = new ArrayList<>(); private AudioOutputConnection mLowestPriorityConnection; @@ -170,7 +171,7 @@ public class AudioManager implements Listener, IAudioController * Primary ingest point for audio produced by all decoding channels, for distribution to audio playback devices. */ @Override - public synchronized void receive(AudioPacket packet) + public synchronized void receive(ReusableAudioPacket packet) { mAudioPacketQueue.add(packet); } @@ -362,7 +363,7 @@ public class AudioManager implements Listener, IAudioController * @param audioPacket from a decoding channel source * @return an audio output connection or null */ - private AudioOutputConnection getConnection(AudioPacket audioPacket) + private AudioOutputConnection getConnection(ReusableAudioPacket audioPacket) { int channelMetadataID = audioPacket.getMetadata().getMetadataID(); @@ -410,6 +411,8 @@ public class AudioManager implements Listener, IAudioController public class AudioPacketProcessor implements Runnable { + private List mPackets = new ArrayList<>(); + @Override public void run() { @@ -419,14 +422,12 @@ public class AudioManager implements Listener, IAudioController if(mAudioPacketQueue != null) { - List packets = new ArrayList(); + mAudioPacketQueue.drainTo(mPackets); - mAudioPacketQueue.drainTo(packets); - - for(AudioPacket packet : packets) + for(ReusableAudioPacket packet : mPackets) { /* Don't process any packet's marked as do not monitor */ - if(!packet.getMetadata().isDoNotMonitor() && packet.getType() == AudioPacket.Type.AUDIO) + if(!packet.getMetadata().isDoNotMonitor() && packet.getType() == ReusableAudioPacket.Type.AUDIO) { AudioOutputConnection connection = getConnection(packet); @@ -434,13 +435,29 @@ public class AudioManager implements Listener, IAudioController { connection.receive(packet); } + else + { + packet.decrementUserCount(); + } + } + else + { + packet.decrementUserCount(); } } + + mPackets.clear(); } } catch(Exception e) { mLog.error("Encountered error while processing audio packets", e); + + while(mPackets.size() > 0) + { + ReusableAudioPacket audioPacket = mPackets.remove(0); + audioPacket.decrementUserCount(); + } } } } @@ -463,7 +480,7 @@ public class AudioManager implements Listener, IAudioController mAudioOutput = audioOutput; } - public void receive(AudioPacket packet) + public void receive(ReusableAudioPacket packet) { if(packet.hasMetadata() && packet.getMetadata().getMetadataID() == mChannelMetadataID) { diff --git a/src/main/java/io/github/dsheirer/audio/AudioModule.java b/src/main/java/io/github/dsheirer/audio/AudioModule.java index 5836d4bc..960ad21e 100644 --- a/src/main/java/io/github/dsheirer/audio/AudioModule.java +++ b/src/main/java/io/github/dsheirer/audio/AudioModule.java @@ -25,6 +25,8 @@ import io.github.dsheirer.dsp.filter.fir.remez.RemezFIRFilterDesigner; import io.github.dsheirer.module.Module; import io.github.dsheirer.sample.Listener; import io.github.dsheirer.sample.buffer.IReusableBufferListener; +import io.github.dsheirer.sample.buffer.ReusableAudioPacket; +import io.github.dsheirer.sample.buffer.ReusableAudioPacketQueue; import io.github.dsheirer.sample.buffer.ReusableBuffer; import org.slf4j.Logger; import org.slf4j.LoggerFactory; @@ -41,6 +43,7 @@ public class AudioModule extends Module implements IAudioPacketProvider, IReusab ISquelchStateListener, Listener { protected static final Logger mLog = LoggerFactory.getLogger(AudioModule.class); + private ReusableAudioPacketQueue mAudioPacketQueue = new ReusableAudioPacketQueue("AudioModule"); private static float[] sHighPassFilterCoefficients; @@ -73,11 +76,12 @@ public class AudioModule extends Module implements IAudioPacketProvider, IReusab private RealFIRFilter2 mHighPassFilter = new RealFIRFilter2(sHighPassFilterCoefficients); private SquelchStateListener mSquelchStateListener = new SquelchStateListener(); private SquelchState mSquelchState = SquelchState.SQUELCH; - private Listener mAudioPacketListener; + private Listener mAudioPacketListener; private Metadata mMetadata; /** * Creates an Audio Module. + * * @param metadata to use for audio packets produced by this audio module. */ public AudioModule(Metadata metadata) @@ -109,12 +113,15 @@ public class AudioModule extends Module implements IAudioPacketProvider, IReusab /* Issue an end-audio packet in case a recorder is still rolling */ if(mAudioPacketListener != null) { - mAudioPacketListener.receive(new AudioPacket(AudioPacket.Type.END, mMetadata.copyOf())); + ReusableAudioPacket endAudioPacket = mAudioPacketQueue.getEndAudioBuffer(); + endAudioPacket.setMetadata(mMetadata.copyOf()); + endAudioPacket.incrementUserCount(); + mAudioPacketListener.receive(endAudioPacket); } } @Override - public void setAudioPacketListener(Listener listener) + public void setAudioPacketListener(Listener listener) { mAudioPacketListener = listener; } @@ -137,8 +144,14 @@ public class AudioModule extends Module implements IAudioPacketProvider, IReusab if(mAudioPacketListener != null && mSquelchState == SquelchState.UNSQUELCH) { ReusableBuffer highPassFiltered = mHighPassFilter.filter(reusableBuffer); - AudioPacket packet = new AudioPacket(highPassFiltered.getSamplesCopy(), mMetadata.copyOf()); - mAudioPacketListener.receive(packet); + + ReusableAudioPacket audioPacket = mAudioPacketQueue.getBuffer(highPassFiltered.getSampleCount()); + audioPacket.loadAudioFrom(highPassFiltered); + audioPacket.setMetadata(mMetadata.copyOf()); + audioPacket.incrementUserCount(); + + mAudioPacketListener.receive(audioPacket); + highPassFiltered.decrementUserCount(); } else @@ -164,7 +177,9 @@ public class AudioModule extends Module implements IAudioPacketProvider, IReusab { if(state == SquelchState.SQUELCH && mAudioPacketListener != null) { - mAudioPacketListener.receive(new AudioPacket(AudioPacket.Type.END, mMetadata.copyOf())); + ReusableAudioPacket endAudioPacket = mAudioPacketQueue.getEndAudioBuffer(); + endAudioPacket.setMetadata(mMetadata.copyOf()); + mAudioPacketListener.receive(endAudioPacket); } mSquelchState = state; diff --git a/src/main/java/io/github/dsheirer/audio/AudioPacket.java b/src/main/java/io/github/dsheirer/audio/AudioPacket.java deleted file mode 100644 index a57f2bdb..00000000 --- a/src/main/java/io/github/dsheirer/audio/AudioPacket.java +++ /dev/null @@ -1,78 +0,0 @@ -/******************************************************************************* - * 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 io.github.dsheirer.audio; - -import io.github.dsheirer.channel.metadata.Metadata; -import io.github.dsheirer.sample.real.RealBuffer; - -public class AudioPacket -{ - private Type mType; - private RealBuffer mAudioData; - private Metadata mMetadata; - - public AudioPacket(Type type, Metadata metadata) - { - mType = type; - mMetadata = metadata; - } - - public AudioPacket(float[] audio, Metadata metadata) - { - this(Type.AUDIO, metadata); - - mAudioData = new RealBuffer(audio); - } - - public boolean hasMetadata() - { - return mMetadata != null; - } - - public Metadata getMetadata() - { - return mMetadata; - } - - public void setMetadata(Metadata metadata) - { - mMetadata = metadata; - } - - public Type getType() - { - return mType; - } - - public RealBuffer getAudioBuffer() - { - return mAudioData; - } - - public boolean hasAudioBuffer() - { - return mAudioData != null; - } - - public enum Type - { - AUDIO, - END; - } -} diff --git a/src/main/java/io/github/dsheirer/audio/AudioPacketRateCounter.java b/src/main/java/io/github/dsheirer/audio/AudioPacketRateCounter.java deleted file mode 100644 index fbc7ea0d..00000000 --- a/src/main/java/io/github/dsheirer/audio/AudioPacketRateCounter.java +++ /dev/null @@ -1,55 +0,0 @@ -package io.github.dsheirer.audio; - -import io.github.dsheirer.sample.Listener; -import org.slf4j.Logger; -import org.slf4j.LoggerFactory; - -public class AudioPacketRateCounter implements Listener -{ - private final static Logger mLog = - LoggerFactory.getLogger( AudioPacketRateCounter.class ); - - private Listener mListener; - private long mCount; - private long mStartTime = -1; - - private String mName; - - public AudioPacketRateCounter() - { - mName = "UNNAMED"; - } - - public AudioPacketRateCounter( String name ) - { - mName = name; - } - - @Override - public void receive( AudioPacket packet ) - { - if( mStartTime == -1 ) - { - mStartTime = System.currentTimeMillis(); - } - - mCount += packet.getAudioBuffer().getSamples().length; - - long elapsed = System.currentTimeMillis() - mStartTime; - - if( elapsed != 0 ) - { - mLog.debug( "[" + mName + "] Audio Rate: " + ( mCount / elapsed ) + " kHz" ); - } - - if( mListener != null ) - { - mListener.receive( packet ); - } - } - - public void setListener( Listener listener ) - { - mListener = listener; - } -} diff --git a/src/main/java/io/github/dsheirer/audio/AudioUtils.java b/src/main/java/io/github/dsheirer/audio/AudioUtils.java index 08b4d3d8..1eca2361 100644 --- a/src/main/java/io/github/dsheirer/audio/AudioUtils.java +++ b/src/main/java/io/github/dsheirer/audio/AudioUtils.java @@ -1,24 +1,22 @@ /******************************************************************************* - * sdrtrunk - * Copyright (C) 2014-2016 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 + * You should have received a copy of the GNU General Public License along with this program. + * If not, see * ******************************************************************************/ package io.github.dsheirer.audio; import io.github.dsheirer.sample.ConversionUtils; +import io.github.dsheirer.sample.buffer.ReusableAudioPacket; import org.slf4j.Logger; import org.slf4j.LoggerFactory; @@ -34,17 +32,18 @@ public class AudioUtils /** * Converts the audio packets into a byte array of 16-bit, little-endian audio samples */ - public static byte[] convertTo16BitSamples(List audioPackets) + public static byte[] convertTo16BitSamples(List audioPackets) { ByteArrayOutputStream stream = new ByteArrayOutputStream(); try { - for(AudioPacket packet: audioPackets) + for(ReusableAudioPacket audioPacket: audioPackets) { //Converting from 32-bit floats to signed 16-bit samples - ByteBuffer buffer = ConversionUtils.convertToSigned16BitSamples(packet.getAudioBuffer().getSamples()); + ByteBuffer buffer = ConversionUtils.convertToSigned16BitSamples(audioPacket.getAudioSamples()); stream.write(buffer.array()); + audioPacket.decrementUserCount(); } } catch(IOException e) diff --git a/src/main/java/io/github/dsheirer/audio/IAudioPacketListener.java b/src/main/java/io/github/dsheirer/audio/IAudioPacketListener.java index 96833a73..76452704 100644 --- a/src/main/java/io/github/dsheirer/audio/IAudioPacketListener.java +++ b/src/main/java/io/github/dsheirer/audio/IAudioPacketListener.java @@ -1,8 +1,9 @@ package io.github.dsheirer.audio; import io.github.dsheirer.sample.Listener; +import io.github.dsheirer.sample.buffer.ReusableAudioPacket; public interface IAudioPacketListener { - public Listener getAudioPacketListener(); + public Listener getAudioPacketListener(); } diff --git a/src/main/java/io/github/dsheirer/audio/IAudioPacketProvider.java b/src/main/java/io/github/dsheirer/audio/IAudioPacketProvider.java index b6623cee..7fc00a58 100644 --- a/src/main/java/io/github/dsheirer/audio/IAudioPacketProvider.java +++ b/src/main/java/io/github/dsheirer/audio/IAudioPacketProvider.java @@ -1,9 +1,10 @@ package io.github.dsheirer.audio; import io.github.dsheirer.sample.Listener; +import io.github.dsheirer.sample.buffer.ReusableAudioPacket; public interface IAudioPacketProvider { - public void setAudioPacketListener( Listener listener ); + public void setAudioPacketListener( Listener listener ); public void removeAudioPacketListener(); } diff --git a/src/main/java/io/github/dsheirer/audio/broadcast/BroadcastModel.java b/src/main/java/io/github/dsheirer/audio/broadcast/BroadcastModel.java index fc7c9eac..986306fd 100644 --- a/src/main/java/io/github/dsheirer/audio/broadcast/BroadcastModel.java +++ b/src/main/java/io/github/dsheirer/audio/broadcast/BroadcastModel.java @@ -19,12 +19,12 @@ package io.github.dsheirer.audio.broadcast; import io.github.dsheirer.alias.id.broadcast.BroadcastChannel; -import io.github.dsheirer.audio.AudioPacket; import io.github.dsheirer.channel.metadata.Metadata; import io.github.dsheirer.icon.IconManager; import io.github.dsheirer.properties.SystemProperties; import io.github.dsheirer.sample.Broadcaster; import io.github.dsheirer.sample.Listener; +import io.github.dsheirer.sample.buffer.ReusableAudioPacket; import io.github.dsheirer.util.ThreadPool; import org.slf4j.Logger; import org.slf4j.LoggerFactory; @@ -46,7 +46,7 @@ import java.util.function.Consumer; import java.util.regex.Matcher; import java.util.regex.Pattern; -public class BroadcastModel extends AbstractTableModel implements Listener +public class BroadcastModel extends AbstractTableModel implements Listener { private final static Logger mLog = LoggerFactory.getLogger(BroadcastModel.class); @@ -276,7 +276,7 @@ public class BroadcastModel extends AbstractTableModel implements Listener

Q{1_iy0}lwen-%kqv(y#q%IH>{Q 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%0ATqJKjO!R^{}kM4|j9oGR_C1fCet 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