From 488b2e02ced82dc0153b886aaa0bfcb7461c8d25 Mon Sep 17 00:00:00 2001 From: Denny Date: Sun, 29 Apr 2018 06:19:39 -0400 Subject: [PATCH] [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;