mirror of
https://github.com/DSheirer/sdrtrunk.git
synced 2026-11-10 23:57:25 -05:00
[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.
This commit is contained in:
@@ -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);
|
||||
}
|
||||
|
||||
/**
|
||||
|
||||
@@ -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<SourceEvent> mSourceEventBroadcaster = new Broadcaster<>();
|
||||
private IReusableBufferProvider<ReusableComplexBuffer> 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:
|
||||
|
||||
@@ -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;
|
||||
}
|
||||
|
||||
/**
|
||||
|
||||
@@ -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);
|
||||
|
||||
|
||||
@@ -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();
|
||||
|
||||
@@ -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;
|
||||
}
|
||||
}
|
||||
|
||||
@@ -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;
|
||||
}
|
||||
|
||||
/**
|
||||
|
||||
@@ -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;
|
||||
}
|
||||
|
||||
/**
|
||||
|
||||
@@ -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
|
||||
*/
|
||||
|
||||
@@ -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 <http://www.gnu.org/licenses/>
|
||||
* *********************************************************************************************************************
|
||||
*
|
||||
******************************************************************************/
|
||||
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;
|
||||
@@ -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 <http://www.gnu.org/licenses/>
|
||||
*
|
||||
******************************************************************************/
|
||||
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<TunerChannel> getTunerChannels(Tuner tuner)
|
||||
{
|
||||
List<TunerChannel> 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<ReusableComplexBuffer>, 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<ReusableComplexBuffer>, 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<ReusableComplexBuffer>()
|
||||
{
|
||||
@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);
|
||||
}
|
||||
});
|
||||
}
|
||||
}
|
||||
@@ -13,10 +13,11 @@
|
||||
* If not, see <http://www.gnu.org/licenses/>
|
||||
*
|
||||
******************************************************************************/
|
||||
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;
|
||||
@@ -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();
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -41,8 +41,7 @@ public class RecorderManager implements Listener<AudioPacket>
|
||||
{
|
||||
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<String,AudioPacketWaveRecorder> mRecorders = new HashMap<>();
|
||||
|
||||
@@ -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<ReusableComplexBuffer>
|
||||
public class ComplexBufferWaveRecorder extends Module implements IReusableComplexBufferListener,
|
||||
Listener<ReusableComplexBuffer>, ISourceEventListener
|
||||
{
|
||||
private final static Logger mLog = LoggerFactory.getLogger(ComplexBufferWaveRecorder.class);
|
||||
|
||||
private WaveWriter mWriter;
|
||||
private ContinuousReusableBufferProcessor<ReusableComplexBuffer> 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<ReusableComplexBuffer> 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<SourceEvent> getSourceEventListener()
|
||||
{
|
||||
public void run()
|
||||
return new Listener<SourceEvent>()
|
||||
{
|
||||
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<List<ReusableComplexBuffer>>
|
||||
{
|
||||
public PoisonPill()
|
||||
public ReusableBufferWaveWriter(AudioFormat format, Path file) throws IOException
|
||||
{
|
||||
super(new IReusableBufferDisposedListener<ReusableComplexBuffer>()
|
||||
super(format, file);
|
||||
}
|
||||
|
||||
@Override
|
||||
public void receive(List<ReusableComplexBuffer> 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();
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -133,7 +133,6 @@ public class WaveWriter implements AutoCloseable
|
||||
{
|
||||
mFileChannel.force(true);
|
||||
mFileChannel.close();
|
||||
mFileChannel = null;
|
||||
|
||||
rename();
|
||||
}
|
||||
|
||||
@@ -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 : "");
|
||||
}
|
||||
}
|
||||
|
||||
@@ -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)
|
||||
{
|
||||
|
||||
@@ -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;
|
||||
|
||||
|
||||
Reference in New Issue
Block a user