mirror of
https://github.com/DSheirer/sdrtrunk.git
synced 2026-11-12 08:37:25 -05:00
[WIP] Updates the AM decoder module.
Removes a number of unused classes in an effort to remove RealBuffer usage across the codebase (has been replaced by ReusableBuffers).
This commit is contained in:
@@ -1,40 +0,0 @@
|
||||
/*******************************************************************************
|
||||
* SDR Trunk
|
||||
* Copyright (C) 2015 Dennis Sheirer
|
||||
*
|
||||
* This program is free software: you can redistribute it and/or modify
|
||||
* it under the terms of the GNU General Public License as published by
|
||||
* the Free Software Foundation, either version 3 of the License, or
|
||||
* (at your option) any later version.
|
||||
*
|
||||
* This program is distributed in the hope that it will be useful,
|
||||
* but WITHOUT ANY WARRANTY; without even the implied warranty of
|
||||
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
|
||||
* GNU General Public License for more details.
|
||||
*
|
||||
* You should have received a copy of the GNU General Public License
|
||||
* along with this program. If not, see <http://www.gnu.org/licenses/>
|
||||
******************************************************************************/
|
||||
|
||||
package io.github.dsheirer.audio.squelch;
|
||||
|
||||
|
||||
/**
|
||||
* Provides squelch control based on presense of decoded audio samples.
|
||||
*
|
||||
* NOTE: this class is not yet fully implemented with audio sample processing
|
||||
* and currently only provides user driven squelch control
|
||||
*/
|
||||
public class ManualSquelchController extends SquelchController
|
||||
{
|
||||
public ManualSquelchController()
|
||||
{
|
||||
mSquelchMode = SquelchMode.NONE;
|
||||
}
|
||||
|
||||
@Override
|
||||
public void setSquelchMode( SquelchMode mode )
|
||||
{
|
||||
//TODO: add functionality
|
||||
}
|
||||
}
|
||||
@@ -1,122 +0,0 @@
|
||||
/*******************************************************************************
|
||||
* SDR Trunk
|
||||
* Copyright (C) 2015 Dennis Sheirer
|
||||
*
|
||||
* This program is free software: you can redistribute it and/or modify
|
||||
* it under the terms of the GNU General Public License as published by
|
||||
* the Free Software Foundation, either version 3 of the License, or
|
||||
* (at your option) any later version.
|
||||
*
|
||||
* This program is distributed in the hope that it will be useful,
|
||||
* but WITHOUT ANY WARRANTY; without even the implied warranty of
|
||||
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
|
||||
* GNU General Public License for more details.
|
||||
*
|
||||
* You should have received a copy of the GNU General Public License
|
||||
* along with this program. If not, see <http://www.gnu.org/licenses/>
|
||||
******************************************************************************/
|
||||
|
||||
package io.github.dsheirer.audio.squelch;
|
||||
|
||||
import io.github.dsheirer.sample.Broadcaster;
|
||||
import io.github.dsheirer.sample.Listener;
|
||||
import io.github.dsheirer.sample.real.RealBuffer;
|
||||
import io.github.dsheirer.sample.real.RealSampleListener;
|
||||
import io.github.dsheirer.sample.real.RealToRealBufferAssembler;
|
||||
|
||||
/**
|
||||
* Squelch control - receives single audio samples or real audio buffers and
|
||||
* adjusts squelch state either based on the audio samples or based on external
|
||||
* control inputs.
|
||||
*
|
||||
* Assembles received single audio samples into real sample buffers for
|
||||
* consumption by an external audio processor, like the AudioPacketProcessor
|
||||
* class.
|
||||
*/
|
||||
public abstract class SquelchController
|
||||
implements Listener<RealBuffer>, RealSampleListener
|
||||
{
|
||||
private Broadcaster<SquelchState> mSquelchStateBroadcaster = new Broadcaster<>();
|
||||
private Listener<RealBuffer> mAudioListener;
|
||||
protected SquelchState mSquelchState = SquelchState.UNSQUELCH;
|
||||
protected SquelchMode mSquelchMode = SquelchMode.MANUAL;
|
||||
|
||||
private RealToRealBufferAssembler mSampleAssembler = new RealToRealBufferAssembler( 512 );
|
||||
|
||||
public SquelchController()
|
||||
{
|
||||
mSampleAssembler.setListener( this );
|
||||
}
|
||||
|
||||
@Override
|
||||
public void receive( float sample )
|
||||
{
|
||||
mSampleAssembler.receive( sample );
|
||||
}
|
||||
|
||||
@Override
|
||||
public void receive( RealBuffer buffer )
|
||||
{
|
||||
if( mAudioListener != null )
|
||||
{
|
||||
mAudioListener.receive( buffer );
|
||||
}
|
||||
}
|
||||
|
||||
public abstract void setSquelchMode( SquelchMode mode );
|
||||
|
||||
public SquelchMode getSquelchMode()
|
||||
{
|
||||
return mSquelchMode;
|
||||
}
|
||||
|
||||
public void setSquelchState( SquelchState state )
|
||||
{
|
||||
mSquelchState = state;
|
||||
}
|
||||
|
||||
public SquelchState getSquelchState()
|
||||
{
|
||||
return mSquelchState;
|
||||
}
|
||||
|
||||
public boolean isSquelched()
|
||||
{
|
||||
switch( mSquelchMode )
|
||||
{
|
||||
case AUTOMATIC:
|
||||
case MANUAL:
|
||||
return mSquelchState == SquelchState.SQUELCH;
|
||||
case NONE:
|
||||
return false;
|
||||
default:
|
||||
return false;
|
||||
}
|
||||
}
|
||||
|
||||
public void addSquelchStateListener( Listener<SquelchState> listener )
|
||||
{
|
||||
if( listener != null )
|
||||
{
|
||||
mSquelchStateBroadcaster.addListener( listener );
|
||||
|
||||
/* Send current squelch state to the listener */
|
||||
listener.receive( mSquelchState );
|
||||
}
|
||||
}
|
||||
|
||||
public void removeSquelchStateListener( Listener<SquelchState> listener )
|
||||
{
|
||||
mSquelchStateBroadcaster.removeListener( listener );
|
||||
}
|
||||
|
||||
public void setAudioListener( Listener<RealBuffer> listener )
|
||||
{
|
||||
mAudioListener = listener;
|
||||
}
|
||||
|
||||
public void removeAudioListener( Listener<RealBuffer> listener )
|
||||
{
|
||||
mAudioListener = null;
|
||||
}
|
||||
}
|
||||
@@ -1,55 +0,0 @@
|
||||
/*******************************************************************************
|
||||
* SDR Trunk
|
||||
* Copyright (C) 2015 Dennis Sheirer
|
||||
*
|
||||
* This program is free software: you can redistribute it and/or modify
|
||||
* it under the terms of the GNU General Public License as published by
|
||||
* the Free Software Foundation, either version 3 of the License, or
|
||||
* (at your option) any later version.
|
||||
*
|
||||
* This program is distributed in the hope that it will be useful,
|
||||
* but WITHOUT ANY WARRANTY; without even the implied warranty of
|
||||
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
|
||||
* GNU General Public License for more details.
|
||||
*
|
||||
* You should have received a copy of the GNU General Public License
|
||||
* along with this program. If not, see <http://www.gnu.org/licenses/>
|
||||
******************************************************************************/
|
||||
|
||||
package io.github.dsheirer.audio.squelch;
|
||||
|
||||
public class TrunkingSquelchController extends SquelchController
|
||||
{
|
||||
private boolean mAllowSquelchOverride = true;
|
||||
|
||||
public TrunkingSquelchController()
|
||||
{
|
||||
}
|
||||
|
||||
/**
|
||||
* Provides control over audio stream as directed by an external trunking
|
||||
* channel state.
|
||||
*
|
||||
* Override automatic squelch control by setting the squelch mode to none.
|
||||
*
|
||||
* Disable automatic control override by setting allowManualOverride to false;
|
||||
*
|
||||
* Note: this control does not respond to squelch mode manual.
|
||||
*/
|
||||
public TrunkingSquelchController( boolean allowSquelchOverride )
|
||||
{
|
||||
mAllowSquelchOverride = allowSquelchOverride;
|
||||
|
||||
mSquelchMode = SquelchMode.AUTOMATIC;
|
||||
}
|
||||
|
||||
@Override
|
||||
public void setSquelchMode( SquelchMode mode )
|
||||
{
|
||||
if( mode == SquelchMode.AUTOMATIC ||
|
||||
( mode == SquelchMode.NONE && mAllowSquelchOverride ) )
|
||||
{
|
||||
mSquelchMode = mode;
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -26,11 +26,10 @@ import io.github.dsheirer.icon.IconManager;
|
||||
import io.github.dsheirer.module.decode.event.CallEventPanel;
|
||||
import io.github.dsheirer.module.decode.event.MessageActivityPanel;
|
||||
import io.github.dsheirer.settings.SettingsManager;
|
||||
import io.github.dsheirer.spectrum.ChannelSpectrumPanel;
|
||||
import net.miginfocom.swing.MigLayout;
|
||||
|
||||
import javax.swing.*;
|
||||
import java.awt.*;
|
||||
import javax.swing.JPanel;
|
||||
import java.awt.Color;
|
||||
|
||||
public class ChannelMetadataViewer extends JPanel
|
||||
{
|
||||
@@ -39,7 +38,6 @@ public class ChannelMetadataViewer extends JPanel
|
||||
private ChannelDetailPanel mChannelDetailPanel;
|
||||
private CallEventPanel mCallEventPanel;
|
||||
private MessageActivityPanel mMessageActivityPanel;
|
||||
private ChannelSpectrumPanel mChannelSpectrumPanel;
|
||||
|
||||
/**
|
||||
* GUI panel that combines the currently decoding channels metadata table and viewers for channel details,
|
||||
@@ -51,7 +49,6 @@ public class ChannelMetadataViewer extends JPanel
|
||||
mChannelDetailPanel = new ChannelDetailPanel(channelProcessingManager);
|
||||
mCallEventPanel = new CallEventPanel(iconManager);
|
||||
mMessageActivityPanel = new MessageActivityPanel(channelProcessingManager);
|
||||
// mChannelSpectrumPanel = new ChannelSpectrumPanel(settingsManager, channelProcessingManager);
|
||||
mChannelMetadataPanel = new ChannelMetadataPanel(channelProcessingManager, iconManager);
|
||||
|
||||
init();
|
||||
|
||||
@@ -1,22 +1,83 @@
|
||||
/*******************************************************************************
|
||||
* sdr-trunk
|
||||
* Copyright (C) 2014-2018 Dennis Sheirer
|
||||
*
|
||||
* This program is free software: you can redistribute it and/or modify it under the terms of the GNU General Public
|
||||
* License as published by the Free Software Foundation, either version 3 of the License, or (at your option) any
|
||||
* later version.
|
||||
*
|
||||
* This program is distributed in the hope that it will be useful, but WITHOUT ANY WARRANTY; without even the implied
|
||||
* warranty of MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU General Public License for more details.
|
||||
*
|
||||
* You should have received a copy of the GNU General Public License along with this program.
|
||||
* If not, see <http://www.gnu.org/licenses/>
|
||||
*
|
||||
******************************************************************************/
|
||||
package io.github.dsheirer.dsp.am;
|
||||
|
||||
public abstract class AMDemodulator
|
||||
import io.github.dsheirer.sample.buffer.ReusableBuffer;
|
||||
import io.github.dsheirer.sample.buffer.ReusableBufferQueue;
|
||||
import io.github.dsheirer.sample.buffer.ReusableComplexBuffer;
|
||||
|
||||
/**
|
||||
* Performs AM demodulation on baseband I/Q samples to produce demodulated float output.
|
||||
*/
|
||||
public class AMDemodulator
|
||||
{
|
||||
private float mGain;
|
||||
|
||||
public AMDemodulator( float gain )
|
||||
{
|
||||
mGain = gain;
|
||||
}
|
||||
|
||||
public float demodulate( float inphase, float quadrature )
|
||||
private ReusableBufferQueue mReusableBufferQueue = new ReusableBufferQueue("AMDemodulator");
|
||||
private int mOutputBufferPointer;
|
||||
private float mGain;
|
||||
|
||||
/**
|
||||
* Constructs this demodulator where the specified gain is applied to demodulated output samples.
|
||||
*
|
||||
* @param gain to apply to demodulated output samples.
|
||||
*/
|
||||
public AMDemodulator(float gain)
|
||||
{
|
||||
return (float)Math.sqrt( ( inphase * inphase ) +
|
||||
( quadrature * quadrature ) ) * mGain;
|
||||
mGain = gain;
|
||||
}
|
||||
|
||||
/**
|
||||
* Sets the gain to the specified level.
|
||||
*/
|
||||
public void setGain(float gain)
|
||||
{
|
||||
mGain = gain;
|
||||
}
|
||||
|
||||
/**
|
||||
* Demodulates the comples I/Q sample and returns the demodulated output sample.
|
||||
*
|
||||
* @param inphase sample
|
||||
* @param quadrature sample
|
||||
* @return AM demodulated sample
|
||||
*/
|
||||
public float demodulate(float inphase, float quadrature)
|
||||
{
|
||||
return (float)Math.sqrt((inphase * inphase) + (quadrature * quadrature)) * mGain;
|
||||
}
|
||||
|
||||
/**
|
||||
* Demodulates the reusable comples sample buffer and returns a demodulated audio buffer.
|
||||
* @param complexBuffer to demodulate
|
||||
* @return demodulated audio buffer.
|
||||
*/
|
||||
public ReusableBuffer demodulate(ReusableComplexBuffer complexBuffer)
|
||||
{
|
||||
ReusableBuffer reusableBuffer = mReusableBufferQueue.getBuffer(complexBuffer.getSampleCount());
|
||||
float[] input = complexBuffer.getSamples();
|
||||
float[] output = reusableBuffer.getSamples();
|
||||
mOutputBufferPointer = 0;
|
||||
|
||||
for(int x= 0; x < complexBuffer.getSamples().length; x += 2)
|
||||
{
|
||||
output[mOutputBufferPointer++] = demodulate(input[x], input[x + 1]);
|
||||
}
|
||||
|
||||
complexBuffer.decrementUserCount();
|
||||
reusableBuffer.incrementUserCount();
|
||||
|
||||
return reusableBuffer;
|
||||
}
|
||||
|
||||
public void setGain( float gain )
|
||||
{
|
||||
mGain = gain;
|
||||
}
|
||||
}
|
||||
|
||||
@@ -58,6 +58,16 @@ public class DCRemovalFilter
|
||||
return filtered;
|
||||
}
|
||||
|
||||
public float[] filter( float[] samples )
|
||||
{
|
||||
for( int x = 0; x < samples.length; x++ )
|
||||
{
|
||||
samples[ x ] = filter( samples[ x ] );
|
||||
}
|
||||
|
||||
return samples;
|
||||
}
|
||||
|
||||
/**
|
||||
* Filters the buffer samples and returns a new reusable buffer with the filtered samples.
|
||||
*
|
||||
|
||||
@@ -1,61 +0,0 @@
|
||||
package io.github.dsheirer.dsp.filter.dc;
|
||||
|
||||
import io.github.dsheirer.sample.Listener;
|
||||
import io.github.dsheirer.sample.real.RealBuffer;
|
||||
|
||||
public class DCRemovalFilter_RB extends DCRemovalFilter
|
||||
implements Listener<RealBuffer>
|
||||
{
|
||||
protected Listener<RealBuffer> mListener;
|
||||
|
||||
public DCRemovalFilter_RB( float ratio )
|
||||
{
|
||||
super( ratio );
|
||||
}
|
||||
|
||||
public void setListener( Listener<RealBuffer> listener )
|
||||
{
|
||||
mListener = listener;
|
||||
}
|
||||
|
||||
public void removeListener()
|
||||
{
|
||||
mListener = null;
|
||||
}
|
||||
|
||||
public void dispose()
|
||||
{
|
||||
mListener = null;
|
||||
}
|
||||
|
||||
public float[] filter( float[] samples )
|
||||
{
|
||||
for( int x = 0; x < samples.length; x++ )
|
||||
{
|
||||
samples[ x ] = filter( samples[ x ] );
|
||||
}
|
||||
|
||||
return samples;
|
||||
}
|
||||
|
||||
public RealBuffer filter( RealBuffer buffer )
|
||||
{
|
||||
float[] samples = buffer.getSamples();
|
||||
|
||||
for( int x = 0; x < samples.length; x++ )
|
||||
{
|
||||
samples[ x ] = filter( samples[ x ] );
|
||||
}
|
||||
|
||||
return buffer;
|
||||
}
|
||||
|
||||
@Override
|
||||
public void receive( RealBuffer buffer )
|
||||
{
|
||||
if( mListener != null )
|
||||
{
|
||||
mListener.receive( filter( buffer ) );
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -1,60 +0,0 @@
|
||||
package io.github.dsheirer.dsp.filter.dc;
|
||||
|
||||
import io.github.dsheirer.sample.Listener;
|
||||
import io.github.dsheirer.sample.real.RealBuffer;
|
||||
|
||||
public class DCRemovalFilter_RealBuffer implements Listener<RealBuffer>
|
||||
{
|
||||
private Listener<RealBuffer> mListener;
|
||||
/**
|
||||
* Removes DC bias present across the samples in the buffer by calculating
|
||||
* the mean DC component and subtracting that component from each of the
|
||||
* samples.
|
||||
*/
|
||||
public DCRemovalFilter_RealBuffer()
|
||||
{
|
||||
}
|
||||
|
||||
public void dispose()
|
||||
{
|
||||
mListener = null;
|
||||
}
|
||||
|
||||
public RealBuffer filter( RealBuffer buffer )
|
||||
{
|
||||
double accumulator = 0.0d;
|
||||
|
||||
for( float sample: buffer.getSamples() )
|
||||
{
|
||||
accumulator += sample;
|
||||
}
|
||||
|
||||
float mean = (float)( accumulator / (double)buffer.getSamples().length );
|
||||
|
||||
for( float sample: buffer.getSamples() )
|
||||
{
|
||||
sample -= mean;
|
||||
}
|
||||
|
||||
return buffer;
|
||||
}
|
||||
|
||||
@Override
|
||||
public void receive( RealBuffer buffer )
|
||||
{
|
||||
if( mListener != null )
|
||||
{
|
||||
mListener.receive( filter( buffer ) );
|
||||
}
|
||||
}
|
||||
|
||||
public void setListener( Listener<RealBuffer> listener )
|
||||
{
|
||||
mListener = listener;
|
||||
}
|
||||
|
||||
public void removeListener()
|
||||
{
|
||||
mListener = null;
|
||||
}
|
||||
}
|
||||
@@ -1,116 +0,0 @@
|
||||
/*******************************************************************************
|
||||
* sdrtrunk
|
||||
* Copyright (C) 2014-2016 Dennis Sheirer
|
||||
*
|
||||
* This program is free software: you can redistribute it and/or modify
|
||||
* it under the terms of the GNU General Public License as published by
|
||||
* the Free Software Foundation, either version 3 of the License, or
|
||||
* (at your option) any later version.
|
||||
*
|
||||
* This program is distributed in the hope that it will be useful,
|
||||
* but WITHOUT ANY WARRANTY; without even the implied warranty of
|
||||
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
|
||||
* GNU General Public License for more details.
|
||||
*
|
||||
* You should have received a copy of the GNU General Public License
|
||||
* along with this program. If not, see <http://www.gnu.org/licenses/>
|
||||
******************************************************************************/
|
||||
package io.github.dsheirer.dsp.filter.polyphase;
|
||||
|
||||
public class FilterStage
|
||||
{
|
||||
private float[] mBuffer;
|
||||
private int mBufferSize;
|
||||
private int mBufferPointer;
|
||||
private int[][] mIndexMap;
|
||||
private float[] mCoefficients;
|
||||
|
||||
/**
|
||||
* Single stage of a polyphase filter implemented with a circular sample buffer that uses an index map to ensure
|
||||
* correct alignment of samples to filter taps.
|
||||
*
|
||||
* @param coefficients
|
||||
*/
|
||||
public FilterStage(float[] coefficients)
|
||||
{
|
||||
mCoefficients = coefficients;
|
||||
|
||||
mBufferSize = mCoefficients.length;
|
||||
|
||||
mBuffer = new float[mBufferSize];
|
||||
|
||||
//Note: buffer pointer starts at an illegal index but is decremented before first usage
|
||||
mBufferPointer = mBufferSize;
|
||||
|
||||
generateIndexMap(mBufferSize);
|
||||
}
|
||||
|
||||
/**
|
||||
* Coefficients used by this filter stage.
|
||||
*/
|
||||
public float[] getCoefficients()
|
||||
{
|
||||
return mCoefficients;
|
||||
}
|
||||
|
||||
/**
|
||||
* Cleanup prior to disposal
|
||||
*/
|
||||
public void dispose()
|
||||
{
|
||||
mCoefficients = null;
|
||||
mIndexMap = null;
|
||||
mBuffer = null;
|
||||
}
|
||||
|
||||
/**
|
||||
* Loads the sample into the sample buffer
|
||||
*/
|
||||
private void load(float sample)
|
||||
{
|
||||
mBufferPointer--;
|
||||
|
||||
if (mBufferPointer < 0)
|
||||
{
|
||||
mBufferPointer += mBufferSize;
|
||||
}
|
||||
|
||||
mBuffer[mBufferPointer] = sample;
|
||||
}
|
||||
|
||||
/**
|
||||
* Applies convolution against the currently loaded buffer samples.
|
||||
*/
|
||||
public float filter(float sample)
|
||||
{
|
||||
load(sample);
|
||||
|
||||
float accumulator = 0.0f;
|
||||
|
||||
for (int x = 0; x < mBufferSize; x++)
|
||||
{
|
||||
accumulator += mCoefficients[x] * mBuffer[mIndexMap[mBufferPointer][x]];
|
||||
}
|
||||
|
||||
return accumulator;
|
||||
}
|
||||
|
||||
/**
|
||||
* Generates a circular buffer index map to support lookup of the translated index based on the current buffer
|
||||
* pointer and the desired sample index.
|
||||
*/
|
||||
private void generateIndexMap(int size)
|
||||
{
|
||||
mIndexMap = new int[size][size];
|
||||
|
||||
for (int x = 0; x < size; x++)
|
||||
{
|
||||
for (int y = 0; y < size; y++)
|
||||
{
|
||||
int z = x + y;
|
||||
|
||||
mIndexMap[x][y] = z < size ? z : z - size;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -1,206 +0,0 @@
|
||||
/*******************************************************************************
|
||||
* sdrtrunk
|
||||
* Copyright (C) 2014-2016 Dennis Sheirer
|
||||
*
|
||||
* This program is free software: you can redistribute it and/or modify
|
||||
* it under the terms of the GNU General Public License as published by
|
||||
* the Free Software Foundation, either version 3 of the License, or
|
||||
* (at your option) any later version.
|
||||
*
|
||||
* This program is distributed in the hope that it will be useful,
|
||||
* but WITHOUT ANY WARRANTY; without even the implied warranty of
|
||||
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
|
||||
* GNU General Public License for more details.
|
||||
*
|
||||
* You should have received a copy of the GNU General Public License
|
||||
* along with this program. If not, see <http://www.gnu.org/licenses/>
|
||||
******************************************************************************/
|
||||
package io.github.dsheirer.dsp.filter.polyphase;
|
||||
|
||||
import io.github.dsheirer.dsp.filter.design.FilterDesignException;
|
||||
import io.github.dsheirer.dsp.filter.fir.FIRFilter;
|
||||
import io.github.dsheirer.dsp.filter.fir.FIRFilterSpecification;
|
||||
import io.github.dsheirer.dsp.filter.fir.remez.RemezFIRFilterDesigner;
|
||||
import io.github.dsheirer.dsp.mixer.IOscillator;
|
||||
import io.github.dsheirer.dsp.mixer.Oscillator;
|
||||
import io.github.dsheirer.sample.Listener;
|
||||
import io.github.dsheirer.sample.real.RealBuffer;
|
||||
import io.github.dsheirer.sample.real.RealToRealBufferAssembler;
|
||||
import org.slf4j.Logger;
|
||||
import org.slf4j.LoggerFactory;
|
||||
|
||||
import java.util.ArrayList;
|
||||
import java.util.Arrays;
|
||||
import java.util.List;
|
||||
|
||||
public class PolyphaseFIRDecimatingFilter_RB extends FIRFilter implements Listener<RealBuffer>
|
||||
{
|
||||
private final static Logger mLog = LoggerFactory.getLogger( PolyphaseFIRDecimatingFilter_RB.class );
|
||||
|
||||
private List<FilterStage> mFilterStages = new ArrayList<>();
|
||||
private int mFilterStagePointer;
|
||||
private Listener<RealBuffer> mListener;
|
||||
private RealToRealBufferAssembler mAssembler;
|
||||
private int mDecimationRatio;
|
||||
private float mAccumulator = 0.0f;
|
||||
private float mGain = 1.0f;
|
||||
|
||||
/**
|
||||
* Polyphase filter stage for decimating real-valued sample buffers by the decimation ratio. Creates a number of
|
||||
* polyphase filter arms/stages equal to the decimation ratio.
|
||||
*
|
||||
* Note: output real-valued buffer sizes are determined by the first input buffer that is presented to the filter
|
||||
* where the output buffer size is equal to the floor of the input buffer size divided by the decimation ratio.
|
||||
*
|
||||
* @param coefficients of a low pass filter designed for the input sample rate with a cutoff frequency that conforms
|
||||
* to the nyquist frequency for an output sample rate equal to the input sample rate divided by the decimation ratio.
|
||||
*
|
||||
* @param decimationRatio to decimate the input sample rate by.
|
||||
* @param gain to apply to the filtered output. Use a value of 1.0 to apply no gain.
|
||||
*/
|
||||
public PolyphaseFIRDecimatingFilter_RB(float[] coefficients, int decimationRatio, float gain)
|
||||
{
|
||||
mDecimationRatio = decimationRatio;
|
||||
mGain = gain;
|
||||
|
||||
createFilterStages(coefficients, mDecimationRatio);
|
||||
mFilterStagePointer = mFilterStages.size() - 1;
|
||||
}
|
||||
|
||||
/**
|
||||
* Creates polyphase filter stages (arms) from the set of coefficients.
|
||||
*
|
||||
* @param coefficients for the low-pass filter
|
||||
* @param decimationRatio to determine the number of polyphase filter stages.
|
||||
*/
|
||||
private void createFilterStages(float[] coefficients, int decimationRatio)
|
||||
{
|
||||
int tapSize = (int)Math.ceil((double)coefficients.length / (double)decimationRatio);
|
||||
|
||||
float[][] coefficientSets = new float[decimationRatio][tapSize];
|
||||
|
||||
int stagePointer = 0;
|
||||
int coefficientPointer = 0;
|
||||
|
||||
//Split the coefficients up into polyphase stage coefficient sets
|
||||
for(int x = 0; x < coefficients.length; x++)
|
||||
{
|
||||
coefficientSets[stagePointer++][coefficientPointer] = coefficients[x];
|
||||
|
||||
if(stagePointer >= decimationRatio)
|
||||
{
|
||||
stagePointer = 0;
|
||||
coefficientPointer++;
|
||||
}
|
||||
}
|
||||
|
||||
//Create the stages
|
||||
for(int x = 0; x < decimationRatio; x++)
|
||||
{
|
||||
mFilterStages.add(new FilterStage(coefficientSets[x]));
|
||||
}
|
||||
}
|
||||
|
||||
public void dispose()
|
||||
{
|
||||
mListener = null;
|
||||
}
|
||||
|
||||
@Override
|
||||
public void receive(RealBuffer buffer)
|
||||
{
|
||||
if(mAssembler == null)
|
||||
{
|
||||
mAssembler = new RealToRealBufferAssembler(buffer.getSamples().length / mDecimationRatio);
|
||||
mAssembler.setListener(mListener);
|
||||
}
|
||||
|
||||
for(float sample: buffer.getSamples())
|
||||
{
|
||||
filter(sample);
|
||||
}
|
||||
|
||||
int a = 0;
|
||||
}
|
||||
|
||||
private void filter(float sample)
|
||||
{
|
||||
FilterStage stage = mFilterStages.get(mFilterStagePointer--);
|
||||
|
||||
mAccumulator += stage.filter(sample);
|
||||
|
||||
if(mFilterStagePointer < 0)
|
||||
{
|
||||
mAssembler.receive(mAccumulator * mGain);
|
||||
mAccumulator = 0.0f;
|
||||
|
||||
mFilterStagePointer = mFilterStages.size() - 1;
|
||||
}
|
||||
}
|
||||
|
||||
public void setListener(Listener<RealBuffer> listener)
|
||||
{
|
||||
mListener = listener;
|
||||
|
||||
if(mAssembler != null)
|
||||
{
|
||||
mAssembler.setListener(mListener);
|
||||
}
|
||||
}
|
||||
|
||||
public void removeListener()
|
||||
{
|
||||
mListener = null;
|
||||
|
||||
if(mAssembler != null)
|
||||
{
|
||||
mAssembler.setListener(null);
|
||||
}
|
||||
}
|
||||
|
||||
public static void main(String[] args)
|
||||
{
|
||||
FIRFilterSpecification specification = FIRFilterSpecification.lowPassBuilder()
|
||||
.sampleRate( 48000 )
|
||||
.gridDensity( 16 )
|
||||
.passBandCutoff( 3000 )
|
||||
.passBandAmplitude( 1.0 )
|
||||
.passBandRipple( 0.01 )
|
||||
.stopBandStart( 4000 )
|
||||
.stopBandAmplitude( 0.0 )
|
||||
.stopBandRipple( 0.027 )
|
||||
.build();
|
||||
|
||||
RemezFIRFilterDesigner designer = new RemezFIRFilterDesigner(specification);
|
||||
|
||||
try
|
||||
{
|
||||
if(!designer.isValid())
|
||||
{
|
||||
throw new FilterDesignException("Couldn't design the filter");
|
||||
}
|
||||
|
||||
float[] coefficients = designer.getImpulseResponse();
|
||||
PolyphaseFIRDecimatingFilter_RB filter = new PolyphaseFIRDecimatingFilter_RB(coefficients, 6, 1.0f);
|
||||
filter.setListener(new Listener<RealBuffer>()
|
||||
{
|
||||
@Override
|
||||
public void receive(RealBuffer realBuffer)
|
||||
{
|
||||
mLog.info("Filtered: " + Arrays.toString(realBuffer.getSamples()));
|
||||
}
|
||||
});
|
||||
|
||||
IOscillator oscillator = new Oscillator(3400, 48000);
|
||||
|
||||
float[] samples = oscillator.generateReal(500);
|
||||
RealBuffer buffer = new RealBuffer(samples);
|
||||
filter.receive(buffer);
|
||||
}
|
||||
catch(FilterDesignException e)
|
||||
{
|
||||
mLog.debug("Error designing filter", e);
|
||||
}
|
||||
|
||||
}
|
||||
}
|
||||
@@ -1,203 +0,0 @@
|
||||
/*******************************************************************************
|
||||
* sdrtrunk
|
||||
* Copyright (C) 2014-2016 Dennis Sheirer
|
||||
*
|
||||
* This program is free software: you can redistribute it and/or modify
|
||||
* it under the terms of the GNU General Public License as published by
|
||||
* the Free Software Foundation, either version 3 of the License, or
|
||||
* (at your option) any later version.
|
||||
*
|
||||
* This program is distributed in the hope that it will be useful,
|
||||
* but WITHOUT ANY WARRANTY; without even the implied warranty of
|
||||
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
|
||||
* GNU General Public License for more details.
|
||||
*
|
||||
* You should have received a copy of the GNU General Public License
|
||||
* along with this program. If not, see <http://www.gnu.org/licenses/>
|
||||
*
|
||||
******************************************************************************/
|
||||
package io.github.dsheirer.dsp.filter.polyphase;
|
||||
|
||||
import org.apache.commons.lang3.Validate;
|
||||
import org.slf4j.Logger;
|
||||
import org.slf4j.LoggerFactory;
|
||||
|
||||
/**
|
||||
* Polyphase interpolating FIR filter with an internal circular buffer as
|
||||
* described by the dspGuru at:
|
||||
*
|
||||
* http://www.dspguru.com/dsp/faqs/multirate/interpolation
|
||||
* See sections 3.4.2 and 3.4.3 )
|
||||
*/
|
||||
public class PolyphaseFIRInterpolatingFilter
|
||||
{
|
||||
private final static Logger mLog =
|
||||
LoggerFactory.getLogger( PolyphaseFIRInterpolatingFilter.class );
|
||||
|
||||
private double[] mBuffer;
|
||||
private int mBufferPointer = 0;
|
||||
private double[] mTaps;
|
||||
private Phase[] mPhases;
|
||||
|
||||
/**
|
||||
* Constructs the interpolating filter. Coefficients length must be an
|
||||
* integer multiple of the interpolation value. Each sample processed by
|
||||
* the filter will generate a quantity of samples equal to the interpolation
|
||||
* value.
|
||||
*
|
||||
* @param coefficients - low pass filter coefficients sized for the desired
|
||||
* interpolated sample rate with a cutoff frequency of no more than half of
|
||||
* the original sample rate.
|
||||
*
|
||||
* @param interpolation - integral interpolation quantity
|
||||
*/
|
||||
public PolyphaseFIRInterpolatingFilter( double[] coefficients, int interpolation )
|
||||
{
|
||||
/* Ensure coefficients are an integral multiple of the interpolation factor */
|
||||
Validate.isTrue(coefficients.length % interpolation == 0);
|
||||
|
||||
mTaps = coefficients;
|
||||
|
||||
mPhases = new Phase[ interpolation ];
|
||||
|
||||
int phaseSize = coefficients.length / interpolation;
|
||||
|
||||
mBuffer = new double[ phaseSize ];
|
||||
|
||||
for( int x = 0; x < interpolation; x++ )
|
||||
{
|
||||
int coefficientIndex = 0;
|
||||
|
||||
int[] indexes = new int[ phaseSize ];
|
||||
|
||||
for( int y = x; y < coefficients.length; y += interpolation )
|
||||
{
|
||||
indexes[ coefficientIndex++ ] = y;
|
||||
}
|
||||
|
||||
/* Create a phase using the tap indexes and the interpolation value
|
||||
* for the gain value, to offset the loss induced in upsampling from
|
||||
* one sample to (interpolation) samples. */
|
||||
mPhases[ interpolation - x - 1 ] = new Phase( indexes, interpolation );
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Inserts the sample into the filter and returns (interpolation) number
|
||||
* of interpolated samples in the return array.
|
||||
*
|
||||
* @param sample - float sample
|
||||
* @return - array of interpolated samples, size = interpolation quantity
|
||||
*/
|
||||
public float[] interpolate( float sample )
|
||||
{
|
||||
mBuffer[ mBufferPointer ] = (double)sample;
|
||||
|
||||
mBufferPointer++;
|
||||
mBufferPointer %= mBuffer.length;
|
||||
|
||||
float[] samples = new float[ mPhases.length ];
|
||||
|
||||
for( int x = 0; x < mPhases.length; x++ )
|
||||
{
|
||||
samples[ x ] = mPhases[ x ].filter();
|
||||
}
|
||||
|
||||
return samples;
|
||||
}
|
||||
|
||||
/**
|
||||
* Upsamples and interpolates the array of samples, returning an array that
|
||||
* is upsampled and low-pass filtered by the interpolation quantity.
|
||||
*
|
||||
* @param samples - array of float samples
|
||||
* @return - array of interpolated samples, size = interpolation * samples size
|
||||
*/
|
||||
public float[] interpolate( float[] samples )
|
||||
{
|
||||
int pointer = 0;
|
||||
float[] interpolated = new float[ samples.length * mPhases.length ];
|
||||
|
||||
for( float sample: samples )
|
||||
{
|
||||
mBuffer[ mBufferPointer ] = (double)sample;
|
||||
|
||||
mBufferPointer++;
|
||||
mBufferPointer %= mBuffer.length;
|
||||
|
||||
for( int x = 0; x < mPhases.length; x++ )
|
||||
{
|
||||
interpolated[ pointer ] = mPhases[ x ].filter();
|
||||
|
||||
pointer++;
|
||||
}
|
||||
}
|
||||
|
||||
return interpolated;
|
||||
}
|
||||
|
||||
/**
|
||||
* Single phase element of a polyphase interpolating FIR filter. Each phase
|
||||
* element shares a common buffer. The tap indexes argument defines the
|
||||
* filter taps used by this phase element to generate an interpolated value
|
||||
* from the shared circular buffer of samples.
|
||||
*/
|
||||
public class Phase
|
||||
{
|
||||
private int[] mIndexes;
|
||||
private double mGain;
|
||||
|
||||
public Phase( int[] tapIndexes, int gain )
|
||||
{
|
||||
mIndexes = tapIndexes;
|
||||
mGain = gain;
|
||||
}
|
||||
|
||||
public float filter()
|
||||
{
|
||||
double accumulator = 0;
|
||||
|
||||
for( int x = 0; x < mIndexes.length; x++ )
|
||||
{
|
||||
accumulator += mTaps[ mIndexes[ x ] ] *
|
||||
mBuffer[ ( x + mBufferPointer ) % mIndexes.length ];
|
||||
}
|
||||
|
||||
return (float)( accumulator * mGain );
|
||||
}
|
||||
}
|
||||
|
||||
// public static void main( String[] args )
|
||||
// {
|
||||
// int interpolation = 6;
|
||||
//
|
||||
// PolyphaseFIRInterpolatingFilter p =
|
||||
// new PolyphaseFIRInterpolatingFilter( IMBESynthesizer.INTERPOLATION_TAPS, interpolation );
|
||||
//
|
||||
// Oscillator o = new Oscillator( 500, 8000 );
|
||||
//
|
||||
// int iterations = 40;
|
||||
//
|
||||
// float[] in = new float[ iterations ];
|
||||
// float[] out = new float[ iterations * interpolation ];
|
||||
//
|
||||
// int inIndex = 0;
|
||||
// int outIndex = 0;
|
||||
//
|
||||
// for( int x = 0; x < iterations; x++ )
|
||||
// {
|
||||
// float next = o.nextFloat();
|
||||
//
|
||||
// in[ inIndex++ ] = next;
|
||||
//
|
||||
// float[] samples = p.interpolate( next );
|
||||
//
|
||||
// System.arraycopy( samples, 0, out, outIndex, samples.length );
|
||||
//
|
||||
// outIndex += interpolation;
|
||||
// }
|
||||
//
|
||||
// mLog.debug( "In: " + Arrays.toString( in ) );
|
||||
// mLog.debug( "Out: " + Arrays.toString( out ) );
|
||||
// }
|
||||
}
|
||||
@@ -2,32 +2,32 @@ package io.github.dsheirer.dsp.gain;
|
||||
|
||||
import io.github.dsheirer.buffer.DoubleCircularBuffer;
|
||||
import io.github.dsheirer.buffer.RealCircularBuffer;
|
||||
|
||||
import java.util.concurrent.atomic.AtomicBoolean;
|
||||
import io.github.dsheirer.sample.buffer.ReusableBuffer;
|
||||
import io.github.dsheirer.sample.buffer.ReusableBufferQueue;
|
||||
|
||||
/*******************************************************************************
|
||||
* SDR Trunk
|
||||
* Copyright (C) 2014 Dennis Sheirer
|
||||
* Copyright (C) 2014 - 2018 Dennis Sheirer
|
||||
* Copyright (C) 2011 Alex Csete
|
||||
* Copyright (C) 2010 Moe Wheatley
|
||||
*
|
||||
*
|
||||
* This program is free software: you can redistribute it and/or modify
|
||||
* it under the terms of the GNU General Public License as published by
|
||||
* the Free Software Foundation, either version 3 of the License, or
|
||||
* (at your option) any later version.
|
||||
*
|
||||
*
|
||||
* This program is distributed in the hope that it will be useful,
|
||||
* but WITHOUT ANY WARRANTY; without even the implied warranty of
|
||||
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
|
||||
* GNU General Public License for more details.
|
||||
*
|
||||
*
|
||||
* You should have received a copy of the GNU General Public License
|
||||
* along with this program. If not, see <http://www.gnu.org/licenses/>
|
||||
*
|
||||
*
|
||||
* ---------------------------------------------------
|
||||
* Ported from Alex Csete's gqrx source code at:
|
||||
* https://github.com/csete/gqrx/blob/74ba2742c89b0dfb66854fc9f72d19e5c6e355b6/dsp/agc_impl.cpp
|
||||
*
|
||||
*
|
||||
* This Software is released under the "Simplified BSD License" + + +
|
||||
* Copyright 2010 Moe Wheatley. All rights reserved.
|
||||
*
|
||||
@@ -60,165 +60,189 @@ import java.util.concurrent.atomic.AtomicBoolean;
|
||||
******************************************************************************/
|
||||
public class AutomaticGainControl
|
||||
{
|
||||
private static final double SAMPLE_RATE = 48000;
|
||||
|
||||
/* Signal delay line - time delay in seconds */
|
||||
private static final double SAMPLE_RATE = 48000;
|
||||
|
||||
/* Signal delay line - time delay in seconds */
|
||||
// private static final double DELAY_TIME_CONSTANT = 0.015;
|
||||
private static final double DELAY_TIME_CONSTANT = 0.01;
|
||||
private static final double DELAY_TIME_CONSTANT = 0.01;
|
||||
|
||||
/* Peak detector window - time delay in seconds */
|
||||
private static final double WINDOW_TIME_CONSTANT = 0.018;
|
||||
/* Peak detector window - time delay in seconds */
|
||||
private static final double WINDOW_TIME_CONSTANT = 0.018;
|
||||
|
||||
/* Attack time constants in seconds */
|
||||
private static final double ATTACK_RISE_TIME_CONSTANT = 0.002;
|
||||
private static final double ATTACK_FALL_TIME_CONSTANT = 0.005;
|
||||
/* Attack time constants in seconds */
|
||||
private static final double ATTACK_RISE_TIME_CONSTANT = 0.002;
|
||||
private static final double ATTACK_FALL_TIME_CONSTANT = 0.005;
|
||||
|
||||
private static final double ATTACK_RISE_ALPHA = 1.0 -
|
||||
Math.exp( -1.0 / SAMPLE_RATE * ATTACK_RISE_TIME_CONSTANT );
|
||||
private static final double ATTACK_RISE_ALPHA = 1.0 -
|
||||
Math.exp(-1.0 / SAMPLE_RATE * ATTACK_RISE_TIME_CONSTANT);
|
||||
|
||||
private static final double ATTACK_FALL_ALPHA = 1.0 -
|
||||
Math.exp( -1.0 / SAMPLE_RATE * ATTACK_FALL_TIME_CONSTANT );
|
||||
private static final double ATTACK_FALL_ALPHA = 1.0 -
|
||||
Math.exp(-1.0 / SAMPLE_RATE * ATTACK_FALL_TIME_CONSTANT);
|
||||
|
||||
/* AGC decay value in milliseconds (20 to 5000) */
|
||||
private static final double DECAY = 200;
|
||||
|
||||
/* Ratio between rise and fall times of decay time constants - adjust for
|
||||
* best action with SSB */
|
||||
private static final double DECAY_RISEFALL_RATIO = 0.3;
|
||||
/* AGC decay value in milliseconds (20 to 5000) */
|
||||
private static final double DECAY = 200;
|
||||
|
||||
private static final double DECAY_RISE_ALPHA = 1.0 -
|
||||
Math.exp( -1.0 / ( SAMPLE_RATE * DECAY * .001 * DECAY_RISEFALL_RATIO ) );
|
||||
/* Ratio between rise and fall times of decay time constants - adjust for
|
||||
* best action with SSB */
|
||||
private static final double DECAY_RISEFALL_RATIO = 0.3;
|
||||
|
||||
private static final double DECAY_FALL_ALPHA = 1.0 -
|
||||
Math.exp( -1.0 / ( SAMPLE_RATE * DECAY * .001 ) );
|
||||
private static final double DECAY_RISE_ALPHA = 1.0 -
|
||||
Math.exp(-1.0 / (SAMPLE_RATE * DECAY * .001 * DECAY_RISEFALL_RATIO));
|
||||
|
||||
/* Hang timer release decay time constant in seconds */
|
||||
@SuppressWarnings( "unused" )
|
||||
private static final double RELEASE_TIME_CONSTANT = 0.05;
|
||||
private static final double DECAY_FALL_ALPHA = 1.0 -
|
||||
Math.exp(-1.0 / (SAMPLE_RATE * DECAY * .001));
|
||||
|
||||
/* Specifies the AGC Knee in dB if AGC is active (nominal range -160 to 0 dB) */
|
||||
private static final double THRESHOLD = -100;
|
||||
/* Hang timer release decay time constant in seconds */
|
||||
@SuppressWarnings("unused")
|
||||
private static final double RELEASE_TIME_CONSTANT = 0.05;
|
||||
|
||||
/* Limit output to about 3db of maximum */
|
||||
private static final double AGC_OUT_SCALE = 0.7;
|
||||
/* Specifies the AGC Knee in dB if AGC is active (nominal range -160 to 0 dB) */
|
||||
private static final double THRESHOLD = -100;
|
||||
|
||||
/* Keep max input and output the same */
|
||||
private static final double MAX_AMPLITUDE = 32767.0; //1.0;
|
||||
private static final double MAX_MANUAL_AMPLITUDE = 32767.0; //1.0;
|
||||
|
||||
/* Specifies AGC manual gain in dB if AGC is not active ( 0 to 100 dB) */
|
||||
private static final double MANUAL_GAIN = 0.0;
|
||||
/* Limit output to about 3db of maximum */
|
||||
private static final double AGC_OUT_SCALE = 0.7;
|
||||
|
||||
private static final double MANUAL_AGC_GAIN = MAX_MANUAL_AMPLITUDE *
|
||||
Math.pow( 10.0, MANUAL_GAIN / 20.0 );
|
||||
/* Keep max input and output the same */
|
||||
private static final double MAX_AMPLITUDE = 32767.0; //1.0;
|
||||
private static final double MAX_MANUAL_AMPLITUDE = 32767.0; //1.0;
|
||||
|
||||
/* Specifies dB reduction in output at knee from max output level (0 - 10dB) */
|
||||
private static final double SLOPE_FACTOR = 2.0;
|
||||
/* Specifies AGC manual gain in dB if AGC is not active ( 0 to 100 dB) */
|
||||
private static final double MANUAL_GAIN = 0.0;
|
||||
|
||||
private static final double KNEE = THRESHOLD / 20.0;
|
||||
private static final double MANUAL_AGC_GAIN = MAX_MANUAL_AMPLITUDE *
|
||||
Math.pow(10.0, MANUAL_GAIN / 20.0);
|
||||
|
||||
private static final double GAIN_SLOPE = SLOPE_FACTOR / 100.0;
|
||||
/* Specifies dB reduction in output at knee from max output level (0 - 10dB) */
|
||||
private static final double SLOPE_FACTOR = 2.0;
|
||||
|
||||
private static final double FIXED_GAIN = AGC_OUT_SCALE *
|
||||
Math.pow( 10.0, KNEE * ( GAIN_SLOPE - 1.0 ) );
|
||||
|
||||
/* Constant for calc log() so that a value of 0 magnitude = -8 */
|
||||
private static final double MIN_CONSTANT = 3.2767E-4;
|
||||
|
||||
private AtomicBoolean mAGCEnabled = new AtomicBoolean( true );
|
||||
|
||||
private double mPeakMagnitude = 0.0;
|
||||
private double mAttackAverage = 0.0;
|
||||
private double mDecayAverage = 0.0;
|
||||
|
||||
private RealCircularBuffer mDelayBuffer =
|
||||
new RealCircularBuffer( (int)( SAMPLE_RATE * DELAY_TIME_CONSTANT ) );
|
||||
private DoubleCircularBuffer mMagnitudeBuffer =
|
||||
new DoubleCircularBuffer( (int)( SAMPLE_RATE * WINDOW_TIME_CONSTANT ) );
|
||||
|
||||
public AutomaticGainControl()
|
||||
{
|
||||
}
|
||||
private static final double KNEE = THRESHOLD / 20.0;
|
||||
|
||||
public float process( float currentSample )
|
||||
private static final double GAIN_SLOPE = SLOPE_FACTOR / 100.0;
|
||||
|
||||
private static final double FIXED_GAIN = AGC_OUT_SCALE *
|
||||
Math.pow(10.0, KNEE * (GAIN_SLOPE - 1.0));
|
||||
|
||||
/* Constant for calc log() so that a value of 0 magnitude = -8 */
|
||||
private static final double MIN_CONSTANT = 3.2767E-4;
|
||||
|
||||
private boolean mAGCEnabled = true;
|
||||
|
||||
private double mPeakMagnitude = 0.0;
|
||||
private double mAttackAverage = 0.0;
|
||||
private double mDecayAverage = 0.0;
|
||||
|
||||
private RealCircularBuffer mDelayBuffer = new RealCircularBuffer((int)(SAMPLE_RATE * DELAY_TIME_CONSTANT));
|
||||
private DoubleCircularBuffer mMagnitudeBuffer = new DoubleCircularBuffer((int)(SAMPLE_RATE * WINDOW_TIME_CONSTANT));
|
||||
|
||||
private ReusableBufferQueue mReusableBufferQueue = new ReusableBufferQueue("AutomaticGainControl");
|
||||
|
||||
public AutomaticGainControl()
|
||||
{
|
||||
float delayedSample = mDelayBuffer.putAndGet( currentSample );
|
||||
|
||||
double gain = MANUAL_AGC_GAIN;
|
||||
|
||||
if( mAGCEnabled.get() )
|
||||
{
|
||||
double currentMagnitude = Math.log10( Math.abs( currentSample ) +
|
||||
MIN_CONSTANT ) - Math.log10( MAX_AMPLITUDE );
|
||||
|
||||
double delayedMagnitude = mMagnitudeBuffer.get( currentMagnitude );
|
||||
|
||||
if( currentMagnitude > mPeakMagnitude )
|
||||
{
|
||||
/* Use current magnitude as peak if it's larger */
|
||||
mPeakMagnitude = currentMagnitude;
|
||||
}
|
||||
else if( delayedMagnitude == mPeakMagnitude )
|
||||
{
|
||||
/* If delayed magnitude is the current peak, then find a new peak */
|
||||
mPeakMagnitude = mMagnitudeBuffer.max();
|
||||
}
|
||||
|
||||
/* Exponential decay mode */
|
||||
if( mPeakMagnitude > mAttackAverage )
|
||||
{
|
||||
mAttackAverage = ( ( 1.0 - ATTACK_RISE_ALPHA ) * mAttackAverage ) +
|
||||
( ATTACK_RISE_ALPHA * mPeakMagnitude );
|
||||
}
|
||||
else
|
||||
{
|
||||
mAttackAverage = ( ( 1.0 - ATTACK_FALL_ALPHA ) * mAttackAverage ) +
|
||||
( ATTACK_FALL_ALPHA * mPeakMagnitude );
|
||||
}
|
||||
|
||||
if( mPeakMagnitude > mDecayAverage )
|
||||
{
|
||||
mDecayAverage = ( ( 1.0 - DECAY_RISE_ALPHA ) * mDecayAverage ) +
|
||||
( DECAY_RISE_ALPHA * mPeakMagnitude );
|
||||
}
|
||||
else
|
||||
{
|
||||
mDecayAverage = ( ( 1.0 - DECAY_FALL_ALPHA ) * mDecayAverage ) +
|
||||
( DECAY_RISE_ALPHA * mPeakMagnitude );
|
||||
}
|
||||
|
||||
double magnitude = ( mAttackAverage > mDecayAverage ) ?
|
||||
mAttackAverage : mDecayAverage;
|
||||
|
||||
if( magnitude < KNEE )
|
||||
{
|
||||
gain = FIXED_GAIN;
|
||||
}
|
||||
else
|
||||
{
|
||||
gain = AGC_OUT_SCALE * Math.pow( 10.0,
|
||||
magnitude * ( GAIN_SLOPE - 1.0 ) );
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
return (float)( delayedSample * gain );
|
||||
}
|
||||
|
||||
/**
|
||||
* Enables or disables Automatic Gain Control (AGC).
|
||||
*/
|
||||
public void setAGCEnabled( boolean enabled )
|
||||
{
|
||||
mAGCEnabled.set( enabled );
|
||||
}
|
||||
|
||||
/**
|
||||
* Indicates if AGC is enabled
|
||||
* @return true=AGC, false=MANUAL GAIN
|
||||
*/
|
||||
public boolean isAGCEnabled()
|
||||
{
|
||||
return mAGCEnabled.get();
|
||||
}
|
||||
/**
|
||||
* Applies gain to the samples contained in the input buffer and returns a new buffer containing the
|
||||
* processed samples.
|
||||
*
|
||||
* @param input samples to process
|
||||
* @return output samples with gain applied
|
||||
*/
|
||||
public ReusableBuffer process(ReusableBuffer input)
|
||||
{
|
||||
ReusableBuffer output = mReusableBufferQueue.getBuffer(input.getSampleCount());
|
||||
|
||||
float[] inputSamples = input.getSamples();
|
||||
float[] outputSamples = output.getSamples();
|
||||
|
||||
for(int x = 0; x < inputSamples.length; x++)
|
||||
{
|
||||
outputSamples[x] = process(inputSamples[x]);
|
||||
}
|
||||
|
||||
input.decrementUserCount();
|
||||
output.incrementUserCount();
|
||||
|
||||
return output;
|
||||
}
|
||||
|
||||
/**
|
||||
* Processes the input sample and applies gain
|
||||
*
|
||||
* @param currentSample to process
|
||||
* @return gain applied sample
|
||||
*/
|
||||
public float process(float currentSample)
|
||||
{
|
||||
float delayedSample = mDelayBuffer.putAndGet(currentSample);
|
||||
|
||||
double gain = MANUAL_AGC_GAIN;
|
||||
|
||||
if(mAGCEnabled)
|
||||
{
|
||||
double currentMagnitude = Math.log10(Math.abs(currentSample) + MIN_CONSTANT) - Math.log10(MAX_AMPLITUDE);
|
||||
|
||||
double delayedMagnitude = mMagnitudeBuffer.get(currentMagnitude);
|
||||
|
||||
if(currentMagnitude > mPeakMagnitude)
|
||||
{
|
||||
/* Use current magnitude as peak if it's larger */
|
||||
mPeakMagnitude = currentMagnitude;
|
||||
}
|
||||
else if(delayedMagnitude == mPeakMagnitude)
|
||||
{
|
||||
/* If delayed magnitude is the current peak, then find a new peak */
|
||||
mPeakMagnitude = mMagnitudeBuffer.max();
|
||||
}
|
||||
|
||||
/* Exponential decay mode */
|
||||
if(mPeakMagnitude > mAttackAverage)
|
||||
{
|
||||
mAttackAverage = ((1.0 - ATTACK_RISE_ALPHA) * mAttackAverage) + (ATTACK_RISE_ALPHA * mPeakMagnitude);
|
||||
}
|
||||
else
|
||||
{
|
||||
mAttackAverage = ((1.0 - ATTACK_FALL_ALPHA) * mAttackAverage) + (ATTACK_FALL_ALPHA * mPeakMagnitude);
|
||||
}
|
||||
|
||||
if(mPeakMagnitude > mDecayAverage)
|
||||
{
|
||||
mDecayAverage = ((1.0 - DECAY_RISE_ALPHA) * mDecayAverage) + (DECAY_RISE_ALPHA * mPeakMagnitude);
|
||||
}
|
||||
else
|
||||
{
|
||||
mDecayAverage = ((1.0 - DECAY_FALL_ALPHA) * mDecayAverage) + (DECAY_RISE_ALPHA * mPeakMagnitude);
|
||||
}
|
||||
|
||||
double magnitude = (mAttackAverage > mDecayAverage) ? mAttackAverage : mDecayAverage;
|
||||
|
||||
if(magnitude < KNEE)
|
||||
{
|
||||
gain = FIXED_GAIN;
|
||||
}
|
||||
else
|
||||
{
|
||||
gain = AGC_OUT_SCALE * Math.pow(10.0, magnitude * (GAIN_SLOPE - 1.0));
|
||||
}
|
||||
}
|
||||
|
||||
return (float)(delayedSample * gain);
|
||||
}
|
||||
|
||||
/**
|
||||
* Enables or disables Automatic Gain Control (AGC).
|
||||
*/
|
||||
public void setAGCEnabled(boolean enabled)
|
||||
{
|
||||
mAGCEnabled = enabled;
|
||||
}
|
||||
|
||||
/**
|
||||
* Indicates if AGC is enabled
|
||||
*
|
||||
* @return true=AGC, false=MANUAL GAIN
|
||||
*/
|
||||
public boolean isAGCEnabled()
|
||||
{
|
||||
return mAGCEnabled;
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1,53 +0,0 @@
|
||||
package io.github.dsheirer.dsp.gain;
|
||||
|
||||
import io.github.dsheirer.sample.Listener;
|
||||
import io.github.dsheirer.sample.real.RealBuffer;
|
||||
|
||||
public class AutomaticGainControl_RB extends AutomaticGainControl
|
||||
implements Listener<RealBuffer>
|
||||
{
|
||||
private Listener<RealBuffer> mListener;
|
||||
|
||||
public AutomaticGainControl_RB()
|
||||
{
|
||||
}
|
||||
|
||||
public void dispose()
|
||||
{
|
||||
mListener = null;
|
||||
}
|
||||
|
||||
/**
|
||||
* Applies AGC to a buffer of real (float) samples
|
||||
*/
|
||||
public RealBuffer process( RealBuffer buffer )
|
||||
{
|
||||
float[] samples = buffer.getSamples();
|
||||
|
||||
for( int x = 0; x < samples.length; x++ )
|
||||
{
|
||||
samples[ x ] = process( samples[ x ] );
|
||||
}
|
||||
|
||||
return buffer;
|
||||
}
|
||||
|
||||
@Override
|
||||
public void receive( RealBuffer buffer )
|
||||
{
|
||||
if( mListener != null )
|
||||
{
|
||||
mListener.receive( process( buffer ) );
|
||||
}
|
||||
}
|
||||
|
||||
public void setListener( Listener<RealBuffer> listener )
|
||||
{
|
||||
mListener = listener;
|
||||
}
|
||||
|
||||
public void removeListener()
|
||||
{
|
||||
mListener = null;
|
||||
}
|
||||
}
|
||||
@@ -45,7 +45,6 @@ import io.github.dsheirer.module.decode.event.ICallEventProvider;
|
||||
import io.github.dsheirer.module.decode.event.MessageActivityModel;
|
||||
import io.github.dsheirer.module.log.EventLogger;
|
||||
import io.github.dsheirer.record.wave.ComplexBufferWaveRecorder;
|
||||
import io.github.dsheirer.record.wave.RealBufferWaveRecorder;
|
||||
import io.github.dsheirer.sample.Broadcaster;
|
||||
import io.github.dsheirer.sample.Listener;
|
||||
import io.github.dsheirer.sample.buffer.IReusableBufferListener;
|
||||
@@ -709,7 +708,7 @@ public class ProcessingChain implements IChannelEventListener
|
||||
|
||||
for(Module module : mModules)
|
||||
{
|
||||
if(module instanceof RealBufferWaveRecorder || module instanceof ComplexBufferWaveRecorder)
|
||||
if(module instanceof ComplexBufferWaveRecorder)
|
||||
{
|
||||
recordingModules.add(module);
|
||||
}
|
||||
|
||||
@@ -87,7 +87,6 @@ import io.github.dsheirer.module.decode.passport.PassportMessageFilter;
|
||||
import io.github.dsheirer.module.decode.tait.Tait1200Decoder;
|
||||
import io.github.dsheirer.module.decode.tait.Tait1200DecoderState;
|
||||
import io.github.dsheirer.module.demodulate.am.AMDemodulatorModule;
|
||||
import io.github.dsheirer.module.demodulate.audio.DemodulatedAudioFilterModule;
|
||||
import io.github.dsheirer.module.demodulate.fm.FMDemodulatorModule;
|
||||
import io.github.dsheirer.source.SourceType;
|
||||
import org.slf4j.Logger;
|
||||
@@ -100,7 +99,8 @@ public class DecoderFactory
|
||||
{
|
||||
private final static Logger mLog = LoggerFactory.getLogger(DecoderFactory.class);
|
||||
|
||||
private static final double CHANNEL_BANDWIDTH = 12500.0;
|
||||
private static final double AM_CHANNEL_BANDWIDTH = 3000.0;
|
||||
private static final double FM_CHANNEL_BANDWIDTH = 12500.0;
|
||||
private static final double DEMODULATED_AUDIO_SAMPLE_RATE = 8000.0;
|
||||
|
||||
/**
|
||||
@@ -148,16 +148,13 @@ public class DecoderFactory
|
||||
switch(decodeConfig.getDecoderType())
|
||||
{
|
||||
case AM:
|
||||
if(channel.getSourceConfiguration().getSourceType() == SourceType.TUNER)
|
||||
{
|
||||
modules.add(new AMDemodulatorModule());
|
||||
//TODO: update AM demodulator to produce demodulated 8 kHz audio and delete the following filter module
|
||||
modules.add(new DemodulatedAudioFilterModule(4000, 6000));
|
||||
}
|
||||
|
||||
modules.add(new AMDecoder(decodeConfig));
|
||||
modules.add(new AlwaysUnsquelchedDecoderState(DecoderType.AM, channel.getName()));
|
||||
modules.add(new AudioModule(metadata));
|
||||
if(channel.getSourceConfiguration().getSourceType() == SourceType.TUNER)
|
||||
{
|
||||
modules.add(new AMDemodulatorModule(AM_CHANNEL_BANDWIDTH, DEMODULATED_AUDIO_SAMPLE_RATE));
|
||||
}
|
||||
break;
|
||||
case NBFM:
|
||||
modules.add(new NBFMDecoder(decodeConfig));
|
||||
@@ -165,7 +162,7 @@ public class DecoderFactory
|
||||
modules.add(new AudioModule(metadata));
|
||||
if(channel.getSourceConfiguration().getSourceType() == SourceType.TUNER)
|
||||
{
|
||||
modules.add(new FMDemodulatorModule(CHANNEL_BANDWIDTH, DEMODULATED_AUDIO_SAMPLE_RATE));
|
||||
modules.add(new FMDemodulatorModule(FM_CHANNEL_BANDWIDTH, DEMODULATED_AUDIO_SAMPLE_RATE));
|
||||
}
|
||||
break;
|
||||
case LTR_STANDARD:
|
||||
@@ -175,7 +172,7 @@ public class DecoderFactory
|
||||
modules.add(new AudioModule(metadata));
|
||||
if(channel.getSourceConfiguration().getSourceType() == SourceType.TUNER)
|
||||
{
|
||||
modules.add(new FMDemodulatorModule(CHANNEL_BANDWIDTH, DEMODULATED_AUDIO_SAMPLE_RATE));
|
||||
modules.add(new FMDemodulatorModule(FM_CHANNEL_BANDWIDTH, DEMODULATED_AUDIO_SAMPLE_RATE));
|
||||
}
|
||||
break;
|
||||
case LTR_NET:
|
||||
@@ -184,7 +181,7 @@ public class DecoderFactory
|
||||
modules.add(new AudioModule(metadata));
|
||||
if(channel.getSourceConfiguration().getSourceType() == SourceType.TUNER)
|
||||
{
|
||||
modules.add(new FMDemodulatorModule(CHANNEL_BANDWIDTH, DEMODULATED_AUDIO_SAMPLE_RATE));
|
||||
modules.add(new FMDemodulatorModule(FM_CHANNEL_BANDWIDTH, DEMODULATED_AUDIO_SAMPLE_RATE));
|
||||
}
|
||||
break;
|
||||
case MPT1327:
|
||||
@@ -196,7 +193,7 @@ public class DecoderFactory
|
||||
modules.add(new AudioModule(metadata));
|
||||
if(channel.getSourceConfiguration().getSourceType() == SourceType.TUNER)
|
||||
{
|
||||
modules.add(new FMDemodulatorModule(CHANNEL_BANDWIDTH, DEMODULATED_AUDIO_SAMPLE_RATE));
|
||||
modules.add(new FMDemodulatorModule(FM_CHANNEL_BANDWIDTH, DEMODULATED_AUDIO_SAMPLE_RATE));
|
||||
}
|
||||
|
||||
if(channelType == ChannelType.STANDARD)
|
||||
@@ -212,7 +209,7 @@ public class DecoderFactory
|
||||
modules.add(new AudioModule(metadata));
|
||||
if(channel.getSourceConfiguration().getSourceType() == SourceType.TUNER)
|
||||
{
|
||||
modules.add(new FMDemodulatorModule(CHANNEL_BANDWIDTH, DEMODULATED_AUDIO_SAMPLE_RATE));
|
||||
modules.add(new FMDemodulatorModule(FM_CHANNEL_BANDWIDTH, DEMODULATED_AUDIO_SAMPLE_RATE));
|
||||
}
|
||||
break;
|
||||
case P25_PHASE1:
|
||||
|
||||
@@ -15,68 +15,144 @@
|
||||
******************************************************************************/
|
||||
package io.github.dsheirer.module.demodulate.am;
|
||||
|
||||
import io.github.dsheirer.dsp.am.AMDemodulator_CB;
|
||||
import io.github.dsheirer.dsp.am.AMDemodulator;
|
||||
import io.github.dsheirer.dsp.filter.FilterFactory;
|
||||
import io.github.dsheirer.dsp.filter.Window;
|
||||
import io.github.dsheirer.dsp.filter.Window.WindowType;
|
||||
import io.github.dsheirer.dsp.filter.fir.complex.ComplexFIRFilter_CB_CB;
|
||||
import io.github.dsheirer.dsp.filter.fir.real.RealFIRFilter_RB_RB;
|
||||
import io.github.dsheirer.dsp.gain.AutomaticGainControl_RB;
|
||||
import io.github.dsheirer.dsp.filter.design.FilterDesignException;
|
||||
import io.github.dsheirer.dsp.filter.fir.FIRFilterSpecification;
|
||||
import io.github.dsheirer.dsp.filter.fir.complex.ComplexFIRFilter2;
|
||||
import io.github.dsheirer.dsp.filter.fir.real.RealFIRFilter2;
|
||||
import io.github.dsheirer.dsp.filter.resample.RealResampler;
|
||||
import io.github.dsheirer.dsp.gain.AutomaticGainControl;
|
||||
import io.github.dsheirer.module.Module;
|
||||
import io.github.dsheirer.sample.Listener;
|
||||
import io.github.dsheirer.sample.buffer.IReusableBufferProvider;
|
||||
import io.github.dsheirer.sample.buffer.IReusableComplexBufferListener;
|
||||
import io.github.dsheirer.sample.buffer.ReusableBuffer;
|
||||
import io.github.dsheirer.sample.buffer.ReusableComplexBuffer;
|
||||
import io.github.dsheirer.sample.complex.ComplexBuffer;
|
||||
import io.github.dsheirer.sample.real.IFilteredRealBufferProvider;
|
||||
import io.github.dsheirer.sample.real.RealBuffer;
|
||||
import io.github.dsheirer.source.ISourceEventListener;
|
||||
import io.github.dsheirer.source.SourceEvent;
|
||||
import org.slf4j.Logger;
|
||||
import org.slf4j.LoggerFactory;
|
||||
|
||||
public class AMDemodulatorModule extends Module implements IReusableComplexBufferListener, IFilteredRealBufferProvider
|
||||
/**
|
||||
* AM Demodulator Module for demodulating complex baseband channel sample buffers and producing demodulated audio
|
||||
* at a specified output sample rate, normally 8 kHz.
|
||||
*
|
||||
* This module requires a sample rate SourceEvent prior to processing baseband sample buffers in order to configure
|
||||
* internal filters and the resampler.
|
||||
*/
|
||||
public class AMDemodulatorModule extends Module implements ISourceEventListener, IReusableComplexBufferListener,
|
||||
IReusableBufferProvider, Listener<ReusableComplexBuffer>
|
||||
{
|
||||
private static final int SAMPLE_RATE = 48000;
|
||||
|
||||
private ComplexFIRFilter_CB_CB mIQFilter;
|
||||
private AMDemodulator_CB mDemodulator;
|
||||
private RealFIRFilter_RB_RB mLowPassFilter;
|
||||
private AutomaticGainControl_RB mAGC;
|
||||
private ReusableBufferListener mReusableBufferListener = new ReusableBufferListener();
|
||||
private final static Logger mLog = LoggerFactory.getLogger(AMDemodulatorModule.class);
|
||||
private static float[] mLowPassFilterTaps;
|
||||
private ComplexFIRFilter2 mIQFilter;
|
||||
private AMDemodulator mDemodulator;
|
||||
private RealFIRFilter2 mLowPassFilter;
|
||||
private AutomaticGainControl mAGC = new AutomaticGainControl();
|
||||
private double mChannelBandwidth;
|
||||
private double mOutputSampleRate;
|
||||
private RealResampler mResampler;
|
||||
private Listener<ReusableBuffer> mResampledReusableBufferListener;
|
||||
private SourceEventProcessor mSourceEventProcessor = new SourceEventProcessor();
|
||||
|
||||
/**
|
||||
* AM Demodulator Module
|
||||
* Constructs an AM demodulator module
|
||||
*
|
||||
* @param channelBandwidth to use in filtering the baseband input buffers
|
||||
* @param outputSampleRate specifies the resampled output sample rate for demodulated audio
|
||||
*/
|
||||
public AMDemodulatorModule()
|
||||
public AMDemodulatorModule(double channelBandwidth, double outputSampleRate)
|
||||
{
|
||||
mIQFilter = new ComplexFIRFilter_CB_CB(FilterFactory.getLowPass(
|
||||
SAMPLE_RATE, 5000, 73, WindowType.HAMMING), 1.0f);
|
||||
mChannelBandwidth = channelBandwidth;
|
||||
mOutputSampleRate = outputSampleRate;
|
||||
|
||||
mDemodulator = new AMDemodulator_CB(500.0f);
|
||||
mIQFilter.setListener(mDemodulator);
|
||||
mDemodulator = new AMDemodulator(500.0f);
|
||||
|
||||
mLowPassFilter = new RealFIRFilter_RB_RB(
|
||||
FilterFactory.getLowPass(48000, 3000, 31, WindowType.COSINE), 1.0f);
|
||||
mDemodulator.setListener(mLowPassFilter);
|
||||
if(mLowPassFilterTaps == null)
|
||||
{
|
||||
FIRFilterSpecification specification = FIRFilterSpecification.lowPassBuilder()
|
||||
.sampleRate(mOutputSampleRate)
|
||||
.gridDensity(16)
|
||||
.oddLength(true)
|
||||
.passBandCutoff(3000)
|
||||
.passBandAmplitude(1.0)
|
||||
.passBandRipple(0.01)
|
||||
.stopBandStart(3500)
|
||||
.stopBandAmplitude(0.0)
|
||||
.stopBandRipple(0.028)
|
||||
.build();
|
||||
|
||||
mAGC = new AutomaticGainControl_RB();
|
||||
mLowPassFilter.setListener(mAGC);
|
||||
try
|
||||
{
|
||||
mLowPassFilterTaps = FilterFactory.getTaps(specification);
|
||||
}
|
||||
catch(FilterDesignException fde)
|
||||
{
|
||||
mLog.error("Couldn't design AM demodulator remez filter for sample rate [" + mOutputSampleRate +
|
||||
"] pass frequency [3000] and stop frequency [3500] - using sinc filter");
|
||||
}
|
||||
|
||||
if(mLowPassFilterTaps == null)
|
||||
{
|
||||
mLowPassFilterTaps = FilterFactory.getLowPass(mOutputSampleRate, 3000, 3500,
|
||||
60, WindowType.HAMMING, true);
|
||||
}
|
||||
}
|
||||
|
||||
mLowPassFilter = new RealFIRFilter2(mLowPassFilterTaps);
|
||||
}
|
||||
|
||||
@Override
|
||||
public void receive(ReusableComplexBuffer basebandBuffer)
|
||||
{
|
||||
ReusableComplexBuffer filteredBuffer = mIQFilter.filter(basebandBuffer);
|
||||
ReusableBuffer demodulated = mDemodulator.demodulate(filteredBuffer);
|
||||
mResampler.resample(demodulated);
|
||||
}
|
||||
|
||||
/**
|
||||
* Processes resampled audio buffers produced by the resampler.
|
||||
* @param resampledAudio buffer to process.
|
||||
*/
|
||||
private void processResampledAudio(ReusableBuffer resampledAudio)
|
||||
{
|
||||
if(mResampledReusableBufferListener != null)
|
||||
{
|
||||
ReusableBuffer filteredAudio = mLowPassFilter.filter(resampledAudio);
|
||||
ReusableBuffer gainApplied = mAGC.process(filteredAudio);
|
||||
mResampledReusableBufferListener.receive(gainApplied);
|
||||
}
|
||||
else
|
||||
{
|
||||
resampledAudio.decrementUserCount();
|
||||
}
|
||||
}
|
||||
|
||||
@Override
|
||||
public Listener<ReusableComplexBuffer> getReusableComplexBufferListener()
|
||||
{
|
||||
return mReusableBufferListener;
|
||||
return this;
|
||||
}
|
||||
|
||||
@Override
|
||||
public void dispose()
|
||||
{
|
||||
mIQFilter.dispose();
|
||||
mIQFilter = null;
|
||||
if(mIQFilter != null)
|
||||
{
|
||||
mIQFilter.dispose();
|
||||
mIQFilter = null;
|
||||
}
|
||||
|
||||
if(mLowPassFilter != null)
|
||||
{
|
||||
mLowPassFilter.dispose();
|
||||
mLowPassFilter = null;
|
||||
}
|
||||
|
||||
mDemodulator.dispose();
|
||||
mDemodulator = null;
|
||||
|
||||
mLowPassFilter.dispose();
|
||||
mLowPassFilter = null;
|
||||
|
||||
mAGC.dispose();
|
||||
mAGC = null;
|
||||
}
|
||||
|
||||
@@ -85,18 +161,6 @@ public class AMDemodulatorModule extends Module implements IReusableComplexBuffe
|
||||
{
|
||||
}
|
||||
|
||||
@Override
|
||||
public void setFilteredRealBufferListener(Listener<RealBuffer> listener)
|
||||
{
|
||||
mAGC.setListener(listener);
|
||||
}
|
||||
|
||||
@Override
|
||||
public void removeFilteredRealBufferListener()
|
||||
{
|
||||
mAGC.removeListener();
|
||||
}
|
||||
|
||||
@Override
|
||||
public void start()
|
||||
{
|
||||
@@ -107,18 +171,97 @@ public class AMDemodulatorModule extends Module implements IReusableComplexBuffe
|
||||
{
|
||||
}
|
||||
|
||||
@Override
|
||||
public void setBufferListener(Listener<ReusableBuffer> listener)
|
||||
{
|
||||
mResampledReusableBufferListener = listener;
|
||||
}
|
||||
|
||||
public class ReusableBufferListener implements Listener<ReusableComplexBuffer>
|
||||
@Override
|
||||
public void removeBufferListener()
|
||||
{
|
||||
mResampledReusableBufferListener = null;
|
||||
}
|
||||
|
||||
@Override
|
||||
public Listener<SourceEvent> getSourceEventListener()
|
||||
{
|
||||
return mSourceEventProcessor;
|
||||
}
|
||||
|
||||
/**
|
||||
* Monitors sample rate change source event(s) to setup the initial I/Q filter and resampler
|
||||
*/
|
||||
public class SourceEventProcessor implements Listener<SourceEvent>
|
||||
{
|
||||
@Override
|
||||
public void receive(ReusableComplexBuffer reusableComplexBuffer)
|
||||
public void receive(SourceEvent sourceEvent)
|
||||
{
|
||||
float[] samples = reusableComplexBuffer.getSamplesCopy();
|
||||
if(sourceEvent.getEvent() == SourceEvent.Event.NOTIFICATION_SAMPLE_RATE_CHANGE)
|
||||
{
|
||||
if(mIQFilter != null)
|
||||
{
|
||||
mIQFilter.dispose();
|
||||
mIQFilter = null;
|
||||
}
|
||||
|
||||
//TODO: redesign the filter chain so that we can simply pass a float array ...
|
||||
double sampleRate = sourceEvent.getValue().doubleValue();
|
||||
|
||||
mIQFilter.receive(new ComplexBuffer(samples));
|
||||
reusableComplexBuffer.decrementUserCount();
|
||||
if((sampleRate < (2.0 * mChannelBandwidth)))
|
||||
{
|
||||
throw new IllegalStateException("AM Demodulator with channel bandwidth [" + mChannelBandwidth +
|
||||
"] requires a channel sample rate of [" + (2.0 * mChannelBandwidth + "] - sample rate of [" +
|
||||
sampleRate + "] is not supported"));
|
||||
}
|
||||
|
||||
double cutoff = sampleRate / 4.0;
|
||||
int passBandStop = (int)cutoff - 500;
|
||||
int stopBandStart = (int)cutoff + 500;
|
||||
|
||||
float[] filterTaps = null;
|
||||
|
||||
FIRFilterSpecification specification = FIRFilterSpecification.lowPassBuilder()
|
||||
.sampleRate(sampleRate)
|
||||
.gridDensity(16)
|
||||
.oddLength(true)
|
||||
.passBandCutoff(passBandStop)
|
||||
.passBandAmplitude(1.0)
|
||||
.passBandRipple(0.01)
|
||||
.stopBandStart(stopBandStart)
|
||||
.stopBandAmplitude(0.0)
|
||||
.stopBandRipple(0.028) //Approximately 60 dB attenuation
|
||||
.build();
|
||||
|
||||
try
|
||||
{
|
||||
filterTaps = FilterFactory.getTaps(specification);
|
||||
}
|
||||
catch(FilterDesignException fde)
|
||||
{
|
||||
mLog.error("Couldn't design FM demodulator remez filter for sample rate [" + sampleRate +
|
||||
"] pass frequency [" + passBandStop + "] and stop frequency [" + stopBandStart +
|
||||
"] - using sinc filter");
|
||||
}
|
||||
|
||||
if(filterTaps == null)
|
||||
{
|
||||
filterTaps = FilterFactory.getLowPass(sampleRate, passBandStop, stopBandStart, 60,
|
||||
Window.WindowType.HAMMING, true);
|
||||
}
|
||||
|
||||
mIQFilter = new ComplexFIRFilter2(filterTaps);
|
||||
|
||||
mResampler = new RealResampler(sampleRate, mOutputSampleRate, 2000, 1000);
|
||||
|
||||
mResampler.setListener(new Listener<ReusableBuffer>()
|
||||
{
|
||||
@Override
|
||||
public void receive(ReusableBuffer resampledAudioBuffer)
|
||||
{
|
||||
processResampledAudio(resampledAudioBuffer);
|
||||
}
|
||||
});
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1,120 +0,0 @@
|
||||
/*******************************************************************************
|
||||
* sdrtrunk
|
||||
* Copyright (C) 2014-2017 Dennis Sheirer
|
||||
*
|
||||
* This program is free software: you can redistribute it and/or modify
|
||||
* it under the terms of the GNU General Public License as published by
|
||||
* the Free Software Foundation, either version 3 of the License, or
|
||||
* (at your option) any later version.
|
||||
*
|
||||
* This program is distributed in the hope that it will be useful,
|
||||
* but WITHOUT ANY WARRANTY; without even the implied warranty of
|
||||
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
|
||||
* GNU General Public License for more details.
|
||||
*
|
||||
* You should have received a copy of the GNU General Public License
|
||||
* along with this program. If not, see <http://www.gnu.org/licenses/>
|
||||
*
|
||||
******************************************************************************/
|
||||
package io.github.dsheirer.module.demodulate.audio;
|
||||
|
||||
import io.github.dsheirer.dsp.filter.FilterFactory;
|
||||
import io.github.dsheirer.dsp.filter.Window.WindowType;
|
||||
import io.github.dsheirer.dsp.filter.dc.DCRemovalFilter_RealBuffer;
|
||||
import io.github.dsheirer.dsp.filter.fir.real.RealFIRFilter_RB_RB;
|
||||
import io.github.dsheirer.module.Module;
|
||||
import io.github.dsheirer.sample.Listener;
|
||||
import io.github.dsheirer.sample.real.IFilteredRealBufferProvider;
|
||||
import io.github.dsheirer.sample.real.IUnFilteredRealBufferListener;
|
||||
import io.github.dsheirer.sample.real.RealBuffer;
|
||||
import org.slf4j.Logger;
|
||||
import org.slf4j.LoggerFactory;
|
||||
|
||||
@Deprecated //Remove once polyphase channelizer enhancement is complete
|
||||
public class DemodulatedAudioFilterModule extends Module implements IUnFilteredRealBufferListener, IFilteredRealBufferProvider
|
||||
{
|
||||
private final static Logger mLog = LoggerFactory.getLogger(DemodulatedAudioFilterModule.class);
|
||||
|
||||
private DCRemovalFilter_RealBuffer mDCFilter;
|
||||
private RealFIRFilter_RB_RB mBandPassFilter;
|
||||
|
||||
/**
|
||||
* Filters demodulated audio and removes any DC component from the buffer
|
||||
*
|
||||
* @param pass - audio pass band pass frequency
|
||||
* @param stop - audio pass band stop frequency
|
||||
*/
|
||||
public DemodulatedAudioFilterModule(int pass, int stop)
|
||||
{
|
||||
assert (stop > pass);
|
||||
|
||||
mDCFilter = new DCRemovalFilter_RealBuffer();
|
||||
|
||||
//TODO: change this to a band pass filter
|
||||
mBandPassFilter = new RealFIRFilter_RB_RB(FilterFactory.getLowPass(48000, pass, stop, 60,
|
||||
WindowType.HANN, true), 1.0f);
|
||||
|
||||
mDCFilter.setListener(mBandPassFilter);
|
||||
}
|
||||
|
||||
/**
|
||||
* Constructs a demodulated audio filter using the filter taps and gain provided
|
||||
*/
|
||||
public DemodulatedAudioFilterModule(float[] taps, float gain)
|
||||
{
|
||||
mBandPassFilter = new RealFIRFilter_RB_RB(taps, gain);
|
||||
}
|
||||
|
||||
@Override
|
||||
public void dispose()
|
||||
{
|
||||
mBandPassFilter.dispose();
|
||||
mBandPassFilter = null;
|
||||
|
||||
if(mDCFilter != null)
|
||||
{
|
||||
mDCFilter.dispose();
|
||||
mDCFilter = null;
|
||||
}
|
||||
}
|
||||
|
||||
@Override
|
||||
public void reset()
|
||||
{
|
||||
}
|
||||
|
||||
@Override
|
||||
public void setFilteredRealBufferListener(Listener<RealBuffer> listener)
|
||||
{
|
||||
mBandPassFilter.setListener(listener);
|
||||
}
|
||||
|
||||
@Override
|
||||
public void removeFilteredRealBufferListener()
|
||||
{
|
||||
mBandPassFilter.removeListener();
|
||||
}
|
||||
|
||||
@Override
|
||||
public void start()
|
||||
{
|
||||
}
|
||||
|
||||
@Override
|
||||
public void stop()
|
||||
{
|
||||
}
|
||||
|
||||
@Override
|
||||
public Listener<RealBuffer> getUnFilteredRealBufferListener()
|
||||
{
|
||||
if(mDCFilter != null)
|
||||
{
|
||||
return mDCFilter;
|
||||
}
|
||||
else
|
||||
{
|
||||
return mBandPassFilter;
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -1,258 +0,0 @@
|
||||
/*******************************************************************************
|
||||
* sdrtrunk
|
||||
* Copyright (C) 2014-2016 Dennis Sheirer
|
||||
*
|
||||
* This program is free software: you can redistribute it and/or modify
|
||||
* it under the terms of the GNU General Public License as published by
|
||||
* the Free Software Foundation, either version 3 of the License, or
|
||||
* (at your option) any later version.
|
||||
*
|
||||
* This program is distributed in the hope that it will be useful,
|
||||
* but WITHOUT ANY WARRANTY; without even the implied warranty of
|
||||
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
|
||||
* GNU General Public License for more details.
|
||||
*
|
||||
* You should have received a copy of the GNU General Public License
|
||||
* along with this program. If not, see <http://www.gnu.org/licenses/>
|
||||
*
|
||||
******************************************************************************/
|
||||
package io.github.dsheirer.record.wave;
|
||||
|
||||
import io.github.dsheirer.sample.ConversionUtils;
|
||||
import io.github.dsheirer.sample.Listener;
|
||||
import io.github.dsheirer.sample.real.RealBuffer;
|
||||
import org.apache.commons.lang3.Validate;
|
||||
import org.slf4j.Logger;
|
||||
import org.slf4j.LoggerFactory;
|
||||
|
||||
import java.io.IOException;
|
||||
import java.io.InputStream;
|
||||
import java.nio.ByteBuffer;
|
||||
import java.nio.ByteOrder;
|
||||
import java.nio.ShortBuffer;
|
||||
import java.nio.file.Files;
|
||||
import java.nio.file.Path;
|
||||
import java.nio.file.Paths;
|
||||
import java.nio.file.StandardOpenOption;
|
||||
import java.util.Arrays;
|
||||
import java.util.regex.Pattern;
|
||||
|
||||
public class MonoWaveReader implements AutoCloseable
|
||||
{
|
||||
private final static Logger mLog = LoggerFactory.getLogger( MonoWaveReader.class);
|
||||
|
||||
private static final Pattern FILENAME_PATTERN = Pattern.compile( "(.*_)(\\d+)(\\.wav)" );
|
||||
|
||||
private Path mPath;
|
||||
private boolean mRealTime;
|
||||
private InputStream mInputStream;
|
||||
private int mDataByteSize = 0;
|
||||
private Listener<RealBuffer> mListener;
|
||||
private byte[] mDataBuffer = new byte[8000];
|
||||
|
||||
/**
|
||||
* Wave file reader for PCM 8kHz, 16-bit little-endian format.
|
||||
*
|
||||
* @param path of the wave file
|
||||
* @param realTime to force replay to real-time. Note: you should run this reader on a separate thread if you
|
||||
* choose real time since this reader will sleep the calling thread as needed to effect real-time playback.
|
||||
*/
|
||||
public MonoWaveReader(Path path, boolean realTime) throws IOException
|
||||
{
|
||||
Validate.isTrue(path != null);
|
||||
mPath = path;
|
||||
mRealTime = realTime;
|
||||
|
||||
open();
|
||||
}
|
||||
|
||||
/**
|
||||
* Opens the file
|
||||
*/
|
||||
private void open() throws IOException
|
||||
{
|
||||
if(!Files.exists(mPath))
|
||||
{
|
||||
throw new IOException("File not found");
|
||||
}
|
||||
|
||||
mInputStream = Files.newInputStream(mPath, StandardOpenOption.READ);
|
||||
|
||||
//Check for RIFF header
|
||||
byte[] buffer = new byte[4];
|
||||
mInputStream.read(buffer);
|
||||
|
||||
if(!Arrays.equals(buffer, WaveUtils.RIFF_CHUNK))
|
||||
{
|
||||
throw new IOException("File is not .wav format - missing RIFF chunk");
|
||||
}
|
||||
|
||||
//Get file size
|
||||
mInputStream.read(buffer);
|
||||
int fileSize = ByteBuffer.wrap(buffer).order(ByteOrder.LITTLE_ENDIAN).asIntBuffer().get();
|
||||
|
||||
//Check for WAVE format
|
||||
mInputStream.read(buffer);
|
||||
|
||||
if(!Arrays.equals(buffer, WaveUtils.WAV_FORMAT))
|
||||
{
|
||||
throw new IOException("File is not .wav format - missing WAVE format");
|
||||
}
|
||||
|
||||
//Check for format chunk
|
||||
mInputStream.read(buffer);
|
||||
|
||||
if(!Arrays.equals(buffer, WaveUtils.CHUNK_FORMAT))
|
||||
{
|
||||
throw new IOException("File is not .wav format - missing format chunk");
|
||||
}
|
||||
|
||||
//Get chunk size
|
||||
mInputStream.read(buffer);
|
||||
int chunkSize = ByteBuffer.wrap(buffer).order(ByteOrder.LITTLE_ENDIAN).asIntBuffer().get();
|
||||
|
||||
//Get format
|
||||
mInputStream.read(buffer);
|
||||
|
||||
ShortBuffer shortBuffer = ByteBuffer.wrap(buffer).order(ByteOrder.LITTLE_ENDIAN).asShortBuffer();
|
||||
short format = shortBuffer.get();
|
||||
if(format != WaveUtils.PCM_FORMAT)
|
||||
{
|
||||
throw new IOException("File format not supported - expecting PCM format");
|
||||
}
|
||||
|
||||
//Get number of channels
|
||||
short channels = shortBuffer.get();
|
||||
if(channels != 1)
|
||||
{
|
||||
throw new IOException("Unsupported channel count - mono audio only");
|
||||
}
|
||||
|
||||
//Get samples per second
|
||||
mInputStream.read(buffer);
|
||||
int sampleRate = ByteBuffer.wrap(buffer).order(ByteOrder.LITTLE_ENDIAN).asIntBuffer().get();
|
||||
|
||||
//Get bytes per second
|
||||
mInputStream.read(buffer);
|
||||
int bytesPerSecond = ByteBuffer.wrap(buffer).order(ByteOrder.LITTLE_ENDIAN).asIntBuffer().get();
|
||||
|
||||
mInputStream.read(buffer);
|
||||
|
||||
//Get frame size
|
||||
shortBuffer = ByteBuffer.wrap(buffer).order(ByteOrder.LITTLE_ENDIAN).asShortBuffer();
|
||||
short frameSize = shortBuffer.get();
|
||||
if(frameSize != 2)
|
||||
{
|
||||
throw new IOException("PCM frame size not supported - expecting 2 bytes per frame");
|
||||
}
|
||||
|
||||
//Get bits per sample
|
||||
short bitsPerSample = shortBuffer.get();
|
||||
if(bitsPerSample != 16)
|
||||
{
|
||||
throw new IOException("PCM sample size not supported - expecting 16 bits per sample");
|
||||
}
|
||||
|
||||
mInputStream.read(buffer);
|
||||
|
||||
if(!Arrays.equals(buffer, WaveUtils.CHUNK_DATA))
|
||||
{
|
||||
throw new IOException("Unexpected chunk - expecting data chunk");
|
||||
}
|
||||
|
||||
//Get data chunk size
|
||||
mInputStream.read(buffer);
|
||||
mDataByteSize = ByteBuffer.wrap(buffer).order(ByteOrder.LITTLE_ENDIAN).asIntBuffer().get();
|
||||
}
|
||||
|
||||
public void read() throws IOException
|
||||
{
|
||||
long start = System.currentTimeMillis();
|
||||
long samplesRead = 0;
|
||||
|
||||
int totalBytesRead = 0;
|
||||
|
||||
while(totalBytesRead < mDataByteSize)
|
||||
{
|
||||
int bytesRead = mInputStream.read(mDataBuffer);
|
||||
|
||||
totalBytesRead += bytesRead;
|
||||
|
||||
float[] samples = ConversionUtils.convertFromSigned16BitSamples(mDataBuffer);
|
||||
|
||||
if(bytesRead != mDataBuffer.length)
|
||||
{
|
||||
samples = Arrays.copyOf(samples, bytesRead / 2);
|
||||
}
|
||||
|
||||
if(mRealTime)
|
||||
{
|
||||
samplesRead += samples.length;
|
||||
|
||||
long actualElapsed = System.currentTimeMillis() - start;
|
||||
|
||||
long audioElapsed = (long)(((double)samplesRead / 8000.0) * 1000.0);
|
||||
|
||||
if(audioElapsed > actualElapsed)
|
||||
{
|
||||
try
|
||||
{
|
||||
Thread.sleep(audioElapsed - actualElapsed);
|
||||
}
|
||||
catch (InterruptedException e)
|
||||
{
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
if(mListener != null)
|
||||
{
|
||||
mListener.receive(new RealBuffer(samples));
|
||||
}
|
||||
}
|
||||
|
||||
close();
|
||||
}
|
||||
|
||||
public void setListener(Listener<RealBuffer> listener)
|
||||
{
|
||||
mListener = listener;
|
||||
}
|
||||
|
||||
/**
|
||||
* Closes the file
|
||||
*/
|
||||
public void close() throws IOException
|
||||
{
|
||||
if(mInputStream != null)
|
||||
{
|
||||
mInputStream.close();
|
||||
}
|
||||
}
|
||||
|
||||
public static void main(String[] args)
|
||||
{
|
||||
Path path = Paths.get("/home/denny/Music/PCM.wav");
|
||||
mLog.debug("Opening: " + path.toString());
|
||||
|
||||
try
|
||||
{
|
||||
MonoWaveReader reader = new MonoWaveReader(path, true);
|
||||
reader.setListener(new Listener<RealBuffer>()
|
||||
{
|
||||
@Override
|
||||
public void receive(RealBuffer realBuffer)
|
||||
{
|
||||
mLog.debug("Received buffer");
|
||||
}
|
||||
});
|
||||
reader.read();
|
||||
}
|
||||
catch(IOException e)
|
||||
{
|
||||
mLog.error("Error", e);
|
||||
}
|
||||
|
||||
mLog.debug("Finished");
|
||||
}
|
||||
}
|
||||
@@ -1,210 +0,0 @@
|
||||
/*******************************************************************************
|
||||
* sdrtrunk
|
||||
* Copyright (C) 2014-2017 Dennis Sheirer
|
||||
*
|
||||
* This program is free software: you can redistribute it and/or modify
|
||||
* it under the terms of the GNU General Public License as published by
|
||||
* the Free Software Foundation, either version 3 of the License, or
|
||||
* (at your option) any later version.
|
||||
*
|
||||
* This program is distributed in the hope that it will be useful,
|
||||
* but WITHOUT ANY WARRANTY; without even the implied warranty of
|
||||
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
|
||||
* GNU General Public License for more details.
|
||||
*
|
||||
* You should have received a copy of the GNU General Public License
|
||||
* along with this program. If not, see <http://www.gnu.org/licenses/>
|
||||
*
|
||||
******************************************************************************/
|
||||
package io.github.dsheirer.record.wave;
|
||||
|
||||
import io.github.dsheirer.module.Module;
|
||||
import io.github.dsheirer.sample.ConversionUtils;
|
||||
import io.github.dsheirer.sample.Listener;
|
||||
import io.github.dsheirer.sample.real.IFilteredRealBufferListener;
|
||||
import io.github.dsheirer.sample.real.RealBuffer;
|
||||
import io.github.dsheirer.util.ThreadPool;
|
||||
import io.github.dsheirer.util.TimeStamp;
|
||||
import org.slf4j.Logger;
|
||||
import org.slf4j.LoggerFactory;
|
||||
|
||||
import javax.sound.sampled.AudioFormat;
|
||||
import java.io.IOException;
|
||||
import java.nio.file.Path;
|
||||
import java.nio.file.Paths;
|
||||
import java.util.concurrent.LinkedBlockingQueue;
|
||||
import java.util.concurrent.ScheduledFuture;
|
||||
import java.util.concurrent.TimeUnit;
|
||||
import java.util.concurrent.atomic.AtomicBoolean;
|
||||
|
||||
/**
|
||||
* WAVE audio recorder module for recording real sample buffers to a wave file
|
||||
*/
|
||||
public class RealBufferWaveRecorder extends Module implements IFilteredRealBufferListener, Listener<RealBuffer>
|
||||
{
|
||||
private final static Logger mLog = LoggerFactory.getLogger(RealBufferWaveRecorder.class);
|
||||
|
||||
private WaveWriter mWriter;
|
||||
private String mFilePrefix;
|
||||
private Path mFile;
|
||||
private AudioFormat mAudioFormat;
|
||||
|
||||
private BufferProcessor mBufferProcessor;
|
||||
private ScheduledFuture<?> mProcessorHandle;
|
||||
private LinkedBlockingQueue<RealBuffer> mBuffers = new LinkedBlockingQueue<>(500);
|
||||
private long mLastBufferReceived;
|
||||
|
||||
private AtomicBoolean mRunning = new AtomicBoolean();
|
||||
|
||||
public RealBufferWaveRecorder(int sampleRate, String filePrefix)
|
||||
{
|
||||
mAudioFormat = new AudioFormat(sampleRate, //SampleRate
|
||||
16, //Sample Size
|
||||
1, //Channels
|
||||
true, //Signed
|
||||
false); //Little Endian
|
||||
|
||||
mFilePrefix = filePrefix;
|
||||
}
|
||||
|
||||
/**
|
||||
* Indicates if the recorder is currently running.
|
||||
*/
|
||||
public boolean isRunning()
|
||||
{
|
||||
return mRunning.get();
|
||||
}
|
||||
|
||||
/**
|
||||
* Timestamp of when the latest buffer was received by this recorder
|
||||
*/
|
||||
public long getLastBufferReceived()
|
||||
{
|
||||
return mLastBufferReceived;
|
||||
}
|
||||
|
||||
public Path getFile()
|
||||
{
|
||||
return mFile;
|
||||
}
|
||||
|
||||
public void start()
|
||||
{
|
||||
if(mRunning.compareAndSet(false, true))
|
||||
{
|
||||
if(mBufferProcessor == null)
|
||||
{
|
||||
mBufferProcessor = new BufferProcessor();
|
||||
}
|
||||
|
||||
try
|
||||
{
|
||||
StringBuilder sb = new StringBuilder();
|
||||
sb.append(mFilePrefix);
|
||||
sb.append("_");
|
||||
sb.append(TimeStamp.getLongTimeStamp("_"));
|
||||
sb.append(".wav");
|
||||
|
||||
mFile = Paths.get(sb.toString());
|
||||
|
||||
mWriter = new WaveWriter(mAudioFormat, mFile);
|
||||
|
||||
/* Schedule the processor to run every 500 milliseconds */
|
||||
mProcessorHandle = ThreadPool.SCHEDULED.scheduleAtFixedRate(mBufferProcessor, 0, 500, TimeUnit.MILLISECONDS);
|
||||
}
|
||||
catch(IOException io)
|
||||
{
|
||||
mLog.error("Error starting real buffer recorder", io);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
public void stop()
|
||||
{
|
||||
if(mRunning.compareAndSet(true, false))
|
||||
{
|
||||
try
|
||||
{
|
||||
write();
|
||||
|
||||
if(mWriter != null)
|
||||
{
|
||||
mWriter.close();
|
||||
mWriter = null;
|
||||
}
|
||||
}
|
||||
catch(IOException ioe)
|
||||
{
|
||||
mLog.error("Error writing final audio buffers to recording during shutdown", ioe);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@Override
|
||||
public void receive(RealBuffer buffer)
|
||||
{
|
||||
if(mRunning.get())
|
||||
{
|
||||
boolean success = mBuffers.offer(buffer);
|
||||
|
||||
if(!success)
|
||||
{
|
||||
mLog.error("recorder buffer overflow - purging [" + mFile.toFile().getAbsolutePath() + "]");
|
||||
mBuffers.clear();
|
||||
}
|
||||
|
||||
mLastBufferReceived = System.currentTimeMillis();
|
||||
}
|
||||
}
|
||||
|
||||
@Override
|
||||
public Listener<RealBuffer> getFilteredRealBufferListener()
|
||||
{
|
||||
return this;
|
||||
}
|
||||
|
||||
@Override
|
||||
public void dispose()
|
||||
{
|
||||
stop();
|
||||
}
|
||||
|
||||
@Override
|
||||
public void reset()
|
||||
{
|
||||
}
|
||||
|
||||
/**
|
||||
* Writes all audio currently in the queue to the file
|
||||
* @throws IOException if there are any errors writing the audio
|
||||
*/
|
||||
private void write() throws IOException
|
||||
{
|
||||
RealBuffer buffer = mBuffers.poll();
|
||||
|
||||
while(buffer != null)
|
||||
{
|
||||
mWriter.writeData(ConversionUtils.convertToSigned16BitSamples(buffer));
|
||||
buffer = mBuffers.poll();
|
||||
}
|
||||
}
|
||||
|
||||
public class BufferProcessor implements Runnable
|
||||
{
|
||||
public void run()
|
||||
{
|
||||
try
|
||||
{
|
||||
write();
|
||||
}
|
||||
catch(IOException ioe)
|
||||
{
|
||||
/* Stop this module if/when we get an IO exception */
|
||||
mBuffers.clear();
|
||||
stop();
|
||||
|
||||
mLog.error("IO Exception while trying to write to the wave writer", ioe);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -1,55 +0,0 @@
|
||||
package io.github.dsheirer.sample.real;
|
||||
|
||||
import io.github.dsheirer.sample.Listener;
|
||||
import org.slf4j.Logger;
|
||||
import org.slf4j.LoggerFactory;
|
||||
|
||||
public class RateCounter_RB implements Listener<RealBuffer>
|
||||
{
|
||||
private final static Logger mLog =
|
||||
LoggerFactory.getLogger( RateCounter_RB.class );
|
||||
|
||||
private Listener<RealBuffer> mListener;
|
||||
private long mCount;
|
||||
private long mStartTime = -1;
|
||||
|
||||
private String mName;
|
||||
|
||||
public RateCounter_RB()
|
||||
{
|
||||
mName = "UNNAMED";
|
||||
}
|
||||
|
||||
public RateCounter_RB( String name )
|
||||
{
|
||||
mName = name;
|
||||
}
|
||||
|
||||
@Override
|
||||
public void receive( RealBuffer buffer )
|
||||
{
|
||||
if( mStartTime == -1 )
|
||||
{
|
||||
mStartTime = System.currentTimeMillis();
|
||||
}
|
||||
|
||||
mCount += buffer.getSamples().length;
|
||||
|
||||
long elapsed = System.currentTimeMillis() - mStartTime;
|
||||
|
||||
if( elapsed != 0 )
|
||||
{
|
||||
mLog.debug( "[" + mName + "] Sample Rate: " + ( mCount / elapsed ) + " kHz" );
|
||||
}
|
||||
|
||||
if( mListener != null )
|
||||
{
|
||||
mListener.receive( buffer );
|
||||
}
|
||||
}
|
||||
|
||||
public void setListener( Listener<RealBuffer> listener )
|
||||
{
|
||||
mListener = listener;
|
||||
}
|
||||
}
|
||||
@@ -1,33 +0,0 @@
|
||||
package io.github.dsheirer.sample.real;
|
||||
|
||||
import io.github.dsheirer.sample.Listener;
|
||||
|
||||
/**
|
||||
* Real sample buffer to stream converter
|
||||
*/
|
||||
public class RealBufferToStreamConverter implements Listener<RealBuffer>
|
||||
{
|
||||
private RealSampleListener mListener;
|
||||
|
||||
@Override
|
||||
public void receive( RealBuffer buffer )
|
||||
{
|
||||
if( mListener != null )
|
||||
{
|
||||
for( float sample: buffer.getSamples() )
|
||||
{
|
||||
mListener.receive( sample );
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
public void dispose()
|
||||
{
|
||||
mListener = null;
|
||||
}
|
||||
|
||||
public void setListener( RealSampleListener listener )
|
||||
{
|
||||
mListener = listener;
|
||||
}
|
||||
}
|
||||
@@ -1,24 +1,21 @@
|
||||
/*******************************************************************************
|
||||
* sdrtrunk
|
||||
* Copyright (C) 2014-2017 Dennis Sheirer
|
||||
* sdr-trunk
|
||||
* Copyright (C) 2014-2018 Dennis Sheirer
|
||||
*
|
||||
* This program is free software: you can redistribute it and/or modify
|
||||
* it under the terms of the GNU General Public License as published by
|
||||
* the Free Software Foundation, either version 3 of the License, or
|
||||
* (at your option) any later version.
|
||||
* This program is free software: you can redistribute it and/or modify it under the terms of the GNU General Public
|
||||
* License as published by the Free Software Foundation, either version 3 of the License, or (at your option) any
|
||||
* later version.
|
||||
*
|
||||
* This program is distributed in the hope that it will be useful,
|
||||
* but WITHOUT ANY WARRANTY; without even the implied warranty of
|
||||
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
|
||||
* GNU General Public License for more details.
|
||||
* This program is distributed in the hope that it will be useful, but WITHOUT ANY WARRANTY; without even the implied
|
||||
* warranty of MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU General Public License for more details.
|
||||
*
|
||||
* You should have received a copy of the GNU General Public License
|
||||
* along with this program. If not, see <http://www.gnu.org/licenses/>
|
||||
* You should have received a copy of the GNU General Public License along with this program.
|
||||
* If not, see <http://www.gnu.org/licenses/>
|
||||
*
|
||||
******************************************************************************/
|
||||
package io.github.dsheirer.source.tuner.airspy;
|
||||
|
||||
import io.github.dsheirer.dsp.filter.dc.DCRemovalFilter_RB;
|
||||
import io.github.dsheirer.dsp.filter.dc.DCRemovalFilter;
|
||||
import io.github.dsheirer.dsp.filter.hilbert.HilbertTransform;
|
||||
import io.github.dsheirer.source.tuner.usb.converter.NativeBufferConverter;
|
||||
|
||||
@@ -30,7 +27,7 @@ public class AirspySampleConverter extends NativeBufferConverter
|
||||
{
|
||||
private static final float SCALE_SIGNED_12_BIT_TO_FLOAT = 1.0f / 2048.0f;
|
||||
|
||||
private DCRemovalFilter_RB mDCFilter = new DCRemovalFilter_RB(0.01f);
|
||||
private DCRemovalFilter mDCFilter = new DCRemovalFilter(0.01f);
|
||||
private HilbertTransform mHilbertTransform = new HilbertTransform();
|
||||
private boolean mSamplePacking = false;
|
||||
private FloatBuffer mFloatBuffer;
|
||||
|
||||
@@ -1,127 +0,0 @@
|
||||
package io.github.dsheirer.source.tuner.frequency;
|
||||
/*******************************************************************************
|
||||
* sdrtrunk
|
||||
* Copyright (C) 2014-2017 Dennis Sheirer
|
||||
*
|
||||
* This program is free software: you can redistribute it and/or modify
|
||||
* it under the terms of the GNU General Public License as published by
|
||||
* the Free Software Foundation, either version 3 of the License, or
|
||||
* (at your option) any later version.
|
||||
*
|
||||
* This program is distributed in the hope that it will be useful,
|
||||
* but WITHOUT ANY WARRANTY; without even the implied warranty of
|
||||
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
|
||||
* GNU General Public License for more details.
|
||||
*
|
||||
* You should have received a copy of the GNU General Public License
|
||||
* along with this program. If not, see <http://www.gnu.org/licenses/>
|
||||
*
|
||||
******************************************************************************/
|
||||
|
||||
import io.github.dsheirer.module.Module;
|
||||
import io.github.dsheirer.sample.Listener;
|
||||
import io.github.dsheirer.sample.real.RealBuffer;
|
||||
import io.github.dsheirer.source.ISourceEventProvider;
|
||||
import io.github.dsheirer.source.SourceEvent;
|
||||
|
||||
public class DCTrackingFrequencyControl extends Module implements Listener<RealBuffer>, ISourceEventProvider
|
||||
{
|
||||
private static float COARSE_THRESHOLD = 0.1f;
|
||||
private static float LOOP_GAIN = 0.1f;
|
||||
private static float COARSE_GAIN = 1000.0f;
|
||||
private static float FINE_GAIN = 100.0f;
|
||||
private int mErrorCorrection = 0;
|
||||
private int mMaximumFrequencyCorrection = 0;
|
||||
private Listener<SourceEvent> mListener;
|
||||
|
||||
/**
|
||||
* DC offset Automatic Frequency Control. Monitors DC bias present in an FM
|
||||
* demodulated audio stream and issues automatic frequency corrections to
|
||||
* maintain the DC bias close to zero.
|
||||
*
|
||||
* @param maximum - maximum allowable (+/-) frequency correction value.
|
||||
*/
|
||||
public DCTrackingFrequencyControl(int maximum)
|
||||
{
|
||||
mMaximumFrequencyCorrection = maximum;
|
||||
}
|
||||
|
||||
public void dispose()
|
||||
{
|
||||
mListener = null;
|
||||
}
|
||||
|
||||
@Override
|
||||
public void receive(RealBuffer buffer)
|
||||
{
|
||||
double sum = 0.0d;
|
||||
|
||||
for(float sample : buffer.getSamples())
|
||||
{
|
||||
sum += sample;
|
||||
}
|
||||
|
||||
float mean = (float)(sum / (double)buffer.getSamples().length);
|
||||
|
||||
float adjustment = 0.0f;
|
||||
|
||||
if(mean > COARSE_THRESHOLD || mean < -COARSE_THRESHOLD)
|
||||
{
|
||||
adjustment = mean * COARSE_GAIN;
|
||||
}
|
||||
else
|
||||
{
|
||||
adjustment = mean * FINE_GAIN;
|
||||
}
|
||||
|
||||
setErrorCorrection(mErrorCorrection + (int)((float)adjustment * LOOP_GAIN));
|
||||
}
|
||||
|
||||
private void setErrorCorrection(int correction)
|
||||
{
|
||||
if(mListener != null)
|
||||
{
|
||||
if(correction > mMaximumFrequencyCorrection)
|
||||
{
|
||||
correction = mMaximumFrequencyCorrection;
|
||||
}
|
||||
else if(correction < -mMaximumFrequencyCorrection)
|
||||
{
|
||||
correction = -mMaximumFrequencyCorrection;
|
||||
}
|
||||
|
||||
if(correction != mErrorCorrection)
|
||||
{
|
||||
mListener.receive(SourceEvent.channelFrequencyCorrectionRequest(correction));
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@Override
|
||||
public void setSourceEventListener(Listener<SourceEvent> listener)
|
||||
{
|
||||
mListener = listener;
|
||||
}
|
||||
|
||||
@Override
|
||||
public void removeSourceEventListener()
|
||||
{
|
||||
mListener = null;
|
||||
}
|
||||
|
||||
@Override
|
||||
public void start()
|
||||
{
|
||||
}
|
||||
|
||||
@Override
|
||||
public void stop()
|
||||
{
|
||||
}
|
||||
|
||||
@Override
|
||||
public void reset()
|
||||
{
|
||||
setErrorCorrection(0);
|
||||
}
|
||||
}
|
||||
@@ -26,9 +26,8 @@ import io.github.dsheirer.dsp.filter.smoothing.SmoothingFilter.SmoothingType;
|
||||
import io.github.dsheirer.module.ProcessingChain;
|
||||
import io.github.dsheirer.sample.Listener;
|
||||
import io.github.dsheirer.sample.SampleType;
|
||||
import io.github.dsheirer.sample.buffer.ReusableComplexBuffer;
|
||||
import io.github.dsheirer.sample.buffer.ReusableBuffer;
|
||||
import io.github.dsheirer.sample.buffer.ReusableComplexBufferQueue;
|
||||
import io.github.dsheirer.sample.real.RealBuffer;
|
||||
import io.github.dsheirer.settings.ColorSetting.ColorSettingName;
|
||||
import io.github.dsheirer.settings.ColorSettingMenuItem;
|
||||
import io.github.dsheirer.settings.Setting;
|
||||
@@ -60,7 +59,7 @@ import java.awt.event.MouseMotionListener;
|
||||
import java.util.concurrent.atomic.AtomicBoolean;
|
||||
|
||||
public class ChannelSpectrumPanel extends JPanel implements ChannelEventListener,
|
||||
Listener<RealBuffer>, SettingChangeListener, SpectralDisplayAdjuster
|
||||
Listener<ReusableBuffer>, SettingChangeListener, SpectralDisplayAdjuster
|
||||
{
|
||||
private static final long serialVersionUID = 1L;
|
||||
|
||||
@@ -164,7 +163,6 @@ public class ChannelSpectrumPanel extends JPanel implements ChannelEventListener
|
||||
}
|
||||
|
||||
@Override
|
||||
@SuppressWarnings("incomplete-switch")
|
||||
public void channelChanged(ChannelEvent event)
|
||||
{
|
||||
switch(event.getEvent())
|
||||
@@ -217,7 +215,7 @@ public class ChannelSpectrumPanel extends JPanel implements ChannelEventListener
|
||||
|
||||
if(processingChain != null)
|
||||
{
|
||||
processingChain.addRealBufferListener(this);
|
||||
// processingChain.addRealBufferListener(this);
|
||||
|
||||
mDFTProcessor.start();
|
||||
}
|
||||
@@ -233,7 +231,7 @@ public class ChannelSpectrumPanel extends JPanel implements ChannelEventListener
|
||||
|
||||
if(processingChain != null)
|
||||
{
|
||||
processingChain.removeRealBufferListener(this);
|
||||
// processingChain.removeRealBufferListener(this);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -270,16 +268,17 @@ public class ChannelSpectrumPanel extends JPanel implements ChannelEventListener
|
||||
}
|
||||
|
||||
@Override
|
||||
public void receive(RealBuffer buffer)
|
||||
public void receive(ReusableBuffer buffer)
|
||||
{
|
||||
RealBuffer decimated = mDecimatingFilter.filter(buffer);
|
||||
|
||||
//Hack: we're placing real samples in a complex buffer that the DFT
|
||||
//processor is expecting.
|
||||
ReusableComplexBuffer reusableComplexBuffer = mReusableComplexBufferQueue.getBuffer(decimated.getSamples().length);
|
||||
reusableComplexBuffer.reloadFrom(decimated.getSamples(), System.currentTimeMillis());
|
||||
reusableComplexBuffer.incrementUserCount();
|
||||
mDFTProcessor.receive(reusableComplexBuffer);
|
||||
throw new IllegalStateException("This must be updated to work with reusable buffers");
|
||||
// RealBuffer decimated = mDecimatingFilter.filter(buffer);
|
||||
//
|
||||
// //Hack: we're placing real samples in a complex buffer that the DFT
|
||||
// //processor is expecting.
|
||||
// ReusableComplexBuffer reusableComplexBuffer = mReusableComplexBufferQueue.getBuffer(decimated.getSamples().length);
|
||||
// reusableComplexBuffer.reloadFrom(decimated.getSamples(), System.currentTimeMillis());
|
||||
// reusableComplexBuffer.incrementUserCount();
|
||||
// mDFTProcessor.receive(reusableComplexBuffer);
|
||||
}
|
||||
|
||||
/**
|
||||
|
||||
Reference in New Issue
Block a user