Merge pull request #357 from DSheirer/39-polyphase-channelizer

39 polyphase channelizer and decoder updates
This commit is contained in:
Denny Sheirer
2018-07-01 07:30:06 -04:00
committed by GitHub
423 changed files with 30622 additions and 24381 deletions

31
.idea/dictionaries/denny.xml generated Normal file
View File

@@ -0,0 +1,31 @@
<component name="ProjectDictionaryState">
<dictionary name="denny">
<words>
<w>bahroo</w>
<w>broadcastify</w>
<w>correlator</w>
<w>dinf</w>
<w>esnid</w>
<w>fleetsync</w>
<w>heterdyned</w>
<w>heterodynes</w>
<w>icecast</w>
<w>ident</w>
<w>nutall</w>
<w>overflowable</w>
<w>polyphase</w>
<w>remez</w>
<w>resampler</w>
<w>samplerate</w>
<w>sdrtrunk</w>
<w>sheirer</w>
<w>shoutcast</w>
<w>streamable</w>
<w>talkgroup</w>
<w>tgid</w>
<w>ultravox</w>
<w>wacn</w>
<w>xtea</w>
</words>
</dictionary>
</component>

14
.idea/libraries/imports.xml generated Normal file
View File

@@ -0,0 +1,14 @@
<component name="libraryTable">
<library name="imports">
<CLASSES>
<root url="jar://$PROJECT_DIR$/imports/jmbe-interface-0.3.3.jar!/" />
<root url="jar://$PROJECT_DIR$/imports/java-lame-3.98.4.jar!/" />
<root url="jar://$PROJECT_DIR$/imports/radioreference-14.jar!/" />
<root url="jar://$PROJECT_DIR$/imports/wsdl4j-1.5.1.jar!/" />
<root url="jar://$PROJECT_DIR$/imports/commons-math3-3.6.1.jar!/" />
<root url="jar://$PROJECT_DIR$/imports/libresample4j-0.1.3.jar!/" />
</CLASSES>
<JAVADOC />
<SOURCES />
</library>
</component>

2
.idea/misc.xml generated
View File

@@ -1,6 +1,6 @@
<?xml version="1.0" encoding="UTF-8"?>
<project version="4">
<component name="ProjectRootManager" version="2" languageLevel="JDK_1_8" default="true" project-jdk-name="1.8" project-jdk-type="JavaSDK">
<component name="ProjectRootManager" version="2" languageLevel="JDK_1_8" default="false" project-jdk-name="1.8" project-jdk-type="JavaSDK">
<output url="file://$PROJECT_DIR$/out" />
</component>
</project>

10
.idea/vcs.xml generated
View File

@@ -1,5 +1,15 @@
<?xml version="1.0" encoding="UTF-8"?>
<project version="4">
<component name="IssueNavigationConfiguration">
<option name="links">
<list>
<IssueNavigationLink>
<option name="issueRegexp" value="#(\d+)" />
<option name="linkRegexp" value="https://github.com/DSheirer/sdrtrunk/issues/$1" />
</IssueNavigationLink>
</list>
</option>
</component>
<component name="VcsDirectoryMappings">
<mapping directory="$PROJECT_DIR$" vcs="Git" />
</component>

View File

@@ -1,17 +0,0 @@
/*******************************************************************************
* SDR Trunk
* Copyright (C) 2014-2016 Dennis Sheirer
*
* This program is free software: you can redistribute it and/or modify
* it under the terms of the GNU General Public License as published by
* the Free Software Foundation, either version 3 of the License, or
* (at your option) any later version.
*
* This program is distributed in the hope that it will be useful,
* but WITHOUT ANY WARRANTY; without even the implied warranty of
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
* GNU General Public License for more details.
*
* You should have received a copy of the GNU General Public License
* along with this program. If not, see <http://www.gnu.org/licenses/>
******************************************************************************/

Binary file not shown.

14
sdrtrunk.iml Normal file
View File

@@ -0,0 +1,14 @@
<?xml version="1.0" encoding="UTF-8"?>
<module type="JAVA_MODULE" version="4">
<component name="NewModuleRootManager" inherit-compiler-output="true">
<exclude-output />
<content url="file://$MODULE_DIR$">
<sourceFolder url="file://$MODULE_DIR$/src" isTestSource="false" />
<sourceFolder url="file://$MODULE_DIR$/images" type="java-resource" />
<sourceFolder url="file://$MODULE_DIR$/config" type="java-resource" />
</content>
<orderEntry type="inheritedJdk" />
<orderEntry type="sourceFolder" forTests="false" />
<orderEntry type="library" name="imports" level="project" />
</component>
</module>

View File

@@ -24,6 +24,7 @@ import io.github.dsheirer.audio.output.StereoAudioOutput;
import io.github.dsheirer.properties.SystemProperties;
import io.github.dsheirer.sample.Broadcaster;
import io.github.dsheirer.sample.Listener;
import io.github.dsheirer.sample.buffer.ReusableAudioPacket;
import io.github.dsheirer.source.mixer.MixerChannel;
import io.github.dsheirer.source.mixer.MixerChannelConfiguration;
import io.github.dsheirer.source.mixer.MixerManager;
@@ -43,7 +44,7 @@ import java.util.concurrent.LinkedTransferQueue;
import java.util.concurrent.ScheduledFuture;
import java.util.concurrent.TimeUnit;
public class AudioManager implements Listener<AudioPacket>, IAudioController
public class AudioManager implements Listener<ReusableAudioPacket>, IAudioController
{
private static final Logger mLog = LoggerFactory.getLogger(AudioManager.class);
@@ -58,7 +59,7 @@ public class AudioManager implements Listener<AudioPacket>, IAudioController
public static final AudioEvent CONFIGURATION_CHANGE_COMPLETE =
new AudioEvent(AudioEvent.Type.AUDIO_CONFIGURATION_CHANGE_COMPLETE, null);
private LinkedTransferQueue<AudioPacket> mAudioPacketQueue = new LinkedTransferQueue<>();
private LinkedTransferQueue<ReusableAudioPacket> mAudioPacketQueue = new LinkedTransferQueue<>();
private Map<Integer,AudioOutputConnection> mChannelConnectionMap = new HashMap<>();
private List<AudioOutputConnection> mAudioOutputConnections = new ArrayList<>();
private AudioOutputConnection mLowestPriorityConnection;
@@ -170,7 +171,7 @@ public class AudioManager implements Listener<AudioPacket>, IAudioController
* Primary ingest point for audio produced by all decoding channels, for distribution to audio playback devices.
*/
@Override
public synchronized void receive(AudioPacket packet)
public synchronized void receive(ReusableAudioPacket packet)
{
mAudioPacketQueue.add(packet);
}
@@ -362,7 +363,7 @@ public class AudioManager implements Listener<AudioPacket>, IAudioController
* @param audioPacket from a decoding channel source
* @return an audio output connection or null
*/
private AudioOutputConnection getConnection(AudioPacket audioPacket)
private AudioOutputConnection getConnection(ReusableAudioPacket audioPacket)
{
int channelMetadataID = audioPacket.getMetadata().getMetadataID();
@@ -410,6 +411,8 @@ public class AudioManager implements Listener<AudioPacket>, IAudioController
public class AudioPacketProcessor implements Runnable
{
private List<ReusableAudioPacket> mPackets = new ArrayList<>();
@Override
public void run()
{
@@ -419,14 +422,12 @@ public class AudioManager implements Listener<AudioPacket>, IAudioController
if(mAudioPacketQueue != null)
{
List<AudioPacket> packets = new ArrayList<AudioPacket>();
mAudioPacketQueue.drainTo(mPackets);
mAudioPacketQueue.drainTo(packets);
for(AudioPacket packet : packets)
for(ReusableAudioPacket packet : mPackets)
{
/* Don't process any packet's marked as do not monitor */
if(!packet.getMetadata().isDoNotMonitor() && packet.getType() == AudioPacket.Type.AUDIO)
if(!packet.getMetadata().isDoNotMonitor() && packet.getType() == ReusableAudioPacket.Type.AUDIO)
{
AudioOutputConnection connection = getConnection(packet);
@@ -434,13 +435,29 @@ public class AudioManager implements Listener<AudioPacket>, IAudioController
{
connection.receive(packet);
}
else
{
packet.decrementUserCount();
}
}
else
{
packet.decrementUserCount();
}
}
mPackets.clear();
}
}
catch(Exception e)
{
mLog.error("Encountered error while processing audio packets", e);
while(mPackets.size() > 0)
{
ReusableAudioPacket audioPacket = mPackets.remove(0);
audioPacket.decrementUserCount();
}
}
}
}
@@ -463,7 +480,7 @@ public class AudioManager implements Listener<AudioPacket>, IAudioController
mAudioOutput = audioOutput;
}
public void receive(AudioPacket packet)
public void receive(ReusableAudioPacket packet)
{
if(packet.hasMetadata() && packet.getMetadata().getMetadataID() == mChannelMetadataID)
{

View File

@@ -1,19 +1,16 @@
/*******************************************************************************
* sdrtrunk
* Copyright (C) 2014-2017 Dennis Sheirer
* sdr-trunk
* Copyright (C) 2014-2018 Dennis Sheirer
*
* This program is free software: you can redistribute it and/or modify
* it under the terms of the GNU General Public License as published by
* the Free Software Foundation, either version 3 of the License, or
* (at your option) any later version.
* This program is free software: you can redistribute it and/or modify it under the terms of the GNU General Public
* License as published by the Free Software Foundation, either version 3 of the License, or (at your option) any
* later version.
*
* This program is distributed in the hope that it will be useful,
* but WITHOUT ANY WARRANTY; without even the implied warranty of
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
* GNU General Public License for more details.
* This program is distributed in the hope that it will be useful, but WITHOUT ANY WARRANTY; without even the implied
* warranty of MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU General Public License for more details.
*
* You should have received a copy of the GNU General Public License
* along with this program. If not, see <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.audio;
@@ -23,92 +20,80 @@ import io.github.dsheirer.audio.squelch.SquelchState;
import io.github.dsheirer.channel.metadata.Metadata;
import io.github.dsheirer.dsp.filter.design.FilterDesignException;
import io.github.dsheirer.dsp.filter.fir.FIRFilterSpecification;
import io.github.dsheirer.dsp.filter.fir.real.RealFIRFilter2;
import io.github.dsheirer.dsp.filter.fir.remez.RemezFIRFilterDesigner;
import io.github.dsheirer.dsp.filter.polyphase.PolyphaseFIRDecimatingFilter_RB;
import io.github.dsheirer.module.Module;
import io.github.dsheirer.sample.Listener;
import io.github.dsheirer.sample.real.IFilteredRealBufferListener;
import io.github.dsheirer.sample.real.RealBuffer;
import io.github.dsheirer.sample.buffer.IReusableBufferListener;
import io.github.dsheirer.sample.buffer.ReusableAudioPacket;
import io.github.dsheirer.sample.buffer.ReusableAudioPacketQueue;
import io.github.dsheirer.sample.buffer.ReusableBuffer;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import java.util.concurrent.ScheduledExecutorService;
/**
* Provides packaging of demodulated audio sample buffers into audio packets for
* broadcast to registered audio packet listeners. Includes audio packet
* metadata in constructed audio packets.
* Provides packaging of demodulated audio sample buffers into audio packets for broadcast to registered audio packet
* listeners. Includes audio packet metadata in constructed audio packets.
*
* Incorporates audio squelch state listener to control if audio packets are
* broadcast or ignored.
* Incorporates audio squelch state listener to control if audio packets are broadcast or ignored.
*
* This class is designed to support 8 kHz sample rate demodulated audio.
*/
public class AudioModule extends Module implements IAudioPacketProvider, IFilteredRealBufferListener,
ISquelchStateListener, Listener<RealBuffer>
public class AudioModule extends Module implements IAudioPacketProvider, IReusableBufferListener,
ISquelchStateListener, Listener<ReusableBuffer>
{
protected static final Logger mLog = LoggerFactory.getLogger(AudioModule.class);
private ReusableAudioPacketQueue mAudioPacketQueue = new ReusableAudioPacketQueue("AudioModule");
private static float[] mDecimationCoefficients;
private static float[] sHighPassFilterCoefficients;
static
{
//Band-pass filter to both decimate (48kHz to 8kHz) and block 0 - 300 Hz (LTR signalling).
FIRFilterSpecification specification = FIRFilterSpecification.bandPassBuilder()
.sampleRate(48000)
.stopFrequency1(150)
.passFrequencyBegin(300)
.passFrequencyEnd(3500)
.stopFrequency2(4000)
.stopRipple(0.0004)
.passRipple(0.008)
FIRFilterSpecification specification = FIRFilterSpecification.highPassBuilder()
.sampleRate(8000)
.stopBandCutoff(200)
.stopBandAmplitude(0.0)
.stopBandRipple(0.025)
.passBandStart(300)
.passBandAmplitude(1.0)
.passBandRipple(0.01)
.build();
RemezFIRFilterDesigner designer = new RemezFIRFilterDesigner(specification);
try
{
if(!designer.isValid())
{
throw new FilterDesignException("Couldn't design the audio decimation filter");
}
RemezFIRFilterDesigner designer = new RemezFIRFilterDesigner(specification);
mDecimationCoefficients = designer.getImpulseResponse();
if(designer.isValid())
{
sHighPassFilterCoefficients = designer.getImpulseResponse();
}
}
catch(FilterDesignException e)
catch(FilterDesignException fde)
{
mLog.debug("Error designing filter", e);
mLog.error("Filter design error", fde);
}
}
private RealFIRFilter2 mHighPassFilter = new RealFIRFilter2(sHighPassFilterCoefficients);
private SquelchStateListener mSquelchStateListener = new SquelchStateListener();
private SquelchState mSquelchState = SquelchState.SQUELCH;
private Listener<AudioPacket> mAudioPacketListener;
private PolyphaseFIRDecimatingFilter_RB mAudioDecimationFilter;
private Listener<ReusableAudioPacket> mAudioPacketListener;
private Metadata mMetadata;
/**
* Creates an Audio Module.
*
* @param metadata to use for audio packets produced by this audio module.
*/
public AudioModule(Metadata metadata)
{
mMetadata = metadata;
mAudioDecimationFilter = new PolyphaseFIRDecimatingFilter_RB(mDecimationCoefficients, 6, 2.0f);
mAudioDecimationFilter.setListener(new Listener<RealBuffer>()
{
@Override
public void receive(RealBuffer realBuffer)
{
if(mAudioPacketListener != null)
{
AudioPacket packet = new AudioPacket(realBuffer.getSamples(), mMetadata.copyOf());
mAudioPacketListener.receive(packet);
}
}
});
}
@Override
public void dispose()
{
mSquelchStateListener = null;
mAudioPacketListener = null;
mSquelchStateListener = null;
}
@Override
@@ -117,7 +102,7 @@ public class AudioModule extends Module implements IAudioPacketProvider, IFilter
}
@Override
public void start(ScheduledExecutorService executor)
public void start()
{
/* No start operations provided */
}
@@ -125,34 +110,18 @@ public class AudioModule extends Module implements IAudioPacketProvider, IFilter
@Override
public void stop()
{
/* Issue an end audio packet in case a recorder is still rolling */
/* Issue an end-audio packet in case a recorder is still rolling */
if(mAudioPacketListener != null)
{
mAudioPacketListener.receive(new AudioPacket(AudioPacket.Type.END, mMetadata.copyOf()));
}
}
/**
* Processes demodulated audio samples into audio packets with current audio
* metadata and sends to the registered listener
*/
@Override
public void receive(RealBuffer buffer)
{
if(mAudioPacketListener != null && mSquelchState == SquelchState.UNSQUELCH)
{
mAudioDecimationFilter.receive(buffer);
ReusableAudioPacket endAudioPacket = mAudioPacketQueue.getEndAudioBuffer();
endAudioPacket.setMetadata(mMetadata.copyOf());
endAudioPacket.incrementUserCount();
mAudioPacketListener.receive(endAudioPacket);
}
}
@Override
public Listener<RealBuffer> getFilteredRealBufferListener()
{
return this;
}
@Override
public void setAudioPacketListener(Listener<AudioPacket> listener)
public void setAudioPacketListener(Listener<ReusableAudioPacket> listener)
{
mAudioPacketListener = listener;
}
@@ -169,6 +138,34 @@ public class AudioModule extends Module implements IAudioPacketProvider, IFilter
return mSquelchStateListener;
}
@Override
public void receive(ReusableBuffer reusableBuffer)
{
if(mAudioPacketListener != null && mSquelchState == SquelchState.UNSQUELCH)
{
ReusableBuffer highPassFiltered = mHighPassFilter.filter(reusableBuffer);
ReusableAudioPacket audioPacket = mAudioPacketQueue.getBuffer(highPassFiltered.getSampleCount());
audioPacket.loadAudioFrom(highPassFiltered);
audioPacket.setMetadata(mMetadata.copyOf());
mAudioPacketListener.receive(audioPacket);
highPassFiltered.decrementUserCount();
}
else
{
reusableBuffer.decrementUserCount();
}
}
@Override
public Listener getReusableBufferListener()
{
//Redirect received reusable buffers to the receive(buffer) method
return this;
}
/**
* Wrapper for squelch state listener
*/
@@ -179,7 +176,9 @@ public class AudioModule extends Module implements IAudioPacketProvider, IFilter
{
if(state == SquelchState.SQUELCH && mAudioPacketListener != null)
{
mAudioPacketListener.receive(new AudioPacket(AudioPacket.Type.END, mMetadata.copyOf()));
ReusableAudioPacket endAudioPacket = mAudioPacketQueue.getEndAudioBuffer();
endAudioPacket.setMetadata(mMetadata.copyOf());
mAudioPacketListener.receive(endAudioPacket);
}
mSquelchState = state;

View File

@@ -1,78 +0,0 @@
/*******************************************************************************
* sdrtrunk
* Copyright (C) 2014-2017 Dennis Sheirer
*
* This program is free software: you can redistribute it and/or modify
* it under the terms of the GNU General Public License as published by
* the Free Software Foundation, either version 3 of the License, or
* (at your option) any later version.
*
* This program is distributed in the hope that it will be useful,
* but WITHOUT ANY WARRANTY; without even the implied warranty of
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
* GNU General Public License for more details.
*
* You should have received a copy of the GNU General Public License
* along with this program. If not, see <http://www.gnu.org/licenses/>
*
******************************************************************************/
package io.github.dsheirer.audio;
import io.github.dsheirer.channel.metadata.Metadata;
import io.github.dsheirer.sample.real.RealBuffer;
public class AudioPacket
{
private Type mType;
private RealBuffer mAudioData;
private Metadata mMetadata;
public AudioPacket(Type type, Metadata metadata)
{
mType = type;
mMetadata = metadata;
}
public AudioPacket(float[] audio, Metadata metadata)
{
this(Type.AUDIO, metadata);
mAudioData = new RealBuffer(audio);
}
public boolean hasMetadata()
{
return mMetadata != null;
}
public Metadata getMetadata()
{
return mMetadata;
}
public void setMetadata(Metadata metadata)
{
mMetadata = metadata;
}
public Type getType()
{
return mType;
}
public RealBuffer getAudioBuffer()
{
return mAudioData;
}
public boolean hasAudioBuffer()
{
return mAudioData != null;
}
public enum Type
{
AUDIO,
END;
}
}

View File

@@ -1,55 +0,0 @@
package io.github.dsheirer.audio;
import io.github.dsheirer.sample.Listener;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
public class AudioPacketRateCounter implements Listener<AudioPacket>
{
private final static Logger mLog =
LoggerFactory.getLogger( AudioPacketRateCounter.class );
private Listener<AudioPacket> mListener;
private long mCount;
private long mStartTime = -1;
private String mName;
public AudioPacketRateCounter()
{
mName = "UNNAMED";
}
public AudioPacketRateCounter( String name )
{
mName = name;
}
@Override
public void receive( AudioPacket packet )
{
if( mStartTime == -1 )
{
mStartTime = System.currentTimeMillis();
}
mCount += packet.getAudioBuffer().getSamples().length;
long elapsed = System.currentTimeMillis() - mStartTime;
if( elapsed != 0 )
{
mLog.debug( "[" + mName + "] Audio Rate: " + ( mCount / elapsed ) + " kHz" );
}
if( mListener != null )
{
mListener.receive( packet );
}
}
public void setListener( Listener<AudioPacket> listener )
{
mListener = listener;
}
}

View File

@@ -1,24 +1,22 @@
/*******************************************************************************
* sdrtrunk
* Copyright (C) 2014-2016 Dennis Sheirer
* sdr-trunk
* Copyright (C) 2014-2018 Dennis Sheirer
*
* This program is free software: you can redistribute it and/or modify
* it under the terms of the GNU General Public License as published by
* the Free Software Foundation, either version 3 of the License, or
* (at your option) any later version.
* This program is free software: you can redistribute it and/or modify it under the terms of the GNU General Public
* License as published by the Free Software Foundation, either version 3 of the License, or (at your option) any
* later version.
*
* This program is distributed in the hope that it will be useful,
* but WITHOUT ANY WARRANTY; without even the implied warranty of
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
* GNU General Public License for more details.
* This program is distributed in the hope that it will be useful, but WITHOUT ANY WARRANTY; without even the implied
* warranty of MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU General Public License for more details.
*
* You should have received a copy of the GNU General Public License
* along with this program. If not, see <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.audio;
import io.github.dsheirer.sample.ConversionUtils;
import io.github.dsheirer.sample.buffer.ReusableAudioPacket;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
@@ -34,17 +32,18 @@ public class AudioUtils
/**
* Converts the audio packets into a byte array of 16-bit, little-endian audio samples
*/
public static byte[] convertTo16BitSamples(List<AudioPacket> audioPackets)
public static byte[] convertTo16BitSamples(List<ReusableAudioPacket> audioPackets)
{
ByteArrayOutputStream stream = new ByteArrayOutputStream();
try
{
for(AudioPacket packet: audioPackets)
for(ReusableAudioPacket audioPacket: audioPackets)
{
//Converting from 32-bit floats to signed 16-bit samples
ByteBuffer buffer = ConversionUtils.convertToSigned16BitSamples(packet.getAudioBuffer().getSamples());
ByteBuffer buffer = ConversionUtils.convertToSigned16BitSamples(audioPacket.getAudioSamples());
stream.write(buffer.array());
audioPacket.decrementUserCount();
}
}
catch(IOException e)

View File

@@ -1,8 +1,9 @@
package io.github.dsheirer.audio;
import io.github.dsheirer.sample.Listener;
import io.github.dsheirer.sample.buffer.ReusableAudioPacket;
public interface IAudioPacketListener
{
public Listener<AudioPacket> getAudioPacketListener();
public Listener<ReusableAudioPacket> getAudioPacketListener();
}

View File

@@ -1,9 +1,10 @@
package io.github.dsheirer.audio;
import io.github.dsheirer.sample.Listener;
import io.github.dsheirer.sample.buffer.ReusableAudioPacket;
public interface IAudioPacketProvider
{
public void setAudioPacketListener( Listener<AudioPacket> listener );
public void setAudioPacketListener( Listener<ReusableAudioPacket> listener );
public void removeAudioPacketListener();
}

View File

@@ -19,12 +19,12 @@
package io.github.dsheirer.audio.broadcast;
import io.github.dsheirer.alias.id.broadcast.BroadcastChannel;
import io.github.dsheirer.audio.AudioPacket;
import io.github.dsheirer.channel.metadata.Metadata;
import io.github.dsheirer.icon.IconManager;
import io.github.dsheirer.properties.SystemProperties;
import io.github.dsheirer.sample.Broadcaster;
import io.github.dsheirer.sample.Listener;
import io.github.dsheirer.sample.buffer.ReusableAudioPacket;
import io.github.dsheirer.util.ThreadPool;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
@@ -46,7 +46,7 @@ import java.util.function.Consumer;
import java.util.regex.Matcher;
import java.util.regex.Pattern;
public class BroadcastModel extends AbstractTableModel implements Listener<AudioPacket>
public class BroadcastModel extends AbstractTableModel implements Listener<ReusableAudioPacket>
{
private final static Logger mLog = LoggerFactory.getLogger(BroadcastModel.class);
@@ -276,7 +276,7 @@ public class BroadcastModel extends AbstractTableModel implements Listener<Audio
}
@Override
public void receive(AudioPacket audioPacket)
public void receive(ReusableAudioPacket audioPacket)
{
if(audioPacket.hasMetadata() && audioPacket.getMetadata().isStreamable())
{
@@ -284,11 +284,14 @@ public class BroadcastModel extends AbstractTableModel implements Listener<Audio
{
if(mBroadcasterMap.containsKey(channel.getChannelName()))
{
audioPacket.incrementUserCount();
mStreamManager.receive(audioPacket);
return;
}
}
}
audioPacket.decrementUserCount();
}
/**

View File

@@ -18,9 +18,9 @@
******************************************************************************/
package io.github.dsheirer.audio.broadcast;
import io.github.dsheirer.audio.AudioPacket;
import io.github.dsheirer.record.AudioRecorder;
import io.github.dsheirer.sample.Listener;
import io.github.dsheirer.sample.buffer.ReusableAudioPacket;
import io.github.dsheirer.util.ThreadPool;
import io.github.dsheirer.util.TimeStamp;
import org.slf4j.Logger;
@@ -37,7 +37,7 @@ import java.util.concurrent.TimeUnit;
import java.util.concurrent.atomic.AtomicBoolean;
import java.util.concurrent.atomic.AtomicInteger;
public class StreamManager implements Listener<AudioPacket>
public class StreamManager implements Listener<ReusableAudioPacket>
{
private final static Logger mLog = LoggerFactory.getLogger(StreamManager.class);
private static final long MAXIMUM_RECORDER_LIFESPAN_MILLIS = 30000; //30 seconds
@@ -115,7 +115,7 @@ public class StreamManager implements Listener<AudioPacket>
}
@Override
public void receive(AudioPacket audioPacket)
public void receive(ReusableAudioPacket audioPacket)
{
if(mRunning.get() && audioPacket.hasMetadata())
{
@@ -123,9 +123,9 @@ public class StreamManager implements Listener<AudioPacket>
{
int channelMetadataID = audioPacket.getMetadata().getMetadataID();
AudioPacket.Type type = audioPacket.getType();
ReusableAudioPacket.Type type = audioPacket.getType();
if(type == AudioPacket.Type.AUDIO)
if(type == ReusableAudioPacket.Type.AUDIO)
{
if(mStreamRecorders.containsKey(channelMetadataID))
{
@@ -133,6 +133,7 @@ public class StreamManager implements Listener<AudioPacket>
if(recorder != null)
{
audioPacket.incrementUserCount();
recorder.receive(audioPacket);
}
}
@@ -140,12 +141,13 @@ public class StreamManager implements Listener<AudioPacket>
{
AudioRecorder recorder = BroadcastFactory.getAudioRecorder(getTemporaryRecordingPath(),
mBroadcastFormat);
recorder.start(ThreadPool.SCHEDULED);
recorder.start();
audioPacket.incrementUserCount();
recorder.receive(audioPacket);
mStreamRecorders.put(channelMetadataID, recorder);
}
}
else if(type == AudioPacket.Type.END)
else if(type == ReusableAudioPacket.Type.END)
{
removeRecorder(channelMetadataID);
}
@@ -155,6 +157,8 @@ public class StreamManager implements Listener<AudioPacket>
}
}
}
audioPacket.decrementUserCount();
}
/**

View File

@@ -18,7 +18,7 @@
******************************************************************************/
package io.github.dsheirer.audio.convert;
import io.github.dsheirer.audio.AudioPacket;
import io.github.dsheirer.sample.buffer.ReusableAudioPacket;
import java.util.List;
@@ -27,7 +27,7 @@ public interface IAudioConverter
/**
* Converts the PCM audio packets to converted audio format. May produce partial audio frame data.
*/
public byte[] convert(List<AudioPacket> audioPackets);
public byte[] convert(List<ReusableAudioPacket> audioPackets);
/**
* Finalizes audio conversion by fully converting any partial frames left in the buffer and returning the

View File

@@ -19,8 +19,8 @@
package io.github.dsheirer.audio.convert;
import io.github.dsheirer.audio.AudioFormats;
import io.github.dsheirer.audio.AudioPacket;
import io.github.dsheirer.audio.AudioUtils;
import io.github.dsheirer.sample.buffer.ReusableAudioPacket;
import net.sourceforge.lame.lowlevel.LameEncoder;
import net.sourceforge.lame.mp3.Lame;
import net.sourceforge.lame.mp3.MPEGMode;
@@ -54,7 +54,7 @@ public class MP3AudioConverter implements IAudioConverter
}
@Override
public byte[] convert(List<AudioPacket> audioPackets)
public byte[] convert(List<ReusableAudioPacket> audioPackets)
{
mMP3Stream.reset();

View File

@@ -18,8 +18,9 @@
******************************************************************************/
package io.github.dsheirer.audio.convert;
import io.github.dsheirer.audio.AudioPacket;
import io.github.dsheirer.record.mp3.MP3Recorder;
import io.github.dsheirer.sample.buffer.ReusableAudioPacket;
import io.github.dsheirer.sample.buffer.ReusableAudioPacketQueue;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
@@ -33,6 +34,7 @@ public class MP3SilenceGenerator implements ISilenceGenerator
private MP3AudioConverter mGenerator = new MP3AudioConverter(MP3Recorder.MP3_BIT_RATE, MP3Recorder.CONSTANT_BIT_RATE);
private byte[] mPreviousPartialFrameData;
private ReusableAudioPacketQueue mAudioPacketQueue = new ReusableAudioPacketQueue("MP3 Silence Generator");
/**
* Generates MP3 audio silence frames
@@ -44,9 +46,10 @@ public class MP3SilenceGenerator implements ISilenceGenerator
public byte[] generate(long duration)
{
int length = (int)(duration * 8); //8000 Hz sample rate
float[] silence = new float[length];
AudioPacket silencePacket = new AudioPacket(silence, null);
List<AudioPacket> silencePackets = new ArrayList<>();
ReusableAudioPacket silencePacket = mAudioPacketQueue.getBuffer(length);
Arrays.fill(silencePacket.getAudioSamples(), 0.0f);
List<ReusableAudioPacket> silencePackets = new ArrayList<>();
silencePackets.add(silencePacket);
byte[] frameData = mGenerator.convert(silencePackets);

View File

@@ -20,6 +20,7 @@ package io.github.dsheirer.audio.invert;
import io.github.dsheirer.dsp.filter.FilterFactory;
import io.github.dsheirer.dsp.filter.FloatFIRFilter;
import io.github.dsheirer.dsp.filter.Window.WindowType;
import io.github.dsheirer.dsp.mixer.IOscillator;
import io.github.dsheirer.dsp.mixer.Oscillator;
/**
@@ -29,7 +30,7 @@ import io.github.dsheirer.dsp.mixer.Oscillator;
public class AudioAdapter
{
private static int mGain = 5;
private Oscillator mSineWaveGenerator;
private IOscillator mSineWaveGenerator;
private FloatFIRFilter mPostInversionFilter;
private FloatFIRFilter mAudioHighPassFilter;
private AudioType mAudioType = AudioType.NORMAL;
@@ -88,7 +89,7 @@ public class AudioAdapter
default:
retVal = (float)( mPostInversionFilter.get(
mAudioHighPassFilter.get( sample ) *
mSineWaveGenerator.getFloat() ) );
mSineWaveGenerator.inphase() ) );
mSineWaveGenerator.rotate();
break;

View File

@@ -20,6 +20,7 @@ package io.github.dsheirer.audio.invert;
import io.github.dsheirer.audio.InversionFrequency;
import io.github.dsheirer.dsp.filter.Filters;
import io.github.dsheirer.dsp.filter.FloatFIRFilter;
import io.github.dsheirer.dsp.mixer.IOscillator;
import io.github.dsheirer.dsp.mixer.Oscillator;
import io.github.dsheirer.sample.real.RealSampleListener;
@@ -29,7 +30,7 @@ import io.github.dsheirer.sample.real.RealSampleListener;
*/
public class AudioInverter implements RealSampleListener
{
private Oscillator mSineWaveGenerator;
private IOscillator mSineWaveGenerator;
private FloatFIRFilter mPostInversionLowPassFilter;
private RealSampleListener mListener;
@@ -61,7 +62,7 @@ public class AudioInverter implements RealSampleListener
if( mPostInversionLowPassFilter != null )
{
mPostInversionLowPassFilter.receive(
sample * mSineWaveGenerator.getFloat() );
sample * mSineWaveGenerator.inphase() );
mSineWaveGenerator.rotate();
}

View File

@@ -19,10 +19,10 @@
package io.github.dsheirer.audio.output;
import io.github.dsheirer.audio.AudioEvent;
import io.github.dsheirer.audio.AudioPacket;
import io.github.dsheirer.channel.metadata.Metadata;
import io.github.dsheirer.sample.Broadcaster;
import io.github.dsheirer.sample.Listener;
import io.github.dsheirer.sample.buffer.ReusableAudioPacket;
import io.github.dsheirer.source.mixer.MixerChannel;
import io.github.dsheirer.util.ThreadPool;
import org.slf4j.Logger;
@@ -46,11 +46,11 @@ import java.util.concurrent.ScheduledFuture;
import java.util.concurrent.TimeUnit;
import java.util.concurrent.atomic.AtomicBoolean;
public abstract class AudioOutput implements Listener<AudioPacket>, LineListener
public abstract class AudioOutput implements Listener<ReusableAudioPacket>, LineListener
{
private final static Logger mLog = LoggerFactory.getLogger(AudioOutput.class);
private LinkedTransferQueue<AudioPacket> mBuffer = new LinkedTransferQueue<>();
private LinkedTransferQueue<ReusableAudioPacket> mBuffer = new LinkedTransferQueue<>();
private int mBufferStartThreshold;
private int mBufferStopThreshold;
@@ -190,7 +190,7 @@ public abstract class AudioOutput implements Listener<AudioPacket>, LineListener
* Converts the audio packet data into a byte buffer format appropriate for
* the underlying source data line.
*/
protected abstract ByteBuffer convert(AudioPacket packet);
protected abstract ByteBuffer convert(ReusableAudioPacket packet);
/**
* Audio output channel name
@@ -271,7 +271,7 @@ public abstract class AudioOutput implements Listener<AudioPacket>, LineListener
}
@Override
public void receive(AudioPacket packet)
public void receive(ReusableAudioPacket packet)
{
if(mCanProcessAudio)
{
@@ -281,11 +281,17 @@ public abstract class AudioOutput implements Listener<AudioPacket>, LineListener
mBuffer.add(packet);
}
else
{
packet.decrementUserCount();
}
}
public class BufferProcessor implements Runnable
{
private AtomicBoolean mProcessing = new AtomicBoolean();
private List<ReusableAudioPacket> mAudioPackets = new ArrayList<ReusableAudioPacket>();
public BufferProcessor()
{
@@ -300,13 +306,11 @@ public abstract class AudioOutput implements Listener<AudioPacket>, LineListener
* processor can run at any given time */
if(mProcessing.compareAndSet(false, true))
{
List<AudioPacket> packets = new ArrayList<AudioPacket>();
mBuffer.drainTo(mAudioPackets);
mBuffer.drainTo(packets);
for(AudioPacket packet : packets)
for(ReusableAudioPacket packet : mAudioPackets)
{
if(packet.getType() == AudioPacket.Type.AUDIO)
if(packet.getType() == ReusableAudioPacket.Type.AUDIO)
{
broadcast(packet.getMetadata());
@@ -338,8 +342,14 @@ public abstract class AudioOutput implements Listener<AudioPacket>, LineListener
updateTimestamp();
}
else
{
packet.decrementUserCount();
}
}
mAudioPackets.clear();
checkStop();
mProcessing.set(false);

View File

@@ -19,7 +19,7 @@
package io.github.dsheirer.audio.output;
import io.github.dsheirer.audio.AudioFormats;
import io.github.dsheirer.audio.AudioPacket;
import io.github.dsheirer.sample.buffer.ReusableAudioPacket;
import io.github.dsheirer.source.mixer.MixerChannel;
import javax.sound.sampled.Mixer;
@@ -43,14 +43,16 @@ public class MonoAudioOutput extends AudioOutput
/**
* Converts the audio packet data into mono audio frames.
*/
protected ByteBuffer convert(AudioPacket packet)
protected ByteBuffer convert(ReusableAudioPacket packet)
{
if(packet.hasAudioBuffer())
ByteBuffer buffer = null;
if(packet.hasAudioSamples())
{
float[] samples = packet.getAudioBuffer().getSamples();
float[] samples = packet.getAudioSamples();
/* Little-endian byte buffer */
ByteBuffer buffer = ByteBuffer.allocate(samples.length * 2).order(ByteOrder.LITTLE_ENDIAN);
buffer = ByteBuffer.allocate(samples.length * 2).order(ByteOrder.LITTLE_ENDIAN);
ShortBuffer shortBuffer = buffer.asShortBuffer();
@@ -58,10 +60,10 @@ public class MonoAudioOutput extends AudioOutput
{
shortBuffer.put((short) (sample * Short.MAX_VALUE));
}
return buffer;
}
return null;
packet.decrementUserCount();
return buffer;
}
}

View File

@@ -19,7 +19,7 @@
package io.github.dsheirer.audio.output;
import io.github.dsheirer.audio.AudioFormats;
import io.github.dsheirer.audio.AudioPacket;
import io.github.dsheirer.sample.buffer.ReusableAudioPacket;
import io.github.dsheirer.source.mixer.MixerChannel;
import javax.sound.sampled.Mixer;
@@ -45,15 +45,16 @@ public class StereoAudioOutput extends AudioOutput
* channel containing the audio and the other channel containing zero
* valued (silent) samples.
*/
protected ByteBuffer convert(AudioPacket packet)
protected ByteBuffer convert(ReusableAudioPacket packet)
{
if(packet.hasAudioBuffer())
ByteBuffer buffer = null;
if(packet.hasAudioSamples())
{
float[] samples = packet.getAudioBuffer().getSamples();
float[] samples = packet.getAudioSamples();
/* Little-endian byte buffer */
ByteBuffer buffer = ByteBuffer.allocate(samples.length * 4)
.order(ByteOrder.LITTLE_ENDIAN);
buffer = ByteBuffer.allocate(samples.length * 4).order(ByteOrder.LITTLE_ENDIAN);
ShortBuffer shortBuffer = buffer.asShortBuffer();
@@ -73,10 +74,10 @@ public class StereoAudioOutput extends AudioOutput
shortBuffer.put((short) (sample * Short.MAX_VALUE));
}
}
return buffer;
}
return null;
packet.decrementUserCount();
return buffer;
}
}

View File

@@ -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
}
}

View File

@@ -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;
}
}

View File

@@ -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;
}
}
}

View File

@@ -0,0 +1,21 @@
/*******************************************************************************
* sdr-trunk
* Copyright (C) 2014-2018 Dennis Sheirer
*
* This program is free software: you can redistribute it and/or modify it under the terms of the GNU General Public
* License as published by the Free Software Foundation, either version 3 of the License, or (at your option) any
* later version.
*
* This program is distributed in the hope that it will be useful, but WITHOUT ANY WARRANTY; without even the implied
* warranty of MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU General Public License for more details.
*
* You should have received a copy of the GNU General Public License along with this program.
* If not, see <http://www.gnu.org/licenses/>
*
******************************************************************************/
package io.github.dsheirer.bits;
public interface IBinarySymbolProcessor
{
void receive(boolean symbol);
}

View File

@@ -1,6 +0,0 @@
package io.github.dsheirer.bits;
public interface ISyncDetectListener
{
public void syncDetected();
}

View File

@@ -1,119 +1,116 @@
/*******************************************************************************
* SDR Trunk
* Copyright (C) 2014 Dennis Sheirer
*
* This program is free software: you can redistribute it and/or modify
* it under the terms of the GNU General Public License as published by
* the Free Software Foundation, either version 3 of the License, or
* (at your option) any later version.
*
* This program is distributed in the hope that it will be useful,
* but WITHOUT ANY WARRANTY; without even the implied warranty of
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
* GNU General Public License for more details.
*
* You should have received a copy of the GNU General Public License
* along with this program. If not, see <http://www.gnu.org/licenses/>
* 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.bits;
import io.github.dsheirer.dsp.symbol.SyncDetectListener;
import io.github.dsheirer.dsp.symbol.ISyncDetectListener;
import io.github.dsheirer.dsp.symbol.SyncDetectProvider;
import io.github.dsheirer.sample.Broadcaster;
import io.github.dsheirer.sample.Listener;
import java.util.ArrayList;
import java.util.Iterator;
import java.util.List;
/**
* MessageFramer - processes bitsets looking for a sync pattern within
* the bits, and then extracts the message, including the sync
* pattern, for a total bit length of messageLength.
*
*
* Will extract multiple messages simultaneously, for each sync pattern that is
* encountered within the bitset bit stream.
*/
public class MessageFramer implements Listener<Boolean>,
SyncDetectProvider
public class MessageFramer implements IBinarySymbolProcessor, Listener<Boolean>, SyncDetectProvider
{
private boolean[] mSyncPattern;
private int mMessageLength;
private SyncDetectListener mSyncDetectListener;
private Broadcaster<BinaryMessage> mBroadcaster =
new Broadcaster<BinaryMessage>();
private ArrayList<MessageAssembler> mMessageAssemblers =
new ArrayList<MessageAssembler>();
private boolean[] mSyncPattern;
private int mMessageLength;
private ISyncDetectListener mSyncDetectListener;
private Broadcaster<BinaryMessage> mBroadcaster = new Broadcaster<BinaryMessage>();
private List<MessageAssembler> mMessageAssemblers = new ArrayList<MessageAssembler>();
private List<MessageAssembler> mCompletedMessageAssemblers = new ArrayList<MessageAssembler>();
private SyncPatternMatcher mMatcher;
private ArrayList<MessageAssembler> mCompletedMessageAssemblers =
new ArrayList<MessageAssembler>();
private BinaryMessage mPreviousBuffer = null;
private SyncPatternMatcher mMatcher;
public MessageFramer( boolean[] syncPattern, int messageLength )
public MessageFramer(boolean[] syncPattern, int messageLength)
{
mSyncPattern = syncPattern;
mMatcher = new SyncPatternMatcher( syncPattern );
mMatcher = new SyncPatternMatcher(syncPattern);
mMessageLength = messageLength;
}
public void reset()
{
for( MessageAssembler assembler: mMessageAssemblers )
{
assembler.dispose();
}
for(MessageAssembler assembler : mMessageAssemblers)
{
assembler.dispose();
}
mMessageAssemblers.clear();
}
public void dispose()
{
mBroadcaster.dispose();
mCompletedMessageAssemblers.clear();
mMessageAssemblers.clear();
}
@Override
public void receive( Boolean bit )
public void dispose()
{
mMatcher.receive( bit );
mBroadcaster.dispose();
mCompletedMessageAssemblers.clear();
mMessageAssemblers.clear();
}
public void receive(boolean bit)
{
mMatcher.receive(bit);
Iterator<MessageAssembler> it = mMessageAssemblers.iterator();
MessageAssembler assembler;
while( it.hasNext() )
while(it.hasNext())
{
assembler = it.next();
/* Dispose and remove any completed assemblers */
if( assembler.complete() )
if(assembler.complete())
{
assembler.dispose();
it.remove();
assembler.dispose();
it.remove();
}
/* Otherwise, send them the bit */
else
{
assembler.receive( bit );
assembler.receive(bit);
}
}
/* Check for sync match and add new message assembler */
if( mMatcher.matches() )
{
addMessageAssembler( new MessageAssembler( mMessageLength, mSyncPattern ) );
if(mMatcher.matches())
{
addMessageAssembler(new MessageAssembler(mMessageLength, mSyncPattern));
/* Notify any sync detect listener(s) */
if( mSyncDetectListener != null )
if(mSyncDetectListener != null)
{
mSyncDetectListener.syncDetected();
mSyncDetectListener.syncDetected();
}
}
}
}
@Deprecated //Legacy support ... remove once all producers are converted to use receive(boolean bit) method
@Override
public void receive(Boolean bit)
{
receive(bit);
}
/**
* Causes all messages currently under assembly to be forcibly
* sent (ie flushed) to all registered message listeners, and
@@ -121,44 +118,44 @@ public class MessageFramer implements Listener<Boolean>,
*/
public void flush()
{
for( MessageAssembler assembler: mMessageAssemblers )
for(MessageAssembler assembler : mMessageAssemblers)
{
assembler.flush();
}
}
@Override
public void setSyncDetectListener( SyncDetectListener listener )
@Override
public void setSyncDetectListener(ISyncDetectListener listener)
{
mSyncDetectListener = listener;
mSyncDetectListener = listener;
}
/**
* Allow a message listener to register with this framer to receive
* all framed messages
*/
public void addMessageListener( Listener<BinaryMessage> listener )
public void addMessageListener(Listener<BinaryMessage> listener)
{
mBroadcaster.addListener( listener );
mBroadcaster.addListener(listener);
}
public void removeMessageListener( Listener<BinaryMessage> listener )
public void removeMessageListener(Listener<BinaryMessage> listener)
{
mBroadcaster.removeListener( listener );
mBroadcaster.removeListener(listener);
}
/*
* Adds a message assembler to receive bits from the bit stream
*/
private void addMessageAssembler( MessageAssembler assembler )
private void addMessageAssembler(MessageAssembler assembler)
{
mMessageAssemblers.add( assembler );
mMessageAssemblers.add(assembler);
}
@SuppressWarnings( "unused" )
private void removeMessageAssembler( MessageAssembler assembler )
@SuppressWarnings("unused")
private void removeMessageAssembler(MessageAssembler assembler)
{
mMessageAssemblers.remove( assembler );
mMessageAssemblers.remove(assembler);
}
/**
@@ -166,7 +163,7 @@ public class MessageFramer implements Listener<Boolean>,
* identified sync pattern, and every bit thereafter. Once the accumulated
* bits equal the message length, the message is sent and the assembler
* flags itself as complete.
*
*
* By design, multiple message assemblers can exist at the same time, each
* assembling different, overlapping potential messages
*/
@@ -174,46 +171,46 @@ public class MessageFramer implements Listener<Boolean>,
{
BinaryMessage mMessage;
boolean mComplete = false;
MessageAssembler( int messageLength )
MessageAssembler(int messageLength)
{
mMessage = new BinaryMessage( messageLength );
mMessage = new BinaryMessage(messageLength);
}
MessageAssembler( int messageLength, boolean[] initialFill )
MessageAssembler(int messageLength, boolean[] initialFill)
{
this( messageLength );
this(messageLength);
/* Pre-load the message with the sync pattern */
for( int x = 0; x < initialFill.length; x++ )
for(int x = 0; x < initialFill.length; x++)
{
receive( initialFill[ x ] );
receive(initialFill[x]);
}
}
public void dispose()
{
mMessage = null;
mMessage = null;
}
@Override
/**
* Receives one bit at a time, and assembles them into a message
*/
public void receive( Boolean bit )
public void receive(Boolean bit)
{
try
{
mMessage.add( bit );
mMessage.add(bit);
}
catch( BitSetFullException e )
catch(BitSetFullException e)
{
e.printStackTrace();
}
/* Once our message is complete (ie full), send it to all registered
* message listeners, and set complete flag so for auto-removal */
if( mMessage.isFull() )
if(mMessage.isFull())
{
mComplete = true;
flush();
@@ -226,7 +223,7 @@ public class MessageFramer implements Listener<Boolean>,
*/
public void flush()
{
mBroadcaster.receive( mMessage );
mBroadcaster.receive(mMessage);
mComplete = true;
}

View File

@@ -1,5 +1,7 @@
package io.github.dsheirer.bits;
import io.github.dsheirer.dsp.symbol.ISyncDetectListener;
public class SoftSyncDetector implements ISyncProcessor
{
private ISyncDetectListener mListener;

View File

@@ -1,5 +1,7 @@
package io.github.dsheirer.bits;
import io.github.dsheirer.dsp.symbol.ISyncDetectListener;
public class SyncDetector implements ISyncProcessor
{
private long mPattern;

View File

@@ -29,7 +29,7 @@ public enum SyncPattern
false, false, false, false //0000 0x0
} ),
/* Revs (0x0A) and Sync (0x23EB) = 10101 0010001111101011 */
/* Revs (0x0A) and Sync (0x23EB) = 0101 000 1 000 11111 0 1 0 11 */
FLEETSYNC2( new boolean[]
{
false, true, false, true, false, //End of bit revs

View File

@@ -1,17 +1,17 @@
/*******************************************************************************
* SDR Trunk
* Copyright (C) 2014 Dennis Sheirer
*
*
* This program is free software: you can redistribute it and/or modify
* it under the terms of the GNU General Public License as published by
* the Free Software Foundation, either version 3 of the License, or
* (at your option) any later version.
*
*
* This program is distributed in the hope that it will be useful,
* but WITHOUT ANY WARRANTY; without even the implied warranty of
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
* GNU General Public License for more details.
*
*
* You should have received a copy of the GNU General Public License
* along with this program. If not, see <http://www.gnu.org/licenses/>
******************************************************************************/
@@ -20,113 +20,112 @@ package io.github.dsheirer.bits;
/**
* Sync pattern matcher.
*
*
* Note: works for sync patterns up to 63 bits (integer size - 1 ) long.
* If a 64 bit or larger sync pattern is applied, then the wrapping feature
* of the Long.rotate methods will wrap the MSB around to the LSB and
* corrupt the matching value.
*
*/
public class SyncPatternMatcher
{
private long mBits = 0;
private long mMask = 0;
private long mSync = 0;
private boolean mSoftMode = false;
private int mSoftModeErrorThreshold = 0;
public SyncPatternMatcher( boolean[] syncPattern )
{
//Setup a bit mask of all ones the length of the sync pattern
mMask = (long)( ( Math.pow( 2, syncPattern.length ) ) - 1 );
//Convert the sync bits into a long value for comparison
for( int x = 0; x < syncPattern.length; x++ )
{
if( syncPattern[ x ] )
{
mSync += 1l << ( syncPattern.length - 1 - x );
}
}
}
public SyncPatternMatcher( long sync )
{
mSync = sync;
mMask = getMask( sync );
}
private long mBits = 0;
private long mMask = 0;
private long mSync = 0;
private boolean mSoftMode = false;
private int mSoftModeErrorThreshold = 0;
/**
* Constructs the sync pattern matcher and defines the soft mode maximum
* number of bit errors to allow in the pattern match.
*
* @param sync pattern
* @param softModeErrorThreshold - maximum bit errors (ie pattern mismatch
* positions) when considering a sync match
*/
public SyncPatternMatcher( long sync, int softModeErrorThreshold )
{
this( sync );
mSoftModeErrorThreshold = softModeErrorThreshold;
}
public SyncPatternMatcher(boolean[] syncPattern)
{
//Setup a bit mask of all ones the length of the sync pattern
mMask = (long)((Math.pow(2, syncPattern.length)) - 1);
/**
* Generates an all ones mask corresponding to the sync pattern
*/
private static long getMask( long sync )
{
long mask = sync;
for( int x = 1; x < 64; x *= 2 )
{
mask |= ( mask >> x );
}
return mask;
}
public void receive( boolean bit )
{
//Left shift the previous value
mBits = Long.rotateLeft( mBits, 1 );
//Apply the mask to erase the previous MSB
mBits &= mMask;
//Add in the new bit
if( bit )
{
mBits += 1;
}
}
//Convert the sync bits into a long value for comparison
for(int x = 0; x < syncPattern.length; x++)
{
if(syncPattern[x])
{
mSync += 1l << (syncPattern.length - 1 - x);
}
}
}
/**
* Indicates if the most recently received bit sequence matches the
* sync pattern
*/
public boolean matches()
{
if( mSoftMode )
{
long difference = mBits ^ mSync;
return difference == 0 ||
Long.bitCount( difference ) <= mSoftModeErrorThreshold;
}
else
{
return ( mBits == mSync );
}
}
public SyncPatternMatcher(long sync)
{
mSync = sync;
mMask = getMask(sync);
}
/**
* Sets the sync pattern matcher in soft mode which allows up to the soft
* mode threshold number of bit errors when considering a sync pattern match
*/
public void setSoftMode( boolean softMode )
{
mSoftMode = softMode;
}
/**
* Constructs the sync pattern matcher and defines the soft mode maximum
* number of bit errors to allow in the pattern match.
*
* @param sync pattern
* @param softModeErrorThreshold - maximum bit errors (ie pattern mismatch
* positions) when considering a sync match
*/
public SyncPatternMatcher(long sync, int softModeErrorThreshold)
{
this(sync);
mSoftModeErrorThreshold = softModeErrorThreshold;
}
/**
* Generates an all ones mask corresponding to the sync pattern
*/
private static long getMask(long sync)
{
long mask = sync;
for(int x = 1; x < 64; x *= 2)
{
mask |= (mask >> x);
}
return mask;
}
public void receive(boolean bit)
{
//Left shift the previous value
mBits = Long.rotateLeft(mBits, 1);
//Apply the mask to erase the previous MSB
mBits &= mMask;
//Add in the new bit
if(bit)
{
mBits += 1;
}
}
/**
* Indicates if the most recently received bit sequence matches the
* sync pattern
*/
public boolean matches()
{
if(mSoftMode)
{
long difference = mBits ^ mSync;
return difference == 0 ||
Long.bitCount(difference) <= mSoftModeErrorThreshold;
}
else
{
return (mBits == mSync);
}
}
/**
* Sets the sync pattern matcher in soft mode which allows up to the soft
* mode threshold number of bit errors when considering a sync pattern match
*/
public void setSoftMode(boolean softMode)
{
mSoftMode = softMode;
}
}

View File

@@ -1,107 +1,89 @@
/*******************************************************************************
* SDR Trunk
* Copyright (C) 2014 Dennis Sheirer
*
*
* This program is free software: you can redistribute it and/or modify
* it under the terms of the GNU General Public License as published by
* the Free Software Foundation, either version 3 of the License, or
* (at your option) any later version.
*
*
* This program is distributed in the hope that it will be useful,
* but WITHOUT ANY WARRANTY; without even the implied warranty of
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
* GNU General Public License for more details.
*
*
* You should have received a copy of the GNU General Public License
* along with this program. If not, see <http://www.gnu.org/licenses/>
******************************************************************************/
package io.github.dsheirer.buffer;
import java.util.Arrays;
/**
* Circular Buffer - implements a circular buffer with a method to get the
* current head of the buffer, and put a new value in its place at the same
* time
*
*
* Also provides averaging over the elements in the buffer
*/
public class BooleanAveragingBuffer {
public class BooleanAveragingBuffer
{
private boolean[] mBuffer;
private int mBufferPointer;
private int mThreshold;
private int mTrueCount;
private boolean[] mBuffer;
private int mBufferPointer;
public BooleanAveragingBuffer( int length )
{
mBuffer = new boolean[ length ];
//Preload the array with false values
Arrays.fill( mBuffer, false );
}
public boolean get( boolean newValue )
{
//Fetch current boolean value
boolean retVal = mBuffer[ mBufferPointer ];
public BooleanAveragingBuffer(int length)
{
mBuffer = new boolean[length];
mThreshold = length / 2;
}
//Load the new value into the buffer
put( newValue );
return retVal;
}
public boolean get(boolean newValue)
{
//Fetch current boolean value
boolean retVal = mBuffer[mBufferPointer];
/**
* Loads the newValue into this buffer and adjusts the buffer pointer
* to prepare for the next get/put cycle
*/
private void put( boolean newValue )
{
//Store the new value to the buffer
mBuffer[ mBufferPointer ] = newValue;
//Increment the buffer pointer
mBufferPointer++;
//Load the new value into the buffer
put(newValue);
//Wrap the buffer pointer around to 0 when necessary
if( mBufferPointer >= mBuffer.length )
{
mBufferPointer = 0;
}
}
return retVal;
}
/**
* Loads the newValue into the buffer, calculates the average
* and returns that average from this method
*
* This effectively performs low-pass filtering
*/
public boolean getAverage( boolean newValue )
{
//Load the new value into the buffer
put( newValue );
int trueCount = 0;
for( int x = 0; x < mBuffer.length; x++ )
{
if( mBuffer[ x ] )
{
trueCount++;
}
}
/**
* Loads the newValue into this buffer and adjusts the buffer pointer
* to prepare for the next get/put cycle
*/
private void put(boolean newValue)
{
//Store the new value to the buffer
mBuffer[mBufferPointer] = newValue;
/**
* If the number of true values in the buffer is
* more than half, return true, otherwise, false
*/
if( trueCount > (int)( mBuffer.length / 2 ) )
{
return true;
}
else
{
return false;
}
}
//Increment the buffer pointer
mBufferPointer++;
mBufferPointer = mBufferPointer % mBuffer.length;
}
/**
* Loads the newValue into the buffer, calculates the average
* and returns that average from this method
*
* This effectively performs low-pass filtering
*/
public boolean getAverage(boolean newValue)
{
//Load the new value into the buffer
put(newValue);
mTrueCount = 0;
for(int x = 0; x < mBuffer.length; x++)
{
if(mBuffer[x])
{
mTrueCount++;
}
}
return mTrueCount > mThreshold;
}
}

View File

@@ -1,3 +1,18 @@
/*******************************************************************************
* sdr-trunk
* Copyright (C) 2014-2018 Dennis Sheirer
*
* This program is free software: you can redistribute it and/or modify it under the terms of the GNU General Public
* License as published by the Free Software Foundation, either version 3 of the License, or (at your option) any
* later version.
*
* This program is distributed in the hope that it will be useful, but WITHOUT ANY WARRANTY; without even the implied
* warranty of MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU General Public License for more details.
*
* You should have received a copy of the GNU General Public License along with this program.
* If not, see <http://www.gnu.org/licenses/>
*
******************************************************************************/
package io.github.dsheirer.buffer;
import io.github.dsheirer.sample.Listener;
@@ -8,87 +23,86 @@ import java.util.List;
public class CircularBuffer<T> implements Listener<T>
{
private ArrayList<T> mElements;
private Listener<T> mListener;
private int mPointer;
private int mSize;
private List<T> mElements;
private Listener<T> mListener;
private int mPointer;
private int mSize;
/**
* Circular Buffer - stores elements in an array list. Once the buffer is
* full, new samples replace older samples and older samples are broadcast
* to an optional listener.
*
* @param size - size of buffer
*/
public CircularBuffer( int size )
{
mSize = size;
mElements = new ArrayList<T>();
}
/**
* Circular Buffer - stores elements in an array list. Once the buffer is
* full, new samples replace older samples and older samples are broadcast
* to an optional listener.
*
* @param size - size of buffer
*/
public CircularBuffer(int size)
{
mSize = size;
mElements = new ArrayList<T>();
}
/**
* Returns the element stored at index. Note: the index argument refers to
* the internal storage index and does not indicate a temporal relationship
* among elements. For example, if you get indexes 1, 2, and 3, you may be
* getting ( oldest - 1, oldest, newest ) elements, if the internal pointer
* is currently pointing to and will insert the next element at index 4.
*
* @param index - element index
*
* @return element or null
*/
public T get( int index )
{
if( index < mElements.size() )
{
return mElements.get( index );
}
return null;
}
public int getSize()
{
return mSize;
}
public List<T> getElements()
{
return Collections.unmodifiableList( mElements );
}
@Override
public void receive( T element )
{
T previous = null;
if( mElements.size() > mPointer )
{
previous = mElements.get( mPointer );
mElements.set( mPointer, element );
}
else
{
mElements.add( element );
}
mPointer++;
if( mPointer >= mSize )
{
mPointer = 0;
}
if( previous != null && mListener != null )
{
mListener.receive( previous );
}
}
/**
* Returns the element stored at index. Note: the index argument refers to
* the internal storage index and does not indicate a temporal relationship
* among elements. For example, if you get indexes 1, 2, and 3, you may be
* getting ( oldest - 1, oldest, newest ) elements, if the internal pointer
* is currently pointing to and will insert the next element at index 4.
*
* @param index - element index
* @return element or null
*/
public T get(int index)
{
if(index < mElements.size())
{
return mElements.get(index);
}
public void setListener( Listener<T> listener )
{
mListener = listener;
}
return null;
}
public int getSize()
{
return mSize;
}
public List<T> getElements()
{
return Collections.unmodifiableList(mElements);
}
@Override
public void receive(T element)
{
T previous = null;
if(mElements.size() > mPointer)
{
previous = mElements.get(mPointer);
mElements.set(mPointer, element);
}
else
{
mElements.add(element);
}
mPointer++;
if(mPointer >= mSize)
{
mPointer = 0;
}
if(previous != null && mListener != null)
{
mListener.receive(previous);
}
}
public void setListener(Listener<T> listener)
{
mListener = listener;
}
}

View File

@@ -0,0 +1,124 @@
/*******************************************************************************
* 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.buffer;
/**
* Circular sample buffer - allocates a buffer and stores samples in a circular
* fashion, overwriting older samples with newly arrived samples.
*
* Can be used as a delay-type buffer, to delay samples by the 'size' amount
*/
public class RealCircularBuffer
{
float[] mBuffer;
int mBufferPointer = 0;
public RealCircularBuffer(int size)
{
mBuffer = new float[size];
}
public int getSize()
{
return mBuffer.length;
}
public float[] getBuffer()
{
return mBuffer;
}
/**
* Puts the new value into the buffer and returns the oldest buffer value
* that it replaced
*
* @param newValue
* @return
*/
public float putAndGet(float newValue)
{
float oldestSample = mBuffer[mBufferPointer];
put(newValue);
return oldestSample;
}
/**
* Adds the new value to this buffer.
*/
public void put(float newValue)
{
mBuffer[mBufferPointer] = newValue;
mBufferPointer++;
if(mBufferPointer >= mBuffer.length)
{
mBufferPointer = 0;
}
}
/**
* Returns the sample at the specified index, where index 0 is the oldest sample and index (length - 1) is the
* newest sample.
* @param index in range of 0 to length-1
* @return value at indexed position
*/
public float get(int index)
{
if(index < mBuffer.length)
{
int pointer = mBufferPointer + index;
if(pointer >= mBuffer.length)
{
pointer -= mBuffer.length;
}
return mBuffer[pointer];
}
else
{
throw new ArrayIndexOutOfBoundsException("Index [" + index + "] is not valid for the buffer size of [" +
mBuffer.length + "]");
}
}
/**
* Returns the contents of the circular buffer unwrapped where index 0 is the oldest sample and index length-1
* is the newest sample.
*
* @return array of samples
*/
public float[] get()
{
float[] samples = new float[mBuffer.length];
System.arraycopy(mBuffer, mBufferPointer, samples, 0, mBuffer.length - mBufferPointer);
if(mBufferPointer != 0)
{
System.arraycopy(mBuffer, 0, samples, mBuffer.length - mBufferPointer, mBufferPointer);
}
return samples;
}
}

View File

@@ -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();

View File

@@ -25,8 +25,6 @@ import io.github.dsheirer.module.decode.DecoderType;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import java.util.concurrent.ScheduledExecutorService;
/**
* Basic decoder channel state - provides the minimum channel state functionality
* to support an always un-squelched audio decoder.
@@ -94,7 +92,7 @@ public class AlwaysUnsquelchedDecoderState extends DecoderState
}
@Override
public void start(ScheduledExecutorService executor)
public void start()
{
broadcast(new AttributeChangeRequest<String>(Attribute.PRIMARY_ADDRESS_TO, NO_SQUELCH));
broadcast(new DecoderStateEvent(this, Event.ALWAYS_UNSQUELCH, State.IDLE));

View File

@@ -20,8 +20,6 @@ package io.github.dsheirer.channel.state;
import io.github.dsheirer.audio.squelch.ISquelchStateProvider;
import io.github.dsheirer.audio.squelch.SquelchState;
import io.github.dsheirer.channel.heartbeat.Heartbeat;
import io.github.dsheirer.channel.heartbeat.IHeartbeatListener;
import io.github.dsheirer.channel.metadata.Attribute;
import io.github.dsheirer.channel.metadata.AttributeChangeRequest;
import io.github.dsheirer.channel.metadata.IAttributeChangeRequestListener;
@@ -34,19 +32,19 @@ import io.github.dsheirer.module.decode.DecoderType;
import io.github.dsheirer.module.decode.config.DecodeConfiguration;
import io.github.dsheirer.module.decode.event.CallEvent;
import io.github.dsheirer.module.decode.event.ICallEventProvider;
import io.github.dsheirer.sample.IOverflowListener;
import io.github.dsheirer.sample.Listener;
import io.github.dsheirer.sample.real.IOverflowListener;
import io.github.dsheirer.source.tuner.frequency.FrequencyChangeEvent;
import io.github.dsheirer.source.tuner.frequency.IFrequencyChangeListener;
import io.github.dsheirer.source.ISourceEventListener;
import io.github.dsheirer.source.SourceEvent;
import io.github.dsheirer.source.heartbeat.Heartbeat;
import io.github.dsheirer.source.heartbeat.IHeartbeatListener;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import java.util.concurrent.ScheduledExecutorService;
import static io.github.dsheirer.source.tuner.frequency.FrequencyChangeEvent.Event.NOTIFICATION_FREQUENCY_CHANGE;
import static io.github.dsheirer.source.SourceEvent.Event.NOTIFICATION_FREQUENCY_CHANGE;
public class ChannelState extends Module implements ICallEventProvider, IDecoderStateEventListener,
IDecoderStateEventProvider, IFrequencyChangeListener, IHeartbeatListener, ISquelchStateProvider,
IDecoderStateEventProvider, ISourceEventListener, IHeartbeatListener, ISquelchStateProvider,
IAttributeChangeRequestListener, IOverflowListener
{
private final static Logger mLog = LoggerFactory.getLogger(ChannelState.class);
@@ -64,7 +62,7 @@ public class ChannelState extends Module implements ICallEventProvider, IDecoder
private ChannelType mChannelType;
private TrafficChannelManager mTrafficChannelEndListener;
private CallEvent mTrafficChannelCallEvent;
private FrequencyChangeListener mFrequencyChangeListener;
private SourceChangeListener mSourceChangeListener;
private boolean mSquelchLocked = false;
private boolean mSelected = false;
@@ -124,7 +122,7 @@ public class ChannelState extends Module implements ICallEventProvider, IDecoder
}
@Override
public void start(ScheduledExecutorService executor)
public void start()
{
mMutableMetadata.receive(new AttributeChangeRequest<State>(Attribute.CHANNEL_STATE, mState));
@@ -160,14 +158,14 @@ public class ChannelState extends Module implements ICallEventProvider, IDecoder
}
@Override
public Listener<FrequencyChangeEvent> getFrequencyChangeListener()
public Listener<SourceEvent> getSourceEventListener()
{
if(mFrequencyChangeListener == null)
if(mSourceChangeListener == null)
{
mFrequencyChangeListener = new FrequencyChangeListener();
mSourceChangeListener = new SourceChangeListener();
}
return mFrequencyChangeListener;
return mSourceChangeListener;
}
private boolean isStandardChannel()
@@ -520,14 +518,14 @@ public class ChannelState extends Module implements ICallEventProvider, IDecoder
/**
* Listener to receive frequency change events and rebroadcast them to the decoder states
*/
public class FrequencyChangeListener implements Listener<FrequencyChangeEvent>
public class SourceChangeListener implements Listener<SourceEvent>
{
@Override
public void receive(FrequencyChangeEvent frequencyChangeEvent)
public void receive(SourceEvent sourceEvent)
{
if(frequencyChangeEvent.getEvent() == NOTIFICATION_FREQUENCY_CHANGE)
if(sourceEvent.getEvent() == NOTIFICATION_FREQUENCY_CHANGE)
{
long frequency = frequencyChangeEvent.getValue().longValue();
long frequency = sourceEvent.getValue().longValue();
broadcast(new DecoderStateEvent(this, Event.SOURCE_FREQUENCY, getState(), frequency));
}

View File

@@ -1,19 +1,16 @@
/*******************************************************************************
* sdrtrunk
* sdr-trunk
* Copyright (C) 2014-2017 Dennis Sheirer
*
* This program is free software: you can redistribute it and/or modify
* it under the terms of the GNU General Public License as published by
* the Free Software Foundation, either version 3 of the License, or
* (at your option) any later version.
* This program is free software: you can redistribute it and/or modify it under the terms of the GNU General Public
* License as published by the Free Software Foundation, either version 3 of the License, or (at your option) any
* later version.
*
* This program is distributed in the hope that it will be useful,
* but WITHOUT ANY WARRANTY; without even the implied warranty of
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
* GNU General Public License for more details.
* This program is distributed in the hope that it will be useful, but WITHOUT ANY WARRANTY; without even the implied
* warranty of MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU General Public License for more details.
*
* You should have received a copy of the GNU General Public License
* along with this program. If not, see <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.channel.traffic;
@@ -29,13 +26,12 @@ import io.github.dsheirer.controller.channel.TrafficChannelEvent;
import io.github.dsheirer.module.Module;
import io.github.dsheirer.module.decode.config.DecodeConfiguration;
import io.github.dsheirer.module.decode.event.CallEvent;
import io.github.dsheirer.module.decode.event.CallEvent.CallEventType;
import io.github.dsheirer.module.decode.event.ICallEventProvider;
import io.github.dsheirer.module.log.config.EventLogConfiguration;
import io.github.dsheirer.record.config.RecordConfiguration;
import io.github.dsheirer.sample.Listener;
import io.github.dsheirer.source.config.SourceConfigTuner;
import io.github.dsheirer.source.tuner.TunerChannel;
import io.github.dsheirer.source.tuner.TunerChannel.Type;
import io.github.dsheirer.source.tuner.channel.TunerChannel;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
@@ -43,7 +39,6 @@ import java.util.ArrayList;
import java.util.List;
import java.util.Map;
import java.util.concurrent.ConcurrentHashMap;
import java.util.concurrent.ScheduledExecutorService;
public class TrafficChannelManager extends Module implements ICallEventProvider, IDecoderStateEventListener
{
@@ -61,7 +56,6 @@ public class TrafficChannelManager extends Module implements ICallEventProvider,
private ChannelModel mChannelModel;
private DecodeConfiguration mDecodeConfiguration;
private EventLogConfiguration mEventLogConfiguration;
private RecordConfiguration mRecordConfiguration;
private String mSystem;
private String mSite;
@@ -75,7 +69,6 @@ public class TrafficChannelManager extends Module implements ICallEventProvider,
* @param channelModel containing channels currently in use
* @param decodeConfiguration - decoder configuration to use for each
* traffic channel allocation.
* @param eventLogConfiguration - logging configuration to use for traffic channels
* @param recordConfiguration - recording options for each traffic channel
* @param system label to use to describe the system
* @param site label to use to describe the site
@@ -85,7 +78,6 @@ public class TrafficChannelManager extends Module implements ICallEventProvider,
*/
public TrafficChannelManager(ChannelModel channelModel,
DecodeConfiguration decodeConfiguration,
EventLogConfiguration eventLogConfiguration,
RecordConfiguration recordConfiguration,
String system,
String site,
@@ -94,7 +86,6 @@ public class TrafficChannelManager extends Module implements ICallEventProvider,
{
mChannelModel = channelModel;
mDecodeConfiguration = decodeConfiguration;
mEventLogConfiguration = eventLogConfiguration;
mRecordConfiguration = recordConfiguration;
mSystem = system;
mSite = site;
@@ -150,8 +141,6 @@ public class TrafficChannelManager extends Module implements ICallEventProvider,
channel.setDecodeConfiguration(mDecodeConfiguration);
channel.setEventLogConfiguration(mEventLogConfiguration);
channel.setRecordConfiguration(mRecordConfiguration);
channel.setAliasListName(mAliasListName);
@@ -161,7 +150,7 @@ public class TrafficChannelManager extends Module implements ICallEventProvider,
mTrafficChannelPool.add(channel);
}
/* If we have a configured channel, update metadata */
/* If we have a configured channel, update metadata */
if(channel != null)
{
channel.setSourceConfiguration(new SourceConfigTuner(tunerChannel));
@@ -184,7 +173,7 @@ public class TrafficChannelManager extends Module implements ICallEventProvider,
{
CallEvent callEvent = event.getCallEvent();
/* Check for duplicate events and suppress */
/* Check for duplicate events and suppress */
synchronized(mTrafficChannelsInUse)
{
if(mTrafficChannelsInUse.containsKey(callEvent.getChannel()))
@@ -196,8 +185,8 @@ public class TrafficChannelManager extends Module implements ICallEventProvider,
if(frequency > 0)
{
Channel channel = getChannel(callEvent.getChannel(), new TunerChannel(Type.TRAFFIC,
frequency, mDecodeConfiguration.getDecoderType().getChannelBandwidth()));
Channel channel = getChannel(callEvent.getChannel(), new TunerChannel(frequency,
mDecodeConfiguration.getDecoderType().getChannelBandwidth()));
if(channel != null)
{
@@ -213,7 +202,7 @@ public class TrafficChannelManager extends Module implements ICallEventProvider,
}
else
{
callEvent.setCallEventType(CallEvent.CallEventType.CALL_DETECT);
callEvent.setCallEventType(CallEventType.CALL_DETECT);
String details = callEvent.getDetails();
@@ -230,7 +219,7 @@ public class TrafficChannelManager extends Module implements ICallEventProvider,
}
else
{
callEvent.setCallEventType(CallEvent.CallEventType.CALL_DETECT);
callEvent.setCallEventType(CallEventType.CALL_DETECT);
String details = callEvent.getDetails();
@@ -247,7 +236,7 @@ public class TrafficChannelManager extends Module implements ICallEventProvider,
}
else
{
callEvent.setCallEventType(CallEvent.CallEventType.CALL_DETECT);
callEvent.setCallEventType(CallEventType.CALL_DETECT);
String details = callEvent.getDetails();
@@ -337,7 +326,7 @@ public class TrafficChannelManager extends Module implements ICallEventProvider,
}
@Override
public void start(ScheduledExecutorService executor)
public void start()
{
}
@@ -348,7 +337,7 @@ public class TrafficChannelManager extends Module implements ICallEventProvider,
{
List<String> channels = new ArrayList<>();
/* Copy the keyset so we don't get concurrent modification of the map */
/* Copy the keyset so we don't get concurrent modification of the map */
channels.addAll(mTrafficChannelsInUse.keySet());
for(String channel : channels)
@@ -399,4 +388,4 @@ public class TrafficChannelManager extends Module implements ICallEventProvider,
}
}
}
}
}

View File

@@ -31,18 +31,16 @@ import io.github.dsheirer.module.log.config.EventLogConfiguration;
import io.github.dsheirer.record.RecorderType;
import io.github.dsheirer.record.config.RecordConfiguration;
import io.github.dsheirer.sample.Listener;
import io.github.dsheirer.source.SourceEvent;
import io.github.dsheirer.source.SourceType;
import io.github.dsheirer.source.config.SourceConfigFactory;
import io.github.dsheirer.source.config.SourceConfigRecording;
import io.github.dsheirer.source.config.SourceConfigTuner;
import io.github.dsheirer.source.config.SourceConfiguration;
import io.github.dsheirer.source.tuner.TunerChannel;
import io.github.dsheirer.source.tuner.TunerChannel.Type;
import io.github.dsheirer.source.tuner.frequency.FrequencyChangeEvent;
import io.github.dsheirer.source.tuner.frequency.FrequencyChangeEvent.Event;
import io.github.dsheirer.source.tuner.channel.TunerChannel;
@JacksonXmlRootElement(localName = "channel")
public class Channel extends Configuration implements Listener<FrequencyChangeEvent>
public class Channel extends Configuration implements Listener<SourceEvent>
{
// Standard channels are persisted and traffic channels are temporary
public enum ChannelType
@@ -490,15 +488,14 @@ public class Channel extends Configuration implements Listener<FrequencyChangeEv
{
SourceConfigTuner config = (SourceConfigTuner)mSourceConfiguration;
mTunerChannel = new TunerChannel(Type.LOCKED, config.getFrequency(),
mTunerChannel = new TunerChannel(config.getFrequency(),
mDecodeConfiguration.getDecoderType().getChannelBandwidth());
}
else if(mSourceConfiguration.getSourceType() == SourceType.RECORDING)
{
SourceConfigRecording config =
(SourceConfigRecording)mSourceConfiguration;
SourceConfigRecording config = (SourceConfigRecording)mSourceConfiguration;
mTunerChannel = new TunerChannel(Type.LOCKED, config.getFrequency(),
mTunerChannel = new TunerChannel(config.getFrequency(),
mDecodeConfiguration.getDecoderType().getChannelBandwidth());
}
}
@@ -524,9 +521,9 @@ public class Channel extends Configuration implements Listener<FrequencyChangeEv
* be used to visually show in the spectral display.
*/
@Override
public void receive(FrequencyChangeEvent event)
public void receive(SourceEvent event)
{
if(event.getEvent() == Event.NOTIFICATION_CHANNEL_FREQUENCY_CORRECTION_CHANGE)
if(event.getEvent() == SourceEvent.Event.NOTIFICATION_CHANNEL_FREQUENCY_CORRECTION_CHANGE)
{
mChannelFrequencyCorrection = event.getValue().intValue();
}

View File

@@ -19,7 +19,6 @@
package io.github.dsheirer.controller.channel;
import io.github.dsheirer.alias.AliasModel;
import io.github.dsheirer.audio.AudioPacket;
import io.github.dsheirer.channel.metadata.Attribute;
import io.github.dsheirer.channel.metadata.AttributeChangeRequest;
import io.github.dsheirer.channel.metadata.ChannelMetadataModel;
@@ -35,6 +34,7 @@ import io.github.dsheirer.module.log.EventLogManager;
import io.github.dsheirer.record.RecorderManager;
import io.github.dsheirer.record.RecorderType;
import io.github.dsheirer.sample.Listener;
import io.github.dsheirer.sample.buffer.ReusableAudioPacket;
import io.github.dsheirer.source.Source;
import io.github.dsheirer.source.SourceException;
import io.github.dsheirer.source.SourceManager;
@@ -54,7 +54,7 @@ public class ChannelProcessingManager implements ChannelEventListener
private Map<Integer,ProcessingChain> mProcessingChains = new HashMap<>();
private List<Listener<AudioPacket>> mAudioPacketListeners = new CopyOnWriteArrayList<>();
private List<Listener<ReusableAudioPacket>> mAudioPacketListeners = new CopyOnWriteArrayList<>();
private List<Listener<Message>> mMessageListeners = new CopyOnWriteArrayList<>();
private ChannelModel mChannelModel;
@@ -232,7 +232,7 @@ public class ChannelProcessingManager implements ChannelEventListener
processingChain = new ProcessingChain(channel.getChannelType());
/* Register global listeners */
for(Listener<AudioPacket> listener : mAudioPacketListeners)
for(Listener<ReusableAudioPacket> listener : mAudioPacketListeners)
{
processingChain.addAudioPacketListener(listener);
}
@@ -367,7 +367,7 @@ public class ChannelProcessingManager implements ChannelEventListener
* Adds a message listener that will be added to all channels to receive
* any messages.
*/
public void addAudioPacketListener(Listener<AudioPacket> listener)
public void addAudioPacketListener(Listener<ReusableAudioPacket> listener)
{
mAudioPacketListeners.add(listener);
}
@@ -375,7 +375,7 @@ public class ChannelProcessingManager implements ChannelEventListener
/**
* Removes a message listener.
*/
public void removeAudioPacketListener(Listener<AudioPacket> listener)
public void removeAudioPacketListener(Listener<ReusableAudioPacket> listener)
{
mAudioPacketListeners.remove(listener);
}

View File

@@ -1,77 +1,76 @@
/*******************************************************************************
* SDR Trunk
* Copyright (C) 2014 Dennis Sheirer
*
* This program is free software: you can redistribute it and/or modify
* it under the terms of the GNU General Public License as published by
* the Free Software Foundation, either version 3 of the License, or
* (at your option) any later version.
*
* This program is distributed in the hope that it will be useful,
* but WITHOUT ANY WARRANTY; without even the implied warranty of
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
* GNU General Public License for more details.
*
* You should have received a copy of the GNU General Public License
* along with this program. If not, see <http://www.gnu.org/licenses/>
* 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;
import io.github.dsheirer.bits.IBinarySymbolProcessor;
import io.github.dsheirer.sample.Listener;
public class NRZDecoder implements Listener<Boolean>
public class NRZDecoder implements IBinarySymbolProcessor
{
public final static boolean MODE_NORMAL = true;
public final static boolean MODE_INVERTED = false;
private Listener<Boolean> mListener;
private boolean mMode = MODE_NORMAL;
private boolean mPrevious;
public final static boolean MODE_NORMAL = true;
public final static boolean MODE_INVERTED = false;
/**
* Non Return To Zero (NRZ) and NRZ Inverted (NRZ-I) Decoder.
*
* Use default constructor for NRZ output. Use the alternate constructor
* with the MODE_INVERTED argument for NRZ-I output.
*
* Performs XOR of incoming bit with previous output and sends the result
* to the registered listener.
*/
public NRZDecoder( boolean mode )
{
mMode = mode;
}
public NRZDecoder()
{
this( MODE_NORMAL );
}
public void dispose()
{
mListener = null;
}
private IBinarySymbolProcessor mListener;
private boolean mMode = MODE_NORMAL;
private boolean mPrevious;
@Override
public void receive( Boolean bit )
/**
* Non Return To Zero (NRZ) and NRZ Inverted (NRZ-I) Decoder.
*
* Use default constructor for NRZ output. Use the alternate constructor
* with the MODE_INVERTED argument for NRZ-I output.
*
* Performs XOR of incoming bit with previous output and sends the result
* to the registered listener.
*/
public NRZDecoder(boolean mode)
{
boolean result = mPrevious ^ bit;
if( mListener != null )
{
mListener.receive( mMode ? result : !result );
}
mPrevious = result;
mMode = mode;
}
public void setListener( Listener<Boolean> listener )
{
mListener = listener;
}
public void removeListener( Listener<Boolean> listener )
{
mListener = null;
}
public NRZDecoder()
{
this(MODE_NORMAL);
}
public void dispose()
{
mListener = null;
}
@Override
public void receive(boolean symbol)
{
boolean result = mPrevious ^ symbol;
if(mListener != null)
{
mListener.receive(mMode ? result : !result);
}
mPrevious = result;
}
public void setListener(IBinarySymbolProcessor listener)
{
mListener = listener;
}
public void removeListener(Listener<Boolean> listener)
{
mListener = null;
}
}

View File

@@ -0,0 +1,290 @@
/*******************************************************************************
* 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.afsk;
import io.github.dsheirer.bits.IBinarySymbolProcessor;
import io.github.dsheirer.buffer.FloatAveragingBuffer;
import io.github.dsheirer.dsp.filter.design.FilterDesignException;
import io.github.dsheirer.dsp.filter.fir.FIRFilterSpecification;
import io.github.dsheirer.dsp.filter.fir.real.RealFIRFilter2;
import io.github.dsheirer.dsp.filter.fir.remez.RemezFIRFilterDesigner;
import io.github.dsheirer.dsp.filter.resample.RealResampler;
import io.github.dsheirer.dsp.mixer.IOscillator;
import io.github.dsheirer.dsp.mixer.Oscillator;
import io.github.dsheirer.sample.Listener;
import io.github.dsheirer.sample.buffer.ReusableBuffer;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
/**
* Audio Frequency Shift Keying (AFSK) 1200-baud correlation decoder for decoding 8 kHz FM-demodulated audio samples.
*
* Note: internally the incoming 8 kHz sample rate is resampled to 7200 Hz to be an integral of the 1200 Hz baud rate
* which equates to six samples per symbol. Each symbol is correlated over an eight sample period with correlation
* values averaged over a seven correlation value period.
*
* Provides normal or inverted decoded output.
*/
public class AFSK1200Decoder implements Listener<ReusableBuffer>
{
private final static Logger mLog = LoggerFactory.getLogger(AFSK1200Decoder.class);
public enum Output
{
NORMAL, INVERTED
}
public static final double SAMPLE_RATE = 7200.0d;
public static final int SAMPLES_PER_SYMBOL = 6; //SAMPLE_RATE / 1200 baud;
//Correlation period of 8 samples works well and should align nicely with SIMD intrinsic vector sizes
public static final int CORRELATION_PERIOD = SAMPLES_PER_SYMBOL + 2;
public static final int AVERAGING_PERIOD = SAMPLES_PER_SYMBOL + 1;
public static final double MARK = 1200.0;
public static final double SPACE = 1800.0;
public static final float TIMING_ERROR_GAIN = 1.0f / 3.0f; //Timing error adjustments over 3 symbol periods
private static float[] sBandPassFilterCoefficients;
static
{
FIRFilterSpecification specification = FIRFilterSpecification.bandPassBuilder()
.sampleRate(8000)
.stopFrequency1(1000)
.passFrequencyBegin(1100)
.passFrequencyEnd(1900)
.stopFrequency2(2000)
.stopRipple(0.000001)
.passRipple(0.00001)
.build();
try
{
RemezFIRFilterDesigner designer = new RemezFIRFilterDesigner(specification);
if(designer.isValid())
{
sBandPassFilterCoefficients = designer.getImpulseResponse();
}
}
catch(FilterDesignException fde)
{
mLog.error("Filter design error", fde);
}
}
private Correlator mCorrelator1200 = new Correlator(SAMPLE_RATE, MARK, AVERAGING_PERIOD, CORRELATION_PERIOD);
private Correlator mCorrelator1800 = new Correlator(SAMPLE_RATE, SPACE, AVERAGING_PERIOD, CORRELATION_PERIOD);
private float[] mCorrelationValues1200;
private float[] mCorrelationValues1800;
protected boolean mNormalOutput;
protected float mSymbolTimingGain = TIMING_ERROR_GAIN;
protected AFSKSampleBuffer mSampleBuffer;
protected AFSKTimingErrorDetector mTimingErrorDetector = new AFSKTimingErrorDetector(SAMPLES_PER_SYMBOL);
private RealFIRFilter2 mBandPassFilter = new RealFIRFilter2(sBandPassFilterCoefficients);
protected IBinarySymbolProcessor mBinarySymbolProcessor;
private boolean mSampleDecision;
//Resample to an integral of the baud rate 1200 baud * 6 samples per symbol = 7200.0 Hertz
private RealResampler mResampler = new RealResampler(8000.0, SAMPLE_RATE, 2000, 1);
/**
* Constructs a decoder using the provided arguments.
*
* @param sampleBuffer for storing incoming samples and calculating symbols
* @param detector for timing error alignment
* @param output NORMAL: 1200Hz = Mark(1) and 1800Hz = Space(0), or INVERTED (vice-versa)
*/
public AFSK1200Decoder(AFSKSampleBuffer sampleBuffer, AFSKTimingErrorDetector detector, Output output)
{
mTimingErrorDetector = detector;
mSampleBuffer = sampleBuffer;
mSampleBuffer.setTimingGain(mSymbolTimingGain);
mResampler.setListener(new Decoder());
mNormalOutput = (output == Output.NORMAL);
}
/**
* Constructs a decoder using the provided arguments.
*
* @param output NORMAL: 1200Hz = Mark(1) and 1800Hz = Space(0), or INVERTED (vice-versa)
*/
public AFSK1200Decoder(Output output)
{
this(new AFSKSampleBuffer(SAMPLES_PER_SYMBOL, TIMING_ERROR_GAIN),
new AFSKTimingErrorDetector(SAMPLES_PER_SYMBOL), output);
}
/**
* Disposes of all references to prepare for garbage collection
*/
public void dispose()
{
}
/**
* Processes the buffer samples by converting all samples to boolean values reflecting if the sample value is
* greater than zero (or not). Average symbol timing offset is calculated for the full buffer and the offset is
* adjusted and then each of the symbols are decoded using a simple majority decision.
*
* @param buffer containing 8.0 kHz unfiltered FM demodulated audio samples with sub-audible LTR signalling.
*/
@Override
public void receive(ReusableBuffer buffer)
{
//TODO: remove the passband filter ... the correlators should be able to deal with the noise and the filters may be
//TODO: introducing noise anyway
// ReusableBuffer bandPassFiltered = mBandPassFilter.filter(buffer);
// mResampler.resample(bandPassFiltered);
mResampler.resample(buffer);
}
protected void dispatch(boolean symbol)
{
if(mBinarySymbolProcessor != null)
{
mBinarySymbolProcessor.receive(mNormalOutput ? symbol : !symbol);
}
}
/**
* Registers a listener to receive decoded LTR symbols.
*
* @param binarySymbolProcessor to receive symbols.
*/
public void setSymbolProcessor(IBinarySymbolProcessor binarySymbolProcessor)
{
mBinarySymbolProcessor = binarySymbolProcessor;
}
/**
* Removes the symbol listener from receiving decoded LTR symbols.
*/
public void removeListener()
{
mBinarySymbolProcessor = null;
}
public class Decoder implements Listener<ReusableBuffer>
{
@Override
public void receive(ReusableBuffer buffer)
{
//Calculate correlation values against each 1200/1800 reference signal
mCorrelationValues1200 = mCorrelator1200.process(buffer);
mCorrelationValues1800 = mCorrelator1800.process(buffer);
buffer.decrementUserCount();
for(int x = 0; x < mCorrelationValues1200.length; x++)
{
//1200 = Mark (1) and 1800 = Space (0)
mSampleDecision = mCorrelationValues1200[x] > mCorrelationValues1800[x];
mSampleBuffer.receive(mSampleDecision);
mTimingErrorDetector.receive(mSampleDecision);
if(mSampleBuffer.hasSymbol())
{
dispatch(mSampleBuffer.getSymbol());
mSampleBuffer.resetAndAdjust(mTimingErrorDetector.getError());
}
}
}
}
/**
* Generates a correlation value for each FM demodulated sample against samples generated for an established
* reference frequency, correlated and averaged over separate periods relative to the samples per symbol period.
*
* The sample rate of the correlator should be an integral value of the target frequency for optimal performance,
* This correlator only works across integral sample periods and does not provide intra-sample interpolation.
*
* Correlation period (ie number of samples) should be relative to the samples per symbol. You may have to
* experiment with various values of (samplesPerSymbol +/- 1, 2 or 3) to obtain the optimal decoder performance.
*
* Averaging period defines the number of correlation values to average before producing the final correlation
* value for each sample.
*/
public class Correlator
{
private FloatAveragingBuffer mAveragingBuffer;
private float[] mReferenceSamples;
private float[] mDemodulatedSamples;
private float[] mCorrelationValues;
private float mCorrelationAccumulator;
/**
* Constructs a correlator instance,
*
* @param sampleRate of the incoming sample stream. Note: this should be an integral of the symbol rate.
* @param frequency of the mark or space symbol to test for correlation
* @param averagingPeriod is the number of correlation values to average each period
* @param correlationPeriod is the number of samples to correlate each period
*/
public Correlator(double sampleRate, double frequency, int averagingPeriod, int correlationPeriod)
{
mAveragingBuffer = new FloatAveragingBuffer(averagingPeriod);
IOscillator referenceSignalGenerator = new Oscillator(frequency, sampleRate);
mReferenceSamples = referenceSignalGenerator.generateReal(correlationPeriod);
mDemodulatedSamples = new float[correlationPeriod];
}
/**
* Processes each sample in the incoming sample buffer against a generated reference sample set for one symbol
* period to derive a correlation value that is in-turn averaged over one symbol period.
*
* @param reusableBuffer containing FM demodulated samples
* @return a reusable array of correlation values for each sample
*/
public float[] process(ReusableBuffer reusableBuffer)
{
if(mCorrelationValues == null || mCorrelationValues.length != reusableBuffer.getSampleCount())
{
mCorrelationValues = new float[reusableBuffer.getSampleCount()];
}
float[] samples = reusableBuffer.getSamples();
int y;
for(int x = 0; x < samples.length; x++)
{
//Note: we're using array copy and dot product structures that the JRE can promote to SIMD intrinsics
//when the host processor supports SIMD instructions
System.arraycopy(mDemodulatedSamples, 1, mDemodulatedSamples, 0, mDemodulatedSamples.length - 1);
mDemodulatedSamples[mDemodulatedSamples.length - 1] = samples[x];
mCorrelationAccumulator = 0.0f;
for(y = 0; y < mDemodulatedSamples.length; y++)
{
mCorrelationAccumulator += mDemodulatedSamples[y] * mReferenceSamples[y];
}
//Add the absolute value of correlation accumulator value to the averaging buffer and store the current
//average as the correlation value for this sample. We use absolute value because we don't care if the
//signal is out of phase with the reference samples
mCorrelationValues[x] = mAveragingBuffer.get(Math.abs(mCorrelationAccumulator));
}
return mCorrelationValues;
}
}
}

View File

@@ -0,0 +1,51 @@
/*******************************************************************************
* sdr-trunk
* Copyright (C) 2014-2018 Dennis Sheirer
*
* This program is free software: you can redistribute it and/or modify it under the terms of the GNU General Public
* License as published by the Free Software Foundation, either version 3 of the License, or (at your option) any
* later version.
*
* This program is distributed in the hope that it will be useful, but WITHOUT ANY WARRANTY; without even the implied
* warranty of MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU General Public License for more details.
*
* You should have received a copy of the GNU General Public License along with this program.
* If not, see <http://www.gnu.org/licenses/>
*
******************************************************************************/
package io.github.dsheirer.dsp.afsk;
import io.github.dsheirer.sample.Broadcaster;
import io.github.dsheirer.sample.Listener;
public class AFSK1200DecoderInstrumented extends AFSK1200Decoder
{
private Broadcaster<Boolean> mSymbolBroadcaster = new Broadcaster<>();
public AFSK1200DecoderInstrumented(boolean invertOutput)
{
super(new AFSKSampleBufferInstrumented(SAMPLES_PER_SYMBOL, TIMING_ERROR_GAIN),
new AFSKTimingErrorDetectorInstrumented(SAMPLES_PER_SYMBOL), Output.NORMAL);
}
public AFSKSampleBufferInstrumented getSampleBuffer()
{
return (AFSKSampleBufferInstrumented)mSampleBuffer;
}
public AFSKTimingErrorDetectorInstrumented getErrorDetector()
{
return (AFSKTimingErrorDetectorInstrumented)mTimingErrorDetector;
}
protected void dispatch(boolean symbol)
{
super.dispatch(symbol);
mSymbolBroadcaster.broadcast(symbol);
}
public void addListener(Listener<Boolean> listener)
{
mSymbolBroadcaster.addListener(listener);
}
}

View File

@@ -0,0 +1,121 @@
/*******************************************************************************
* sdr-trunk
* Copyright (C) 2014-2018 Dennis Sheirer
*
* This program is free software: you can redistribute it and/or modify it under the terms of the GNU General Public
* License as published by the Free Software Foundation, either version 3 of the License, or (at your option) any
* later version.
*
* This program is distributed in the hope that it will be useful, but WITHOUT ANY WARRANTY; without even the implied
* warranty of MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU General Public License for more details.
*
* You should have received a copy of the GNU General Public License along with this program.
* If not, see <http://www.gnu.org/licenses/>
*
******************************************************************************/
package io.github.dsheirer.dsp.afsk;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
public class AFSKSampleBuffer
{
private final static Logger mLog = LoggerFactory.getLogger(AFSKSampleBuffer.class);
private final static float SYMBOL_SAMPLE_THRESHOLD = 1.0f;
protected boolean[] mDelayLine;
protected int mDelayLinePointer = 0;
private int mHalfIntegerSamplesPerSymbol;
private int mPositiveSampleDecisionCount;
protected int mTwiceSamplesPerSymbol;
protected int mSymbolStart;
protected int mSymbolEnd;
protected int mSamplesPerSymbol;
protected float mSymbolSamplingPoint;
protected float mSymbolTimingGain;
/**
* Buffer to store complex sample data and produce interpolated samples.
* @param samplesPerSymbol
*/
public AFSKSampleBuffer(int samplesPerSymbol, float symbolTimingGain)
{
mSamplesPerSymbol = samplesPerSymbol;
mSymbolTimingGain = symbolTimingGain;
mHalfIntegerSamplesPerSymbol = samplesPerSymbol / 2;
mSymbolSamplingPoint = samplesPerSymbol;
mTwiceSamplesPerSymbol = 2 * samplesPerSymbol;
mDelayLine = new boolean[2 * mTwiceSamplesPerSymbol];
}
/**
* Stores the sample in the sample buffer to support making symbol decisions.
*
* Note: use the hasSymbol() method to determine if sufficient samples have been collected to make a symbol
* determination.
*
* @param sample to store in the sample buffer
*/
public void receive(boolean sample)
{
mSymbolSamplingPoint--;
mDelayLine[mDelayLinePointer] = sample;
mDelayLine[mDelayLinePointer + mTwiceSamplesPerSymbol] = sample;
//Increment pointer and keep pointer in bounds
mDelayLinePointer++;
mDelayLinePointer = mDelayLinePointer % mTwiceSamplesPerSymbol;
}
/**
* Adjusts samples per symbol and symbol timing counters and increments the sample counter to collect another
* symbol.
*
* @param symbolTimingError from a symbol timing error detector
*/
public void resetAndAdjust(float symbolTimingError)
{
mSymbolSamplingPoint += (mSamplesPerSymbol + (symbolTimingError * mSymbolTimingGain));
}
public void setTimingGain(float gain)
{
mSymbolTimingGain = gain;
}
/**
* Indicates if this buffer has accumulated enough samples to represent a full symbol
*/
public boolean hasSymbol()
{
return mSymbolSamplingPoint < SYMBOL_SAMPLE_THRESHOLD;
}
/**
* Calculates the symbol as a majority decision where the number of positive/true samples are greater than half
* of the samples per symbol. Symbol decision is based on the samples collected over the preceeding 1.0 to 2.0
* symbol periods.
*
* @return a true symbol if the majority of the samples were true or positive, otherwise a false symbol.
*/
public boolean getSymbol()
{
mPositiveSampleDecisionCount = 0;
mSymbolStart = mDelayLinePointer;
mSymbolEnd = mDelayLinePointer + mSamplesPerSymbol - 1;
for(int x = mSymbolStart; x <= mSymbolEnd; x++)
{
if(mDelayLine[x])
{
mPositiveSampleDecisionCount++;
}
}
return mPositiveSampleDecisionCount > mHalfIntegerSamplesPerSymbol;
}
}

View File

@@ -0,0 +1,90 @@
/*******************************************************************************
* sdr-trunk
* Copyright (C) 2014-2018 Dennis Sheirer
*
* This program is free software: you can redistribute it and/or modify it under the terms of the GNU General Public
* License as published by the Free Software Foundation, either version 3 of the License, or (at your option) any
* later version.
*
* This program is distributed in the hope that it will be useful, but WITHOUT ANY WARRANTY; without even the implied
* warranty of MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU General Public License for more details.
*
* You should have received a copy of the GNU General Public License along with this program.
* If not, see <http://www.gnu.org/licenses/>
*
******************************************************************************/
package io.github.dsheirer.dsp.afsk;
public class AFSKSampleBufferInstrumented extends AFSKSampleBuffer
{
public boolean mLastSymbol;
/**
* Buffer to store complex sample data and produce interpolated samples.
*
* @param samplesPerSymbol
* @param symbolTimingGain
*/
public AFSKSampleBufferInstrumented(int samplesPerSymbol, float symbolTimingGain)
{
super(samplesPerSymbol, symbolTimingGain);
}
/**
* Sample delay buffer containing boolean (ie positive or negative) samples.
*/
public boolean[] getDelayLine()
{
return mDelayLine;
}
/**
* Current delay buffer pointer which points to where the next sample will be stored and/or the oldest sample in
* the buffer.
*/
public int getDelayLinePointer()
{
return mDelayLinePointer;
}
public int getDelayLineSecondPointer()
{
return mDelayLinePointer + mTwiceSamplesPerSymbol;
}
/**
* Current symbol timing gain value
* @return
*/
public float getSymbolTimingGain()
{
return mSymbolTimingGain;
}
public float getSamplesPerSymbol()
{
return mSamplesPerSymbol;
}
@Override
public boolean getSymbol()
{
mLastSymbol = super.getSymbol();
return mLastSymbol;
}
public boolean getLastSymbol()
{
return mLastSymbol;
}
public int getSymbolStart()
{
return mSymbolStart;
}
public int getSymbolEnd()
{
return mSymbolEnd;
}
}

View File

@@ -0,0 +1,122 @@
/*******************************************************************************
* sdr-trunk
* Copyright (C) 2014-2018 Dennis Sheirer
*
* This program is free software: you can redistribute it and/or modify it under the terms of the GNU General Public
* License as published by the Free Software Foundation, either version 3 of the License, or (at your option) any
* later version.
*
* This program is distributed in the hope that it will be useful, but WITHOUT ANY WARRANTY; without even the implied
* warranty of MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU General Public License for more details.
*
* You should have received a copy of the GNU General Public License along with this program.
* If not, see <http://www.gnu.org/licenses/>
*
******************************************************************************/
package io.github.dsheirer.dsp.afsk;
import io.github.dsheirer.buffer.FloatAveragingBuffer;
import io.github.dsheirer.dsp.mixer.IOscillator;
import io.github.dsheirer.dsp.mixer.Oscillator;
import io.github.dsheirer.sample.complex.Complex;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import java.text.DecimalFormat;
public class AFSKTest
{
private final static Logger mLog = LoggerFactory.getLogger(AFSK1200Decoder.class);
private static final DecimalFormat df = new DecimalFormat("00.000");
public static String format(double value)
{
if(value > 0)
{
return " " + df.format(value);
}
else
{
return df.format(value);
}
}
public static float[] generateAFSK1200(boolean[] symbols, int samplesPerSymbol, float gain)
{
float[] samples = new float[symbols.length * samplesPerSymbol];
Oscillator oscillator = new Oscillator(1200.0, (double)samplesPerSymbol * 1200);
for(int x = 0; x < symbols.length; x++)
{
oscillator.setFrequency(symbols[x] ? 1200.0 : 1800.0);
for(int y = 0; y < samplesPerSymbol; y++)
{
Complex sample = new Complex(oscillator.inphase(), oscillator.quadrature());
sample.multiply(gain);
samples[x * samplesPerSymbol + y] = sample.inphase();
oscillator.rotate();
}
}
return samples;
}
public static void main(String[] args)
{
int sampleSize = 6;
FloatAveragingBuffer avg1200 = new FloatAveragingBuffer(sampleSize);
FloatAveragingBuffer avg1800 = new FloatAveragingBuffer(sampleSize);
IOscillator oscillator1200 = new Oscillator(1200.0, 7200.0);
IOscillator oscillator1800 = new Oscillator(1800.0, 7200.0);
oscillator1200.generateReal(1);
oscillator1800.generateReal(1);
float[] reference1200 = oscillator1200.generateReal(sampleSize);
float[] reference1800 = oscillator1800.generateReal(sampleSize);
boolean[] symbols = new boolean[]{true,false,true,false,true,false,true,false,true,true,true,false,true,false,
false,false,true,false,false,false,false,true,false,true,true,true,true,false,true,true,false,true,false,
true,false,true,false,true,false,true,false,true,false,true,false,true,false,true,false,true,false,true,
false,true,false,true,false,true,false,true,false,true,false,true,false,true,false,true,false,true,false,
true,false,true,false,true,false,true,false,true,false,true,false,true,false,true,false,true,false,true,
false,true,false,true,false,true,false,true,false,true,false,true,false,true,false,true,false,true,false,
true,false,true,false,true,false,true,false,true,false,true,false,true,false,true,false,true,false,true,
false,true,false,true,false,true,false,true,false,true,false,true,false,true,false,true,false,true,false,
true,false,false,false,false,false,false,false,false,false,false,false,false,false,false,false,false,
false,false,false,false,false,false,false,false,false,false,false,false,false,false,false,false,false};
float[] samples = generateAFSK1200(symbols, 6, 0.5f);
mLog.debug("Length:" + samples.length);
int offset = 0;
float correlation1200 = 0.0f;
float correlation1800 = 0.0f;
while(offset + sampleSize < samples.length)
{
correlation1200 = 0.0f;
correlation1800 = 0.0f;
for(int x = 0; x < sampleSize; x++)
{
correlation1200 += reference1200[x] * samples[x + offset];
correlation1800 += reference1800[x] * samples[x + offset];
}
correlation1200 = avg1200.get(Math.abs(correlation1200));
correlation1800 = avg1800.get(Math.abs(correlation1800));
mLog.debug(offset + " 1200:" + format(correlation1200) + " 1800:" + format(correlation1800) +
" Decision: " + (correlation1200 > correlation1800 ? "0" : "1"));
offset++;
}
}
}

View File

@@ -0,0 +1,103 @@
/*******************************************************************************
* sdr-trunk
* Copyright (C) 2014-2018 Dennis Sheirer
*
* This program is free software: you can redistribute it and/or modify it under the terms of the GNU General Public
* License as published by the Free Software Foundation, either version 3 of the License, or (at your option) any
* later version.
*
* This program is distributed in the hope that it will be useful, but WITHOUT ANY WARRANTY; without even the implied
* warranty of MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU General Public License for more details.
*
* You should have received a copy of the GNU General Public License along with this program.
* If not, see <http://www.gnu.org/licenses/>
*
******************************************************************************/
package io.github.dsheirer.dsp.afsk;
/**
* Zero crossing error detector. Provides symbol timing error correction values by detecting zero-crossings within
* each symbol period. Ideally there should be only 0 or 1 zero crossings within each symbol period. However, when
* there is a slight DC offset in the demodulated samples, there can be up to 2 zero crossings within a symbol
* period.
*
* This detector will only provide error corrections when there are 1 or 2 zero crossings detected. If the number
* of zero crossings is greater than 2, then it is assumed that there is no signal present and the returned timing
* error value is zero. This detector provides an error correction value relative to (approximately) the middle of
* a symbol period and therefore continuously attempts to align zero crossings to the middle of the period.
*
* Since this detector does NOT use interpolation, the error signal will contain minor levels of symbol timing
* jitter. Error signal values are reported as integrals of the sampling period.
*/
public class AFSKTimingErrorDetector
{
protected boolean[] mBuffer;
protected int mDetectedZeroCrossing;
protected float mSymbolMiddle;
private int mZeroCrossingCount;
/**
* Constructs the detector for the specified samples per symbol.
*
* @param samplesPerSymbol
*/
public AFSKTimingErrorDetector(int samplesPerSymbol)
{
mBuffer = new boolean[samplesPerSymbol];
mSymbolMiddle = (float)(samplesPerSymbol - 1) / 2.0f;
}
/**
* Stores the sample in the buffer. Use the getError() method once enough samples have been processed to determine
* the error for a symbol period.
*
* @param sample
*/
public void receive(boolean sample)
{
//Shift all samples left and store new sample at the end
System.arraycopy(mBuffer, 1, mBuffer, 0, mBuffer.length - 1);
mBuffer[mBuffer.length - 1] = sample;
}
/**
* Determines the symbol timing error for the samples processed thus far. This method should be invoked once per
* symbol period, triggered via an external samples per symbol tracking process.
*
* @return symbol timing error calculated as the distance between the approximate symbol center and the next closest
* zero crossing index. An error value of zero is returned if the number of zero crossings is less than one or
* greater than two.
*/
public int getError()
{
mZeroCrossingCount = 0;
for(int x = 0; x < mBuffer.length - 1; x++)
{
if(mBuffer[x] ^ mBuffer[x + 1])
{
mDetectedZeroCrossing = x + 1;
mZeroCrossingCount++;
if(mZeroCrossingCount > 1)
{
return 0; //Too many crossings
}
}
}
if(mZeroCrossingCount == 1)
{
if(mDetectedZeroCrossing > mSymbolMiddle)
{
return -(mBuffer.length - mDetectedZeroCrossing);
}
else
{
return mDetectedZeroCrossing + 1;
}
}
return 0;
}
}

View File

@@ -0,0 +1,53 @@
/*******************************************************************************
* sdr-trunk
* Copyright (C) 2014-2018 Dennis Sheirer
*
* This program is free software: you can redistribute it and/or modify it under the terms of the GNU General Public
* License as published by the Free Software Foundation, either version 3 of the License, or (at your option) any
* later version.
*
* This program is distributed in the hope that it will be useful, but WITHOUT ANY WARRANTY; without even the implied
* warranty of MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU General Public License for more details.
*
* You should have received a copy of the GNU General Public License along with this program.
* If not, see <http://www.gnu.org/licenses/>
*
******************************************************************************/
package io.github.dsheirer.dsp.afsk;
import javafx.beans.property.SimpleIntegerProperty;
public class AFSKTimingErrorDetectorInstrumented extends AFSKTimingErrorDetector
{
public SimpleIntegerProperty timingError = new SimpleIntegerProperty();
/**
* Constructs the detector for the specified samples per symbol.
*
* @param samplesPerSymbol
*/
public AFSKTimingErrorDetectorInstrumented(int samplesPerSymbol)
{
super(samplesPerSymbol);
}
@Override
public int getError()
{
int error = super.getError();
timingError.setValue(error);
return error;
}
public int getDetectedZeroCrossing()
{
return mDetectedZeroCrossing;
}
public boolean[] getBuffer()
{
return mBuffer;
}
}

View File

@@ -1,22 +1,82 @@
/*******************************************************************************
* 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();
return reusableBuffer;
}
public void setGain( float gain )
{
mGain = gain;
}
}

View File

@@ -1,69 +0,0 @@
/*******************************************************************************
* SDR Trunk
* Copyright (C) 2014,2015 Dennis Sheirer
*
* This program is free software: you can redistribute it and/or modify
* it under the terms of the GNU General Public License as published by
* the Free Software Foundation, either version 3 of the License, or
* (at your option) any later version.
*
* This program is distributed in the hope that it will be useful,
* but WITHOUT ANY WARRANTY; without even the implied warranty of
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
* GNU General Public License for more details.
*
* You should have received a copy of the GNU General Public License
* along with this program. If not, see <http://www.gnu.org/licenses/>
******************************************************************************/
package io.github.dsheirer.dsp.am;
import io.github.dsheirer.sample.Listener;
import io.github.dsheirer.sample.complex.ComplexBuffer;
import io.github.dsheirer.sample.real.RealBuffer;
public class AMDemodulator_CB extends AMDemodulator implements Listener<ComplexBuffer>
{
private Listener<RealBuffer> mListener;
public AMDemodulator_CB( float gain )
{
super( gain );
}
@Override
public void receive( ComplexBuffer buffer )
{
if( mListener != null )
{
float[] samples = buffer.getSamples();
int half = samples.length / 2;
float[] demodulated = new float[ half ];
for( int x = 0; x < half; x++ )
{
int index = x * 2;
demodulated[ x ] = demodulate( samples[ index ], samples[ index + 1 ] );
}
mListener.receive( new RealBuffer( demodulated ) );
}
}
public void setListener( Listener<RealBuffer> listener )
{
mListener = listener;
}
public void removeListener()
{
mListener = null;
}
public void dispose()
{
mListener = null;
}
}

View File

@@ -1,55 +0,0 @@
/*******************************************************************************
* SDR Trunk
* Copyright (C) 2014 Dennis Sheirer
*
* This program is free software: you can redistribute it and/or modify
* it under the terms of the GNU General Public License as published by
* the Free Software Foundation, either version 3 of the License, or
* (at your option) any later version.
*
* This program is distributed in the hope that it will be useful,
* but WITHOUT ANY WARRANTY; without even the implied warranty of
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
* GNU General Public License for more details.
*
* You should have received a copy of the GNU General Public License
* along with this program. If not, see <http://www.gnu.org/licenses/>
******************************************************************************/
package io.github.dsheirer.dsp.filter;
import io.github.dsheirer.sample.Listener;
import io.github.dsheirer.sample.complex.Complex;
@Deprecated
public abstract class ComplexFilter implements Listener<Complex>
{
private Listener<Complex> mListener;
public void setListener( Listener<Complex> listener )
{
mListener = listener;
}
public void dispose()
{
mListener = null;
}
public Listener<Complex> getListener()
{
return mListener;
}
public boolean hasListener()
{
return mListener != null;
}
protected void send( Complex sample )
{
if( mListener != null )
{
mListener.receive( sample );
}
}
}

View File

@@ -26,6 +26,7 @@ import io.github.dsheirer.dsp.filter.cic.ComplexPrimeCICDecimate;
import io.github.dsheirer.dsp.filter.design.FilterDesignException;
import io.github.dsheirer.dsp.filter.fir.FIRFilterSpecification;
import io.github.dsheirer.dsp.filter.fir.remez.RemezFIRFilterDesigner;
import io.github.dsheirer.dsp.filter.fir.remez.RemezFIRFilterDesignerWithLagrange;
import org.jtransforms.fft.FloatFFT_1D;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
@@ -34,10 +35,14 @@ import java.util.Arrays;
import java.util.Set;
import java.util.TreeSet;
/**
* Utility methods for designing various types of filters.
*/
public class FilterFactory
{
private final static Logger mLog =
LoggerFactory.getLogger(FilterFactory.class);
private final static Logger mLog = LoggerFactory.getLogger(FilterFactory.class);
private static final double PERFECT_RECONSTRUCTION_GAIN_AT_BAND_EDGE = -6.020599842071533; //decibel(0.5, 0.0)
private static final double MARGIN_OF_ERROR = 0.0003;
/**
* Generates coefficients for a unity-gain, windowed low-pass filter
@@ -48,43 +53,49 @@ public class FilterFactory
* @param window - to apply against the coefficients
* @return
*/
public static float[] getSinc(int sampleRate,
long frequency,
int length,
Window.WindowType window)
public static float[] getSinc(double sampleRate, long frequency, int length, Window.WindowType window)
{
//Ensure we have an odd length
assert (length % 2 == 0);
int evenLength = length % 2 == 0 ? length : length + 1;
//Get unity response array (one element longer to align with IDFT size)
float[] frequencyResponse = getUnityResponseArray(sampleRate, frequency, length + 1);
float[] frequencyResponse = getUnityResponseArray(sampleRate, frequency, evenLength);
//Apply Inverse DFT against frequency response unity values, leaving the
//IDFT bin results in the frequency response array
FloatFFT_1D idft = new FloatFFT_1D(length + 1);
idft.realInverseFull(frequencyResponse, false);
FloatFFT_1D idft = new FloatFFT_1D(evenLength);
idft.realInverseFull(frequencyResponse, true);
//Transfer the IDFT results to the return array
float[] coefficients = new float[length];
int middleCoefficient = (int) (length / 2);
int middleCoefficient = (int)(length / 2);
//Bin 0 of the idft is our center coefficient
coefficients[middleCoefficient] = frequencyResponse[0];
//The remaining idft bins from 1 to (middle - 1) are the mirror image
//coefficients
for(int x = 1; x < middleCoefficient; x++)
if(length % 2 == 0) //Even length
{
coefficients[middleCoefficient + x] = frequencyResponse[2 * x];
coefficients[middleCoefficient - x] = frequencyResponse[2 * x];
//The remaining idft bins from 1 to (middle - 1) are the mirror image
//coefficients
for(int x = 0; x < middleCoefficient; x++)
{
coefficients[middleCoefficient + x] = frequencyResponse[2 * x];
coefficients[middleCoefficient - x] = frequencyResponse[2 * x];
}
}
else
{
//Bin 0 of the idft is our center coefficient
coefficients[middleCoefficient] = frequencyResponse[0];
//The remaining idft bins from 1 to (middle - 1) are the mirror image
//coefficients
for(int x = 1; x < middleCoefficient; x++)
{
coefficients[middleCoefficient + x] = frequencyResponse[2 * x];
coefficients[middleCoefficient - x] = frequencyResponse[2 * x];
}
}
//Apply the window against the coefficients
coefficients = Window.apply(window, coefficients);
//Normalize to unity (1) gain
coefficients = normalize(coefficients);
return coefficients;
}
@@ -95,7 +106,7 @@ public class FilterFactory
* @param coefficients
* @return
*/
private static float[] normalize(float[] coefficients)
public static float[] normalize(float[] coefficients)
{
float accumulator = 0;
@@ -112,6 +123,32 @@ public class FilterFactory
return coefficients;
}
/**
* Normalizes all filter coefficients to achieve an overall objective gain, by ensuring
* that the sum of the absolute value of all coefficients adds up to the objective.
*
* @param coefficients
* @return
*/
public static float[] normalize(float[] coefficients, float objective)
{
float accumulator = 0;
for(int x = 0; x < coefficients.length; x++)
{
accumulator += Math.abs(coefficients[x]);
}
accumulator /= objective;
for(int x = 0; x < coefficients.length; x++)
{
coefficients[x] = coefficients[x] / accumulator;
}
return coefficients;
}
/**
* Constructs an array of unity (1) response values representing the
* desired (pre-windowing) frequency response, used by the sync function
@@ -128,17 +165,15 @@ public class FilterFactory
* @param length
* @return
*/
public static float[] getUnityResponseArray(int sampleRate,
long frequency,
int length)
public static float[] getUnityResponseArray(double sampleRate, long frequency, int length)
{
float[] unityArray = new float[length * 2];
int binCount = (int) ((Math.round(
(float) frequency / (float) sampleRate * (float) length)));
if(length % 2 == 0) //even length
{
int binCount = (int)((Math.round((float)frequency / (float)sampleRate * (float)length)));
for(int x = 0; x < binCount; x++)
{
unityArray[x] = 1.0f;
@@ -147,9 +182,11 @@ public class FilterFactory
}
else //odd length
{
int binCount = (int)((Math.round((float)frequency / (float)sampleRate * (float)length + 0.5)));
unityArray[0] = 1.0f;
for(int x = 1; x <= binCount; x++)
for(int x = 1; x < binCount; x++)
{
unityArray[x] = 1.0f;
unityArray[length - x] = 1.0f;
@@ -159,25 +196,6 @@ public class FilterFactory
return unityArray;
}
public static float[] getSine(float sampleRate, float frequency, int length)
{
float[] retVal = new float[length];
float radianFrequency = (float) (2.0d * Math.PI * (frequency / sampleRate));
int middle = (int) (length / 2);
for(int x = 0; x < middle; x++)
{
float val = (float) Math.sin(radianFrequency * x);
retVal[middle + x] = val;
retVal[middle - x] = -val;
}
return retVal;
}
/**
* Applies a repeating sequence of 1, -1 to the coefficients to invert
* the frequency response of the filter. Used in converting a low-pass
@@ -211,25 +229,9 @@ public class FilterFactory
* @param windowType - window to apply against the generated coefficients
* @return
*/
public static float[] getLowPass(int sampleRate,
long cutoff,
int filterLength,
Window.WindowType windowType)
public static float[] getLowPass(double sampleRate, long cutoff, int filterLength, Window.WindowType windowType)
{
if(filterLength % 2 == 0) //even length
{
float[] values = getSinc(sampleRate, cutoff, filterLength + 2, windowType);
//throw away the 0 index and the last index
return Arrays.copyOfRange(values, 1, values.length - 2);
}
else
{
float[] values = getSinc(sampleRate, cutoff, filterLength + 1, windowType);
//throw away the 0 index
return Arrays.copyOfRange(values, 1, values.length);
}
return getSinc(sampleRate, cutoff, filterLength, windowType);
}
/**
@@ -246,26 +248,17 @@ public class FilterFactory
* - passFrequency < stopFrequency
* - stopFrequency <= sampleRate/2
*/
public static float[] getLowPass(int sampleRate,
int passFrequency,
int stopFrequency,
int attenuation,
Window.WindowType windowType,
boolean forceOddLength)
public static float[] getLowPass(double sampleRate, int passFrequency, int stopFrequency, int attenuation,
Window.WindowType windowType, boolean forceOddLength)
{
if(stopFrequency < passFrequency || stopFrequency > (sampleRate / 2))
{
throw new IllegalArgumentException("FilterFactory - low pass "
+ "filter pass frequency [" + passFrequency + "] must be "
+ "less than the stop frequency [" + stopFrequency + "] "
+ "and must be less than half [" + (int) (sampleRate / 2) +
"] of the sample rate [" + sampleRate + "]");
throw new IllegalArgumentException("FilterFactory - low pass filter pass frequency [" + passFrequency +
"] must be less than the stop frequency [" + stopFrequency + "] and must be less than half [" +
(int)(sampleRate / 2) + "] of the sample rate [" + sampleRate + "]");
}
int tapCount = getTapCount(sampleRate,
passFrequency,
stopFrequency,
attenuation);
int tapCount = getTapCount(sampleRate, passFrequency, stopFrequency, attenuation);
if(forceOddLength)
{
@@ -289,34 +282,21 @@ public class FilterFactory
* @param windowType - window to apply against the generated coefficients
* @return
*/
public static float[] getHighPass(int sampleRate,
long cutoff,
int filterLength,
Window.WindowType windowType)
public static float[] getHighPass(int sampleRate, long cutoff, int filterLength, Window.WindowType windowType)
{
//Convert the high frequency cutoff to its low frequency cutoff
//equivalent, so that when we generate the low pass filter, prior to
//inversion, its at the correct frequency
long convertedCutoff = sampleRate / 2 - cutoff;
return invert(getSinc(sampleRate,
convertedCutoff,
filterLength,
windowType));
return invert(getSinc(sampleRate, convertedCutoff, filterLength, windowType));
}
public static float[] getHighPass(int sampleRate,
long stopFrequency,
long passFrequency,
int attenuation,
Window.WindowType windowType,
boolean forceOddLength)
public static float[] getHighPass(int sampleRate, long stopFrequency, long passFrequency, int attenuation,
Window.WindowType windowType, boolean forceOddLength)
{
/* reverse the stop and pass frequency to get the low pass variant */
int tapCount = getTapCount(sampleRate,
stopFrequency,
passFrequency,
attenuation);
int tapCount = getTapCount(sampleRate, stopFrequency, passFrequency, attenuation);
if(forceOddLength)
{
@@ -360,11 +340,10 @@ public class FilterFactory
}
/**
* Determines the number of fir filter taps required to produce the
* specified frequency response with passband ripple near .1dB.
* Determines the number of fir filter taps required to produce the specified frequency response with passband
* ripple near 0.1dB.
*
* Implements the algorithm from Understanding Digital Signal Processing, 3e
* , Lyons, section 5.10.5.
* Implements the algorithm from Understanding Digital Signal Processing, 3e, Lyons, section 5.10.5.
*
* @param sampleRate in hertz
* @param pass pass frequency in hertz
@@ -372,21 +351,17 @@ public class FilterFactory
* @param attenuation in dB
* @return
*/
public static int getTapCount(int sampleRate,
long pass,
long stop,
int attenuation)
public static int getTapCount(double sampleRate, long pass, long stop, int attenuation)
{
double frequency = ((double) stop - (double) pass) / (double) sampleRate;
double frequency = ((double)stop - (double)pass) / sampleRate;
return (int) (Math.round((double) attenuation / (22.0d * frequency)));
return (int)(Math.round((double)attenuation / (22.0d * frequency)));
}
public static ComplexPrimeCICDecimate getDecimationFilter(int sampleRate,
int decimatedRate, int order, int passFrequency, int attenuation,
Window.WindowType windowType)
public static ComplexPrimeCICDecimate getDecimationFilter(int sampleRate, int decimatedRate, int order,
int passFrequency, int attenuation, Window.WindowType windowType)
{
int decimationRate = (int) (sampleRate / decimatedRate);
int decimationRate = (int)(sampleRate / decimatedRate);
return new ComplexPrimeCICDecimate(decimationRate, order,
passFrequency, attenuation, windowType);
@@ -401,10 +376,7 @@ public class FilterFactory
* polyphase filter decimation chain
* @throws - AssertionException if sample rate is not a multiple of 48 kHz
*/
public static int[] getPolyphaseDecimationRates(int sampleRate,
int decimatedRate,
long passFrequency,
long stopFrequency)
public static int[] getPolyphaseDecimationRates(int sampleRate, int decimatedRate, long passFrequency, long stopFrequency)
{
int[] rates;
@@ -414,7 +386,7 @@ public class FilterFactory
+ "integer multiple of sample rate");
}
int decimation = (int) (sampleRate / decimatedRate);
int decimation = (int)(sampleRate / decimatedRate);
//Decimation rates below 20 will use single stage polyphase filter
if(decimation < 20)
@@ -432,11 +404,6 @@ public class FilterFactory
int stage1 = findClosest(optimalStage1, factors);
// mLog.info( "Decimation rate [" + decimation +
// "] stage1 optimal [" + optimalStage1 +
// "] stage1 actual [" + stage1 +
// "]");
if(stage1 == decimation || stage1 == 1)
{
rates = new int[1];
@@ -447,7 +414,7 @@ public class FilterFactory
{
rates = new int[2];
int stage2 = (int) (decimation / stage1);
int stage2 = (int)(decimation / stage1);
if(stage1 > stage2)
{
@@ -465,8 +432,7 @@ public class FilterFactory
}
/**
* Finds the factor that is closest to the desired factor, from an ordered
* list of factors.
* Finds the factor that is closest to the desired factor, from an ordered list of factors.
*/
private static int findClosest(int desiredFactor, Set<Integer> factors)
{
@@ -488,9 +454,8 @@ public class FilterFactory
}
/**
* Determines the factors that make up an integer. Uses a brute force
* method to iterate all integers from 1 to value, determining which factors
* are evenly divisible into the value.
* Determines the factors that make up an integer. Uses a brute force method to iterate all integers from 1 to
* value, determining which factors are evenly divisible into the value.
*
* @param value - integer decimation value
* @return - ordered set of factors for value
@@ -498,11 +463,11 @@ public class FilterFactory
private static Set<Integer> getFactors(int value)
{
Set<Integer> factors = new TreeSet<Integer>();
/* Brute force */
/* Brute force */
for(int x = 1; x <= value; x++)
{
int remainder = (int) (value / x);
int remainder = (int)(value / x);
if(remainder * x == value)
{
@@ -514,14 +479,10 @@ public class FilterFactory
}
/**
* Determines the optimal decimation rate for stage 1 of a two-stage
* poly-phase decimation filter chain, to produce a final sample rate of
* 48 kHz using a pass bandwidth of 25 kHz.
* Determines the optimal decimation rate for stage 1 of a two-stage poly-phase decimation filter chain, to produce
* a final sample rate of 48 kHz using a pass bandwidth of 25 kHz. Use for total decimation rates of 20 or higher.
*
* Use for total decimation rates of 20 or higher.
*
* Implements the algorithm described in Lyons, Understanding Digital Signal
* Processing, 3e, section 10.2.1, page 511.
* Implements the algorithm described in Lyons, Understanding Digital Signal Processing, 3e, section 10.2.1, page 511.
*
* @param sampleRate
* @param decimation
@@ -529,42 +490,29 @@ public class FilterFactory
* @return optimum integer decimation rate for the first stage decimation
* filter
*/
public static int getOptimalStageOneRate(int sampleRate,
int decimation,
long passFrequency,
long stopFrequency)
public static int getOptimalStageOneRate(int sampleRate, int decimation, long passFrequency, long stopFrequency)
{
double ratio = getBandwidthRatio(passFrequency, stopFrequency);
double numerator = 1.0d - (
Math.sqrt((double) decimation * ratio / (2.0d - ratio)));
double numerator = 1.0d - (Math.sqrt((double)decimation * ratio / (2.0d - ratio)));
double denominator = 2.0d - (ratio * (decimation + 1.0d));
int retVal = (int) (2.0d * decimation * (numerator / denominator));
// mLog.info( "Optimal Stage 1 Decimation - rate [" + sampleRate +
// "] pass [" + passFrequency +
// "] bw ratio [" + ratio +
// "] optimal [" + retVal +
// "]" );
int retVal = (int)(2.0d * decimation * (numerator / denominator));
return retVal;
}
/**
* Determines the F ratio as described in Lyons, Understanding Digital
* Signal Processing, 3e, section 10.2.1, page 511
*
* Used in conjunction with the optimal stage one decimation rate method
* above.
* Determines the F ratio as described in Lyons, Understanding Digital Signal Processing, 3e, section 10.2.1, page 511
* Used in conjunction with the optimal stage one decimation rate method above.
*/
private static double getBandwidthRatio(long passFrequency, long stopFrequency)
{
assert (passFrequency < stopFrequency);
return ((double) (stopFrequency - passFrequency) /
(double) stopFrequency);
return ((double)(stopFrequency - passFrequency) /
(double)stopFrequency);
}
/**
@@ -575,15 +523,12 @@ public class FilterFactory
* @param outputSampleRate
* @return
*/
public static float[] getCICCleanupFilter(int outputSampleRate,
int passFrequency,
int attenuation,
Window.WindowType window)
public static float[] getCICCleanupFilter(int outputSampleRate, int passFrequency, int attenuation, Window.WindowType window)
{
int taps = getTapCount(outputSampleRate, passFrequency, passFrequency + 1500,
attenuation);
/* Make tap count odd */
/* Make tap count odd */
if(taps % 2 == 0)
{
taps++;
@@ -599,7 +544,7 @@ public class FilterFactory
//Transfer the IDFT results to the odd length return array
float[] coefficients = new float[taps];
int middleCoefficient = (int) (taps / 2);
int middleCoefficient = (int)(taps / 2);
//Bin 0 of the idft is our center coefficient
coefficients[middleCoefficient] = frequencyResponse[0];
@@ -620,26 +565,22 @@ public class FilterFactory
return coefficients;
}
public static float[] getCICResponseArray(int sampleRate,
int frequency,
int length)
public static float[] getCICResponseArray(int sampleRate, int frequency, int length)
{
float[] cicArray = new float[length * 2];
int binCount = (int) ((Math.round(
(double) frequency / (double) sampleRate * 2.0d * (double) length)));
int binCount = (int)((Math.round(
(double)frequency / (double)sampleRate * 2.0d * (double)length)));
cicArray[0] = 1.0f;
float unityResponse = (float) (Math.sin(1.0d / (double) length) /
(1.0d / (double) length));
float unityResponse = (float)(Math.sin(1.0d / (double)length) /
(1.0d / (double)length));
for(int x = 1; x <= binCount; x++)
{
/* Calculate unity response plus amplification for droop */
float compensated = 1.0f + (unityResponse -
(float) (Math.sin((double) x / (double) length) /
((double) x / (double) length)));
/* Calculate unity response plus amplification for droop */
float compensated = 1.0f + (unityResponse - (float)(Math.sin((double)x / (double)length) / ((double)x / (double)length)));
cicArray[x] = compensated;
cicArray[length - x] = compensated;
@@ -649,28 +590,29 @@ public class FilterFactory
}
/**
* Creates root raised cosine filter coefficients with a tap count equal
* to the symbols x samplesPerSymbol + 1.
* Creates root raised cosine filter coefficients with a tap count equal to the symbols x samplesPerSymbol + 1.
*
* Symbol count should be an even integer.
*
* Ported to java from gnuradio/filter/firdes.cc
*
* For 40db attenuation, calculate the number of symbols based on the
* following formula:
*
* For 40db attenuation, calculate the number of symbols based on the following formula:
* Symbols = -44 * alpha + 33
*
* Polyphase Channelizer notes:
* -Set samples Per Symbol at 2 (or more) * number of channels
* -Set symbolCount to sufficient size to produce required attenuation
* -Set symbol rate in hertz
* -Set alpha = (desired channel bandwidth / (symbol rate * samples per symbol) - 1.0
* -Channel bandwidth must be larger than symbol rate * samples per symbol, in order for there to be a positive alpha
*
* @param samplesPerSymbol - number of samples per symbol
* @param symbols - number of symbols - must be even
* @param alpha - roll-off factor
* @param symbolCount - number of symbol intervals for the filter. This directly impacts the filter size
* @param alpha - roll-off factor.
* @return - filter coefficients
*/
public static float[] getRootRaisedCosine(float samplesPerSymbol,
int symbols,
float alpha)
public static float[] getRootRaisedCosine(int samplesPerSymbol, int symbolCount, float alpha)
{
int taps = (int)(samplesPerSymbol * (float)symbols + 1.0f);
int taps = samplesPerSymbol * symbolCount;
float scale = 0;
@@ -678,10 +620,10 @@ public class FilterFactory
for(int x = 0; x < taps; x++)
{
float index = (float) x - ((float) taps / 2.0f);
float index = (float)x - ((float)taps / 2.0f);
float x1 = (float) Math.PI * index / samplesPerSymbol;
float x2 = 4.0f * alpha * index / samplesPerSymbol;
float x1 = (float)Math.PI * index / (float)samplesPerSymbol;
float x2 = 4.0f * alpha * index / (float)samplesPerSymbol;
float x3 = x2 * x2 - 1.0f;
float numerator, denominator;
@@ -690,17 +632,15 @@ public class FilterFactory
{
if(x != taps / 2)
{
numerator = (float) Math.cos((1.0 + alpha) * x1) +
(float) Math.sin((1.0f - alpha) * x1) /
(4.0f * alpha * index / samplesPerSymbol);
numerator = (float)Math.cos((1.0 + alpha) * x1) +
(float)Math.sin((1.0f - alpha) * x1) / (4.0f * alpha * index / (float)samplesPerSymbol);
}
else
{
numerator = (float) Math.cos((1.0f + alpha) * x1) +
(1.0f - alpha) * (float) Math.PI / (4.0f * alpha);
numerator = (float)Math.cos((1.0f + alpha) * x1) + (1.0f - alpha) * (float)Math.PI / (4.0f * alpha);
}
denominator = x3 * (float) Math.PI;
denominator = x3 * (float)Math.PI;
}
else
{
@@ -713,14 +653,11 @@ public class FilterFactory
x3 = (1.0f - alpha) * x1;
x2 = (1.0f + alpha) * x1;
numerator = (float) (Math.sin(x2) * (1.0 + alpha) * Math.PI -
Math.cos(x3) * ((1.0 - alpha) * Math.PI *
(double) samplesPerSymbol) / (4.0 * alpha * index) +
Math.sin(x3) * (double) samplesPerSymbol *
(double) samplesPerSymbol / (4.0 * alpha * index * index));
numerator = (float)(Math.sin(x2) * (1.0f + alpha) * Math.PI - Math.cos(x3) * ((1.0 - alpha) * Math.PI *
(double)samplesPerSymbol) / (4.0 * alpha * index) + Math.sin(x3) * (double)samplesPerSymbol *
(double)samplesPerSymbol / (4.0 * alpha * index * index));
denominator = (float) (-32.0 * Math.PI * alpha * alpha * index /
(double) samplesPerSymbol);
denominator = (float)(-32.0 * Math.PI * alpha * alpha * index / (double)samplesPerSymbol);
}
coefficients[x] = 4.0f * alpha * numerator / denominator;
@@ -740,10 +677,10 @@ public class FilterFactory
}
/**
* Creates a filter from the filter specification
* Creates a filter from the filter specification using the remez exchange design algorithm
*
* @param specification
* @return
* @return filter coefficients
* @throws FilterDesignException if the filter cannot be designed
*/
public static float[] getTaps(FIRFilterSpecification specification) throws FilterDesignException
@@ -757,4 +694,346 @@ public class FilterFactory
return null;
}
/**
* Evaluates the filter to determine the magnitude response (dB) at the specified frequency.
*
* @param filter to evaluate
* @param frequency to calculate (0 <> 0.5)
* @return magnitude frequency response in decibels
*/
public static double evaluate(float[] filter, double frequency)
{
double real = 0.0;
double imag = 0.0;
for(int x = 0; x < filter.length; x++)
{
real += filter[x] * Math.cos(Math.PI * frequency * (double)x);
imag += filter[x] * Math.sin(Math.PI * frequency * (double)x);
}
return decibel(real, imag);
}
/**
* Calculates the decibel magnitude of the real and imaginary complex sample
*
* @param real (x-axis) value
* @param imag (y-axis) value
* @return magnitude in decibels
*/
public static double decibel(double real, double imag)
{
return (float)(10.0 * Math.log10(Math.pow(real, 2.0) + Math.pow(imag, 2.0)));
}
public static float[] getRemezChannelizer(int channelBandwidth, int channels, int tapsPerChannel, double alpha,
double passRipple, double stopRipple) throws FilterDesignException
{
FIRFilterSpecification specification = FIRFilterSpecification.channelizerBuilder()
.sampleRate(channels * channelBandwidth * 4)
.channels(channels)
.channelBandwidth(channelBandwidth)
.tapsPerChannel(tapsPerChannel)
.alpha(alpha)
.passRipple(passRipple)
.stopRipple(stopRipple)
.build();
RemezFIRFilterDesignerWithLagrange designer = new RemezFIRFilterDesignerWithLagrange(specification);
float[] taps = designer.getImpulseResponse();
double bandEdgeFrequency = (double)channelBandwidth / (double)(channels * channelBandwidth * 2);
double response = decibel(designer.getFrequencyResponse(Math.cos(bandEdgeFrequency * Math.PI)), 0.0);
mLog.debug("Frequency Response at 1.0: " + response);
response = decibel(designer.getFrequencyResponse(Math.cos(bandEdgeFrequency * Math.PI * 2.0)), 0.0);
mLog.debug("Frequency Response at 2.0: " + response);
return taps;
}
/**
* Polyphase M2 synthesizer sync filter. Designed for multiple M2 oversampled channel inputs and
* an M*channel count output.
*
* @param channelBandwidth per channel
* @param channels count
* @param tapsPerChannel minumum. This may be increased to meet the band edge -6.02dB requirement
* @return filter
* @throws FilterDesignException
*/
public static float[] getSincM2Synthesizer(double channelBandwidth, int channels, int tapsPerChannel)
throws FilterDesignException
{
int filterLength = (channels * tapsPerChannel) - 1;
//Design the prototype synthesizer with 105% of the channel bandwidth produced by the channelizer.
float[] taps = FilterFactory.getKaiserSinc(filterLength, channelBandwidth * 1.05, 80.0);
//This is an odd length filter - increase the length by 1 by pre-padding a zero coefficient
float[] extendedTaps = new float[taps.length + 1];
System.arraycopy(taps, 0, extendedTaps, 1, taps.length);
//Adjust the overall gain of the filter to an objective of 1.0 divided by the number of channels
return normalize(extendedTaps, 1.0f);
}
/**
* Creates a windowed-sync (Nyquist) M/2 prototype FIR filter for use with a Polyphase Channelizer/Synthesizer.
* The filter is designed for x2 oversampling of each channel and the filter cutoff frequency is incrementally
* adjusted to achieve a -6.02 dB attenuation at the channel band edge to enable perfect reconstruction of adjacent
* channels.
*
* The odd length sync filter that meets the design objectives will be increased in length by pre-padding a zero
* coefficient to produce a filter with a length that is an integral of the channel count.
*
* Note: the tapsPerChannel value will be iteratively increased (up to 10x) if a filter cannot be designed to meet
* the channel band edge attenuation objective.
*
* Note: a channelizer filter designed using a Blackman-Harris-7 window and 18-21 taps per channel creates a filter
* with attenuation in range of -60 to -80 dB at the edge of the 2x sampled channel.
*
* @param channelBandwidth - full bandwidth of the channel in hertz
* @param channels - number of channels
* @param tapsPerChannel - desired filter tap count per channel (may be increased to meet objective)
* @param windowType - to use when designing the windowed-sync filter
* @param logResults - to log a debug summary of the filter characteristics
*
* @return filter coefficients
*
* @throws FilterDesignException if a filter cannot be designed with a band edge attenuation of -6.02 dB
*/
/**
* Polyphase M2 channelizer sync filter. Designed for an M*channel input and an M2 channel output.
*
* @param channelBandwidth per channel
* @param channels count
* @param tapsPerChannel minimum. This may be increased to meet the band edge -6.02dB requirement
* @param logResults to log the results of the design
* @return filter
* @throws FilterDesignException if the filter cannot be designed with a band edge of -6.02dB
*/
public static float[] getSincM2Channelizer(double channelBandwidth, int channels, int tapsPerChannel,
boolean logResults) throws FilterDesignException
{
int currentTapsPerChannel = tapsPerChannel;
int filterLength = (channels * currentTapsPerChannel) - 1;
double sampleRate = channelBandwidth * channels;
double bandEdge = channelBandwidth / sampleRate;
double cutoffFrequency = bandEdge / 2.0;
double increment = cutoffFrequency * 0.1;
//Get an initial filter and band edge frequency response using the channel bandwidth as a cutoff
float[] taps = null;
taps = FilterFactory.getKaiserSinc(filterLength, cutoffFrequency, 80.0);
double response = FilterFactory.evaluate(taps, bandEdge);
//Set cutoff adjustment threshold - we'll test cutoff frequencies to around 1 hertz resolution
double incrementThreshold = 1.0 / sampleRate;
//Iteratively evaluate filters to find the filter with the highest cutoff frequency that meets the objective
while(increment > incrementThreshold)
{
//If the current cutoff meets the objective, test if a higher cutoff also meets the objective
if(matchesObjective(response, PERFECT_RECONSTRUCTION_GAIN_AT_BAND_EDGE, MARGIN_OF_ERROR) &&
(cutoffFrequency + increment <= bandEdge))
{
float[] higherCutoffTaps = FilterFactory.getKaiserSinc(filterLength, cutoffFrequency + increment, 80.0);
double higherCutoffResponse = FilterFactory.evaluate(higherCutoffTaps, bandEdge);
if(matchesObjective(higherCutoffResponse, PERFECT_RECONSTRUCTION_GAIN_AT_BAND_EDGE, MARGIN_OF_ERROR))
{
cutoffFrequency += increment;
taps = higherCutoffTaps;
response = higherCutoffResponse;
}
else
{
increment /= 2.0;
}
}
//If the current cutoff meets the objective, decrease the increment to test for smaller resolution cutoffs
else if(matchesObjective(response, PERFECT_RECONSTRUCTION_GAIN_AT_BAND_EDGE, MARGIN_OF_ERROR))
{
increment /= 2.0;
}
//Decrease the cutoff frequency - we can't meet the objective
else
{
cutoffFrequency -= increment;
if(cutoffFrequency <= 0)
{
if(logResults)
{
mLog.debug("Warning: cannot design channelizer filter with tap count [" + currentTapsPerChannel +
"] increasing tap count and starting over");
}
currentTapsPerChannel++;
//After we've attempted increasing the taps per channel 10x, give up
if(currentTapsPerChannel > (tapsPerChannel + 10))
{
throw new FilterDesignException("Couldn't design filter with taps per channel count in the " +
"range of " + tapsPerChannel + " - " + (tapsPerChannel + 10) + " Sample Rate:" + sampleRate + " Channels:" + channels);
}
filterLength = channels * currentTapsPerChannel - 1;
cutoffFrequency = channelBandwidth / sampleRate;
increment = cutoffFrequency * 0.1;
}
taps = FilterFactory.getKaiserSinc(filterLength, cutoffFrequency, 80.0);
response = FilterFactory.evaluate(taps, bandEdge);
}
}
//This will probably never happen, but ...
if(!matchesObjective(response, PERFECT_RECONSTRUCTION_GAIN_AT_BAND_EDGE, MARGIN_OF_ERROR))
{
throw new FilterDesignException("Cannot design filter to specifications");
}
//This is an odd length filter - increase the length by 1 by pre-padding a zero coefficient
float[] extendedTaps = new float[taps.length + 1];
System.arraycopy(taps, 0, extendedTaps, 1, taps.length);
if(logResults)
{
mLog.debug("Polyphase Channelizer Filter Design Summary");
mLog.debug("-----------------------------------------------------");
mLog.debug("Input Sample Rate: " + sampleRate);
mLog.debug("Channel Bandwidth: " + channelBandwidth);
mLog.debug("Channels: " + channels);
mLog.debug("Window Type: " + Window.WindowType.KAISER.name());
mLog.debug("Taps Per Channel - Requested:" + tapsPerChannel + " Actual:" + ((double)extendedTaps.length / (double)channels));
mLog.debug("Filter Length: " + (extendedTaps.length));
mLog.debug("Requested Cutoff Frequency: " + (sampleRate * bandEdge));
mLog.debug("Actual Cutoff Frequency: " + (sampleRate * cutoffFrequency));
mLog.debug("Attenuation at 0.0 OBJECTIVE: " + 0.0);
mLog.debug("Attenuation at 1.0 OBJECTIVE: " + PERFECT_RECONSTRUCTION_GAIN_AT_BAND_EDGE);
mLog.debug("Attenuation at 1.00 Channels: " + evaluate(taps, bandEdge * 1.00) + "\tFrequency: " + (sampleRate * bandEdge * 1.0));
mLog.debug("Attenuation at 1.25 Channels: " + evaluate(taps, bandEdge * 1.25) + "\tFrequency: " + (sampleRate * bandEdge * 1.25));
mLog.debug("Attenuation at 1.50 Channels: " + evaluate(taps, bandEdge * 1.50) + "\tFrequency: " + (sampleRate * bandEdge * 1.5));
mLog.debug("Attenuation at 1.75 Channels: " + evaluate(taps, bandEdge * 1.75) + "\tFrequency: " + (sampleRate * bandEdge * 1.75));
mLog.debug("Attenuation at 2.00 Channels: " + evaluate(taps, bandEdge * 2.00) + "\tFrequency: " + (sampleRate * bandEdge * 2.0));
}
//Adjust the overall gain of the filter to an objective of 1.0 times the number of channels
return normalize(extendedTaps, 1.0f * (float)channels);
}
/**
* Creates a windowed-sync (Nyquist) filter.
*
* @param cutoff frequency (0.0 <> 0.5)
* @param length of the filter - must be odd-length
* @param windowType to use in designing the filter
* @return filter coefficients.
* @throws FilterDesignException if the requested length is not odd
*/
public static float[] getSinc(double cutoff, int length, Window.WindowType windowType) throws FilterDesignException
{
if(length % 2 == 0)
{
throw new FilterDesignException("Sinc filters must be odd-length");
}
float[] coefficients = new float[length];
int half = length / 2;
double[] window = Window.getWindow(windowType, length);
double scalor = 2.0 * cutoff;
double piScalor = Math.PI * scalor;
coefficients[half] = (float)(1.0 * scalor * window[half]);
for(int x = 1; x <= half; x++)
{
double a = piScalor * x;
double coefficient = scalor * Math.sin(a) / a;
coefficient *= window[half + x];
coefficients[half + x] = (float)coefficient;
coefficients[half - x] = (float)coefficient;
}
return coefficients;
}
/**
* Creates a Nyquist filter using a Kaiser Window.
*
* @param cutoff frequency (0.0 <> 0.5)
* @param length of the filter - must be odd-length
* @param attenuation desired for adjacent channels
* @return filter coefficients.
* @throws FilterDesignException if the requested length is not odd
*/
public static float[] getKaiserSinc(int length, double cutoff, double attenuation) throws FilterDesignException
{
if(length % 2 == 0)
{
throw new FilterDesignException("Sinc filters must be odd-length");
}
float[] coefficients = new float[length];
int half = length / 2;
double[] window = Window.getKaiser(length, attenuation);
double scalor = 2.0 * cutoff;
double piScalor = Math.PI * scalor;
coefficients[half] = (float)(1.0 * scalor * window[half]);
for(int x = 1; x <= half; x++)
{
double a = piScalor * x;
double coefficient = scalor * Math.sin(a) / a;
coefficient *= window[half + x];
coefficients[half + x] = (float)coefficient;
coefficients[half - x] = (float)coefficient;
}
return coefficients;
}
/**
* Compares two doubles for equals and avoids any rounding error that are present.
*
* @param a value to compare
* @param objective value to compare
* @return true if they are equivalent within the margin of error
*/
private static boolean matchesObjective(double a, double objective, double marginOfError)
{
return Math.abs(a - objective) <= marginOfError;
}
public static void main(String[] args)
{
float[] taps = null;
try
{
taps = FilterFactory.getSincM2Channelizer(12500.0, 800, 9, true);
}
catch(FilterDesignException fde)
{
mLog.error("Error", fde);
}
mLog.debug("Done: " + Arrays.toString(taps));
}
}

View File

@@ -1,234 +1,523 @@
/*******************************************************************************
* SDR Trunk
* Copyright (C) 2014 Dennis Sheirer
*
* This program is free software: you can redistribute it and/or modify
* it under the terms of the GNU General Public License as published by
* the Free Software Foundation, either version 3 of the License, or
* (at your option) any later version.
*
* This program is distributed in the hope that it will be useful,
* but WITHOUT ANY WARRANTY; without even the implied warranty of
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
* GNU General Public License for more details.
*
* You should have received a copy of the GNU General Public License
* along with this program. If not, see <http://www.gnu.org/licenses/>
* 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/>
*
******************************************************************************/
/*
* Kaiser Window design methods: getKaiser(), getKaiserBeta() and getBesselZerothOrder() with minor naming changes were
* adopted from:
*
* https://github.com/rjeschke/neetutils-base/blob/master/src/main/java/com/github/rjeschke/neetutils/audio/FIRUtils.java
*
* Copyright (C) 2012 René Jeschke <rene_jeschke@yahoo.de>
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
package io.github.dsheirer.dsp.filter;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import java.util.Arrays;
/**
* Window creator
*
* Blackman, Hamming and Hanning windows
* based on windowing methods described at:
* http://www.mstarlabs.com/dsp/goertzel/goertzel.html
* and
* Understanding Digital Signal Processing 3e, Lyons, p.91
* and
* Digital Filters for Everyone, Allred, p. 132
*
* Window factory and corresponding utility methods.
*/
public class Window
{
public static double[] getWindow( WindowType type, int length )
private final static Logger mLog = LoggerFactory.getLogger(Window.class);
private static final double TWO_PI = Math.PI * 2.0d;
private static final double FOUR_PI = Math.PI * 4.0d;
private static final double SIX_PI = Math.PI * 6.0d;
private static final double EIGHT_PI = Math.PI * 8.0d;
/**
* Creates a window of the specified type and length.
*
* @param type of window
* @param length of window
* @return window of the specified type and length
*/
public static double[] getWindow(WindowType type, int length)
{
switch( type )
{
case BLACKMAN:
return getBlackmanWindow( length );
case COSINE:
return getCosineWindow( length );
case HAMMING:
return getHammingWindow( length );
case HANNING:
return getHanningWindow( length );
case NONE:
default:
return getRectangularWindow( length );
}
switch(type)
{
case BLACKMAN:
return getBlackman(length);
case BLACKMAN_HARRIS_4:
return getBlackmanHarris4(length);
case BLACKMAN_HARRIS_7:
return getBlackmanHarris7(length);
case BLACKMAN_NUTALL:
return getBlackmanNutall(length);
case COSINE:
return getCosine(length);
case FLAT_TOP:
return getFlatTop(length);
case HAMMING:
return getHamming(length);
case HANN:
return getHann(length);
case KAISER:
throw new IllegalArgumentException("Kaiser Window cannot be created via this method. Use the getKaiser() method instead.");
case NUTALL:
return getNutall(length);
case NONE:
default:
return getRectangular(length);
}
}
public static double[] getRectangularWindow( int length )
/**
* Creates an all-pass or rectangular window.
*
* @param length of the window
* @return rectangular window
*/
public static double[] getRectangular(int length)
{
double[] coefficients = new double[ length ];
double[] coefficients = new double[length];
Arrays.fill( coefficients, 1.0d );
return coefficients;
}
public static double[] getCosineWindow( int length )
{
double[] coefficients = new double[ length ];
Arrays.fill(coefficients, 1.0d);
if( length % 2 == 0 ) //Even length
{
int half = (int)( ( length - 1 ) / 2 );
for( int x = -half; x < length / 2 + 1; x++ )
{
coefficients[ x + half ] = Math.cos(
( (double)x * Math.PI ) / ( (double)length + 1.0d ) );
}
}
else //Odd length
{
int half = (int) length / 2;
for( int x = -half; x < half + 1; x++ )
{
coefficients[ x + half ] = Math.cos(
( (double)x * Math.PI ) / ( (double)length + 1.0d ) );
}
}
return coefficients;
return coefficients;
}
public static double[] getBlackmanWindow( int length )
/**
* Creates a cosine window of the specified length.
*
* @param length of the window
* @return cosine window
*/
public static double[] getCosine(int length)
{
double[] coefficients = new double[ length ];
for( int x = 0; x < length; x++ )
{
coefficients[ x ] = .426591D -
( .496561D * Math.cos( ( Math.PI * 2.0D * (double)x ) / (double)( length - 1 ) ) +
( .076848D * Math.cos( ( Math.PI * 4.0D * (double)x ) / (double)( length - 1 ) ) ) );
}
double[] coefficients = new double[length];
return coefficients;
}
public static double[] getHammingWindow( int length )
{
double[] coefficients = new double[ length ];
if( length % 2 == 0 ) //Even length
{
for( int x = 0; x < length; x++ )
{
coefficients[ x ] = .54D - ( .46D *
Math.cos( ( Math.PI * ( 2.0D * (double)x + 1.0d ) ) / (double)( length - 1 ) ) );
}
}
else //Odd length
{
for( int x = 0; x < length; x++ )
{
coefficients[ x ] = .54D - ( .46D *
Math.cos( ( 2.0D * Math.PI * (double)x ) / (double)( length - 1 ) ) );
}
}
if(length % 2 == 0) //Even length
{
int half = (int)((length - 1) / 2);
return coefficients;
for(int x = -half; x < length / 2 + 1; x++)
{
coefficients[x + half] = Math.cos(
((double)x * Math.PI) / ((double)length + 1.0d));
}
}
else //Odd length
{
int half = (int)length / 2;
for(int x = -half; x < half + 1; x++)
{
coefficients[x + half] = Math.cos(
((double)x * Math.PI) / ((double)length + 1.0d));
}
}
return coefficients;
}
public static double[] getHanningWindow( int length )
/**
* Creates a 3-term Blackman window.
*
* @param length of the window
* @return Blackman window
*/
public static double[] getBlackman(int length)
{
double[] coefficients = new double[ length ];
double[] coefficients = new double[length];
if( length % 2 == 0 ) //Even length
{
for( int x = 0; x < length; x++ )
{
coefficients[ x ] = .5D - ( .5D *
Math.cos( ( Math.PI * ( 2.0D * (double)x + 1 ) ) / (double)( length - 1 ) ) );
}
}
else //Odd length
{
for( int x = 0; x < length; x++ )
{
coefficients[ x ] = .5D - ( .5D *
Math.cos( ( 2.0D * Math.PI * (double)x ) / (double)( length - 1 ) ) );
}
}
double denominator = length - 1;
double a0 = 0.426590713672;
double a1 = 0.496560619089;
double a2 = 0.0768486672399;
for(int x = 0; x < length; x++)
{
coefficients[x] = a0 -
(a1 * Math.cos((TWO_PI * (double)x) / denominator)) +
(a2 * Math.cos((FOUR_PI * (double)x) / denominator));
}
return coefficients;
}
/**
* Creates a 4-term Nutall window
*
* @param length of the window
* @return Nutall window
*/
public static double[] getNutall(int length)
{
double[] coefficients = new double[length];
double denominator = length - 1;
double a0 = 0.355768;
double a1 = 0.487396;
double a2 = 0.144232;
double a3 = 0.012604;
for(int x = 0; x < length; x++)
{
coefficients[x] = a0 -
(a1 * Math.cos(TWO_PI * (double)x / denominator)) +
(a2 * Math.cos(FOUR_PI * (double)x / denominator)) -
(a3 * Math.cos(SIX_PI * (double)x / denominator));
}
return coefficients;
}
/**
* Creates a 4-term Blackman-Nutall window
*
* @param length of the window
* @return Blackman-Nutall window
*/
public static double[] getBlackmanNutall(int length)
{
double[] coefficients = new double[length];
double denominator = length - 1;
double a0 = 0.3635819;
double a1 = 0.4891775;
double a2 = 0.1365995;
double a3 = 0.0106411;
for(int x = 0; x < length; x++)
{
coefficients[x] = a0 -
(a1 * Math.cos(TWO_PI * (double)x / denominator)) +
(a2 * Math.cos(FOUR_PI * (double)x / denominator)) -
(a3 * Math.cos(SIX_PI * (double)x / denominator));
}
return coefficients;
}
/**
* Creates a 4-term Blackman-Harris window
*
* @param length of the window
* @return Blackman-Harris window
*/
public static double[] getBlackmanHarris4(int length)
{
double[] coefficients = new double[length];
double denominator = length - 1;
double a0 = 0.35875;
double a1 = 0.48829;
double a2 = 0.14128;
double a3 = 0.01168;
for(int x = 0; x < length; x++)
{
coefficients[x] = a0 -
(a1 * Math.cos(TWO_PI * (double)x / denominator)) +
(a2 * Math.cos(FOUR_PI * (double)x / denominator)) -
(a3 * Math.cos(SIX_PI * (double)x / denominator));
}
return coefficients;
}
/**
* Creates a 7-term Blackman-Harris window
*
* @param length of the window
* @return Blackman-Harris window
*/
public static double[] getBlackmanHarris7(int length)
{
double[] coefficients = new double[length];
double denominator = length - 1;
double a0 = 0.27105140069342;
double a1 = 0.43329793923448;
double a2 = 0.21812299954311;
double a3 = 0.06592544638803;
double a4 = 0.01081174209837;
double a5 = 0.00077658482522;
double a6 = 0.00001388721735;
return coefficients;
for(int x = 0; x < length; x++)
{
double w = TWO_PI * (double)x / denominator;
coefficients[x] = a0 -
(a1 * Math.cos(w)) +
(a2 * Math.cos(2.0 * w)) -
(a3 * Math.cos(3.0 * w)) +
(a4 * Math.cos(4.0 * w)) -
(a5 * Math.cos(5.0 * w)) +
(a6 * Math.cos(6.0 * w));
}
return coefficients;
}
/**
* Creates a Hamming window
*
* @param length of the window
* @return Hamming window
*/
public static double[] getHamming(int length)
{
double[] coefficients = new double[length];
double a0 = 0.54;
double a1 = 0.46;
double denominator = length;
for(int x = 0; x < length; x++)
{
coefficients[x] = a0 - (a1 * Math.cos(TWO_PI * (double)x / denominator));
}
return coefficients;
}
/**
* Creates a Hann window
*
* @param length of the window
* @return Hann window
*/
public static double[] getHann(int length)
{
double[] coefficients = new double[length];
double a0 = 0.5;
double a1 = 0.5;
double denominator = length;
for(int x = 0; x < length; x++)
{
coefficients[x] = a0 - (a1 * Math.cos(TWO_PI * (double)x / denominator));
}
return coefficients;
}
/**
* Creates a 4-term Blackman-Harris window
*
* @param length of the window
* @return Blackman-Harris window
*/
public static double[] getFlatTop(int length)
{
double[] coefficients = new double[length];
double denominator = length - 1;
double a0 = 0.215578948;
double a1 = 0.41663158;
double a2 = 0.277263158;
double a3 = 0.083578947;
double a4 = 0.006947368;
for(int x = 0; x < length; x++)
{
coefficients[x] = a0 -
(a1 * Math.cos(TWO_PI * (double)x / denominator)) +
(a2 * Math.cos(FOUR_PI * (double)x / denominator)) -
(a3 * Math.cos(SIX_PI * (double)x / denominator)) +
(a4 * Math.cos(EIGHT_PI * (double)x / denominator));
}
return coefficients;
}
/**
* Calculates the beta (roll-off factor) for a Kaiser window based on the specified attenuation.
*
* @param attenuation in decibels
* @return beta value in the range of 0.0 to 1.0
*/
public static double getKaiserBeta(double attenuation)
{
if(attenuation > 50.0)
{
return 0.1102 * (attenuation - 8.7);
}
else if(attenuation >= 21.0)
{
return (0.5842 * Math.pow(attenuation - 21.0, 0.4)) + (0.07886 * (attenuation - 21.0));
}
else
{
return 0.0;
}
}
/**
* Creates a Kaiser window.
*
* @param length of the window
* @param attenuation desired
* @return window
*/
public static double[] getKaiser(int length, double attenuation)
{
double[] coefficients = new double[length];
double beta = getKaiserBeta(attenuation);
double betaBesselZerothOrder = getBesselZerothOrder(beta);
double temp;
for(int x = 0; x < coefficients.length; x++)
{
temp = beta * Math.sqrt(1.0 - Math.pow(2.0 * x / (length - 1) - 1.0, 2));
coefficients[x] = getBesselZerothOrder(temp) / betaBesselZerothOrder;
}
return coefficients;
}
/**
* Zeroth order modified Bessel function.
*
* @param x Value.
* @return Return value.
*/
public final static double getBesselZerothOrder(final double x)
{
double f = 1;
final double x2 = x * x * 0.25;
double xc = x2;
double v = 1 + x2;
for (int i = 2; i < 100; i++)
{
f *= i;
xc *= x2;
final double a = xc / (f * f);
v += a;
if (a < 1e-20) break;
}
return v;
}
/**
* Apply the window against an array of float-type samples
*/
public static float[] apply( double[] coefficients, float[] samples )
public static float[] apply(double[] coefficients, float[] samples)
{
for( int x = 0; x < coefficients.length; x++ )
{
samples[ x ] = (float)( samples[ x ] * coefficients[ x ] );
}
return samples;
}
public static float[] apply( WindowType type, float[] samples )
{
double[] coefficients = getWindow( type, samples.length );
return apply( coefficients, samples );
}
/**
* Apply the window against an array of float-type samples
*/
public static double[] apply( double[] coefficients, double[] samples )
{
for( int x = 0; x < coefficients.length; x++ )
{
samples[ x ] = samples[ x ] * coefficients[ x ];
}
return samples;
for(int x = 0; x < coefficients.length; x++)
{
samples[x] = (float)(samples[x] * coefficients[x]);
}
return samples;
}
public static double[] apply( WindowType type, double[] samples )
/**
* Apply the window type against the float array of samples
*
* @param type of window to use
* @param samples to be windowed
* @return windowed samples
*/
public static float[] apply(WindowType type, float[] samples)
{
double[] coefficients = getWindow( type, samples.length );
return apply( coefficients, samples );
double[] coefficients = getWindow(type, samples.length);
return apply(coefficients, samples);
}
/**
* Apply the window against an array of double-type samples
*/
public static double[] apply(double[] coefficients, double[] samples)
{
for(int x = 0; x < coefficients.length; x++)
{
samples[x] = samples[x] * coefficients[x];
}
return samples;
}
/**
* Apply the window type against the double array of samples
*
* @param type of window to use
* @param samples to be windowed
* @return windowed samples
*/
public static double[] apply(WindowType type, double[] samples)
{
double[] coefficients = getWindow(type, samples.length);
return apply(coefficients, samples);
}
/**
* Window types
*/
public enum WindowType
{
BLACKMAN, COSINE, HAMMING, HANNING, NONE;
}
/**
* Utility to log the double arrays
*/
public static String arrayToString( double[] array, boolean breaks )
{
StringBuilder sb = new StringBuilder();
for( int x = 0; x < array.length; x++ )
{
sb.append( x + ":" + array[ x ] );
BLACKMAN("Blackman"),
BLACKMAN_HARRIS_4("Blackman-Harris 4"),
BLACKMAN_HARRIS_7("Blackman-Harris 7"),
BLACKMAN_NUTALL("Blackman-Nutall"),
COSINE("Cosine"),
FLAT_TOP("Flat Top"),
HAMMING("Hamming"),
HANN("Hann"),
KAISER("Kaiser"),
NUTALL("Nutall"),
NONE("None");
if( breaks )
{
sb.append( "\n" );
}
else
{
if( x % 8 == 7 )
{
sb.append( "\n" );
}
else
{
sb.append( "\t" );
}
}
}
return sb.toString();
}
private String mLabel;
WindowType(String label)
{
mLabel = label;
}
public String toString()
{
return mLabel;
}
}
public static void main(String[] args)
{
double[] kaiser = getKaiser(16, 80.0);
mLog.debug("Window:" + Arrays.toString(kaiser));
}
}

View File

@@ -0,0 +1,169 @@
/*******************************************************************************
* sdrtrunk
* Copyright (C) 2014-2017 Dennis Sheirer
*
* This program is free software: you can redistribute it and/or modify
* it under the terms of the GNU General Public License as published by
* the Free Software Foundation, either version 3 of the License, or
* (at your option) any later version.
*
* This program is distributed in the hope that it will be useful,
* but WITHOUT ANY WARRANTY; without even the implied warranty of
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
* GNU General Public License for more details.
*
* You should have received a copy of the GNU General Public License
* along with this program. If not, see <http://www.gnu.org/licenses/>
*
******************************************************************************/
package io.github.dsheirer.dsp.filter.channelizer;
import io.github.dsheirer.sample.Broadcaster;
import io.github.dsheirer.sample.Listener;
import io.github.dsheirer.sample.buffer.ReusableChannelResultsBuffer;
import io.github.dsheirer.sample.buffer.ReusableChannelResultsBufferQueue;
import io.github.dsheirer.sample.buffer.ReusableComplexBuffer;
import io.github.dsheirer.source.ISourceEventListener;
import io.github.dsheirer.source.SourceEvent;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import java.util.List;
import java.util.concurrent.CopyOnWriteArrayList;
public abstract class AbstractComplexPolyphaseChannelizer implements Listener<ReusableComplexBuffer>, ISourceEventListener
{
private final static Logger mLog = LoggerFactory.getLogger(AbstractComplexPolyphaseChannelizer.class);
private ReusableChannelResultsBufferQueue mBufferQueue = new ReusableChannelResultsBufferQueue("AbstractComplexPolyphaseChannelizer");
private Broadcaster<SourceEvent> mSourceChangeBroadcaster = new Broadcaster();
private List<PolyphaseChannelSource> mChannels = new CopyOnWriteArrayList<>();
private double mSampleRate;
private int mChannelCount;
private int mSubChannelCount;
private double mChannelSampleRate;
/**
* Complex sample polyphase channelizer
*
* @param channelCount
*/
public AbstractComplexPolyphaseChannelizer(double sampleRate, int channelCount)
{
mChannelCount = channelCount;
mSubChannelCount = channelCount * 2; //Number of I/Q channels
mSampleRate = sampleRate;
mChannelSampleRate = (double)mSampleRate / (double)mChannelCount;
}
/**
* Input sample rate for this channelizer
* @return sample rate in hertz
*/
public double getSampleRate()
{
return mSampleRate;
}
/**
* Sets the input sample rate for for this channelizer
* @param sampleRate in hertz
*/
public void setSampleRate(double sampleRate)
{
mSampleRate = sampleRate;
mChannelSampleRate = mSampleRate / (double)mChannelCount;
}
/**
* Number of channels being processed by this channelizer.
*/
public int getChannelCount()
{
return mChannelCount;
}
/**
* Number of sub channels (I/Q) being processed by this channelizer. This is 2 * channel count.
*/
public int getSubChannelCount()
{
return mSubChannelCount;
}
/**
* Output channel sample rate
* @return sample rate in hertz
*/
public double getChannelSampleRate()
{
return mChannelSampleRate;
}
/**
* Dispatches the processed channel samples to any registered polyphase channel outputs.
*
* @param channelResultsBuffer containing an array of an array of I/Q samples per channel
*/
protected void dispatch(ReusableChannelResultsBuffer channelResultsBuffer)
{
for(PolyphaseChannelSource channel : mChannels)
{
channelResultsBuffer.incrementUserCount();
channel.receiveChannelResults(channelResultsBuffer);
}
channelResultsBuffer.decrementUserCount();
}
/**
* Adds the polyphase channel source to receive processed output channel samples
*
* @param polyphaseChannelSource
*/
public void addChannel(PolyphaseChannelSource polyphaseChannelSource)
{
if(polyphaseChannelSource != null && !mChannels.contains(polyphaseChannelSource))
{
mChannels.add(polyphaseChannelSource);
mSourceChangeBroadcaster.addListener(polyphaseChannelSource.getSourceEventListener());
}
}
/**
* Removes the polyphase channel source from receiving output channel samples.
*
* @param polyphaseChannelSource
*/
public void removeChannel(PolyphaseChannelSource polyphaseChannelSource)
{
if(polyphaseChannelSource != null && mChannels.contains(polyphaseChannelSource))
{
mChannels.remove(polyphaseChannelSource);
mSourceChangeBroadcaster.removeListener(polyphaseChannelSource.getSourceEventListener());
}
}
/**
* Number of polyphase channels registered to receive sample streams
*/
public int getRegisteredChannelCount()
{
return mChannels.size();
}
/**
* Creates or reuses a channel results buffer for subclass implementations to use for temporary
* storage of channel results through distribution and consumption of channel results by
* channel output processors.
*/
protected ReusableChannelResultsBuffer getChannelResultsBuffer()
{
return mBufferQueue.getBuffer();
}
@Override
public Listener<SourceEvent> getSourceEventListener()
{
return mSourceChangeBroadcaster;
}
}

View File

@@ -0,0 +1,594 @@
/*******************************************************************************
* *********************************************************************************************************************
* sdr-trunk
* Copyright (C) 2014-2017 Dennis Sheirer
*
* This program is free software: you can redistribute it and/or modify it under the terms of the GNU General Public
* License as published by the Free Software Foundation, either version 3 of the License, or (at your option) any
* later version.
*
* This program is distributed in the hope that it will be useful, but WITHOUT ANY WARRANTY; without even the implied
* warranty of MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU General Public License for more details.
*
* You should have received a copy of the GNU General Public License along with this program.
* If not, see <http://www.gnu.org/licenses/>
* *********************************************************************************************************************
******************************************************************************/
package io.github.dsheirer.dsp.filter.channelizer;
import io.github.dsheirer.source.tuner.channel.TunerChannel;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import java.text.DecimalFormat;
import java.util.ArrayList;
import java.util.List;
public class ChannelCalculator
{
private final static Logger mLog = LoggerFactory.getLogger(ChannelCalculator.class);
/**
* Policy to determine desired corrective action for methods that calculate an index for a frequency or a frequency
* from an index where the frequency may exist either at the overlapping boundary of two indexes, or in the middle
* of the wrap-around (N/2) index.
*/
public enum IndexBoundaryPolicy
{
ADJUST_POSITIVE,
ADJUST_NEGATIVE
}
private double mSampleRate;
private int mChannelCount;
private double mCenterFrequency;
private double mChannelBandwidth;
private double mOversampling = 1.0;
/**
* Calculates channel index(es) from a polyphase channelizer output to source a DDC polyphase channel source
* with a specified channel center frequency and bandwidth.
*
* Polyphase channel indexes are not linear/consecutive owing to the use of the FFT to rotate channels to their
* center frequencies following filtering in the polyphase filter bank. As such, this calculator provides methods
* to determine the min/center/max frequency of each channel that is adjusted for the current center frequency
* value and to determine the channel index(es) that need to be targeted to produce a requested output channel
* with a specific frequency and bandwdith.
*
* @param sampleRate in hertz for the input sample rate from the sample source (ie tuner)
* @param channelCount number of channels in the polyphase filter bank
* @param centerFrequency center frequency for the input sample stream
* @param oversampling set to 1.0 for no oversample, or greater than 1.0 for oversampling
*/
public ChannelCalculator(double sampleRate, int channelCount, double centerFrequency, double oversampling)
{
mSampleRate = sampleRate;
mChannelCount = channelCount;
mCenterFrequency = centerFrequency;
mOversampling = oversampling;
updateChannelBandwidth();;
}
/**
* Calculates the channel bandwidth from the sample rate and channel count.
*/
private void updateChannelBandwidth()
{
mChannelBandwidth = (double)mSampleRate / (double)mChannelCount;
}
/**
* Center frequency for the channelizer
*/
public double getCenterFrequency()
{
return mCenterFrequency;
}
/**
* Updates the center frequency for the channelizer
*/
public void setCenterFrequency(double frequency)
{
mCenterFrequency = frequency;
}
/**
* Number of channels being channelized by the polyphase channelizer
*/
public int getChannelCount()
{
return mChannelCount;
}
/**
* Updates the number of channels being channelized by the polyphase channelizer
*/
public void setChannelCount(int channelCount)
{
mChannelCount = channelCount;
updateChannelBandwidth();
}
/**
* Identifies the index of the wrap-around channel meaning that the lower half of the channel exists at the upper
* end of the spectrum and the upper half of the channel exists at the lower end of the spectrum
*
* @return wrap-around channel index
*/
public int getWrapAroundIndex()
{
return mChannelCount / 2;
}
/**
* PolyphaseChannelManager input sample rate
* @return input sample rate in hertz
*/
public double getSampleRate()
{
return mSampleRate;
}
/**
* Updates the channel count using the sample rate argument to calculate the number of channels from the
* already established channel bandwidth.
*
* @param sampleRate to use for channel count
*/
public void setSampleRate(double sampleRate)
{
setChannelCount((int)(sampleRate / getChannelBandwidth()));
}
/**
* Channel bandwidth. Note: this value may be smaller than the channel sample rate when oversampling
* is being used.
*/
public double getChannelBandwidth()
{
return mChannelBandwidth;
}
/**
* Half of the channel bandwidth.
*/
public double getHalfChannelBandwidth()
{
return mChannelBandwidth / 2.0;
}
/**
* Oversampling rate produced by the channelizer.
*
* @return 1.0 for no oversampling, or a value greater than 1.0 when oversampling is applied.
*/
public double getOversampling()
{
return mOversampling;
}
/**
* Sets the oversampling rate being applied by the polyphase channelizer
* @param oversampling rate where 1.0 indicates no oversampling, and a value greater than 1.0 indicates
* that the polyphase channelizer is oversampling each channel.
*/
public void setOversampling(double oversampling)
{
mOversampling = oversampling;
}
/**
* Channel sample rate. This value is the product of the filtered channel bandwidth times the
* oversampling rate.
*/
public double getChannelSampleRate()
{
return getChannelBandwidth() * getOversampling();
}
/**
* PolyphaseChannelManager input minimum frequency
* @return minimum frequency in hertz
*/
public long getMinimumFrequency()
{
return (long)(getCenterFrequency() - (getSampleRate() / 2.0));
}
/**
* PolyphaseChannelManager input maximum frequency
* @return maximum frequency in hertz
*/
public long getMaximumFrequency()
{
return (long)(getCenterFrequency() + (getSampleRate() / 2.0));
}
/**
* Identifies the channelizer channel index(es) required to produce a tuner channel with the specified center
* frequency and channel bandwidth.
*
* @param tunerChannel specifying the desired channel frequency and bandwidth
*
* @return an array of one or more polyphase channelizer index(es) to target in order to produce an output channel
* for the desired frequency and bandwidth.
*
* @throws IllegalArgumentException if the tuner channel's minimum or maximum frequency is not contained
* within the channelizer's minimum or maximum frequency range
*/
public List<Integer> getChannelIndexes(TunerChannel tunerChannel) throws IllegalArgumentException
{
if(tunerChannel.getMinFrequency() < getMinimumFrequency() ||
tunerChannel.getMaxFrequency() > getMaximumFrequency())
{
throw new IllegalArgumentException("Requested channel cannot be provided by this " +
"channelizer. Requested channel [" + tunerChannel.getMinFrequency() + " - " +
tunerChannel.getMaxFrequency() + "] exceeds current channelizer frequency range [" +
getMinimumFrequency() + " - " + getMaximumFrequency() + "]");
}
int minIndex = getIndexForFrequency(tunerChannel.getMinFrequency(), IndexBoundaryPolicy.ADJUST_POSITIVE);
int maxIndex = getIndexForFrequency(tunerChannel.getMaxFrequency(), IndexBoundaryPolicy.ADJUST_NEGATIVE);
//Detect the unsourceable situation where the wrap around index would be included in both the minimum index
//and the maximum index, meaning that the tuner channel bandwidth is nearly the same as the input sample rate.
if(minIndex == getWrapAroundIndex() && maxIndex == getWrapAroundIndex())
{
throw new IllegalArgumentException("Requested tuner channel cannot be provided. Requested bandwidth is " +
"within two channel bandwidths of the sample rate.");
}
List<Integer> indexes = new ArrayList<>();
indexes.add(minIndex);
if(minIndex != maxIndex)
{
//Ensure our min/max indexes are within the correct bounds 0 <> N-1 to safeguard the while loop below
if(minIndex < 0 || minIndex >= getChannelCount() || maxIndex < 0 || maxIndex >= getChannelCount())
{
throw new IllegalArgumentException("Something went wrong while calculating the min and max polyphase " +
"channel indexes for tuner channel: " + tunerChannel);
}
int pointer = minIndex + 1;
if(pointer >= getChannelCount())
{
pointer -= getChannelCount();
}
while(pointer != maxIndex)
{
indexes.add(pointer);
pointer++;
if(pointer >= getChannelCount())
{
pointer -= getChannelCount();
}
}
indexes.add(maxIndex);
}
return indexes;
}
/**
* Calculates the index for the specified frequency. Certain frequencies can exist as both the maximum frequency
* of one channel and the minimum frequency of an adjacent channel. The index boundary policy identifies your
* choice to select the higher frequency channel index (ADJUST_POSITIVE) or the lower frequency channel
* index (ADJUST_NEGATIVE).
*
* @param frequency to determine channel index for
* @param indexBoundaryPolicy to determine the behavior when the frequency overlaps two channel boundaries
* @return index for the frequency
*/
public int getIndexForFrequency(long frequency, IndexBoundaryPolicy indexBoundaryPolicy)
{
double offset = frequency - getCenterFrequency();
if(Math.abs(offset) < getHalfChannelBandwidth())
{
return 0; //Center index
}
if(offset > 0)
{
offset += getHalfChannelBandwidth();
}
else
{
offset -= getHalfChannelBandwidth();
}
int indexOffset = (int)(offset / getChannelBandwidth());
if(indexOffset < 0)
{
indexOffset += getChannelCount();
}
if(indexBoundaryPolicy == IndexBoundaryPolicy.ADJUST_POSITIVE &&
isOverlapFrequency(frequency, indexOffset, indexOffset + 1))
{
indexOffset = normalize(indexOffset + 1);
}
else if(indexBoundaryPolicy == IndexBoundaryPolicy.ADJUST_NEGATIVE &&
isOverlapFrequency(frequency, indexOffset - 1, indexOffset))
{
indexOffset = normalize(indexOffset - 1);
}
return indexOffset;
}
/**
* Indicates if the frequency is an overlapping frequency between channel index 1 and channel index 2 where the
* maximum frequency of index 1 is the same as the minimum frequency of index 2 and both of these frequencies are
* the same as the frequency argument.
*
* Note: both index 1 and index 2 will be normalized to the range of 0 <> N-1 prior to determining overlap
*
* @param frequency to test for overlap between index 1 and 2
* @param index1 which is the lower channel index located next to index 2.
* @param index2 which is the higher channel index located next to index 1.
* @return true if frequency = index1 maximum frequency = index2 minimum frequency
*/
public boolean isOverlapFrequency(long frequency, int index1, int index2)
{
index1 = normalize(index1);
index2 = normalize(index2);
//Check that these two indexes are side by side
int delta = normalize(index2 - index1);
if(delta != 1)
{
return false;
}
long index1MaximumFrequency = (long)getIndexMaximumFrequency(index1, IndexBoundaryPolicy.ADJUST_POSITIVE);
long index2MinimumFrequency = (long)getIndexMinimumFrequency(index2, IndexBoundaryPolicy.ADJUST_NEGATIVE);
if(index1 == getWrapAroundIndex())
{
index1MaximumFrequency = (long)getIndexMaximumFrequency(index1, IndexBoundaryPolicy.ADJUST_NEGATIVE);
}
if(index2 == getWrapAroundIndex())
{
index2MinimumFrequency = (long)getIndexMinimumFrequency(index2, IndexBoundaryPolicy.ADJUST_POSITIVE);
}
return frequency == index1MaximumFrequency && frequency == index2MinimumFrequency;
}
/**
* Normalizes the index to the range of 0 <> N-1 channel indexes</>
*/
private int normalize(int index)
{
while(index < 0)
{
index += getChannelCount();
}
while(index >= getChannelCount())
{
index -= getChannelCount();
}
return index;
}
/**
* Calculates the center frequency for the channel identified by the index in the channel results array.
*
* @param index to determine the channel center frequency
* @return center frequency for the indexed channel
*/
public double getIndexCenterFrequency(int index, IndexBoundaryPolicy indexBoundaryPolicy)
{
if(index < 0 || index >= getChannelCount())
{
throw new IllegalArgumentException("Illegal channel index [" + index + "] current max channels[" +
getChannelCount() + "]");
}
int wrapAroundIndex = getWrapAroundIndex();
if(index == wrapAroundIndex)
{
if(indexBoundaryPolicy == IndexBoundaryPolicy.ADJUST_POSITIVE)
{
return getCenterFrequency() + (index * getChannelBandwidth());
}
else
{
return getCenterFrequency() - (index * getChannelBandwidth());
}
}
else if(index < wrapAroundIndex)
{
return getCenterFrequency() + (index * getChannelBandwidth());
}
else
{
return getCenterFrequency() - ((getChannelCount() - index) * getChannelBandwidth());
}
}
/**
* Calculates the minimum frequency band edge for the channel identified by the index in the channel results
* array.
* @param index to calculate the minimum band edge frequency for the channel.
* @param indexBoundaryPolicy for the wrap around index to indicate if this method should return the minimum
* frequency of the upper spectrum half of the wrap around channel (ADJUST_POSITIVE) or the minimum frequency of the
* lower spectrum half of the wrap around channel (ADJUST_NEGATIVE)
* @return frequency of the minimum band edge
*/
public double getIndexMinimumFrequency(int index, IndexBoundaryPolicy indexBoundaryPolicy)
{
if(index < 0 || index >= getChannelCount())
{
throw new IllegalArgumentException("Can't determine minimum frequency - illegal channel index [" +
index + "]");
}
int wrapAroundIndex = getWrapAroundIndex();
if(index == wrapAroundIndex)
{
if(indexBoundaryPolicy == IndexBoundaryPolicy.ADJUST_POSITIVE)
{
return getCenterFrequency() + ((double)index * getChannelBandwidth()) - getHalfChannelBandwidth();
}
else
{
return getIndexCenterFrequency(index, indexBoundaryPolicy);
}
}
else if(index <= wrapAroundIndex)
{
return getCenterFrequency() + ((double)index * getChannelBandwidth()) - getHalfChannelBandwidth();
}
else
{
return getCenterFrequency() - ((double)(getChannelCount() - index) * getChannelBandwidth()) -
getHalfChannelBandwidth();
}
}
/**
* Calculates the maximum frequency band edge for the channel identified by the index in the channel results
* array.
* @param index to calculate the maximum band edge frequency for the channel.
* @param indexBoundaryPolicy for the wrap around index to indicate if this method should return the maximum
* frequency of the upper spectrum half of the wrap around channel (ADJUST_POSITIVE) or the maximum frequency of the
* lower spectrum half of the wrap around channel (ADJUST_NEGATIVE)
* @return frequency of the maximum band edge
*/
public double getIndexMaximumFrequency(int index, IndexBoundaryPolicy indexBoundaryPolicy)
{
if(index < 0 || index >= getChannelCount())
{
throw new IllegalArgumentException("Can't determine maximum frequency - illegal channel index [" +
index + "]");
}
int wrapAroundIndex = getWrapAroundIndex();
if(index == wrapAroundIndex)
{
if(indexBoundaryPolicy == IndexBoundaryPolicy.ADJUST_POSITIVE)
{
return getIndexCenterFrequency(index, indexBoundaryPolicy);
}
else
{
return getCenterFrequency() - ((double)index * getChannelBandwidth() - getHalfChannelBandwidth());
}
}
else if(index <= wrapAroundIndex)
{
return getCenterFrequency() + ((double)index * getChannelBandwidth()) + getHalfChannelBandwidth();
}
else
{
return getCenterFrequency() - ((double)(getChannelCount() - index) * getChannelBandwidth()) +
getHalfChannelBandwidth();
}
}
/**
* Calculates the center frequency for a channel that will be synthesized from the channel indexes.
* @param indexes that are contiguous in the spectrum
* @return center frequency in hertz
*/
public long getCenterFrequencyForIndexes(List<Integer> indexes)
{
if(indexes.isEmpty())
{
throw new IllegalArgumentException("Indexes cannot be empty");
}
int centerIndex = (indexes.size() - 1) / 2;
int index = indexes.get(centerIndex);
if(indexes.size() % 2 == 0)
{
return (long)getIndexMaximumFrequency(index, IndexBoundaryPolicy.ADJUST_NEGATIVE);
}
else
{
if(index == getWrapAroundIndex())
{
return (long)getIndexCenterFrequency(index, IndexBoundaryPolicy.ADJUST_NEGATIVE);
}
else
{
return (long)getIndexCenterFrequency(index, IndexBoundaryPolicy.ADJUST_POSITIVE);
}
}
}
/**
* String representation of the channel calculator settings
*/
public String toString()
{
StringBuilder sb = new StringBuilder();
sb.append("Channels:").append(getChannelCount());
sb.append(" Sample Rate:").append(getSampleRate());
sb.append(" Channel Bandwidth:").append(getChannelBandwidth());
sb.append(" Channel Rate:").append(getChannelSampleRate());
sb.append(" Min:").append(getMinimumFrequency());
sb.append(" Max:").append(getMaximumFrequency());
return sb.toString();
}
public static void main(String[] args)
{
mLog.debug("Starting ....");
double channelBandwidth = 12500.0;
double sampleRate = 100000.0; //Airspy
int channelCount = (int)(sampleRate / channelBandwidth);
double centerFrequency = 455000000;
ChannelCalculator calculator = new ChannelCalculator(sampleRate, channelCount, centerFrequency, 2.0);
mLog.debug(calculator.toString());
DecimalFormat decimalFormat = new DecimalFormat("000,000,000.0");
int wrapAroundIndex = calculator.getWrapAroundIndex();
IndexBoundaryPolicy indexBoundaryPolicy = IndexBoundaryPolicy.ADJUST_NEGATIVE;
for(int index = wrapAroundIndex; index < calculator.getChannelCount(); index++)
{
mLog.debug("Index:" + index + " Min:" + decimalFormat.format(calculator.getIndexMinimumFrequency(index, indexBoundaryPolicy)) +
" Center:" + decimalFormat.format(calculator.getIndexCenterFrequency(index, indexBoundaryPolicy)) +
" Max:" + decimalFormat.format(calculator.getIndexMaximumFrequency(index, indexBoundaryPolicy)));
}
indexBoundaryPolicy = IndexBoundaryPolicy.ADJUST_POSITIVE;
for(int index = 0; index <= wrapAroundIndex; index++)
{
mLog.debug("Index:" + index + " Min:" + decimalFormat.format(calculator.getIndexMinimumFrequency(index, indexBoundaryPolicy)) +
" Center:" + decimalFormat.format(calculator.getIndexCenterFrequency(index, indexBoundaryPolicy)) +
" Max:" + decimalFormat.format(calculator.getIndexMaximumFrequency(index, indexBoundaryPolicy)));
}
mLog.debug("Finished");
}
}

View File

@@ -0,0 +1,458 @@
/*******************************************************************************
* 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.dsp.filter.channelizer;
import io.github.dsheirer.dsp.filter.FilterFactory;
import io.github.dsheirer.dsp.filter.design.FilterDesignException;
import io.github.dsheirer.sample.IOverflowListener;
import io.github.dsheirer.sample.Listener;
import io.github.dsheirer.sample.buffer.ReusableChannelResultsBuffer;
import io.github.dsheirer.sample.buffer.ReusableComplexBuffer;
import org.jtransforms.fft.FloatFFT_1D;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import java.util.Arrays;
import java.util.Collection;
import java.util.List;
/**
* Non-Maximally Decimated Polyphase Filter Bank (NMDPFB) channelizer that divides the input baseband complex sample
* stream into equal bandwidth channels that are each oversampled by 2x for output.
*
* This polyphase channelizer is based off of the channelizer described by Fred Harris in Multirate Signal
* Processing for Communications Systems, p230-233.
*
* Samples are loaded into this filter one block at a time (1/2 channel count) and a filtered output is calculated
* to produce an overall 2x oversampled channel sample rate. Each sample block load is preceded by a serpentine
* shift of the existing sample blocks. We use the java System.arrayCopy() method which is able to leverage
* native processor intrinsics for efficiency.
*
* The prototype filter for the channelizer is rearranged to align with the structure of the sample buffer.
*
* Instead of using an array of channel filters as described in the Harris text, this filter and the sample buffer
* are arranged as a contiguous array to maximize Java's ability to leverage native processor Single Instruction
* Multiple Data (SIMD) intrinsics (since Java 8). The filter process is broken into four steps:
*
* -Multiply the inline array of samples and filter coefficients
* -Accumulate the results for each sub-channel
* -Rearrange the sub-channel results to correctly order the sub-channels
* -Perform IFFT
*
* Note: design the prototype filter as a Nyquist windowed filter with a -6.02 db attenuation at the channel edge
* frequency if you need Perfect Reconstruction where you'll later re-join two or more channels to form a wider
* bandwidth channel or to isolate a signal that located between two channels.
*/
public class ComplexPolyphaseChannelizerM2 extends AbstractComplexPolyphaseChannelizer
{
private final static Logger mLog = LoggerFactory.getLogger(ComplexPolyphaseChannelizerM2.class);
private static final int DEFAULT_MINIMUM_CHANNEL_BANDWIDTH = 12500;
//Sized at 152 buffers a second where max = 5 seconds and reset = 2 seconds worth of buffers
private IFFTProcessor mIFFTProcessor = new IFFTProcessor((5 * 152), (2 * 152));
private FloatFFT_1D mFFT;
private float[] mInlineSamples;
private float[] mInlineFilter;
private float[] mInlineInterimOutput;
private float[] mFilterAccumulator;
private boolean mTopBlockIndicator = true;
private int[] mTopBlockMap;
private int[] mMiddleBlockMap;
private int mSampleBufferPointer;
private int mSamplesPerBlock;
private int mTapsPerChannel;
/**
* Creates a NMDPFB channelizer instance.
*
* @param taps of a low-pass filter designed for the inbound sample rate with a cutoff frequency
* equal to the channel bandwidth (sample rate / filters). If you need to synthesize (combine two or more
* channel outputs) a new bandwidth signal from the outputs of this filter, then the filter should be designed
* as a nyquist filter with -6 dB attenuation at the channel bandwidth cutoff frequency
* @param sampleRate of the incoming sample stream
* @param channelCount - number of filters/channels to output. Since this filter bank performs 2x oversampling for
* each channel output, this number must be even (divisible by 2).
*/
public ComplexPolyphaseChannelizerM2(float[] taps, int sampleRate, int channelCount)
{
super(sampleRate, channelCount);
if(channelCount % 2 != 0)
{
throw new IllegalArgumentException("Channel count must be an even multiple of the over-sample rate (2x)");
}
mTapsPerChannel = (int)Math.ceil((double)taps.length / (double)channelCount);
init(taps);
}
/**
* Creates a NMDPFB channelizer instance and designs a Perfect Reconstruction prototype filter appropriate for
* the baseband sample rate and quantity of filter taps per polyphase sub-channel.
*
* @param sampleRate to be channelized.
* @param tapsPerChannel to use when designing the filter
*/
public ComplexPolyphaseChannelizerM2(double sampleRate, int tapsPerChannel) throws FilterDesignException
{
super(sampleRate, getChannelCount(sampleRate));
mTapsPerChannel = tapsPerChannel;
float[] filterTaps = FilterFactory.getSincM2Channelizer(getChannelSampleRate(), getChannelCount(),
mTapsPerChannel, false);
init(filterTaps);
}
/**
* Starts sample processing
*/
public void start()
{
mIFFTProcessor.start();
}
/**
* Stops sample processing.
*/
public void stop()
{
mIFFTProcessor.stop();
}
/**
* Calculates the multiple of two number of channels that can be channelized from the specified sample rate so that
* each channel has a minimum bandwidth of the default channel bandwidth (12.5 kHz).
* @param sampleRate to channelize
* @return number of multiple of two channels that can be channelized.
*/
private static int getChannelCount(double sampleRate)
{
int channels = (int)(sampleRate / DEFAULT_MINIMUM_CHANNEL_BANDWIDTH);
if(channels % 2 != 0)
{
channels--;
}
mLog.debug("Sample Rate [" + sampleRate + "] providing [" + channels + "] channels at [" + (sampleRate / (double)channels) + "] Hz each");
return channels;
}
/**
* Updates this channelizer to use the new sample rate. This method creates a new filter suitable for the
* sample rate and reinitializes all internal data structures to prepare for processing the new sample rate.
* @param sampleRate in hertz
*/
@Override
public void setSampleRate(double sampleRate)
{
try
{
super.setSampleRate(sampleRate);
float[] filterTaps = FilterFactory.getSincM2Channelizer(getChannelSampleRate(), getChannelCount(),
mTapsPerChannel, false);
init(filterTaps);
}
catch(FilterDesignException fde)
{
throw new IllegalArgumentException("Cannot create a channelizer filter for the specified sample rate [" +
sampleRate + "]");
}
}
/**
* Receives the complex sample buffer and processes the results through the channelizer.
*/
@Override
public void receive(ReusableComplexBuffer reusableComplexBuffer)
{
ReusableChannelResultsBuffer channelResultsBuffer = getChannelResultsBuffer();
channelResultsBuffer.setTimestamp(reusableComplexBuffer.getTimestamp());
float[] samples = reusableComplexBuffer.getSamples();
int samplesPointer = 0;
int samplesToCopy;
while(samplesPointer < samples.length)
{
if(mSampleBufferPointer < mSamplesPerBlock)
{
samplesToCopy = mSamplesPerBlock - mSampleBufferPointer;
if((samples.length - samplesPointer) < samplesToCopy)
{
samplesToCopy = (samples.length - samplesPointer);
}
System.arraycopy(samples, samplesPointer, mInlineSamples, mSampleBufferPointer, samplesToCopy);
mSampleBufferPointer += samplesToCopy;
samplesPointer += samplesToCopy;
}
if(mSampleBufferPointer >= mSamplesPerBlock)
{
//Filter buffered samples and produce a single sample across each of the polyphase channels
process(channelResultsBuffer);
//Right-shift the samples in the buffer over to make room for a new block of samples
//Note: since JDK 8, hotspot JIT compiler uses native processor intrinsics for efficiency
System.arraycopy(mInlineSamples, 0, mInlineSamples, mSamplesPerBlock, (mInlineSamples.length - mSamplesPerBlock));
mSampleBufferPointer = 0;
}
}
//Enqueue the channel results buffer for IFFT processing and distribution on a different thread
mIFFTProcessor.receive(channelResultsBuffer);
//Decrement the user count to let the originator know we're done with their buffer
reusableComplexBuffer.decrementUserCount();
}
/**
* Creates a top-block processing accumulator map that maps each interim filter and sample index product
* to the corresponding final output index for the array that will feed the IFFT.
*
* @param channelCount - number of channels
* @return output index to filter accumulator index mapping
*/
private static int[] getTopBlockMap(int channelCount)
{
int[] newMap = new int[channelCount * 2];
//Reorder the subchannel arrays to the structure needed for top-block processing
int blockSize = channelCount / 2;
for(int channel = 0; channel < blockSize; channel++)
{
int newIndex = 2 * channel;
int originalIndex = 2 * (blockSize - channel - 1);
int offset = 2 * blockSize;
newMap[originalIndex] = newIndex;
newMap[originalIndex + 1] = newIndex + 1;
newMap[offset + originalIndex] = offset + newIndex;
newMap[offset + originalIndex + 1] = offset + newIndex + 1;
}
return newMap;
}
/**
* Creates a top-block processing accumulator map that maps each interim filter and sample index product
* to the corresponding final output index for the array that will feed the IFFT.
*
* @param channelCount - number of channels
* @return output index to filter accumulator index mapping
*/
private static int[] getMiddleBlockMap(int channelCount)
{
int[] newMap = new int[channelCount * 2];
//Reorder the subchannel arrays to the structure needed for top-block processing
int blockSize = channelCount / 2;
for(int channel = 0; channel < blockSize; channel++)
{
int newIndex = 2 * channel;
int originalIndex = 2 * (blockSize - channel - 1);
int offset = 2 * blockSize;
newMap[offset + originalIndex] = newIndex;
newMap[offset + originalIndex + 1] = newIndex + 1;
newMap[originalIndex] = offset + newIndex;
newMap[originalIndex + 1] = offset + newIndex + 1;
}
return newMap;
}
/**
* Rearranges the filter coefficients to align with a contiguous sample buffer for processing efficiency.
* @param coefficients of the polyphase filter
* @param channelCount number of channels where each channel is an I/Q pair
* @return filter rearranged for inline sample buffer processing
*/
private static float[] getAlignedFilter(float[] coefficients, int channelCount, int tapsPerChannel)
{
float[] filter = new float[channelCount * tapsPerChannel * 2];
int blockSize = channelCount;
int coefficientPointer = 0;
int filterPointer = 0;
//Create a new filter that duplicates each tap to produce an interleaved I/Q filter
while(coefficientPointer < coefficients.length)
{
filter[filterPointer++] = coefficients[coefficientPointer];
filter[filterPointer++] = coefficients[coefficientPointer++];
}
//Swap each of the coefficients on block size boundaries
for(int x = 0; x < filter.length; x += blockSize)
{
for(int y = 0; y < blockSize / 2; y++)
{
int index1 = x + y;
int index2 = x + (blockSize - y - 1);
float temp = filter[index2];
filter[index2] = filter[index1];
filter[index1] = temp;
}
}
return filter;
}
/**
* Processes the sample buffer for each new block of sample data that is loaded and distributes the results to any
* registered channel listeners.
*/
private void process(ReusableChannelResultsBuffer channelResultsBuffer)
{
//Multiply each of the samples by the corresponding filter tap
for(int x = 0; x < mInlineSamples.length; x++)
{
mInlineInterimOutput[x] = mInlineSamples[x] * mInlineFilter[x];
}
Arrays.fill(mFilterAccumulator, 0.0f);
int tapOffset = 0;
//Accumulate the sample/filter product results into each of the I/Q sub-channels
for(int tap = 0; tap < mTapsPerChannel; tap++)
{
tapOffset = tap * getSubChannelCount();
for(int channel = 0; channel < getSubChannelCount(); channel++)
{
mFilterAccumulator[channel] += mInlineInterimOutput[tapOffset + channel];
}
}
float[] processed = channelResultsBuffer.getEmptyBuffer(getSubChannelCount());
if(mTopBlockIndicator)
{
for(int x = 0; x < getSubChannelCount(); x++)
{
processed[x] = mFilterAccumulator[mTopBlockMap[x]];
}
}
else
{
for(int x = 0; x < getSubChannelCount(); x++)
{
processed[x] = mFilterAccumulator[mMiddleBlockMap[x]];
}
}
channelResultsBuffer.addChannelResults(processed);
mTopBlockIndicator = !mTopBlockIndicator;
}
/**
* Initializes the channelizer filter structures.
*
* @param coefficients of the prototype filter for this channelizer
*/
private void init(float[] coefficients)
{
mFFT = new FloatFFT_1D(getChannelCount());
int channelCount = getChannelCount();
int bufferLength = getSubChannelCount() * mTapsPerChannel;
mSamplesPerBlock = getChannelCount(); //Same as subChannelCount / 2
mTopBlockMap = getTopBlockMap(channelCount);
mMiddleBlockMap = getMiddleBlockMap(channelCount);
mInlineFilter = getAlignedFilter(coefficients, channelCount, mTapsPerChannel);
mInlineSamples = new float[bufferLength];
mInlineInterimOutput = new float[bufferLength];
mFilterAccumulator = new float[getSubChannelCount()];
}
/**
* Separate threaded processor to receive and enqueue filtered channel results buffers, perform IFFT on each array
* as required to align the phase of each polyphase channel, and then dispatch the results to any registered
* sample consumer channels.
*/
public class IFFTProcessor extends ContinuousBufferProcessor<ReusableChannelResultsBuffer>
{
public IFFTProcessor(int maximumSize, int resetThreshold)
{
super(maximumSize, resetThreshold);
//We create a listener interface to receive the buffers from the scheduled thread pool
//dispatcher thread that is part of this continuous buffer processor. We perform an IFFT on each
//channel results array contained in each results buffer and then dispatch the buffer
//so that it can be distributed to each channel listener.
setListener(new Listener<List<ReusableChannelResultsBuffer>>()
{
@Override
public void receive(List<ReusableChannelResultsBuffer> buffers)
{
for(ReusableChannelResultsBuffer buffer: buffers)
{
for(float[] channelResults: buffer.getChannelResults())
{
//Rotate each of the channels to the correct phase using the IFFT
mFFT.complexInverse(channelResults, true);
}
dispatch(buffer);
}
}
});
setOverflowListener(new IOverflowListener()
{
@Override
public void sourceOverflow(boolean overflow)
{
mLog.debug("IFFTProcessor overflow changed - overflow:" + overflow);
}
});
}
/**
* Clears any buffers from the dispatch/processing queue. Overrides the parent method so that we can set
* the user count to 0 to allow the buffer to be reclaimed.
*/
protected Collection<ReusableChannelResultsBuffer> clearQueue()
{
Collection<ReusableChannelResultsBuffer> buffersToDispose = super.clearQueue();
for(ReusableChannelResultsBuffer buffer: buffersToDispose)
{
buffer.clearUserCount();
}
return buffersToDispose;
}
}
}

View File

@@ -0,0 +1,182 @@
/*******************************************************************************
* sdr-trunk
* Copyright (C) 2014-2017 Dennis Sheirer
*
* This program is free software: you can redistribute it and/or modify it under the terms of the GNU General Public
* License as published by the Free Software Foundation, either version 3 of the License, or (at your option) any
* later version.
*
* This program is distributed in the hope that it will be useful, but WITHOUT ANY WARRANTY; without even the implied
* warranty of MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU General Public License for more details.
*
* You should have received a copy of the GNU General Public License along with this program.
* If not, see <http://www.gnu.org/licenses/>
*
******************************************************************************/
package io.github.dsheirer.dsp.filter.channelizer;
import io.github.dsheirer.sample.IOverflowListener;
import io.github.dsheirer.sample.Listener;
import io.github.dsheirer.sample.OverflowableTransferQueue;
import io.github.dsheirer.util.ThreadPool;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import java.util.ArrayList;
import java.util.Collection;
import java.util.List;
import java.util.concurrent.ScheduledFuture;
import java.util.concurrent.TimeUnit;
import java.util.concurrent.atomic.AtomicBoolean;
public class ContinuousBufferProcessor<E> implements Listener<E>
{
private final static Logger mLog = LoggerFactory.getLogger(ContinuousBufferProcessor.class);
private OverflowableTransferQueue<E> mQueue;
private Listener<List<E>> mListener;
private ScheduledFuture<?> mScheduledFuture;
private AtomicBoolean mRunning = new AtomicBoolean();
/**
* Scheduled Buffer Processor combines an internal overflowable buffer with a scheduled runnable processing task
* for periodically distributing internally queued elements to the registered listener. This processor provides
* a convenient way to create a thread-safe buffer for receiving elements from one thread/runnable and then
* distributing those elements to a registered listener where distribution occurs on a separate scheduled thread
* pool runnable thread. This allows the calling input thread to quickly return without incurring any subsequent
* processing workload.
*
* The internal queue is an overflowable queue implementation that allows a listener to be registered to receive
* notifications of overflow and reset state. Queue sizing parameters are specified in the constructor.
*
* @param maximumSize of the internal queue (overflow happens when this is exceeded)
* @param resetThreshold of the internal queue (overflow reset happens once queue size falls below this threshold
*/
public ContinuousBufferProcessor(int maximumSize, int resetThreshold)
{
this(new OverflowableTransferQueue<>(maximumSize, resetThreshold));
}
/**
* Scheduled Buffer Processor combines an internal overflowable buffer with a scheduled runnable processing task
* for periodically distributing internally queued elements to the registered listener. This processor provides
* a convenient way to create a thread-safe buffer for receiving elements from one thread/runnable and then
* distributing those elements to a registered listener where distribution occurs on a separate scheduled thread
* pool runnable thread. This allows the calling input thread to quickly return without incurring any subsequent
* processing workload.
*
* The internal queue is an overflowable queue implementation that allows a listener to be registered to receive
* notifications of overflow and reset state. Queue sizing parameters are specified in the constructor.
*
* @param queue implmentation of an overflowable transfer queue
*/
public ContinuousBufferProcessor(OverflowableTransferQueue<E> queue)
{
mQueue = queue;
}
/**
* Sets the listener to receive notifications of buffer overflow and/or reset. Note: this method can also be
* invoked with a null argument to clear the previously registered overflow listener.
*
* @param listener to receive overflow/reset notifications.
*/
public void setOverflowListener(IOverflowListener listener)
{
mQueue.setOverflowListener(listener);
}
/**
* Sets or changes the listener to receive buffers from this processor.
* @param listener to receive buffers
*/
public void setListener(Listener<List<E>> listener)
{
mListener = listener;
}
/**
* Primary input method for adding buffers to this processor. Note: incoming buffers will be ignored if this
* processor is in a stopped state. You must invoke start() to allow incoming buffers and initiate buffer
* processing.
*
* @param e to enqueue for distribution to a registered listener
*/
@Override
public void receive(E e)
{
mQueue.offer(e);
}
/**
* Starts this buffer processor and allows queuing of incoming buffers.
*/
public void start()
{
if(mRunning.compareAndSet(false, true))
{
mScheduledFuture = ThreadPool.SCHEDULED.scheduleAtFixedRate(new Processor(), 0, 5, TimeUnit.MILLISECONDS);
}
}
/**
* Stops this buffer processor, stops queuing of incoming buffers and clears the internal buffer queue.
*/
public void stop()
{
if(mRunning.compareAndSet(true, false))
{
if(mScheduledFuture != null)
{
mScheduledFuture.cancel(true);
mScheduledFuture = null;
}
clearQueue();
}
}
/**
* Clears any buffers from the queue
*/
protected Collection<E> clearQueue()
{
Collection<E> clearedElements = new ArrayList<>();
mQueue.drainTo(clearedElements);
return clearedElements;
}
/**
* Indicates if this processor is currently running
*/
public boolean isRunning()
{
return mRunning.get();
}
/**
* Processor to service the buffer queue and distribute the buffers to the registered listener
*/
class Processor implements Runnable
{
@Override
public void run()
{
List<E> buffers = new ArrayList<>();
try
{
mQueue.drainTo(buffers);
if(mListener != null)
{
mListener.receive(buffers);
}
}
catch(Throwable throwable)
{
mLog.error("Error while dispatching buffers to listeners", throwable);
}
}
}
}

View File

@@ -0,0 +1,41 @@
/*******************************************************************************
* 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.filter.channelizer;
import io.github.dsheirer.sample.buffer.OverflowableReusableBufferTransferQueue;
import io.github.dsheirer.sample.buffer.ReusableBuffer;
public class ContinuousReusableBufferProcessor<T extends ReusableBuffer> extends ContinuousBufferProcessor<T>
{
/**
* Scheduled Reusable Buffer Processor combines an internal overflowable buffer with a scheduled runnable processing
* task for periodically distributing internally queued elements to the registered listener. This processor
* provides a convenient way to create a thread-safe buffer for receiving elements from one thread/runnable and then
* distributing those elements to a registered listener where distribution occurs on a separate scheduled thread
* pool runnable thread. This allows the calling input thread to quickly return without incurring any subsequent
* processing workload.
*
* The internal queue is an overflowable queue implementation that allows a listener to be registered to receive
* notifications of overflow and reset state. Queue sizing parameters are specified in the constructor.
*
* @param maximumSize of the internal queue (overflow happens when this is exceeded)
* @param resetThreshold of the internal queue (overflow reset happens once queue size falls below this threshold
*/
public ContinuousReusableBufferProcessor(int maximumSize, int resetThreshold)
{
super(new OverflowableReusableBufferTransferQueue<T>(maximumSize, resetThreshold));
}
}

View File

@@ -0,0 +1,633 @@
/*******************************************************************************
* 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.filter.channelizer;
import io.github.dsheirer.dsp.filter.FilterFactory;
import io.github.dsheirer.dsp.filter.channelizer.output.IPolyphaseChannelOutputProcessor;
import io.github.dsheirer.dsp.filter.channelizer.output.OneChannelOutputProcessor;
import io.github.dsheirer.dsp.filter.channelizer.output.TwoChannelOutputProcessor;
import io.github.dsheirer.dsp.filter.design.FilterDesignException;
import io.github.dsheirer.sample.Broadcaster;
import io.github.dsheirer.sample.Listener;
import io.github.dsheirer.sample.buffer.IReusableComplexBufferProvider;
import io.github.dsheirer.sample.buffer.ReusableComplexBuffer;
import io.github.dsheirer.source.ISourceEventProcessor;
import io.github.dsheirer.source.Source;
import io.github.dsheirer.source.SourceEvent;
import io.github.dsheirer.source.SourceException;
import io.github.dsheirer.source.tuner.TunerController;
import io.github.dsheirer.source.tuner.channel.TunerChannel;
import io.github.dsheirer.source.tuner.channel.TunerChannelSource;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import java.util.ArrayList;
import java.util.HashMap;
import java.util.List;
import java.util.Map;
import java.util.Queue;
import java.util.SortedSet;
import java.util.TreeSet;
import java.util.concurrent.ConcurrentLinkedQueue;
import java.util.concurrent.CopyOnWriteArrayList;
/**
* Polyphase Channel Manager is a DDC channel manager and complex buffer queue/processor for a tuner. This class
* provides DDC polyphase channel sources and wraps a polyphase channelizer processing sample buffers produced by
* the tuner and distributing channelized sample buffers to each allocated DDC polyphase channel source. This
* class is responsible for monitoring the tuner for changes in center frequency and/or sample rate and updating
* active DDC polyphase channel sources accordingly. This class also monitors source event requests and
* notifications received from active DDC polyphase channel sources to adjust sample streams as required.
*
* Channel bandwidth and channel count are determined by the sample rate of the baseband buffer stream provider. This
* class is currently designed to provide channels each with a minimum usable bandwidth of 12.5 kHz and oversampled by
* 2.0 to a minimum of 25.0 kHz channel sample rate. If the baseband stream provider sample rate is not evenly
* divisible by 12.5 kHz channels for an even number of channels, the channel bandwidth will be increased.
*
* Note: add this channel manager as a source event listener to the complex buffer provider to ensure this manager
* adapts to changes in source frequency and sample rate.
*/
public class PolyphaseChannelManager implements ISourceEventProcessor
{
private final static Logger mLog = LoggerFactory.getLogger(PolyphaseChannelManager.class);
private static final double MINIMUM_CHANNEL_BANDWIDTH = 12500.0;
private static final double CHANNEL_OVERSAMPLING = 2.0;
private static final int POLYPHASE_CHANNELIZER_TAPS_PER_CHANNEL = 9;
private static final int POLYPHASE_SYNTHESIZER_TAPS_PER_CHANNEL = 9;
private Broadcaster<SourceEvent> mSourceEventBroadcaster = new Broadcaster<>();
private IReusableComplexBufferProvider mReusableBufferProvider;
private List<PolyphaseChannelSource> mChannelSources = new CopyOnWriteArrayList<>();
private ChannelCalculator mChannelCalculator;
private ComplexPolyphaseChannelizerM2 mPolyphaseChannelizer;
private ChannelSourceEventListener mChannelSourceEventListener = new ChannelSourceEventListener();
private BufferSourceEventMonitor mBufferSourceEventMonitor = new BufferSourceEventMonitor();
private ContinuousBufferProcessor<ReusableComplexBuffer> mBufferProcessor;
private Map<Integer,float[]> mOutputProcessorFilters = new HashMap<>();
/**
* Creates a polyphase channel manager instance.
*
* @param reusableComplexBufferProvider (ie tuner) that supports register/deregister for reusable baseband sample buffer
* streams
* @param frequency of the baseband complex buffer sample stream (ie center frequency)
* @param sampleRate of the baseband complex buffer sample stream
*/
public PolyphaseChannelManager(IReusableComplexBufferProvider reusableComplexBufferProvider,
long frequency, double sampleRate)
{
if(reusableComplexBufferProvider == null)
{
throw new IllegalArgumentException("Complex buffer provider argument cannot be null");
}
mReusableBufferProvider = reusableComplexBufferProvider;
int channelCount = (int)(sampleRate / MINIMUM_CHANNEL_BANDWIDTH);
//Ensure channel count is an even integer since we're using a 2x oversampling polyphase channelizer
if(channelCount % 2 != 0)
{
channelCount--;
}
mChannelCalculator = new ChannelCalculator(sampleRate, channelCount, frequency, CHANNEL_OVERSAMPLING);
mBufferProcessor = new ContinuousBufferProcessor(200, 50);
mBufferProcessor.setListener(mBufferSourceEventMonitor);
updateChannelizerSampleRate(sampleRate);
}
/**
* Creates a polyphase channel manager for the tuner controller
*
* @param tunerController for a tuner that provides a baseband complex buffer stream.
*/
public PolyphaseChannelManager(TunerController tunerController)
{
this(tunerController, tunerController.getFrequency(), tunerController.getSampleRate());
}
/**
* Current channel sample rate which is (2 * channel bandwidth).
*/
public double getChannelSampleRate()
{
return mChannelCalculator.getChannelSampleRate();
}
/**
* Current channel bandwidth/spacing.
*/
public double getChannelBandwidth()
{
return mChannelCalculator.getChannelBandwidth();
}
/**
* Provides a Digital Drop Channel (DDC) for the specified tuner channel or returns null if the channel can't be
* sourced due to the current center frequency and/or sample rate.
* @param tunerChannel specifying center frequency and bandwidth.
* @return source or null.
*/
public TunerChannelSource getChannel(TunerChannel tunerChannel)
{
PolyphaseChannelSource channelSource = null;
try
{
List<Integer> polyphaseIndexes = mChannelCalculator.getChannelIndexes(tunerChannel);
IPolyphaseChannelOutputProcessor outputProcessor = getOutputProcessor(polyphaseIndexes);
if(outputProcessor != null)
{
long centerFrequency = mChannelCalculator.getCenterFrequencyForIndexes(polyphaseIndexes);
channelSource = new PolyphaseChannelSource(tunerChannel, outputProcessor, mChannelSourceEventListener,
mChannelCalculator.getChannelSampleRate(), centerFrequency);
mChannelSources.add(channelSource);
}
}
catch(IllegalArgumentException iae)
{
mLog.info("Can't provide DDC for " + tunerChannel.toString() + " due to channelizer frequency [" +
mChannelCalculator.getCenterFrequency() + "] and sample rate [" +
(mChannelCalculator.getChannelCount() * mChannelCalculator.getChannelBandwidth()) + "]");
}
return channelSource;
}
public TunerChannelSource getTestChannel(int index)
{
PolyphaseChannelSource channelSource = null;
try
{
List<Integer> polyphaseIndexes = new ArrayList<>();
polyphaseIndexes.add(index);
IPolyphaseChannelOutputProcessor outputProcessor = getOutputProcessor(polyphaseIndexes);
if(outputProcessor != null)
{
long centerFrequency = mChannelCalculator.getCenterFrequencyForIndexes(polyphaseIndexes);
TunerChannel tunerChannel = new TunerChannel(centerFrequency, 12500);
channelSource = new PolyphaseChannelSource(tunerChannel, outputProcessor, mChannelSourceEventListener,
mChannelCalculator.getChannelSampleRate(), centerFrequency);
}
}
catch(IllegalArgumentException iae)
{
mLog.error("Couldn't get test channel with index:" + index, iae);
}
return channelSource;
}
/**
* Creates a processor to process the channelizer channel indexes into a composite output stream providing
* channelized complex sample buffers to a registered source listener.
* @param indexes to target by the output processor
* @return output processor compatible with the number of indexes to monitor
*/
private IPolyphaseChannelOutputProcessor getOutputProcessor(List<Integer> indexes)
{
switch(indexes.size())
{
case 1:
return new OneChannelOutputProcessor(mChannelCalculator.getChannelSampleRate(), indexes);
case 2:
try
{
float[] filter = getOutputProcessorFilter(2);
return new TwoChannelOutputProcessor(mChannelCalculator.getChannelSampleRate(), indexes, filter);
}
catch(FilterDesignException fde)
{
mLog.error("Error designing 2 channel synthesis filter for output processor");
}
default:
//TODO: create output processor for greater than 2 input channels
mLog.error("Request to create an output processor for unexpected channel index size:" + indexes.size());
return null;
}
}
/**
* Starts/adds the channel source to receive channelized sample buffers, registering with the tuner to receive
* sample buffers when this is the first channel.
*
* @param channelSource to start
*/
private void startChannelSource(PolyphaseChannelSource channelSource)
{
synchronized(mBufferProcessor)
{
//Note: the polyphase channel source has already been added to the mChannelSources in getChannel() method
mPolyphaseChannelizer.addChannel(channelSource);
mSourceEventBroadcaster.broadcast(SourceEvent.channelCountChange(getTunerChannelCount()));
//If this is the first channel, register to start the sample buffers flowing
if(mPolyphaseChannelizer.getRegisteredChannelCount() == 1)
{
mReusableBufferProvider.addBufferListener(mBufferProcessor);
mPolyphaseChannelizer.start();
mBufferProcessor.start();
}
}
}
/**
* Stops/removes the channel source from receiving channelized sample buffers and deregisters from the tuner
* when this is the last channel being sourced.
*
* @param channelSource to stop
*/
private void stopChannelSource(PolyphaseChannelSource channelSource)
{
synchronized(mBufferProcessor)
{
mChannelSources.remove(channelSource);
mPolyphaseChannelizer.removeChannel(channelSource);
mSourceEventBroadcaster.broadcast(SourceEvent.channelCountChange(getTunerChannelCount()));
//If this is the last/only channel, deregister to stop the sample buffers
if(mPolyphaseChannelizer.getRegisteredChannelCount() == 0)
{
mReusableBufferProvider.removeBufferListener(mBufferProcessor);
mBufferProcessor.stop();
mPolyphaseChannelizer.stop();
}
}
try
{
//Broadcast a stop sample stream notification in case this was a forced-stop so consumers are aware
channelSource.process(SourceEvent.stopSampleStreamNotification(channelSource));
}
catch(SourceException se)
{
//Do nothing
}
}
/**
* Process source events received from the source (ie tuner controller) for frequency and sample rate change
* notifications.
* @param sourceEvent to process
* @throws SourceException
*/
@Override
public void process(SourceEvent sourceEvent) throws SourceException
{
switch(sourceEvent.getEvent())
{
case NOTIFICATION_FREQUENCY_CHANGE:
//Update channel calculator immediately so that channels can be allocated
mChannelCalculator.setCenterFrequency(sourceEvent.getValue().longValue());
//Defer channelizer configuration changes to be handled on the buffer processor thread
mBufferSourceEventMonitor.receive(sourceEvent);
break;
case NOTIFICATION_SAMPLE_RATE_CHANGE:
//Update channel calculator immediately so that channels can be allocated
mChannelCalculator.setSampleRate(sourceEvent.getValue().doubleValue());
//Defer channelizer configuration changes to be handled on the buffer processor thread
mBufferSourceEventMonitor.receive(sourceEvent);
break;
case NOTIFICATION_FREQUENCY_AND_SAMPLE_RATE_LOCKED:
case NOTIFICATION_FREQUENCY_AND_SAMPLE_RATE_UNLOCKED:
//no-op
break;
default:
mLog.info("Unrecognized source event: " + sourceEvent.toString());
break;
}
}
/**
* Creates or updates the channelizer to process the incoming sample rate and updates any channel processors.
*
* Note: this method should only be invoked on the mBufferProcessor thread or prior to starting the mBufferProcessor.
* Sample rate source events will normally arrive via the incoming complex buffer stream from the mBufferProcessor
* and will be handled as they arrive.
*
* @param sampleRate to use for the channelizer
*/
private void updateChannelizerSampleRate(double sampleRate)
{
if(mPolyphaseChannelizer == null || Math.abs(mPolyphaseChannelizer.getSampleRate() - sampleRate) > 0.5)
{
try
{
if(mPolyphaseChannelizer == null)
{
mPolyphaseChannelizer = new ComplexPolyphaseChannelizerM2(sampleRate,
POLYPHASE_CHANNELIZER_TAPS_PER_CHANNEL);
}
else
{
mPolyphaseChannelizer.setSampleRate(sampleRate);
}
}
catch(IllegalArgumentException iae)
{
mLog.error("Could not create polyphase channelizer for sample rate [" + sampleRate + "]", iae);
}
catch(FilterDesignException fde)
{
mLog.error("Could not create filter for polyphase channelizer for sample rate [" + sampleRate + "]", fde);
}
//Clear the previous channel synthesis filters so they can be recreated for the new channel sample rate
mOutputProcessorFilters.clear();
//Broadcast the new channel sample rate
double updatedChannelSampleRate = mChannelCalculator.getChannelSampleRate();
updateOutputProcessors(SourceEvent.sampleRateChange(updatedChannelSampleRate, "Channelizer Sample Rate Update"));
}
}
/**
* Updates each of the output processors for any changes in the tuner's center frequency or sample rate, which
* would cause the output processors to change the polyphase channelizer results channel(s) that the processor is
* consuming
*
* @param sourceEvent (optional-can be null) to broadcast to each output processor following the update
*/
private void updateOutputProcessors(SourceEvent sourceEvent)
{
for(PolyphaseChannelSource channelSource: mChannelSources)
{
updateOutputProcessor(channelSource);
//Send the non-null source event to each channel source
if(sourceEvent != null)
{
try
{
channelSource.process(sourceEvent);
}
catch(SourceException se)
{
mLog.error("Error while notifying polyphase channel source of a source event", se);
}
}
}
}
/**
* Updates the polyphase channel source's output processor due to a change in the center frequency or sample
* rate for the source providing sample buffers to the polyphase channelizer, or whenever the DDC channel's
* center tuned frequency changes.
*
* @param channelSource that requires an update to its output processor
*/
private void updateOutputProcessor(PolyphaseChannelSource channelSource)
{
try
{
//If a change in sample rate or center frequency makes this channel no longer viable, then the channel
//calculator will throw an IllegalArgException ... handled below
List<Integer> indexes = mChannelCalculator.getChannelIndexes(channelSource.getTunerChannel());
long centerFrequency = mChannelCalculator.getCenterFrequencyForIndexes(indexes);
//If the indexes size is the same then update the current processor, otherwise create a new one
if(channelSource.getPolyphaseChannelOutputProcessor().getInputChannelCount() == indexes.size())
{
channelSource.getPolyphaseChannelOutputProcessor().setPolyphaseChannelIndices(indexes);
channelSource.setFrequency(centerFrequency);
if(indexes.size() > 1)
{
try
{
float[] filter = getOutputProcessorFilter(indexes.size());
channelSource.getPolyphaseChannelOutputProcessor().setSynthesisFilter(filter);
}
catch(FilterDesignException fde)
{
mLog.error("Error creating an updated synthesis filter for the channel output processor");
}
}
}
else
{
channelSource.setPolyphaseChannelOutputProcessor(getOutputProcessor(indexes), centerFrequency);
}
}
catch(IllegalArgumentException iae)
{
mLog.error("Error updating polyphase channel source - can't determine output channel indexes for " +
"updated tuner center frequency and sample rate. Stopping channel source", iae);
stopChannelSource(channelSource);
}
}
/**
* Sorted set of currently sourced tuner channels being provided by this channel manager. The set is ordered by
* frequency (low to high).
*/
public SortedSet<TunerChannel> getTunerChannels()
{
SortedSet<TunerChannel> tunerChannels = new TreeSet<>();
for(PolyphaseChannelSource channelSource: mChannelSources)
{
tunerChannels.add(channelSource.getTunerChannel());
}
return tunerChannels;
}
/**
* Count of currently sourced tuner channels
*/
public int getTunerChannelCount()
{
return mChannelSources.size();
}
/**
* Adds the listener to receive source events
*/
public void addSourceEventListener(Listener<SourceEvent> listener)
{
mSourceEventBroadcaster.addListener(listener);
}
/**
* Removes the listener from receiving source events
*/
public void removeSourceEventListener(Listener<SourceEvent> listener)
{
mSourceEventBroadcaster.removeListener(listener);
}
/**
* Generates (or reuses) an output processor filter for the specified number of channels. Each
* filter is created only once and stored in a map for reuse. This map is cleared anytime that the
* input sample rate changes, so that the filters can be recreated with the new channel sample rate.
* @param channels count
* @return filter
* @throws FilterDesignException if the filter cannot be designed to specification (-6 dB band edge)
*/
private float[] getOutputProcessorFilter(int channels) throws FilterDesignException
{
float[] taps = mOutputProcessorFilters.get(channels);
if(taps == null)
{
taps = FilterFactory.getSincM2Synthesizer(mChannelCalculator.getChannelBandwidth(), channels,
POLYPHASE_SYNTHESIZER_TAPS_PER_CHANNEL);
mOutputProcessorFilters.put(channels, taps);
}
return taps;
}
/**
* Internal class for handling requests for start/stop sample stream from polyphase channel sources
*/
private class ChannelSourceEventListener implements Listener<SourceEvent>
{
@Override
public void receive(SourceEvent sourceEvent)
{
switch(sourceEvent.getEvent())
{
case REQUEST_START_SAMPLE_STREAM:
if(sourceEvent.hasSource() && sourceEvent.getSource() instanceof PolyphaseChannelSource)
{
startChannelSource((PolyphaseChannelSource)sourceEvent.getSource());
}
else
{
mLog.error("Request to start sample stream for unrecognized source: " +
(sourceEvent.hasSource() ? sourceEvent.getSource().getClass() : "null source"));
}
break;
case REQUEST_STOP_SAMPLE_STREAM:
if(sourceEvent.hasSource() && sourceEvent.getSource() instanceof PolyphaseChannelSource)
{
stopChannelSource((PolyphaseChannelSource)sourceEvent.getSource());
}
else
{
mLog.error("Request to stop sample stream for unrecognized source: " +
(sourceEvent.hasSource() ? sourceEvent.getSource().getClass() : "null source"));
}
break;
case REQUEST_SOURCE_DISPOSE:
Source source = sourceEvent.getSource();
if(source instanceof PolyphaseChannelSource)
{
source.dispose();
}
break;
case NOTIFICATION_FREQUENCY_CORRECTION_CHANGE:
//ignore
break;
default:
mLog.error("Received unrecognized source event from polyphase channel source [" +
sourceEvent.getEvent() + "]");
break;
}
}
}
/**
* Processes the incoming buffer stream from the provider and transfers the buffers to the polyphase channelizer.
*
* This monitor incorporates a queue for receiving source events that affect configuration of the channelizer so
* that they can be processed on the buffer processor calling thread, avoiding unnecessary locks on the channelizer
* and/or the channel sources and output processors.
*/
public class BufferSourceEventMonitor implements Listener<List<ReusableComplexBuffer>>
{
private Queue<SourceEvent> mQueuedSourceEvents = new ConcurrentLinkedQueue<>();
/**
* Queues the source event for deferred execution on the buffer processing thread.
* @param event that affects configuration of the channelizer (frequency or sample rate change events)
*/
public void receive(SourceEvent event)
{
mQueuedSourceEvents.offer(event);
}
@Override
public void receive(List<ReusableComplexBuffer> reusableComplexBuffers)
{
try
{
//Process any queued source events before processing the buffers
SourceEvent queuedSourceEvent = mQueuedSourceEvents.poll();
while(queuedSourceEvent != null)
{
switch(queuedSourceEvent.getEvent())
{
case NOTIFICATION_FREQUENCY_CHANGE:
//Don't send the tuner's frequency change event down to the channels - it would cause chaos
updateOutputProcessors(null);
break;
case NOTIFICATION_SAMPLE_RATE_CHANGE:
updateChannelizerSampleRate(queuedSourceEvent.getValue().intValue());
break;
}
queuedSourceEvent = mQueuedSourceEvents.poll();
}
for(ReusableComplexBuffer reusableComplexBuffer: reusableComplexBuffers)
{
if(mPolyphaseChannelizer != null)
{
//User count management is handled by the channelizer
mPolyphaseChannelizer.receive(reusableComplexBuffer);
}
else
{
reusableComplexBuffer.decrementUserCount();
}
}
}
catch(Throwable throwable)
{
mLog.error("Error", throwable);
for(ReusableComplexBuffer buffer: reusableComplexBuffers)
{
buffer.decrementUserCount();
}
}
}
}
}

View File

@@ -0,0 +1,171 @@
/*******************************************************************************
* sdr-trunk
* Copyright (C) 2014-2018 Dennis Sheirer
*
* This program is free software: you can redistribute it and/or modify it under the terms of the GNU General Public
* License as published by the Free Software Foundation, either version 3 of the License, or (at your option) any
* later version.
*
* This program is distributed in the hope that it will be useful, but WITHOUT ANY WARRANTY; without even the implied
* warranty of MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU General Public License for more details.
*
* You should have received a copy of the GNU General Public License along with this program.
* If not, see <http://www.gnu.org/licenses/>
*
******************************************************************************/
package io.github.dsheirer.dsp.filter.channelizer;
import java.util.Arrays;
public class PolyphaseChannelResultsBuffer
{
private long mTimestamp;
private int mPointer = 0;
private int mMaxSize;
private float[][] mChannelResults;
/**
* Buffer containing an array of polyphase channelizer complex channel results.
* @param timestamp reference relative to the first channel results.
* @param maxSize of channel results to store in this buffer
*/
public PolyphaseChannelResultsBuffer(long timestamp, int maxSize)
{
mTimestamp = timestamp;
mMaxSize = maxSize;
}
/**
* Reference timestamp relative to the first channel results contained in this buffer.
* @return reference timestamp in milliseconds since epoch
*/
public long getTimestamp()
{
return mTimestamp;
}
/**
* Indicates if this buffer is full
*/
public boolean isFull()
{
return mPointer >= mMaxSize;
}
/**
* Indicates if this buffer is empty
*/
public boolean isEmpty()
{
return mPointer == 0;
}
/**
* Adds the channel results to this buffer
* @param channelResults to add
* @throws IllegalArgumentException if the buffer is full or if the size of the channel results array is a different
* length than any of the preceeding channel results that were added to this buffer
*/
public void add(float[] channelResults) throws IllegalArgumentException
{
if(mPointer >= mMaxSize)
{
throw new IllegalArgumentException("Buffer is full - cannot add new channel results");
}
if(mPointer == 0)
{
mChannelResults = new float[mMaxSize][channelResults.length];
}
else
{
int currentLength = mChannelResults[0].length;
if(channelResults.length != currentLength)
{
throw new IllegalArgumentException("Channel results length [" + channelResults.length + "] cannot be " +
"a different length than the channel results that are already contained in this buffer [" +
currentLength + "]");
}
}
mChannelResults[mPointer++] = channelResults;
}
/**
* Channel results array contained in this buffer
*/
public float[][] getChannelResults()
{
//If this buffer is full, return all of the channel results
if(mPointer == mMaxSize)
{
return mChannelResults;
}
//Otherwise, truncate the results to return
else
{
return Arrays.copyOf(mChannelResults, mPointer);
}
}
/**
* Extracts a single I/Q interleaved sample buffer from the channel results
* @param iChannelIndex to extract for the inphase sample. The q index is assumed to be one greater than i.
* @return float[] containing interleaved I and Q samples
* @throws IllegalArgumentException if this buffer doesn't contain any samples or if the requested channel is
* not within the channel range of the contained channel results.
*/
public float[] getChannel(int iChannelIndex)
{
if(isEmpty())
{
throw new IllegalArgumentException("Buffer is empty - cannot extract channel [" + iChannelIndex + "]");
}
if(!isValidChannelIndex(iChannelIndex))
{
throw new IllegalArgumentException("Channel [" + iChannelIndex + "] is not valid for the contained channel " +
"results -- max channel is " + getMaxChannelIndex());
}
float[] samples = new float[mChannelResults.length * 2];
int pointer = 0;
int qChannelIndex = iChannelIndex + 1;
for(int x = 0; x < mPointer; x++)
{
float[] channelResults = mChannelResults[x];
samples[pointer++] = channelResults[iChannelIndex];
samples[pointer++] = channelResults[qChannelIndex];
}
return samples;
}
/**
* Indicates if the channel argument is valid for the contained channel results.
* @param channelIndex number (0 to N) to check
* @return false if the buffer is empty or if the channel is greater than the contained channel results
*/
private boolean isValidChannelIndex(int channelIndex)
{
return channelIndex <= getMaxChannelIndex();
}
/**
* Maximum channel index represented in the channel results contained in this buffer.
* @return max channel number, or 0 if the buffer is empty
*/
private int getMaxChannelIndex()
{
if(isEmpty())
{
return 0;
}
return mChannelResults[0].length;
}
}

View File

@@ -0,0 +1,240 @@
/*******************************************************************************
* 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.filter.channelizer;
import io.github.dsheirer.dsp.filter.channelizer.output.IPolyphaseChannelOutputProcessor;
import io.github.dsheirer.sample.Listener;
import io.github.dsheirer.sample.buffer.ReusableChannelResultsBuffer;
import io.github.dsheirer.sample.buffer.ReusableComplexBuffer;
import io.github.dsheirer.sample.buffer.ReusableComplexBufferAssembler;
import io.github.dsheirer.source.SourceEvent;
import io.github.dsheirer.source.tuner.channel.TunerChannel;
import io.github.dsheirer.source.tuner.channel.TunerChannelSource;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
public class PolyphaseChannelSource extends TunerChannelSource
{
private final static Logger mLog = LoggerFactory.getLogger(PolyphaseChannelSource.class);
private ReusableComplexBufferAssembler mReusableComplexBufferAssembler;
private IPolyphaseChannelOutputProcessor mPolyphaseChannelOutputProcessor;
private double mChannelSampleRate;
private long mIndexCenterFrequency;
private long mChannelFrequencyCorrection;
/**
* Polyphase channelizer tuner channel source implementation. Adapts the channel array output samples from the
* polyphase channelizer into a single channel, or a channel synthesized from two adjacent channels that is
* frequency translated and decimated to a single channel.
*
* @param tunerChannel describing the desired channel frequency and bandwidth/minimum sample rate
* @param outputProcessor - to process polyphase channelizer channel results into a channel stream
* @param producerSourceEventListener to receive source event requests (e.g. start/stop sample stream)
* @param channelSampleRate for the downstream sample output
* @param centerFrequency of the incoming polyphase channel(s)
*/
public PolyphaseChannelSource(TunerChannel tunerChannel, IPolyphaseChannelOutputProcessor outputProcessor,
Listener<SourceEvent> producerSourceEventListener, double channelSampleRate,
long centerFrequency)
{
super(producerSourceEventListener, tunerChannel);
mPolyphaseChannelOutputProcessor = outputProcessor;
mChannelSampleRate = channelSampleRate;
mReusableComplexBufferAssembler = new ReusableComplexBufferAssembler(2500, mChannelSampleRate);
setFrequency(centerFrequency);
}
/**
* Registers the listener to receive complex sample buffers from this channel source
*/
@Override
public void setListener(final Listener<ReusableComplexBuffer> listener)
{
mReusableComplexBufferAssembler.setListener(listener);
}
/**
* Removes the listener from receiving complex sample buffers from this channel source
*/
@Override
public void removeListener(Listener<ReusableComplexBuffer> listener)
{
mReusableComplexBufferAssembler.setListener(null);
}
/**
* Channel output processor used by this channel source to convert polyphase channel results into a specific
* channel complex buffer output stream.
*/
public IPolyphaseChannelOutputProcessor getPolyphaseChannelOutputProcessor()
{
return mPolyphaseChannelOutputProcessor;
}
/**
* Sets/updates the output processor for this channel source, replacing the existing output processor.
*
* @param outputProcessor to use
* @param frequency center for the channels processed by the output processor
*/
public void setPolyphaseChannelOutputProcessor(IPolyphaseChannelOutputProcessor outputProcessor, long frequency)
{
//Lock on the channel output processor to block the channel results processor thread from servicing the
//channel output processor's buffer while we change out the processors.
synchronized(mPolyphaseChannelOutputProcessor)
{
IPolyphaseChannelOutputProcessor existingProcessor = mPolyphaseChannelOutputProcessor;
//Swap out the processor so that incoming samples can accumulate in the new channel output processor
mPolyphaseChannelOutputProcessor = outputProcessor;
//Remove the buffer overflow listener and clear any previous buffer overflow state
existingProcessor.setSourceOverflowListener(null);
broadcastOverflowState(false);
//Register to receive buffer overflow notifications so we can push them up through the source chain
mPolyphaseChannelOutputProcessor.setSourceOverflowListener(this);
//Fully process the residual channel results buffer of the previous channel output processor
if(existingProcessor != null)
{
existingProcessor.processChannelResults(mReusableComplexBufferAssembler);
}
//Finally, setup the frequency offset for the output processor and reset the frequency correction value
//to allow consumers to adjust and calculate a new frequency correction value, if needed.
mIndexCenterFrequency = frequency;
mChannelFrequencyCorrection = 0;
mPolyphaseChannelOutputProcessor.setFrequencyOffset(getFrequencyOffset());
broadcastConsumerSourceEvent(SourceEvent.frequencyCorrectionChange(mChannelFrequencyCorrection));
}
}
/**
* Primary method for receiving channel results output from a polyphase channelizer. The results buffer will be
* queued for processing to extract the target channel samples, process them for frequency correction and/or
* channel aggregation, and dispatch the results to the downstream sample listener/consumer.
*
* @param channelResultsBuffer containing an array of polyphase channelizer outputs.
*/
public void receiveChannelResults(ReusableChannelResultsBuffer channelResultsBuffer)
{
mReusableComplexBufferAssembler.updateTimestamp(channelResultsBuffer.getTimestamp());
mPolyphaseChannelOutputProcessor.receiveChannelResults(channelResultsBuffer);
}
/**
* Downstream channel sample rate
*
* @return sample rate in Hertz
*/
@Override
public double getSampleRate()
{
return mChannelSampleRate;
}
/**
* Sets the downstream channel sample rate
* @param sampleRate in hertz
*/
@Override
protected void setSampleRate(double sampleRate)
{
mChannelSampleRate = sampleRate;
}
@Override
public void dispose()
{
if(mReusableComplexBufferAssembler != null)
{
mReusableComplexBufferAssembler.dispose();
}
if(mPolyphaseChannelOutputProcessor != null)
{
mPolyphaseChannelOutputProcessor.dispose();
mPolyphaseChannelOutputProcessor = null;
}
}
@Override
public long getChannelFrequencyCorrection()
{
return mChannelFrequencyCorrection;
}
/**
* Adjusts the frequency correction value that is being applied to the channelized output stream by the
* polyphase channel output processor.
*
* @param value to apply for frequency correction in hertz
*/
protected void setChannelFrequencyCorrection(long value)
{
mChannelFrequencyCorrection = value;
updateFrequencyOffset();
broadcastConsumerSourceEvent(SourceEvent.frequencyCorrectionChange(mChannelFrequencyCorrection));
}
/**
* Sets the center frequency for this channel.frequency for the incoming sample stream channel results and resets frequency correction to zero.
* @param frequency in hertz
*/
@Override
protected void setFrequency(long frequency)
{
mIndexCenterFrequency = frequency;
//Set frequency correction to zero to trigger an update to the mixer and allow downstream monitors to
//recalculate the frequency error correction again
setChannelFrequencyCorrection(0);
}
@Override
protected void processSamples()
{
//Lock on the output processor so that it can't be changed in the middle of processing
synchronized(mPolyphaseChannelOutputProcessor)
{
mPolyphaseChannelOutputProcessor.processChannelResults(mReusableComplexBufferAssembler);
}
}
/**
* Calculates the frequency offset required to mix the incoming signal to center the desired frequency
* within the channel
*/
private long getFrequencyOffset()
{
return mIndexCenterFrequency - getTunerChannel().getFrequency() + mChannelFrequencyCorrection;
}
/**
* Updates the frequency offset being applied by the output processor. Calculates the offset using the center
* frequency of the incoming polyphase channel results and the desired output channel frequency adjusted by any
* requested frequency correction value.
*/
private void updateFrequencyOffset()
{
synchronized(mPolyphaseChannelOutputProcessor)
{
mPolyphaseChannelOutputProcessor.setFrequencyOffset(getFrequencyOffset());
}
}
}

View File

@@ -0,0 +1,83 @@
/*******************************************************************************
* sdr-trunk
* Copyright (C) 2014-2018 Dennis Sheirer
*
* This program is free software: you can redistribute it and/or modify it under the terms of the GNU General Public
* License as published by the Free Software Foundation, either version 3 of the License, or (at your option) any
* later version.
*
* This program is distributed in the hope that it will be useful, but WITHOUT ANY WARRANTY; without even the implied
* warranty of MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU General Public License for more details.
*
* You should have received a copy of the GNU General Public License along with this program.
* If not, see <http://www.gnu.org/licenses/>
*
******************************************************************************/
package io.github.dsheirer.dsp.filter.channelizer;
public class SampleTimestampManager
{
private long mReferenceTimestamp = System.currentTimeMillis();
private double mSamplesPerMilliSecond;
private long mSampleCount;
/**
* Tracks the number of samples produced relative to a reference timestamp to derive a current reference timestamp
* for use in generating sample buffers.
*
* @param sampleRate per second
*/
public SampleTimestampManager(double sampleRate)
{
}
/**
* Updates the internal reference timestamp and resets the sample counter to zero.
*
* @param referenceTimestamp in milli-seconds since epoch
*/
public void setReferenceTimestamp(long referenceTimestamp)
{
mReferenceTimestamp = referenceTimestamp;
mSampleCount = 0;
}
/**
* Updates the sample rate that is used in calculating the reference timestamp
*
* @param sampleRate
*/
public void setSampleRate(double sampleRate)
{
//Convert to samples per milli-second for compatibility with reference timestamp
mSamplesPerMilliSecond = sampleRate / 1000.0d;
}
/**
* Increments the internal count of dispatched samples to use in calculating an accurate reference timestamp.
*/
public void increment()
{
mSampleCount++;
}
/**
* Increments the internal count of dispatched samples to use in calculating an accurate reference timestamp.
*
* @param value to add to the sample count
*/
public void increment(int value)
{
mSampleCount += value;
}
public long getCurrentTimestamp()
{
if(mSampleCount > 0)
{
return mReferenceTimestamp + (long)((double)mSampleCount / mSamplesPerMilliSecond);
}
return mReferenceTimestamp;
}
}

View File

@@ -0,0 +1,191 @@
/*******************************************************************************
* 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.filter.channelizer;
import io.github.dsheirer.sample.buffer.ReusableComplexBuffer;
import io.github.dsheirer.sample.buffer.ReusableComplexBufferQueue;
import org.jtransforms.fft.FloatFFT_1D;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
/**
* Implements a two-channel polyphase filer synthesizer. This class is intended to be used with an M2 polyphase
* channelizer in order to recover a signal that overlaps the boundary between two channels. This synthesizer will
* rejoin the two M2 neighboring channels, mix the signal of interest to baseband, and filter the output stream to
* half of the sample rate, producing an overall M2 output sample rate.
*
* The synthesizer concept is modeled on the structure described by Fred Harris et al, in 'Interleaving Different
* Bandwidth Narrowband Channels in Perfect Reconstruction Cascade Polyphase Filter Banks for Efficient Flexible
* Variable Bandwidth Filters in Wideband Digital Transceivers'.
*
* The data buffers and sub-filter kernels bear some resemblance to the structures defined in the paper. The data and
* filter structures in this class are organized to take advantage of Java's ability to leverage SIMD processor
* intrinsics for optimal efficiency in processing. The paper specifies splitting the filter kernel into N polyphase
* partitions and creating N data buffers. This class organizes the data buffer as a contiguous sample
* array and creates an I/Q interleaved filter kernel. Instead of calculating the dot-product for each sub-filter,
* we calculate the product of the full data array against the filter and then accumulate each sub-filter. This allows
* java to use SIMD for the array product and then normal processing for the accumulation. Since this is a two-channel
* processor and the results of each filter accumulation are added, we use a single accumulator across both filters.
*
*/
public class TwoChannelSynthesizerM2
{
private final static Logger mLog = LoggerFactory.getLogger(TwoChannelSynthesizerM2.class);
private ReusableComplexBufferQueue mReusableComplexBufferQueue = new ReusableComplexBufferQueue("Two Channel Synthesizer M2");
private float[] mSerpentineDataBuffer;
private float[] mIQInterleavedFilter;
private float[] mFilterVectorProduct;
private float[] mIFFTBuffer = new float[4];
private float mIAccumulator;
private float mQAccumulator;
private FloatFFT_1D mFFT = new FloatFFT_1D(2);
private boolean mTopBlockFlag = true;
/**
* Polyphase synthesizer for mixing two M2 oversampled channels into a composite channel using perfect
* reconstruction filters (-6db at band edge). Output sample rate is the same as the input sample rate of one of
* the channels.
*/
public TwoChannelSynthesizerM2(float[] filter)
{
init(filter);
}
/**
* Initializes the synthesizer filter and data buffers for operation.
*
* @param filter to use for polyphase synthesis of two channels.
*/
private void init(float[] filter)
{
int tapsPerChannel = (int)Math.ceil(filter.length / 2);
mIQInterleavedFilter = getInterleavedFilter(filter, tapsPerChannel);
mSerpentineDataBuffer = new float[mIQInterleavedFilter.length];
mFilterVectorProduct = new float[mIQInterleavedFilter.length];
}
/**
* Synthesizes a new channel from the channel 1 and channel 2 sample arrays that are arranged as
* i0, q0, i1, q1 ... iN-1, qN-1
*
* @param channelBuffer1 input channel
* @param channelBuffer2 input channel
* @return synthesized channel results of the same length as both channel 1/2 input arrays.
*/
public ReusableComplexBuffer process(ReusableComplexBuffer channelBuffer1, ReusableComplexBuffer channelBuffer2)
{
if(channelBuffer1.getSamples().length != channelBuffer1.getSamples().length)
{
throw new IllegalArgumentException("Channel 1 and 2 array length must be equal");
}
float[] channel1 = channelBuffer1.getSamples();
float[] channel2 = channelBuffer2.getSamples();
ReusableComplexBuffer synthesizedComplexBuffer = mReusableComplexBufferQueue.getBuffer(channel1.length);
float[] output = synthesizedComplexBuffer.getSamples();
for(int x = 0; x < channel1.length; x += 2)
{
//Load samples from each channel into buffer for IFFT
mIFFTBuffer[0] = channel1[x]; //i
mIFFTBuffer[1] = channel1[x + 1]; //q
mIFFTBuffer[2] = channel2[x]; //i
mIFFTBuffer[3] = channel2[x + 1]; //q
//Perform Inverse FFT (IFFT)
mFFT.complexInverse(mIFFTBuffer, true);
//Perform serpentine shift of data blocks in the data buffer - make room for 4 new samples
System.arraycopy(mSerpentineDataBuffer, 0, mSerpentineDataBuffer, 4, mSerpentineDataBuffer.length - 4);
//Top Block - load samples into data buffer in normal order
if(mTopBlockFlag)
{
System.arraycopy(mIFFTBuffer, 0, mSerpentineDataBuffer, 0, mIFFTBuffer.length);
}
//Bottom Block - swap samples via data loading to account for phase shift
else
{
System.arraycopy(mIFFTBuffer, 0, mSerpentineDataBuffer, 2, 2);
System.arraycopy(mIFFTBuffer, 2, mSerpentineDataBuffer, 0, 2);
}
//Note: in order to use Java's ability to leverage SIMD intrinsics, we perform filtering in two steps
//(multiply then accumulate) since our filter and data are structured with I and Q vectors interleaved.
//This approach allows the Hotspot compiler to more easily recognize the scalor operations.
//Multiply data samples by the I/Q interleaved filter to form the vector product
for(int y = 0; y < mSerpentineDataBuffer.length; y++)
{
mFilterVectorProduct[y] = mSerpentineDataBuffer[y] * mIQInterleavedFilter[y];
}
//Accumulate output I/Q samples from vector product
mIAccumulator = 0.0f;
mQAccumulator = 0.0f;
for(int y = 0; y < mFilterVectorProduct.length; y += 2)
{
mIAccumulator += mFilterVectorProduct[y];
mQAccumulator += mFilterVectorProduct[y + 1];
}
output[x] = mIAccumulator;
output[x + 1] = mQAccumulator;
mTopBlockFlag = !mTopBlockFlag;
}
channelBuffer1.decrementUserCount();
channelBuffer2.decrementUserCount();
return synthesizedComplexBuffer;
}
/**
* Creates an interleaved I/Q filter where each coefficient from the filter argument is duplicated and the returned
* filter is twice the length of the original filter.
*
* Note: the returned filter array is sized to: 2 * channel count * taps per channel, which may be slightly more
* than twice the length of the original filter. Any Added filter array elements will contain zero values.
*
* @param coefficients to create an interleaved filter
* @param tapsPerChannel count
* @return
*/
private static float[] getInterleavedFilter(float[] coefficients, int tapsPerChannel)
{
int channelCount = 2;
float[] filter = new float[channelCount * tapsPerChannel * 2];
int coefficientPointer = 0;
int filterPointer = 0;
//Create a new filter that duplicates each tap to produce an interleaved I/Q filter
while(coefficientPointer < coefficients.length)
{
filter[filterPointer++] = coefficients[coefficientPointer];
filter[filterPointer++] = coefficients[coefficientPointer++];
}
return filter;
}
}

View File

@@ -0,0 +1,161 @@
/*******************************************************************************
* sdrtrunk
* Copyright (C) 2014-2017 Dennis Sheirer
*
* This program is free software: you can redistribute it and/or modify
* it under the terms of the GNU General Public License as published by
* the Free Software Foundation, either version 3 of the License, or
* (at your option) any later version.
*
* This program is distributed in the hope that it will be useful,
* but WITHOUT ANY WARRANTY; without even the implied warranty of
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
* GNU General Public License for more details.
*
* You should have received a copy of the GNU General Public License
* along with this program. If not, see <http://www.gnu.org/licenses/>
*
******************************************************************************/
package io.github.dsheirer.dsp.filter.channelizer.output;
import io.github.dsheirer.dsp.mixer.IOscillator;
import io.github.dsheirer.dsp.mixer.Oscillator;
import io.github.dsheirer.sample.IOverflowListener;
import io.github.dsheirer.sample.buffer.OverflowableReusableBufferTransferQueue;
import io.github.dsheirer.sample.buffer.ReusableChannelResultsBuffer;
import io.github.dsheirer.sample.buffer.ReusableComplexBufferAssembler;
import io.github.dsheirer.source.Source;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import java.util.ArrayList;
import java.util.List;
public abstract class ChannelOutputProcessor implements IPolyphaseChannelOutputProcessor
{
private final static Logger mLog = LoggerFactory.getLogger(ChannelOutputProcessor.class);
private OverflowableReusableBufferTransferQueue<ReusableChannelResultsBuffer> mChannelResultsQueue;
private List<ReusableChannelResultsBuffer> mChannelResultsToProcess = new ArrayList<>();
private int mMaxResultsToProcess;
private int mInputChannelCount;
private IOscillator mFrequencyCorrectionMixer;
private boolean mFrequencyCorrectionEnabled;
/**
* Base class for polyphase channelizer output channel processing. Provides built-in frequency translation
* oscillator support to apply frequency correction to the channel sample stream as requested by sample consumer.
*
* @param inputChannelCount is the number of input channels for this output processor
* @param sampleRate of the output channel. This is used to match the oscillator's sample rate to the output
* channel sample rate for frequency translation/correction.
*/
public ChannelOutputProcessor(int inputChannelCount, double sampleRate)
{
mInputChannelCount = inputChannelCount;
//TODO: swap this out and use the LowPhaseNoiseOscillator
mFrequencyCorrectionMixer = new Oscillator(0, sampleRate);
mMaxResultsToProcess = (int)(sampleRate / 10) * 2; //process at 100 millis interval, twice the expected inflow rate
mChannelResultsQueue = new OverflowableReusableBufferTransferQueue<>((int)(sampleRate * 3), (int)(sampleRate * 0.5));
}
public void dispose()
{
if(mChannelResultsQueue != null)
{
mChannelResultsQueue.dispose();
mChannelResultsQueue = null;
}
}
/**
* Sets the frequency offset to apply to the incoming samples to mix the desired signal to baseband.
* @param frequencyOffset in hertz
*/
@Override
public void setFrequencyOffset(long frequencyOffset)
{
mFrequencyCorrectionMixer.setFrequency(frequencyOffset);
mFrequencyCorrectionEnabled = (frequencyOffset != 0);
}
/**
* Oscillator/mixer to use for frequency correction against incoming sample stream
*/
protected IOscillator getFrequencyCorrectionMixer()
{
return mFrequencyCorrectionMixer;
}
@Override
public void receiveChannelResults(ReusableChannelResultsBuffer channelResults)
{
mChannelResultsQueue.offer(channelResults);
}
/**
* Processes all enqueued polyphase channelizer results until the internal queue is empty
* @param reusableComplexBufferAssembler to receive the processed channel results
*/
@Override
public void processChannelResults(ReusableComplexBufferAssembler reusableComplexBufferAssembler)
{
try
{
int toProcess = mChannelResultsQueue.drainTo(mChannelResultsToProcess, mMaxResultsToProcess);
if(toProcess > 0)
{
process(mChannelResultsToProcess, reusableComplexBufferAssembler);
}
}
catch(Throwable throwable)
{
mLog.error("Error while processing polyphase channel samples", throwable);
}
mChannelResultsToProcess.clear();
}
/**
* Sub-class implementation to process one polyphase channelizer result array.
* @param channelResults to process
* @param reusableComplexBufferAssembler to receive the processed channelizer results
*/
public abstract void process(List<ReusableChannelResultsBuffer> channelResults,
ReusableComplexBufferAssembler reusableComplexBufferAssembler);
/**
* Sets the overflow listener to monitor the internal channelizer channel results queue overflow state
*/
public void setSourceOverflowListener(Source source)
{
mChannelResultsQueue.setSourceOverflowListener(source);
}
/**
* Removes the overflow listener from monitoring the internal channelizer channel results queue overflow state
*/
public void removeOverflowListener(IOverflowListener listener)
{
mChannelResultsQueue.setOverflowListener(null);
}
@Override
public int getInputChannelCount()
{
return mInputChannelCount;
}
/**
* Indicates if this channel has a frequency correction offset
*/
public boolean hasFrequencyCorrection()
{
return mFrequencyCorrectionEnabled;
}
}

View File

@@ -0,0 +1,76 @@
/*******************************************************************************
* 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.dsp.filter.channelizer.output;
import io.github.dsheirer.sample.buffer.ReusableChannelResultsBuffer;
import io.github.dsheirer.sample.buffer.ReusableComplexBufferAssembler;
import io.github.dsheirer.source.Source;
import java.util.List;
public interface IPolyphaseChannelOutputProcessor
{
/**
* Receive and enqueue output results from the polyphase analysis channelizer
* @param channelResultsBuffer to enqueue
*/
void receiveChannelResults(ReusableChannelResultsBuffer channelResultsBuffer);
/**
* Process the channel output channel results queue and deliver the output to the listener
*
* @param reusableComplexBufferAssembler to receive the processed channel results
*/
void processChannelResults(ReusableComplexBufferAssembler reusableComplexBufferAssembler);
/**
* Sets the desired frequency offset from center. The samples will be mixed with an oscillator set to this offset
* frequency to produce an output where the desired signal is centered in the passband.
*
* @param frequency in hertz
*/
void setFrequencyOffset(long frequency);
/**
* Indicates the number of input channels processed by this output processor
* @return
*/
int getInputChannelCount();
/**
* Updates the input polyphase channel index(es) used by this output processor
* @param indexes
*/
void setPolyphaseChannelIndices(List<Integer> indexes);
/**
* Updates the synthesis filter taps for this output processor
* @param filter
*/
void setSynthesisFilter(float[] filter);
/**
* Sets the listener to receive buffer overflow notifications
* @param source to receive overflow notifications
*/
void setSourceOverflowListener(Source source);
void dispose();
}

View File

@@ -0,0 +1,104 @@
/*******************************************************************************
* 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.dsp.filter.channelizer.output;
import io.github.dsheirer.sample.buffer.ReusableChannelResultsBuffer;
import io.github.dsheirer.sample.buffer.ReusableComplexBuffer;
import io.github.dsheirer.sample.buffer.ReusableComplexBufferAssembler;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import java.util.List;
public class OneChannelOutputProcessor extends ChannelOutputProcessor
{
private final static Logger mLog = LoggerFactory.getLogger(OneChannelOutputProcessor.class);
private int mChannelOffset;
/**
* Processor to extract a single channel from a polyphase channelizer and produce an output I/Q complex sample
* from each polyphase channelizer output results array.
*
* @param sampleRate of the output sample stream.
* @param channelIndexes containing a single channel index.
*/
public OneChannelOutputProcessor(double sampleRate, List<Integer> channelIndexes)
{
super(1, sampleRate);
setPolyphaseChannelIndices(channelIndexes);
}
@Override
public void setSynthesisFilter(float[] filter)
{
throw new IllegalArgumentException("The one channel output processor does not support filter updates");
}
/**
* Updates this processor to extract a single, specified channel index.
* @param indexes containing a single channel index value.
* @throws IllegalArgumentException if the list of indexes does not contain a single channel index.
*/
public void setPolyphaseChannelIndices(List<Integer> indexes)
{
if(indexes.size() != 1)
{
throw new IllegalArgumentException("Single channel output processor requires a single index to " +
"process - provided indexes " + indexes.toString());
}
//Set the channelized output results offset to twice the channel index to account for each channel having
//an I/Q pair
mChannelOffset = indexes.get(0) * 2;
}
/**
* Extract the channel from the channel results array, apply frequency translation, and deliver the
* extracted frequency-corrected channel I/Q sample set to the complex sample listener.
*
* @param channelResults to process containing a list of channel array of I/Q sample pairs (I0,Q0,I1,Q1...In,Qn)
* @param reusableComplexBufferAssembler to receive the extracted, frequency-translated channel results
*/
@Override
public void process(List<ReusableChannelResultsBuffer> channelResults,
ReusableComplexBufferAssembler reusableComplexBufferAssembler)
{
for(ReusableChannelResultsBuffer channelResultsBuffer: channelResults)
{
try
{
ReusableComplexBuffer channelBuffer = channelResultsBuffer.getChannel(mChannelOffset);
if(hasFrequencyCorrection())
{
getFrequencyCorrectionMixer().mixComplex(channelBuffer.getSamples());
}
reusableComplexBufferAssembler.receive(channelBuffer);
}
catch(IllegalArgumentException iae)
{
mLog.error("Error extracting channel samples from polyphase channel results buffer");
}
channelResultsBuffer.decrementUserCount();
}
}
}

View File

@@ -0,0 +1,184 @@
/*******************************************************************************
* 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.filter.channelizer.output;
import io.github.dsheirer.dsp.filter.channelizer.TwoChannelSynthesizerM2;
import io.github.dsheirer.dsp.filter.design.FilterDesignException;
import io.github.dsheirer.dsp.filter.fir.FIRFilterSpecification;
import io.github.dsheirer.dsp.filter.fir.complex.ComplexFIRFilter2;
import io.github.dsheirer.dsp.filter.fir.remez.RemezFIRFilterDesigner;
import io.github.dsheirer.dsp.mixer.FS4DownConverter;
import io.github.dsheirer.sample.buffer.ReusableChannelResultsBuffer;
import io.github.dsheirer.sample.buffer.ReusableComplexBuffer;
import io.github.dsheirer.sample.buffer.ReusableComplexBufferAssembler;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import java.util.HashMap;
import java.util.List;
import java.util.Map;
public class TwoChannelOutputProcessor extends ChannelOutputProcessor
{
private final static Logger mLog = LoggerFactory.getLogger(TwoChannelOutputProcessor.class);
private static Map<Integer,float[]> sLowPassFilterKernelMap = new HashMap<>();
private TwoChannelSynthesizerM2 mSynthesizer;
private FS4DownConverter mFS4DownConverter = new FS4DownConverter();
private ComplexFIRFilter2 mLowPassFilter;
private int mChannelOffset1;
private int mChannelOffset2;
/**
* Processor to extract two channels from a polyphase channelizer, synthesize/recombine the channels, apply
* frequency translation and frequency correction, down-sample by a factor of two and output an I/Q complex sample.
*
* @param sampleRate of the output sample stream.
* @param channelIndexes containing two channel indices.
*/
public TwoChannelOutputProcessor(double sampleRate, List<Integer> channelIndexes, float[] filter)
{
//Set the frequency correction oscillator to 2 x output sample rate since we'll be correcting the frequency
//after synthesizing both input channels
super(2, sampleRate);
setPolyphaseChannelIndices(channelIndexes);
setSynthesisFilter(filter);
float[] lowPassFilter = sLowPassFilterKernelMap.get((int)sampleRate);
if(lowPassFilter == null)
{
try
{
lowPassFilter = getLowPassFilter(sampleRate);
sLowPassFilterKernelMap.put((int)sampleRate, lowPassFilter);
}
catch(FilterDesignException fde)
{
mLog.error("Couldn't design a low-pass filter for 2-channel synthesizer", fde);
throw new IllegalStateException("Cannot design low-pass filter for 2 channel synthesizer", fde);
}
}
mLowPassFilter = new ComplexFIRFilter2(lowPassFilter, 1.0f);
}
/**
* Sets the frequency offset to apply to the incoming samples to mix the desired signal to baseband.
* @param frequencyOffset in hertz
*/
@Override
public void setFrequencyOffset(long frequencyOffset)
{
super.setFrequencyOffset(frequencyOffset);
}
@Override
public void setSynthesisFilter(float[] filter)
{
mSynthesizer = new TwoChannelSynthesizerM2(filter);
}
/**
* Updates this processor to extract the two specified channel indexes.
*
* @param indexes containing a single channel index value.
* @throws IllegalArgumentException if the list of indexes does not contain a single channel index.
*/
public void setPolyphaseChannelIndices(List<Integer> indexes)
{
if(indexes.size() != 2)
{
throw new IllegalArgumentException("Double channel output processor requires two indexes to " +
"process - provided indexes " + indexes.toString());
}
//Set the channelized output results offsets to twice the channel index to account for each channel having
//an I/Q pair
mChannelOffset1 = indexes.get(0) * 2;
mChannelOffset2 = indexes.get(1) * 2;
}
/**
* Extract the channel from the channel results array, apply frequency translation, and deliver the
* extracted frequency-corrected channel I/Q sample set to the complex sample listener.
*
* @param channelResultsBuffers to process containing an array of channel I/Q sample pairs (I0,Q0,I1,Q1...In,Qn)
* @param reusableComplexBufferAssembler to receive the extracted, frequency-translated channel results
*/
@Override
public void process(List<ReusableChannelResultsBuffer> channelResultsBuffers,
ReusableComplexBufferAssembler reusableComplexBufferAssembler)
{
for(ReusableChannelResultsBuffer buffer: channelResultsBuffers)
{
ReusableComplexBuffer channel1 = buffer.getChannel(mChannelOffset1);
ReusableComplexBuffer channel2 = buffer.getChannel(mChannelOffset2);
//Join the two channels using the synthesizer
ReusableComplexBuffer synthesized = mSynthesizer.process(channel1, channel2);
//The synthesized channels are centered at +FS/4 ... downconvert to center the spectrum
mFS4DownConverter.mixComplex(synthesized.getSamples());
//Apply offset and frequency correction to center the signal of interest within the synthesized channel
getFrequencyCorrectionMixer().mixComplex(synthesized.getSamples());
ReusableComplexBuffer lowPassFiltered = mLowPassFilter.filter(synthesized);
reusableComplexBufferAssembler.receive(lowPassFiltered);
buffer.decrementUserCount();
}
}
/**
* Creates a low-pass filter kernel for the specified sample rate.
* @param sampleRate for the channel
* @return filter
* @throws FilterDesignException if the filter cannot be designed
*/
private static float[] getLowPassFilter(double sampleRate) throws FilterDesignException
{
double cutoff = sampleRate / 4.0;
int passBandCutoff = (int)(cutoff * 0.92);
int stopBandStart = (int)(cutoff * 1.08);
FIRFilterSpecification specification = FIRFilterSpecification.lowPassBuilder()
.sampleRate(sampleRate)
.gridDensity(16)
.oddLength(true)
.passBandCutoff((int)passBandCutoff)
.passBandAmplitude(1.0)
.passBandRipple(0.01)
.stopBandStart((int)stopBandStart)
.stopBandAmplitude(0.0)
.stopBandRipple(0.005) //Approximately 94 dB attenuation
.build();
RemezFIRFilterDesigner designer = new RemezFIRFilterDesigner(specification);
if(designer.isValid())
{
return designer.getImpulseResponse();
}
throw new FilterDesignException("Couldn't design low-pass filter for sample rate: " + sampleRate +
" Pass End:" + passBandCutoff + " Stop Begin:" + stopBandStart);
}
}

View File

@@ -1,19 +1,16 @@
/*******************************************************************************
* sdrtrunk
* Copyright (C) 2014-2017 Dennis Sheirer
* sdr-trunk
* Copyright (C) 2014-2018 Dennis Sheirer
*
* This program is free software: you can redistribute it and/or modify
* it under the terms of the GNU General Public License as published by
* the Free Software Foundation, either version 3 of the License, or
* (at your option) any later version.
* This program is free software: you can redistribute it and/or modify it under the terms of the GNU General Public
* License as published by the Free Software Foundation, either version 3 of the License, or (at your option) any
* later version.
*
* This program is distributed in the hope that it will be useful,
* but WITHOUT ANY WARRANTY; without even the implied warranty of
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
* GNU General Public License for more details.
* This program is distributed in the hope that it will be useful, but WITHOUT ANY WARRANTY; without even the implied
* warranty of MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU General Public License for more details.
*
* You should have received a copy of the GNU General Public License
* along with this program. If not, see <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.dsp.filter.cic;
@@ -21,19 +18,19 @@ package io.github.dsheirer.dsp.filter.cic;
import io.github.dsheirer.dsp.filter.FilterFactory;
import io.github.dsheirer.dsp.filter.Filters;
import io.github.dsheirer.dsp.filter.Window;
import io.github.dsheirer.dsp.filter.fir.complex.ComplexFIRFilter_CB_CB;
import io.github.dsheirer.dsp.filter.halfband.complex.HalfBandFilter_CB_CB;
import io.github.dsheirer.dsp.filter.fir.complex.ComplexFIRFilter2;
import io.github.dsheirer.dsp.filter.halfband.complex.ComplexHalfBandFilter;
import io.github.dsheirer.sample.Listener;
import io.github.dsheirer.sample.complex.ComplexBuffer;
import io.github.dsheirer.sample.buffer.ReusableComplexBuffer;
import io.github.dsheirer.sample.buffer.ReusableComplexBufferAssembler;
import io.github.dsheirer.sample.complex.ComplexSampleListener;
import io.github.dsheirer.sample.complex.ComplexToComplexBufferAssembler;
import io.github.dsheirer.sample.decimator.ComplexDecimator;
import org.apache.commons.lang3.Validate;
import java.util.ArrayList;
import java.util.List;
public class ComplexPrimeCICDecimate implements Listener<ComplexBuffer>
public class ComplexPrimeCICDecimate implements Listener<ReusableComplexBuffer>
{
public static int[] PRIMES = {2, 3, 5, 7, 11, 13, 17, 19, 23, 29, 31, 37, 41, 43, 47, 53,
59, 61, 67, 71, 73, 79, 83, 89, 97, 101, 103, 107, 109, 113, 127, 131, 137, 139, 149, 151,
@@ -87,7 +84,7 @@ public class ComplexPrimeCICDecimate implements Listener<ComplexBuffer>
}
else
{
/* Wire the current stage to the previous stage */
/* Wire the current stage to the previous stage */
mDecimatingStages.get(x - 1).setListener(stage);
}
}
@@ -116,7 +113,7 @@ public class ComplexPrimeCICDecimate implements Listener<ComplexBuffer>
/**
* Adds a listener to receive the output of this CIC decimation filter
*/
public void setListener(Listener<ComplexBuffer> listener)
public void setListener(Listener<ReusableComplexBuffer> listener)
{
mOutput.setListener(listener);
}
@@ -166,7 +163,7 @@ public class ComplexPrimeCICDecimate implements Listener<ComplexBuffer>
* Primary input method for receiving sample arrays composed as I,Q,I,Q, etc.
*/
@Override
public void receive(ComplexBuffer buffer)
public void receive(ReusableComplexBuffer buffer)
{
if(mFirstDecimatingStage != null)
{
@@ -177,6 +174,8 @@ public class ComplexPrimeCICDecimate implements Listener<ComplexBuffer>
mFirstDecimatingStage.receive(samples[x], samples[x + 1]);
}
}
buffer.decrementUserCount();
}
/**
@@ -289,11 +288,11 @@ public class ComplexPrimeCICDecimate implements Listener<ComplexBuffer>
public void receive(float i, float q)
{
/* Subtract the oldest sample and add back in the newest */
/* Subtract the oldest sample and add back in the newest */
mISum = mISum - mISamples[mSamplePointer] + i;
mQSum = mQSum - mQSamples[mSamplePointer] + q;
/* Overwrite the oldest sample with the newest */
/* Overwrite the oldest sample with the newest */
mISamples[mSamplePointer] = i;
mQSamples[mSamplePointer] = q;
@@ -352,27 +351,37 @@ public class ComplexPrimeCICDecimate implements Listener<ComplexBuffer>
public class Output implements ComplexSampleListener
{
/* Decimated output buffers will contain 1024 samples */
private ComplexToComplexBufferAssembler mAssembler =
new ComplexToComplexBufferAssembler(2048);
private ReusableComplexBufferAssembler mAssembler = new ReusableComplexBufferAssembler(2048, 48000.0);
private ComplexFIRFilter_CB_CB mCleanupFilter;
private HalfBandFilter_CB_CB mHalfBandFilter = new HalfBandFilter_CB_CB(
private ComplexFIRFilter2 mCleanupFilter;
private ComplexHalfBandFilter mHalfBandFilter = new ComplexHalfBandFilter(
Filters.FIR_HALF_BAND_31T_ONE_EIGHTH_FCO.getCoefficients(), 0.4f, false);
private Listener<ReusableComplexBuffer> mReusableComplexBufferListener;
public Output(int outputSampleRate, int passFrequency, int attenuation,
Window.WindowType windowType)
{
mCleanupFilter = new ComplexFIRFilter_CB_CB(FilterFactory
.getCICCleanupFilter(outputSampleRate,
passFrequency,
attenuation,
windowType), 0.4f);
mCleanupFilter = new ComplexFIRFilter2(FilterFactory
.getCICCleanupFilter(outputSampleRate, passFrequency, attenuation, windowType), 0.4f);
//Bypassing the CIC cleanup filter for now
mAssembler.setListener(mHalfBandFilter);
// mAssembler.setListener( mCleanupFilter );
// mCleanupFilter.setListener( mHalfBandFilter );
mAssembler.setListener(new Listener<ReusableComplexBuffer>()
{
@Override
public void receive(ReusableComplexBuffer reusableComplexBuffer)
{
if(mReusableComplexBufferListener != null)
{
ReusableComplexBuffer cleanupFiltered = mCleanupFilter.filter(reusableComplexBuffer);
ReusableComplexBuffer halfBandFiltered = mHalfBandFilter.filter(cleanupFiltered);
mReusableComplexBufferListener.receive(halfBandFiltered);
}
else
{
reusableComplexBuffer.decrementUserCount();
}
}
});
}
public void dispose()
@@ -390,23 +399,17 @@ public class ComplexPrimeCICDecimate implements Listener<ComplexBuffer>
@Override
public void receive(float inphase, float quadrature)
{
mAssembler.receive(inphase, quadrature);
mAssembler.receive(new float[]{inphase, quadrature});
}
/**
* Adds a listener to receive output samples
*/
public void setListener(Listener<ComplexBuffer> listener)
public void setListener(Listener<ReusableComplexBuffer> listener)
{
mHalfBandFilter.setListener(listener);
mReusableComplexBufferListener = listener;
}
/**
* Removes the listener from receiving output samples
*/
public void removeListener()
{
mHalfBandFilter.removeListener();
mReusableComplexBufferListener = null;
}
}
}

View File

@@ -1,17 +1,17 @@
/*******************************************************************************
* SDR Trunk
* Copyright (C) 2015 Dennis Sheirer
*
*
* This program is free software: you can redistribute it and/or modify
* it under the terms of the GNU General Public License as published by
* the Free Software Foundation, either version 3 of the License, or
* (at your option) any later version.
*
*
* This program is distributed in the hope that it will be useful,
* but WITHOUT ANY WARRANTY; without even the implied warranty of
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
* GNU General Public License for more details.
*
*
* You should have received a copy of the GNU General Public License
* along with this program. If not, see <http://www.gnu.org/licenses/>
******************************************************************************/
@@ -20,13 +20,13 @@ package io.github.dsheirer.dsp.filter.cic;
import io.github.dsheirer.dsp.filter.FilterFactory;
import io.github.dsheirer.dsp.filter.Filters;
import io.github.dsheirer.dsp.filter.Window.WindowType;
import io.github.dsheirer.dsp.filter.fir.real.RealFIRFilter_RB_RB;
import io.github.dsheirer.dsp.filter.halfband.real.HalfBandFilter_RB_RB;
import io.github.dsheirer.dsp.filter.fir.real.RealFIRFilter2;
import io.github.dsheirer.dsp.filter.halfband.real.HalfBandFilter;
import io.github.dsheirer.sample.Listener;
import io.github.dsheirer.sample.buffer.ReusableBuffer;
import io.github.dsheirer.sample.buffer.ReusableBufferAssembler;
import io.github.dsheirer.sample.decimator.RealDecimator;
import io.github.dsheirer.sample.real.RealBuffer;
import io.github.dsheirer.sample.real.RealSampleListener;
import io.github.dsheirer.sample.real.RealToRealBufferAssembler;
import org.apache.commons.lang3.Validate;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
@@ -34,381 +34,389 @@ import org.slf4j.LoggerFactory;
import java.util.ArrayList;
import java.util.List;
public class RealPrimeCICDecimate
public class RealPrimeCICDecimate
{
private final static Logger mLog =
LoggerFactory.getLogger( RealPrimeCICDecimate.class );
public static int[] PRIMES = { 2,3,5,7,11,13,17,19,23,29,31,37,41,43,47,53,
59,61,67,71,73,79,83,89,97,101,103,107,109,113,127,131,137,139,149,151,
157,163,167,173,179,181,191,193,197,199,211,223,227,229,233,239,241,251,
257,263,269,271,277,281,283,293,307,311,313,317,331,337,347,349,353,359,
367,373,379,383,389,397,401,409,419,421,431,433,439,443,449,457,461,463,
467,479,487,491,499,503,509,521,523,541,547,557,563,569,571,577,587,593,
599,601,607,613,617,619,631,641,643,647,653,659,661,673,677,683,691 };
private ArrayList<DecimatingStage> mDecimatingStages =
new ArrayList<DecimatingStage>();
private DecimatingStage mFirstDecimatingStage;
private final static Logger mLog = LoggerFactory.getLogger(RealPrimeCICDecimate.class);
private Output mOutput;
private int mOutputBufferSize = 2048;
public static int[] PRIMES = {2, 3, 5, 7, 11, 13, 17, 19, 23, 29, 31, 37, 41, 43, 47, 53,
59, 61, 67, 71, 73, 79, 83, 89, 97, 101, 103, 107, 109, 113, 127, 131, 137, 139, 149, 151,
157, 163, 167, 173, 179, 181, 191, 193, 197, 199, 211, 223, 227, 229, 233, 239, 241, 251,
257, 263, 269, 271, 277, 281, 283, 293, 307, 311, 313, 317, 331, 337, 347, 349, 353, 359,
367, 373, 379, 383, 389, 397, 401, 409, 419, 421, 431, 433, 439, 443, 449, 457, 461, 463,
467, 479, 487, 491, 499, 503, 509, 521, 523, 541, 547, 557, 563, 569, 571, 577, 587, 593,
599, 601, 607, 613, 617, 619, 631, 641, 643, 647, 653, 659, 661, 673, 677, 683, 691};
/**
* Non-Recursive Prime-Factor CIC Filter with float sample array inputs and
* decimated float sample output.
*
* Implements the CIC filter described in Understanding Digital Signal
* Processing, 3e, Lyons, on page 769. This filter is comprised of multiple
* decimating stages each with a prime factor decimation rate. Multiple
* stages are cascaded to achieve the overall decimation rate.
*
* This filter supports a maximum decimation rate of 700. This filter can
* be adapted to higher decimation rates by adding additional prime factors
* to the PRIMES array.
*
* @param decimation - overall decimation rate
* @param order - filter order
*/
public RealPrimeCICDecimate( int decimation, int order, int passFrequency,
int attenuation, WindowType windowType )
{
this( decimation, order, passFrequency, attenuation, windowType, 2048 );
}
public RealPrimeCICDecimate( int decimation, int order, int passFrequency,
int attenuation, WindowType windowType, int outputBufferSize )
{
Validate.isTrue(decimation <= 700);
private List<DecimatingStage> mDecimatingStages = new ArrayList<DecimatingStage>();
private DecimatingStage mFirstDecimatingStage;
private Output mOutput;
mOutputBufferSize = outputBufferSize;
private int mOutputBufferSize = 2048;
List<Integer> stageSizes = getPrimeFactors( decimation );
for( int x = 0; x < stageSizes.size(); x++ )
{
DecimatingStage stage = new DecimatingStage( stageSizes.get( x ), order );
mDecimatingStages.add( stage );
if( x == 0 )
{
/* Reference to first stage -- will receive all samples */
mFirstDecimatingStage = stage;
}
else
{
/* Wire the current stage to the previous stage */
mDecimatingStages.get( x - 1 ).setListener( stage );
}
}
mOutput = new Output( 48000, passFrequency, attenuation, windowType,
outputBufferSize );
mDecimatingStages.get( mDecimatingStages.size() - 1 )
.setListener( mOutput );
}
public void dispose()
{
for( DecimatingStage stage: mDecimatingStages )
{
stage.dispose();
}
mDecimatingStages.clear();
mDecimatingStages = null;
mFirstDecimatingStage = null;
mOutput.dispose();
mOutput = null;
}
/**
* Adds a listener to receive the output of this CIC decimation filter
*/
public void setListener( Listener<RealBuffer> listener )
{
mOutput.setListener( listener );
}
/**
* Removes listener from output of this CIC decimation filter
*/
public void removeListener()
{
mOutput.removeListener();
}
/**
* Calculates the prime factors of the decimation rate up to a maximum
* decimation rate of 700. If you wish to have decimation rates higher
* than 700, then add additional prime factors to the PRIMES array.
*
* @param decimation - integral decimation rate
* @return - ordered list (smallest to largest) of prime factors
*/
public static List<Integer> getPrimeFactors( int decimation )
{
ArrayList<Integer> stages = new ArrayList<Integer>();
int pointer = 0;
while( decimation > 0 && pointer < PRIMES.length )
{
int prime = PRIMES[ pointer ];
if( decimation % prime == 0 )
{
stages.add( prime );
decimation /= prime;
}
else
{
pointer++;
}
}
return stages;
}
/**
* Primary input method for receiving real buffers.
*/
public void receive( RealBuffer buffer )
/**
* Non-Recursive Prime-Factor CIC Filter with float sample array inputs and
* decimated float sample output.
*
* Implements the CIC filter described in Understanding Digital Signal
* Processing, 3e, Lyons, on page 769. This filter is comprised of multiple
* decimating stages each with a prime factor decimation rate. Multiple
* stages are cascaded to achieve the overall decimation rate.
*
* This filter supports a maximum decimation rate of 700. This filter can
* be adapted to higher decimation rates by adding additional prime factors
* to the PRIMES array.
*
* @param decimation - overall decimation rate
* @param order - filter order
*/
public RealPrimeCICDecimate(int decimation, int order, int passFrequency, int attenuation, WindowType windowType)
{
if( mFirstDecimatingStage != null )
{
for( float sample: buffer.getSamples() )
{
mFirstDecimatingStage.receive( sample );
}
}
this(decimation, order, passFrequency, attenuation, windowType, 2048);
}
/**
* Decimating stage combines multiple CIC stages with a decimator. The
* number of stages is indicated by the order value and the size indicates
* the decimation rate of this stage.
*/
public class DecimatingStage implements RealSampleListener
{
private ArrayList<Stage> mStages = new ArrayList<Stage>();
private Stage mFirstStage;
private RealDecimator mDecimator;
public DecimatingStage( int size, int order )
{
for( int x = 0; x < order; x++ )
{
Stage stage;
if( size == 2 )
{
stage = new TwoStage();
}
else
{
stage = new Stage( size );
}
mStages.add( stage );
if( x == 0 )
{
mFirstStage = stage;
}
else
{
mStages.get( x - 1 ).setListener( stage );
}
}
mDecimator = new RealDecimator( size );
mStages.get( mStages.size() - 1 ).setListener( mDecimator );
}
public void dispose()
{
for( Stage stage: mStages )
{
stage.dispose();
}
mStages.clear();
mDecimator.dispose();
mDecimator = null;
mFirstStage = null;
mStages = null;
}
@Override
public void receive( float sample )
public RealPrimeCICDecimate(int decimation, int order, int passFrequency, int attenuation, WindowType windowType, int outputBufferSize)
{
Validate.isTrue(decimation <= 700);
mOutputBufferSize = outputBufferSize;
List<Integer> stageSizes = getPrimeFactors(decimation);
for(int x = 0; x < stageSizes.size(); x++)
{
mFirstStage.receive( sample );
}
public void setListener( RealSampleListener listener )
{
mDecimator.setListener( listener );
}
}
/**
* Single non-decimating CIC stage component. Uses a circular buffer and a
* running average internally to implement the stage so that stage size has
* essentially no impact on the computational requirements of the stage
*/
public class Stage implements RealSampleListener
{
protected RealSampleListener mListener;
private float[] mSamples;
protected float mSum;
private int mSamplePointer = 0;
private int mSize;
protected float mGain;
public Stage()
{
}
DecimatingStage stage = new DecimatingStage(stageSizes.get(x), order);
public Stage( int size )
{
mSize = size - 1;
mSamples = new float[ mSize ];
mGain = 1.0f / (float)size;
}
public void dispose()
{
mListener = null;
}
public void receive( float sample )
{
/* Subtract the oldest sample and add back in the newest */
mSum = mSum - mSamples[ mSamplePointer ] + sample;
mDecimatingStages.add(stage);
/* Overwrite the oldest sample with the newest */
mSamples[ mSamplePointer ] = sample;
mSamplePointer++;
if( mSamplePointer >= mSize )
{
mSamplePointer = 0;
}
if( mListener != null )
{
mListener.receive( mSum * mGain );
}
}
public void setListener( RealSampleListener listener )
{
mListener = listener;
}
}
/**
* Size 2 stage that removes the unnecessary circular buffer management for
* a two-stage.
*/
public class TwoStage extends Stage
{
public TwoStage()
{
super();
mGain = 0.5f;
}
public void receive( float sample )
{
float sum = mSum + sample;
mSum = sample;
if( mListener != null )
{
mListener.receive( sum * mGain );
}
}
}
/**
* Output adapter - applies gain correction, applies cleanup filter, casts
* double-valued samples into float-valued complex sample and sends the
* result to the registered listener.
*/
public class Output implements RealSampleListener
{
/* Decimated output buffers will contain 1024 samples */
private RealToRealBufferAssembler mAssembler;
private RealFIRFilter_RB_RB mCleanupFilter;
private HalfBandFilter_RB_RB mHalfBandFilter = new HalfBandFilter_RB_RB(
Filters.FIR_HALF_BAND_31T_ONE_EIGHTH_FCO.getCoefficients(), 0.4f, false );
public Output( int outputSampleRate, int passFrequency, int attenuation,
WindowType windowType, int outputBufferSize )
{
mCleanupFilter = new RealFIRFilter_RB_RB( FilterFactory
.getCICCleanupFilter( outputSampleRate,
passFrequency,
attenuation,
windowType ), 0.4f );
mAssembler = new RealToRealBufferAssembler( outputBufferSize );
mAssembler.setListener( mCleanupFilter );
mCleanupFilter.setListener( mHalfBandFilter );
}
public void dispose()
{
mAssembler.dispose();
mCleanupFilter.dispose();
mHalfBandFilter.dispose();
}
/**
* Receiver method for the output adapter to receive a filtered,
* decimated sample, apply gain correction, apply cleanup filtering
* and output the sample values as a complex sample.
*/
@Override
public void receive( float sample )
{
mAssembler.receive( sample * 32.0f );
}
/**
* Adds a listener to receive output samples
*/
public void setListener( Listener<RealBuffer> listener )
{
mHalfBandFilter.setListener( listener );
if(x == 0)
{
/* Reference to first stage -- will receive all samples */
mFirstDecimatingStage = stage;
}
else
{
/* Wire the current stage to the previous stage */
mDecimatingStages.get(x - 1).setListener(stage);
}
}
/**
* Removes the listener from receiving output samples
*/
mOutput = new Output(48000, passFrequency, attenuation, windowType,
outputBufferSize);
mDecimatingStages.get(mDecimatingStages.size() - 1)
.setListener(mOutput);
}
public void dispose()
{
for(DecimatingStage stage : mDecimatingStages)
{
stage.dispose();
}
mDecimatingStages.clear();
mDecimatingStages = null;
mFirstDecimatingStage = null;
mOutput.dispose();
mOutput = null;
}
/**
* Adds a listener to receive the output of this CIC decimation filter
*/
public void setListener(Listener<ReusableBuffer> listener)
{
mOutput.setListener(listener);
}
/**
* Removes listener from output of this CIC decimation filter
*/
public void removeListener()
{
mOutput.removeListener();
}
/**
* Calculates the prime factors of the decimation rate up to a maximum
* decimation rate of 700. If you wish to have decimation rates higher
* than 700, then add additional prime factors to the PRIMES array.
*
* @param decimation - integral decimation rate
* @return - ordered list (smallest to largest) of prime factors
*/
public static List<Integer> getPrimeFactors(int decimation)
{
ArrayList<Integer> stages = new ArrayList<Integer>();
int pointer = 0;
while(decimation > 0 && pointer < PRIMES.length)
{
int prime = PRIMES[pointer];
if(decimation % prime == 0)
{
stages.add(prime);
decimation /= prime;
}
else
{
pointer++;
}
}
return stages;
}
/**
* Primary input method for receiving real buffers.
*/
public void receive(ReusableBuffer buffer)
{
if(mFirstDecimatingStage != null)
{
for(float sample : buffer.getSamples())
{
mFirstDecimatingStage.receive(sample);
}
}
buffer.decrementUserCount();
}
/**
* Decimating stage combines multiple CIC stages with a decimator. The
* number of stages is indicated by the order value and the size indicates
* the decimation rate of this stage.
*/
public class DecimatingStage implements RealSampleListener
{
private ArrayList<Stage> mStages = new ArrayList<Stage>();
private Stage mFirstStage;
private RealDecimator mDecimator;
public DecimatingStage(int size, int order)
{
for(int x = 0; x < order; x++)
{
Stage stage;
if(size == 2)
{
stage = new TwoStage();
}
else
{
stage = new Stage(size);
}
mStages.add(stage);
if(x == 0)
{
mFirstStage = stage;
}
else
{
mStages.get(x - 1).setListener(stage);
}
}
mDecimator = new RealDecimator(size);
mStages.get(mStages.size() - 1).setListener(mDecimator);
}
public void dispose()
{
for(Stage stage : mStages)
{
stage.dispose();
}
mStages.clear();
mDecimator.dispose();
mDecimator = null;
mFirstStage = null;
mStages = null;
}
@Override
public void receive(float sample)
{
mFirstStage.receive(sample);
}
public void setListener(RealSampleListener listener)
{
mDecimator.setListener(listener);
}
}
/**
* Single non-decimating CIC stage component. Uses a circular buffer and a
* running average internally to implement the stage so that stage size has
* essentially no impact on the computational requirements of the stage
*/
public class Stage implements RealSampleListener
{
protected RealSampleListener mListener;
private float[] mSamples;
protected float mSum;
private int mSamplePointer = 0;
private int mSize;
protected float mGain;
public Stage()
{
}
public Stage(int size)
{
mSize = size - 1;
mSamples = new float[mSize];
mGain = 1.0f / (float)size;
}
public void dispose()
{
mListener = null;
}
public void receive(float sample)
{
/* Subtract the oldest sample and add back in the newest */
mSum = mSum - mSamples[mSamplePointer] + sample;
/* Overwrite the oldest sample with the newest */
mSamples[mSamplePointer] = sample;
mSamplePointer++;
if(mSamplePointer >= mSize)
{
mSamplePointer = 0;
}
if(mListener != null)
{
mListener.receive(mSum * mGain);
}
}
public void setListener(RealSampleListener listener)
{
mListener = listener;
}
}
/**
* Size 2 stage that removes the unnecessary circular buffer management for
* a two-stage.
*/
public class TwoStage extends Stage
{
public TwoStage()
{
super();
mGain = 0.5f;
}
public void receive(float sample)
{
float sum = mSum + sample;
mSum = sample;
if(mListener != null)
{
mListener.receive(sum * mGain);
}
}
}
/**
* Output adapter - applies gain correction, applies cleanup filter, casts
* double-valued samples into float-valued complex sample and sends the
* result to the registered listener.
*/
public class Output implements RealSampleListener
{
/* Decimated output buffers will contain 1024 samples */
private ReusableBufferAssembler mAssembler;
private Listener<ReusableBuffer> mReusableBufferListener;
private RealFIRFilter2 mCleanupFilter;
private HalfBandFilter mHalfBandFilter = new HalfBandFilter(
Filters.FIR_HALF_BAND_31T_ONE_EIGHTH_FCO.getCoefficients(), 0.4f, false);
public Output(int outputSampleRate, int passFrequency, int attenuation, WindowType windowType, int outputBufferSize)
{
mCleanupFilter = new RealFIRFilter2(FilterFactory.getCICCleanupFilter(outputSampleRate, passFrequency,
attenuation, windowType), 0.4f);
mAssembler = new ReusableBufferAssembler(outputBufferSize);
mAssembler.setListener(new Listener<ReusableBuffer>()
{
@Override
public void receive(ReusableBuffer reusableBuffer)
{
if(mReusableBufferListener != null)
{
ReusableBuffer cleanupFiltered = mCleanupFilter.filter(reusableBuffer);
ReusableBuffer halfBandFiltered = mHalfBandFilter.filter(cleanupFiltered);
mReusableBufferListener.receive(halfBandFiltered);
}
else
{
reusableBuffer.decrementUserCount();
}
}
});
}
public void dispose()
{
mAssembler.dispose();
mCleanupFilter.dispose();
mHalfBandFilter.dispose();
}
/**
* Receiver method for the output adapter to receive a filtered,
* decimated sample, apply gain correction, apply cleanup filtering
* and output the sample values as a complex sample.
*/
@Override
public void receive(float sample)
{
mAssembler.receive(sample * 32.0f);
}
/**
* Adds a listener to receive output samples
*/
public void setListener(Listener<ReusableBuffer> listener)
{
mReusableBufferListener = listener;
}
/**
* Removes the listener from receiving output samples
*/
public void removeListener()
{
mHalfBandFilter.removeListener();
mReusableBufferListener = null;
}
}
}
}

View File

@@ -0,0 +1,58 @@
/*******************************************************************************
* sdrtrunk
* Copyright (C) 2014-2017 Dennis Sheirer
*
* This program is free software: you can redistribute it and/or modify
* it under the terms of the GNU General Public License as published by
* the Free Software Foundation, either version 3 of the License, or
* (at your option) any later version.
*
* This program is distributed in the hope that it will be useful,
* but WITHOUT ANY WARRANTY; without even the implied warranty of
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
* GNU General Public License for more details.
*
* You should have received a copy of the GNU General Public License
* along with this program. If not, see <http://www.gnu.org/licenses/>
*
******************************************************************************/
package io.github.dsheirer.dsp.filter.correction;
import io.github.dsheirer.sample.buffer.ReusableComplexBuffer;
import io.github.dsheirer.sample.buffer.ReusableComplexBufferQueue;
public class IQCorrectionFilter
{
private float mRatio = 0.00001f;
private float mAverageInphase = 0.0f;
private float mAverageQuadrature = 0.0f;
private ReusableComplexBufferQueue mReusableComplexBufferQueue = new ReusableComplexBufferQueue("IQCorrectionFilter");
public IQCorrectionFilter(float ratio)
{
mRatio = ratio;
}
public IQCorrectionFilter()
{
}
public ReusableComplexBuffer filter(ReusableComplexBuffer buffer)
{
float[] samples = buffer.getSamples();
ReusableComplexBuffer filtered = mReusableComplexBufferQueue.getBuffer(samples.length);
float[] filteredSamples = filtered.getSamples();
for(int x = 0; x < samples.length; x += 2)
{
mAverageInphase = mAverageInphase + (mRatio * (samples[x] - mAverageInphase));
filteredSamples[x] = samples[x] - mAverageInphase;
mAverageQuadrature = mAverageQuadrature + (mRatio * (samples[x + 1] - mAverageQuadrature));
filteredSamples[x + 1] = samples[x + 1] - mAverageQuadrature;
}
return filtered;
}
}

View File

@@ -0,0 +1,98 @@
/*******************************************************************************
* sdr-trunk
* Copyright (C) 2014-2018 Dennis Sheirer
*
* This program is free software: you can redistribute it and/or modify it under the terms of the GNU General Public
* License as published by the Free Software Foundation, either version 3 of the License, or (at your option) any
* later version.
*
* This program is distributed in the hope that it will be useful, but WITHOUT ANY WARRANTY; without even the implied
* warranty of MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU General Public License for more details.
*
* You should have received a copy of the GNU General Public License along with this program.
* If not, see <http://www.gnu.org/licenses/>
*
******************************************************************************/
package io.github.dsheirer.dsp.filter.dc;
import io.github.dsheirer.sample.buffer.ReusableBuffer;
import io.github.dsheirer.sample.buffer.ReusableBufferQueue;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
public class BufferDCFilter
{
private final static Logger mLog = LoggerFactory.getLogger(BufferDCFilter.class);
private ReusableBufferQueue mReusableBufferQueue = new ReusableBufferQueue("Buffer DC Filter");
private double mAccumulator;
/**
* Calculates the DC offset present in the samples and subtracts the offset from all samples.
*/
public void filterReal(float[] samples)
{
float adjustment = getOffset(samples);
for(int x = 0; x < samples.length; x++)
{
samples[x] = samples[x] - adjustment;
}
}
private float getOffset(float[] samples)
{
mAccumulator = 0.0;
for(int x = 0; x < samples.length; x++)
{
mAccumulator += samples[x];
}
return (float)(mAccumulator / (double)samples.length);
}
/**
* Calculates the DC offset present in the samples and subtracts the offset from all samples.
*/
public static void filterComplex(float[] samples)
{
double iAccumulator = 0.0;
double qAccumulator = 0.0;
for(int x = 0; x < samples.length; x += 2)
{
iAccumulator += samples[x];
qAccumulator += samples[x + 1];
}
double half = (double)samples.length / 2.0;
float iAdjustment = (float)(iAccumulator / half);
float qAdjustment = (float)(qAccumulator / half);
for(int x = 0; x < samples.length; x += 2)
{
samples[x] = samples[x] - iAdjustment;
samples[x + 1] = samples[x + 1] - qAdjustment;
}
}
public ReusableBuffer filter(ReusableBuffer unfilteredBuffer)
{
float[] unfilteredSamples = unfilteredBuffer.getSamples();
ReusableBuffer filteredBuffer = mReusableBufferQueue.getBuffer(unfilteredSamples.length);
float[] filteredSamples = filteredBuffer.getSamples();
float offset = getOffset(unfilteredSamples);
for(int x = 0; x < unfilteredSamples.length; x++)
{
filteredSamples[x] = unfilteredSamples[x] - offset;
}
unfilteredBuffer.decrementUserCount();
return filteredBuffer;
}
}

View File

@@ -1,41 +1,96 @@
/*******************************************************************************
* SDR Trunk
* Copyright (C) 2014,2015 Dennis Sheirer
*
* This program is free software: you can redistribute it and/or modify
* it under the terms of the GNU General Public License as published by
* the Free Software Foundation, either version 3 of the License, or
* (at your option) any later version.
*
* This program is distributed in the hope that it will be useful,
* but WITHOUT ANY WARRANTY; without even the implied warranty of
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
* GNU General Public License for more details.
*
* You should have received a copy of the GNU General Public License
* along with this program. If not, see <http://www.gnu.org/licenses/>
* 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.filter.dc;
import io.github.dsheirer.sample.buffer.ReusableBuffer;
import io.github.dsheirer.sample.buffer.ReusableBufferQueue;
/**
* DC removal filter for removing any DC offset that may be present when a signal is not aligned within the
* demodulated channel.
*/
public class DCRemovalFilter
{
protected float mAverage;
protected float mRatio;
private ReusableBufferQueue mReusableBufferQueue = new ReusableBufferQueue("DC Removal Filter");
protected float mAverage;
protected float mRatio;
public DCRemovalFilter( float ratio )
{
mRatio = ratio;
}
/**
* Creates a DC removal filter.
*
* @param ratio
*/
public DCRemovalFilter(float ratio)
{
mRatio = ratio;
}
public void reset()
{
mAverage = 0.0f;
}
/**
* Resets the running average to zero.
*/
public void reset()
{
mAverage = 0.0f;
}
public float filter( float sample )
{
float filtered = sample - mAverage;
mAverage += mRatio * filtered;
return filtered;
}
/**
* Filters the sample and returns a filtered (ie DC-removed) sample.
* @param sample to filter
* @return filtered sample
*/
public float filter(float sample)
{
float filtered = sample - mAverage;
mAverage += mRatio * filtered;
return filtered;
}
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.
*
* Note: this method automatically manages the user accounting between the buffer argument and the new returned
* filtered sample buffer.
*
* @param unfilteredBuffer containing samples to filter
* @return a new reusable buffer containing the filtered samples with the user count set to 1.
*/
public ReusableBuffer filter(ReusableBuffer unfilteredBuffer)
{
ReusableBuffer filteredBuffer = mReusableBufferQueue.getBuffer(unfilteredBuffer.getSampleCount());
float[] unfilteredSamples = unfilteredBuffer.getSamples();
float[] filteredSamples = filteredBuffer.getSamples();
for(int x = 0; x < unfilteredSamples.length; x++)
{
filteredSamples[x] = filter(unfilteredSamples[x]);
}
unfilteredBuffer.decrementUserCount();
return filteredBuffer;
}
}

View File

@@ -1,71 +0,0 @@
/*******************************************************************************
* SDR Trunk
* Copyright (C) 2014 Dennis Sheirer
*
* This program is free software: you can redistribute it and/or modify
* it under the terms of the GNU General Public License as published by
* the Free Software Foundation, either version 3 of the License, or
* (at your option) any later version.
*
* This program is distributed in the hope that it will be useful,
* but WITHOUT ANY WARRANTY; without even the implied warranty of
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
* GNU General Public License for more details.
*
* You should have received a copy of the GNU General Public License
* along with this program. If not, see <http://www.gnu.org/licenses/>
******************************************************************************/
package io.github.dsheirer.dsp.filter.dc;
import io.github.dsheirer.sample.Listener;
import io.github.dsheirer.sample.complex.ComplexBuffer;
public class DCRemovalFilter_CB implements Listener<ComplexBuffer>
{
private Listener<ComplexBuffer> mListener;
private DCRemovalFilter mIFilter;
private DCRemovalFilter mQFilter;
public DCRemovalFilter_CB( float ratio )
{
mIFilter = new DCRemovalFilter( ratio );
mQFilter = new DCRemovalFilter( ratio );
}
public void dispose()
{
mListener = null;
}
public float[] filter( float[] samples )
{
for( int x = 0; x < samples.length; x += 2 )
{
samples[ x ] = mIFilter.filter( samples[ x ] );
samples[ x + 1 ] = mQFilter.filter( samples[ x + 1 ] );
}
return samples;
}
public ComplexBuffer filter( ComplexBuffer buffer )
{
filter( buffer.getSamples() );
return buffer;
}
@Override
public void receive( ComplexBuffer buffer )
{
if( mListener != null )
{
mListener.receive( filter( buffer ) );
}
}
public void setListener( Listener<ComplexBuffer> listener )
{
mListener = listener;
}
}

View File

@@ -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 ) );
}
}
}

View File

@@ -1,54 +0,0 @@
/*******************************************************************************
* SDR Trunk
* Copyright (C) 2014 Dennis Sheirer
*
* This program is free software: you can redistribute it and/or modify
* it under the terms of the GNU General Public License as published by
* the Free Software Foundation, either version 3 of the License, or
* (at your option) any later version.
*
* This program is distributed in the hope that it will be useful,
* but WITHOUT ANY WARRANTY; without even the implied warranty of
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
* GNU General Public License for more details.
*
* You should have received a copy of the GNU General Public License
* along with this program. If not, see <http://www.gnu.org/licenses/>
******************************************************************************/
package io.github.dsheirer.dsp.filter.dc;
import io.github.dsheirer.sample.real.RealSampleListener;
public class DCRemovalFilter_R_R extends DCRemovalFilter implements RealSampleListener
{
RealSampleListener mListener;
public DCRemovalFilter_R_R( float ratio )
{
super( ratio );
}
public void dispose()
{
mListener = null;
}
@Override
public void receive( float sample )
{
if( mListener != null )
{
mListener.receive( filter( sample ) );
}
}
public void setListener( RealSampleListener listener )
{
mListener = listener;
}
public void removeListener()
{
mListener = null;
}
}

View File

@@ -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;
}
}

View File

@@ -1,45 +1,105 @@
/*******************************************************************************
* 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.filter.dc;
import io.github.dsheirer.sample.buffer.ReusableBuffer;
import io.github.dsheirer.sample.buffer.ReusableBufferQueue;
import io.github.dsheirer.sample.real.RealSampleListener;
public class IIRSinglePoleDCRemovalFilter implements RealSampleListener
{
private float mAlpha;
private float mPreviousInput = 0.0f;
private float mPreviousOutput = 0.0f;
private ReusableBufferQueue mReusableBufferQueue = new ReusableBufferQueue("IIR DC Filter");
private float mAlpha;
private float mPreviousInput = 0.0f;
private float mPreviousOutput = 0.0f;
private float mCurrentOutput = 0.0f;
private RealSampleListener mListener;
/**
* IIR single-pole DC removal filter, as described by J M de Freitas in
* 29Jan2007 paper at:
*
* http://www.mathworks.com/matlabcentral/fileexchange/downloads/72134/download
*
* @param alpha 0.0 - 1.0 float - the closer alpha is to unity, the closer
* the cutoff frequency is to DC.
*/
public IIRSinglePoleDCRemovalFilter( float alpha )
{
mAlpha = alpha;
}
@Override
public void receive( float currentInput )
private RealSampleListener mListener;
/**
* IIR single-pole DC removal filter, as described by J M de Freitas in
* 29Jan2007 paper at:
*
* http://www.mathworks.com/matlabcentral/fileexchange/downloads/72134/download
*
* @param alpha 0.0 - 1.0 float - the closer alpha is to unity, the closer
* the cutoff frequency is to DC.
*/
public IIRSinglePoleDCRemovalFilter(float alpha)
{
float currentOutput = ( currentInput - mPreviousInput ) +
( mAlpha * mPreviousOutput );
if( mListener != null )
{
mListener.receive( currentOutput );
}
mPreviousInput = currentInput;
mPreviousOutput = currentOutput;
mAlpha = alpha;
}
public void setListener( RealSampleListener listener )
{
mListener = listener;
}
/**
* Filters the sample
* @param sample to filter
* @return filtered output sample
*/
public float filter(float sample)
{
mCurrentOutput = (sample - mPreviousInput) + (mAlpha * mPreviousOutput);
mPreviousInput = sample;
mPreviousOutput = mCurrentOutput;
return mCurrentOutput;
}
/**
* Filters the input buffer and returns a new filtered output buffer.
*
* Buffer user accounting is handled by this method where the input buffer user count is decremented and the
* returned output buffer has the user count incremented to one.
*
* @param inputBuffer to filter
* @return filtered samples in a new (ie reused) output buffer
*/
public ReusableBuffer filter(ReusableBuffer inputBuffer)
{
ReusableBuffer outputBuffer = mReusableBufferQueue.getBuffer(inputBuffer.getSampleCount());
float[] inputSamples = inputBuffer.getSamples();
float[] outputSamples = outputBuffer.getSamples();
for(int x = 0; x < inputSamples.length; x++)
{
outputSamples[x] = filter(inputSamples[x]);
}
inputBuffer.decrementUserCount();
return outputBuffer;
}
/**
* Implements the RealSampleListener interface for single sample streams.
*/
@Override
public void receive(float sample)
{
if(mListener != null)
{
mListener.receive(filter(sample));
}
}
/**
* Assigns the listener to receive filtered samples processed by the RealSampleListener interface
*/
public void setListener(RealSampleListener listener)
{
mListener = listener;
}
}

View File

@@ -1,69 +0,0 @@
package io.github.dsheirer.dsp.filter.dc;
import io.github.dsheirer.sample.Listener;
import io.github.dsheirer.sample.real.RealBuffer;
public class IIRSinglePoleDCRemovalFilter_RB implements Listener<RealBuffer>
{
private float mAlpha;
private float mPreviousInput = 0.0f;
private float mPreviousOutput = 0.0f;
private Listener<RealBuffer> mListener;
/**
* IIR single-pole DC removal filter, as described by J M de Freitas in
* 29Jan2007 paper at:
*
* http://www.mathworks.com/matlabcentral/fileexchange/downloads/72134/download
*
* @param alpha 0.0 - 1.0 float - the closer alpha is to unity, the closer
* the cutoff frequency is to DC.
*/
public IIRSinglePoleDCRemovalFilter_RB( float alpha )
{
mAlpha = alpha;
}
@Override
public void receive( RealBuffer buffer )
{
if( mListener != null )
{
mListener.receive( filter( buffer ) );
}
}
public RealBuffer filter( RealBuffer buffer )
{
float[] samples = buffer.getSamples();
for( int x = 0; x < samples.length; x++ )
{
samples[ x ] = filter( samples[ x ] );
}
return buffer;
}
public float filter( float sample )
{
float currentOutput = ( sample - mPreviousInput ) +
( mAlpha * mPreviousOutput );
mPreviousInput = sample;
mPreviousOutput = currentOutput;
return currentOutput;
}
public void reset()
{
mPreviousInput = 0.0f;
mPreviousOutput = 0.0f;
}
public void setListener( Listener<RealBuffer> listener )
{
mListener = listener;
}
}

View File

@@ -1,3 +1,18 @@
/*******************************************************************************
* sdr-trunk
* Copyright (C) 2014-2018 Dennis Sheirer
*
* This program is free software: you can redistribute it and/or modify it under the terms of the GNU General Public
* License as published by the Free Software Foundation, either version 3 of the License, or (at your option) any
* later version.
*
* This program is distributed in the hope that it will be useful, but WITHOUT ANY WARRANTY; without even the implied
* warranty of MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU General Public License for more details.
*
* You should have received a copy of the GNU General Public License along with this program.
* If not, see <http://www.gnu.org/licenses/>
*
******************************************************************************/
package io.github.dsheirer.dsp.filter.design;
import io.github.dsheirer.gui.control.CurveFittedAreaChart;
@@ -58,16 +73,6 @@ public class FilterView extends BorderPane
mImpulseChart.setAlternativeColumnFillVisible(false);
mImpulseChart.setAlternativeRowFillVisible(false);
URL cssURL = FilterView.class.getResource("FilterView.css");
String css = null;
if(cssURL != null)
{
css = cssURL.toExternalForm();
}
hbox.getChildren().add(mImpulseChart);
HBox.setHgrow(mImpulseChart, Priority.ALWAYS);
@@ -81,8 +86,12 @@ public class FilterView extends BorderPane
mDFTChart.setAlternativeColumnFillVisible(false);
mDFTChart.setAlternativeRowFillVisible(false);
if(css != null)
URL url = getClass().getResource("/../resources/FilterView.css");
if(url != null)
{
String css = url.toExternalForm();
mImpulseChart.getStylesheets().add(css);
mDFTChart.getStylesheets().add(css);
}

View File

@@ -40,52 +40,48 @@ public class FilterViewer extends Application
private float[] getFilter()
{
// FIRFilterSpecification specification = FIRFilterSpecification.lowPassBuilder()
// .sampleRate(48000)
// .sampleRate(8000)
// .gridDensity(16)
// .passBandCutoff(2500)
// .oddLength(true)
// .passBandCutoff(300)
// .passBandAmplitude(1.0)
// .passBandRipple(0.01)
// .stopBandStart(4000)
// .stopBandStart(500)
// .stopBandAmplitude(0.0)
// .stopBandRipple(0.008)
// .stopBandRipple(0.03) //Approximately 60 dB attenuation
// .build();
FIRFilterSpecification specification = FIRFilterSpecification.lowPassBuilder()
.sampleRate(48000)
.passBandCutoff(5100)
.passBandAmplitude(1.0)
.passBandRipple(0.01)
.stopBandAmplitude(0.0)
.stopBandStart(6500)
.stopBandRipple(0.01)
.build();
//
// FIRFilterSpecification specification = FIRFilterSpecification.highPassBuilder()
// .sampleRate(24000)
// .stopBandCutoff(800)
// .sampleRate(8000)
// .stopBandCutoff(200)
// .stopBandAmplitude(0.0)
// .stopBandRipple(0.03)
// .passBandStart(1000)
// .stopBandRipple(0.025)
// .passBandStart(300)
// .passBandAmplitude(1.0)
// .passBandRipple(0.08)
// .passBandRipple(0.01)
// .build();
// FIRFilterSpecification specification = FIRFilterSpecification.bandPassBuilder()
// .sampleRate( 48000 )
// .stopFrequency1( 200 )
// .passFrequencyBegin( 400 )
// .passFrequencyEnd( 3200 )
// .stopFrequency2( 3400 )
// .stopRipple( 0.0003 )
// .passRipple( 0.008)
// .build();
FIRFilterSpecification specification = FIRFilterSpecification.bandPassBuilder()
.sampleRate(8000)
.stopFrequency1(1000)
.passFrequencyBegin(1100)
.passFrequencyEnd(1900)
.stopFrequency2(2000)
.stopRipple(0.000001)
.passRipple(0.00001)
.build();
float[] taps = null;
// taps = FilterFactory.getSinc(6000, 1920, 47, Window.WindowType.BLACKMAN);
// taps = FilterFactory.getRootRaisedCosine(10, 10, 0.2f);
// taps = FilterFactory.getLowPass(48000, 6750, 7500, 60,
// Window.WindowType.HAMMING, true);
// try
// {
// taps = FilterFactory.getSincM2Synthesizer(12500.0, 2, 19);
// taps = FilterFactory.getSincFilter(25000.0, 12500.0, 2, 19, Window.WindowType.BLACKMAN_HARRIS_7, true);
// }
// catch(FilterDesignException fde)
// {
// mLog.error("Error");
// }
try
{
@@ -101,6 +97,12 @@ public class FilterViewer extends Application
mLog.error("Filter design error", fde);
}
if(taps == null)
{
throw new IllegalStateException("Couldn't design filter");
}
return taps;
}
}

View File

@@ -1,90 +0,0 @@
/*******************************************************************************
* sdr-trunk
* Copyright (C) 2014-2018 Dennis Sheirer
*
* This program is free software: you can redistribute it and/or modify it under the terms of the GNU General Public
* License as published by the Free Software Foundation, either version 3 of the License, or (at your option) any
* later version.
*
* This program is distributed in the hope that it will be useful, but WITHOUT ANY WARRANTY; without even the implied
* warranty of MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU General Public License for more details.
*
* You should have received a copy of the GNU General Public License along with this program.
* If not, see <http://www.gnu.org/licenses/>
*
******************************************************************************/
package io.github.dsheirer.dsp.filter.fir.complex;
import io.github.dsheirer.dsp.filter.fir.FIRFilter;
import io.github.dsheirer.dsp.filter.fir.real.RealFIRFilter;
import io.github.dsheirer.sample.complex.Complex;
import io.github.dsheirer.sample.complex.ComplexBuffer;
public class ComplexFIRFilter extends FIRFilter
{
private RealFIRFilter mIFilter;
private RealFIRFilter mQFilter;
/**
* Complex FIR Filter for processing complex sample pairs. Wraps two real
* FIR filters for processing each of the inphase and quadrature samples.
*
* @param coefficients - filter taps
* @param gain - gain to apply to filtered outputs - use 1.0f for no gain
*/
public ComplexFIRFilter(float[] coefficients, float gain)
{
mIFilter = new RealFIRFilter(coefficients, gain);
mQFilter = new RealFIRFilter(coefficients, gain);
}
public float[] getCoefficients()
{
return mIFilter.getCoefficients();
}
public float filterInphase(float sample)
{
return mIFilter.filter(sample);
}
public float filterQuadrature(float sample)
{
return mQFilter.filter(sample);
}
public Complex filter(Complex sample)
{
float i = filterInphase(sample.inphase());
float q = filterQuadrature(sample.quadrature());
return new Complex(i, q);
}
/**
* Filters the complex samples from the buffer and returns a new complex buffer with the filtered output
*
* @param buffer with complex samples to filter
* @return new buffer containing filtered complex samples
*/
public ComplexBuffer filter(ComplexBuffer buffer)
{
float[] samples = buffer.getSamples();
float[] filtered = new float[samples.length];
for(int x = 0; x < samples.length; x += 2)
{
filtered[x] = filterInphase(samples[x]);
filtered[x + 1] = filterQuadrature(samples[x + 1]);
}
return new ComplexBuffer(filtered);
}
@Override
public void dispose()
{
mIFilter.dispose();
mQFilter.dispose();
}
}

View File

@@ -0,0 +1,138 @@
/*******************************************************************************
* 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.filter.fir.complex;
import io.github.dsheirer.dsp.filter.fir.FIRFilter;
import io.github.dsheirer.dsp.filter.fir.real.RealFIRFilter2;
import io.github.dsheirer.sample.buffer.ReusableComplexBuffer;
import io.github.dsheirer.sample.buffer.ReusableComplexBufferQueue;
import io.github.dsheirer.sample.complex.Complex;
public class ComplexFIRFilter2 extends FIRFilter
{
private ReusableComplexBufferQueue mReusableComplexBufferQueue = new ReusableComplexBufferQueue("Complex FIR Filter");
private RealFIRFilter2 mIFilter;
private RealFIRFilter2 mQFilter;
/**
* Complex FIR Filter for processing complex sample pairs that internally uses two RealFIRFilter
* instances.
*
* @param coefficients - filter taps
* @param gain - gain to apply to filtered outputs - use 1.0f for no gain
*/
public ComplexFIRFilter2(float[] coefficients, float gain)
{
mIFilter = new RealFIRFilter2(coefficients, gain);
mQFilter = new RealFIRFilter2(coefficients, gain);
}
/**
* Complex FIR Filter for processing complex sample pairs that internally uses two RealFIRFilter
* instances. This constructor uses a default gain of 1.0f.
*
* @param coefficients - filter taps
*/
public ComplexFIRFilter2(float[] coefficients)
{
this(coefficients, 1.0f);
}
/**
* Filters the inphase sample value.
* @param sample to filter
* @return filtered sample
*/
public float filterInphase(float sample)
{
return mIFilter.filter(sample);
}
/**
* Current filtered inphase sample value after invoking the filterInphase() method.
*/
public float currentInphaseValue()
{
return mIFilter.currentValue();
}
/**
* Filters the quadrature sample value.
* @param sample to filter
* @return filtered sample
*/
public float filterQuadrature(float sample)
{
return mQFilter.filter(sample);
}
/**
* Current filtered quadrature sample value after invoking the filterInphase() method.
*/
public float currentQuadratureValue()
{
return mQFilter.currentValue();
}
/**
* Filters the complex sample.
* @param sample to filter
* @return filtered sample
*/
public Complex filter(Complex sample)
{
float i = filterInphase(sample.inphase());
float q = filterQuadrature(sample.quadrature());
return new Complex(i, q);
}
/**
* Filters the complex samples from the reusable buffer and returns a new complex buffer with the filtered output
*
* Note: the original reusable buffer user count is decremented and a new reusable buffer is returned
* with the user count already incremented to one.
*
* @param originalBuffer with complex samples to filter
* @return new buffer containing filtered complex samples
*/
public ReusableComplexBuffer filter(ReusableComplexBuffer originalBuffer)
{
ReusableComplexBuffer filteredBuffer = mReusableComplexBufferQueue.getBuffer(originalBuffer.getSamples().length);
filteredBuffer.setTimestamp(originalBuffer.getTimestamp());
float[] samples = originalBuffer.getSamples();
float[] filteredSamples = filteredBuffer.getSamples();
for(int x = 0; x < originalBuffer.getSamples().length; x += 2)
{
filteredSamples[x] = filterInphase(samples[x]);
filteredSamples[x + 1] = filterQuadrature(samples[x + 1]);
}
originalBuffer.decrementUserCount();
return filteredBuffer;
}
@Override
public void dispose()
{
mIFilter.dispose();
mQFilter.dispose();
mReusableComplexBufferQueue.dispose();
}
}

View File

@@ -1,53 +0,0 @@
package io.github.dsheirer.dsp.filter.fir.complex;
import io.github.dsheirer.sample.Listener;
import io.github.dsheirer.sample.complex.ComplexBuffer;
public class ComplexFIRFilter_CB_CB extends ComplexFIRFilter implements Listener<ComplexBuffer>
{
private Listener<ComplexBuffer> mListener;
/**
* Complex FIR Filter for processing complex buffers. Wraps two real FIR
* filters for processing each of the inphase and quadrature samples.
*
* @param coefficients - odd length symmetric filter taps
* @param gain - gain value to apply to each of the filtered samples
*/
public ComplexFIRFilter_CB_CB( float[] coefficients, float gain )
{
super( coefficients, gain );
}
public void dispose()
{
mListener = null;
}
@Override
public void receive( ComplexBuffer buffer )
{
if( mListener != null )
{
float[] samples = buffer.getSamples();
for( int x = 0; x < samples.length; x += 2 )
{
samples[ x ] = filterInphase( samples[ x ] );
samples[ x + 1 ] = filterQuadrature( samples[ x + 1 ] );
}
mListener.receive( buffer );
}
}
public void setListener( Listener<ComplexBuffer> listener )
{
mListener = listener;
}
public void removeListener()
{
mListener = null;
}
}

View File

@@ -1,105 +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.dsp.filter.fir.real;
import io.github.dsheirer.dsp.filter.fir.FIRFilter;
public class RealFIRFilter extends FIRFilter
{
private float[] mBuffer;
private int mBufferSize = 1;
private int mBufferPointer = 0;
private int[][] mIndexMap;
private float[] mCoefficients;
private float mGain;
/**
* Float sample FIR filter base class.
*
* @param coefficients - filter coefficients
* @param gain value to apply to the filtered output - use 1.0f for no gain
*/
public RealFIRFilter( float[] coefficients, float gain )
{
mCoefficients = coefficients;
mGain = gain;
mBufferSize = mCoefficients.length;
mBuffer = new float[ mBufferSize ];
mBufferPointer = mBufferSize - 1;
generateIndexMap( mBufferSize );
}
public float[] getCoefficients()
{
return mCoefficients;
}
@Override
public void dispose()
{
mCoefficients = null;
mIndexMap = null;
mBuffer = null;
}
public float filter( float sample )
{
mBuffer[ mBufferPointer ] = sample;
float accumulator = 0.0f;
for( int x = 0; x < mBufferSize; x++ )
{
accumulator += mCoefficients[ x ] *
mBuffer[ mIndexMap[ mBufferPointer][ x ] ];
}
mBufferPointer--;
if( mBufferPointer < 0 )
{
mBufferPointer += mBufferSize;
}
/* Apply gain and return the filtered value */
return accumulator * mGain;
}
/**
* Generates a circular buffer index map to support lookup of the translated
* index based on the current buffer pointer and the desired sample index.
*/
private void generateIndexMap( int size )
{
mIndexMap = new int[ size ][ size ];
for( int x = 0; x < size; x++ )
{
for( int y = 0; y < size; y++ )
{
int z = x + y;
mIndexMap[ x ][ y ] = z < size ? z : z - size;
}
}
}
}

View File

@@ -0,0 +1,132 @@
/*******************************************************************************
* sdr-trunk
* Copyright (C) 2014-2018 Dennis Sheirer
*
* This program is free software: you can redistribute it and/or modify it under the terms of the GNU General Public
* License as published by the Free Software Foundation, either version 3 of the License, or (at your option) any
* later version.
*
* This program is distributed in the hope that it will be useful, but WITHOUT ANY WARRANTY; without even the implied
* warranty of MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU General Public License for more details.
*
* You should have received a copy of the GNU General Public License along with this program.
* If not, see <http://www.gnu.org/licenses/>
*
******************************************************************************/
package io.github.dsheirer.dsp.filter.fir.real;
import io.github.dsheirer.dsp.filter.fir.FIRFilter;
import io.github.dsheirer.sample.buffer.ReusableBuffer;
import io.github.dsheirer.sample.buffer.ReusableBufferQueue;
/**
* Finite Impulse Response (FIR) filter for filtering individual float samples or float sample arrays.
*
* Note: filtering operations in this class are structured to leverage SIMD processor intrinsics when
* available to the Java runtime.
*/
public class RealFIRFilter2 extends FIRFilter
{
private ReusableBufferQueue mReusableBufferQueue = new ReusableBufferQueue("RealFIRFilter2");
private float[] mData;
private float[] mCoefficients;
private float mGain;
private float mAccumulator;
/**
* Float sample FIR filter base class.
*
* @param coefficients - filter coefficients in normal order.
* @param gain value to apply to the filtered output. Use 1.0f for no gain
*/
public RealFIRFilter2(float[] coefficients, float gain)
{
mGain = gain;
mCoefficients = coefficients;
mData = new float[coefficients.length];
}
/**
* Float sample FIR filter base class that uses a default gain value of 1.0f.
*
* @param coefficients - filter coefficients in normal order.
*/
public RealFIRFilter2(float[] coefficients)
{
this(coefficients, 1.0f);
}
/**
* Disposes this filter to prepare for garbage collection.
*/
@Override
public void dispose()
{
mCoefficients = null;
mData = null;
}
/**
* Filters the sample argument. Loads the sample into the internal data buffer and performs
* convolution between the sample buffer and the filter coefficients.
*
* @param sample to load
* @return filtered value
*/
public float filter(float sample)
{
//Use array copy to leverage SIMD intrinsics
System.arraycopy(mData, 0, mData, 1, mData.length - 1);
mData[0] = sample;
mAccumulator = 0.0f;
//Use vector dot product to leverage SIMD intrinsics
for(int x = 0; x < mCoefficients.length; x++)
{
mAccumulator += mData[x] * mCoefficients[x];
}
/* Apply gain and return the filtered value */
mAccumulator *= mGain;
return mAccumulator;
}
/**
* Current filtered output value for the filter after the filter() method has been invoked.
*/
public float currentValue()
{
return mAccumulator;
}
/**
* Filters the samples contained in the unfilteredBuffer and returns a new reusable buffer with the
* filtered samples.
*
* Note: user count on the returned (new) buffer is set to one and the user count is decremented on
* the unfiltered buffer argument.
*
* @param unfilteredBuffer containing a sample array to be filtered
* @return a new reusable buffer with the filtered samples.
*/
public ReusableBuffer filter(ReusableBuffer unfilteredBuffer)
{
float[] unfilteredSamples = unfilteredBuffer.getSamples();
ReusableBuffer filteredBuffer = mReusableBufferQueue.getBuffer(unfilteredSamples.length);
float[] filteredSamples = filteredBuffer.getSamples();
for(int x = 0; x < unfilteredSamples.length; x++)
{
filteredSamples[x] = filter(unfilteredSamples[x]);
}
unfilteredBuffer.decrementUserCount();
return filteredBuffer;
}
}

View File

@@ -1,67 +0,0 @@
package io.github.dsheirer.dsp.filter.fir.real;
import io.github.dsheirer.sample.Listener;
import io.github.dsheirer.sample.real.RealBuffer;
public class RealFIRFilter_RB_RB extends RealFIRFilter
implements Listener<RealBuffer>
{
private Listener<RealBuffer> mListener;
/**
* Real Buffer Float FIR filter with streaming float buffer provider and
* listener interfaces.
*
* @param coefficients
* @param gain
*/
public RealFIRFilter_RB_RB( float[] coefficients, float gain )
{
super( coefficients, gain );
}
@Override
public void dispose()
{
super.dispose();
mListener = null;
}
@Override
public void receive( RealBuffer buffer )
{
if( mListener != null )
{
mListener.receive( filter( buffer ) );
}
}
/**
* Filters the buffer samples
*
* @param buffer
* @return
*/
public RealBuffer filter( RealBuffer buffer )
{
float[] samples = buffer.getSamples();
for( int x = 0; x < samples.length; x++ )
{
samples[ x ] = filter( samples[ x ] );
}
return buffer;
}
public void setListener( Listener<RealBuffer> listener )
{
mListener = listener;
}
public void removeListener()
{
mListener = null;
}
}

View File

@@ -1,56 +0,0 @@
package io.github.dsheirer.dsp.filter.fir.real;
import io.github.dsheirer.sample.real.RealSampleListener;
public class RealFIRFilter_R_R extends RealFIRFilter implements RealSampleListener
{
private RealSampleListener mListener;
/**
* Float FIR filter with streaming float provider and listener interfaces.
*
* @param coefficients
* @param gain
*/
public RealFIRFilter_R_R( float[] coefficients, float gain )
{
super( coefficients, gain );
}
@Override
public void dispose()
{
super.dispose();
mListener = null;
}
@Override
public void receive( float sample )
{
if( mListener != null )
{
mListener.receive( filter( sample ) );
}
}
public float[] filter( float[] samples )
{
for( int x = 0; x < samples.length; x++ )
{
samples[ x ] = filter( samples[ x ] );
}
return samples;
}
public void setListener( RealSampleListener listener )
{
mListener = listener;
}
public void removeListener()
{
mListener = null;
}
}

View File

@@ -0,0 +1,37 @@
/*******************************************************************************
* sdrtrunk
* Copyright (C) 2014-2017 Dennis Sheirer
*
* This program is free software: you can redistribute it and/or modify
* it under the terms of the GNU General Public License as published by
* the Free Software Foundation, either version 3 of the License, or
* (at your option) any later version.
*
* This program is distributed in the hope that it will be useful,
* but WITHOUT ANY WARRANTY; without even the implied warranty of
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
* GNU General Public License for more details.
*
* You should have received a copy of the GNU General Public License
* along with this program. If not, see <http://www.gnu.org/licenses/>
*
******************************************************************************/
package io.github.dsheirer.dsp.filter.fir.remez;
import io.github.dsheirer.dsp.filter.fir.FIRFilterSpecification;
public class PolyphaseChannelizerDesigner extends FIRFilterSpecification
{
/**
* Constructs a Polyphase TunerChannelizer Remez filter specification.
*
* Constructor is package private -- use the PolyphaseChannelizerFilterFactory to create a filter
*
* @param order of the filter
* @param gridDensity to use when calculating the coefficients
*/
PolyphaseChannelizerDesigner(int order, int gridDensity)
{
super(FIRLinearPhaseFilterType.TYPE_1_ODD_LENGTH_EVEN_ORDER_SYMMETRICAL, order, gridDensity);
}
}

View File

@@ -0,0 +1,97 @@
/*******************************************************************************
* 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.dsp.filter.fir.remez;
import io.github.dsheirer.dsp.filter.fir.FIRFilterSpecification;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
public class PolyphaseChannelizerFilterFactory
{
private final static Logger mLog = LoggerFactory.getLogger(PolyphaseChannelizerFilterFactory.class);
private static final double OBJECTIVE_BAND_EDGE_COEFFICIENT_AMPLITUDE = Math.sqrt(2.0) / 2.0; //.707xxx
public static float[] getFilter(int sampleRate, int channelBandwidth, double alpha)
{
FIRLinearPhaseFilterType type = FIRLinearPhaseFilterType.TYPE_2_EVEN_LENGTH_ODD_ORDER_SYMMETRICAL;
int passBandStart = 0;
int gridDensity = 60;
int rolloffFrequency = (int)(alpha * (double)channelBandwidth);
int passBandStop = channelBandwidth - rolloffFrequency;
//Set the stop band to start at 40% into the adjacent channel, since we're oversampling by 2.
int stopBandStart = channelBandwidth + rolloffFrequency;
double passRipple = 0.01d;
double transitionRipple = 0.02d;
double stopRipple = 0.001d; //Approximately 90 dB of attenuation
int order = FIRFilterSpecification.estimateFilterOrder( sampleRate, passBandStop, stopBandStart,
passRipple, stopRipple );
// order = 1001;
// Ensure odd order since we're designing a Type 2 filter
if(order % 2 == 0)
{
order++;
}
mLog.info("Filter Order: " + order);
FIRFilterSpecification specification = new PolyphaseChannelizerDesigner(order, gridDensity);
FIRFilterSpecification.FrequencyBand passBand = new FIRFilterSpecification.FrequencyBand(sampleRate, passBandStart,
passBandStop, 1.0, passRipple, 1.0);
//Use the filter order as the weighting for the pass band edge frequency
// double weight = order;
FIRFilterSpecification.FrequencyBand transitionBand = new FIRFilterSpecification.FrequencyBand(sampleRate,
channelBandwidth, channelBandwidth, OBJECTIVE_BAND_EDGE_COEFFICIENT_AMPLITUDE, transitionRipple, 5.0);
FIRFilterSpecification.FrequencyBand stopBand = new FIRFilterSpecification.FrequencyBand(sampleRate,
stopBandStart, (int)(sampleRate / 2), 0.0, stopRipple);
specification.addFrequencyBand(passBand);
specification.addFrequencyBand(transitionBand);
specification.addFrequencyBand(stopBand);
double bandEdgeFrequency = Math.cos(Math.PI * (double)channelBandwidth / (double)(sampleRate / 2));
float[] filter = null;
try
{
RemezFIRFilterDesignerWithLagrange designer = new RemezFIRFilterDesignerWithLagrange(specification);
filter = designer.getImpulseResponse();
double bandEdgeAmplitude = designer.getFrequencyResponse(bandEdgeFrequency);
mLog.debug("Coefficient Amplitude at Band Edge is: " + bandEdgeAmplitude + " for frequency: " + passBandStop);
}
catch(Exception e)
{
mLog.error("Error designing filter", e);
}
return filter;
}
}

View File

@@ -211,15 +211,12 @@ public class RemezFIRFilterDesigner
for( int k = 0; k < mExtremalIndices.size() - 1; k++ )
{
double cosineDelta = cosineOfFrequency -
mGrid.getCosineFrequencyGrid()[ mExtremalIndices.get( k ) ];
double cosineDelta = cosineOfFrequency - mGrid.getCosineFrequencyGrid()[ mExtremalIndices.get( k ) ];
//If this frequency is or is close to one of the polynomial points, use c[k] for that point
//If this frequency is close to one of the polynomial points, use the polynomial point for the response
if( Math.abs( cosineDelta ) < 1.0e-7 )
{
numerator = mIdealFrequencyResponse[ k ];
denominator = 1.0;
break;
return mIdealFrequencyResponse[ k ];
}
else
{

View File

@@ -0,0 +1,710 @@
/*******************************************************************************
* 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.dsp.filter.fir.remez;
import io.github.dsheirer.dsp.filter.design.FilterDesignException;
import io.github.dsheirer.dsp.filter.fir.FIRFilterSpecification;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import java.math.BigDecimal;
import java.math.RoundingMode;
import java.util.ArrayList;
import java.util.Iterator;
import java.util.List;
public class RemezFIRFilterDesigner2
{
private final static Logger mLog = LoggerFactory.getLogger(RemezFIRFilterDesigner2.class);
private static final double CONVERGENCE_THRESHOLD = 0.0001;
public static final int MAXIMUM_ITERATION_COUNT = 40;
public static final double TWO_PI = 2.0 * Math.PI;
public static final double FREQUENCY_PROXIMITY_THRESHOLD = 1.0e-7;
private FIRFilterSpecification mSpecification;
private Grid mGrid;
private List<Integer> mExtremalIndices;
private BigDecimal[] mD;
private double[] mGridErrors;
private double[] mGridFrequencyResponse;
private double[] mIdealFrequencyResponse;
private double mDelta;
private boolean mConverged;
/**
* Linear FIR filter designer using the Parks-McClelland Remez exchange algorithm.
*
* Construct a filter specification using the builder methods in the FIRFilterSpecification
* class and construct an instance of this class with the specification. Check the isValid()
* method after construction to see if the filter design was successful. If successful, use the
* getFrequencyResponse() and getImpulseResponse() methods to access the filter taps as the
* specified length.
*
* If the filter cannot be designed from the specification the isValid() method will return
* false and any attempts to access the getFrequencyResponse() or getImpulseResponse() methods
* will throw a FilterDesignException.
*
* @param specification that defines a Type 1-4 linear phase FIR Filter
*/
public RemezFIRFilterDesigner2(FIRFilterSpecification specification)
{
mSpecification = specification;
design();
}
/**
* Designs the filter according to the specification
*/
private void design()
{
mLog.debug("Designing filter ...");
mGrid = new Grid(mSpecification);
mExtremalIndices = getInitialExtremalIndices();
int iterationCount = 0;
try
{
do
{
mLog.debug("Calculating grid frequency response");
calculateGridFrequencyResponse();
mLog.debug("Calculating grid error");
calculateGridError();
mLog.debug("Finding extremal indices");
findExtremalIndices();
mLog.debug("Checking Convergence");
checkConvergence();
iterationCount++;
mLog.debug("Iteration: " + iterationCount);
}
while(!mConverged && iterationCount < MAXIMUM_ITERATION_COUNT);
}
catch(FilterDesignException fde)
{
mLog.error("Filter design error - couldn't find extremal indices at count [" +
iterationCount + "] try changing filter order up/down from [" +
mSpecification.getOrder() + "]");
mConverged = false;
}
if(mConverged)
{
//Update frequency response using the final set of extremal indices
calculateGridFrequencyResponse();
}
}
/**
* Indicates if the filter was successfully created from the filter specification. Check this
* method before accessing frequency response or filter response, otherwise a FilterDesignException
* is thrown.
*
* @return true if the filter was successfully designed.
*/
public boolean isValid()
{
return mConverged;
}
/**
* Frequency response of the designed filter with double precision
*
* @return half ( length / 2 ) of filter frequency response sampled at 0.0 - PI radians
* @throws FilterDesignException if the specified filter cannot be designed
*/
public double[] getFrequencyResponse() throws FilterDesignException
{
if(!mConverged)
{
throw new FilterDesignException("Can't create filter from specification - failed "
+ "to converge");
}
//Resample the final polynomial at the desired filter length
double[] resampled = resample();
//Apply Parks/McClelland frequency response corrections according to the filter type
return correctFrequencyResponse(resampled);
}
/**
* Impulse response of the designed filter with floating point precision.
*
* @return filter impulse response
* @throws FilterDesignException if the specified filter cannot be designed
*/
public float[] getImpulseResponse() throws FilterDesignException
{
return convertToFloatArray(getImpulseResponseDoubles());
}
/**
* Impulse response of the designed filter with double precision
*/
public double[] getImpulseResponseDoubles() throws FilterDesignException
{
double[] frequencyResponse = getFrequencyResponse();
int length = mSpecification.getFilterLength();
double[] impulseResponse = new double[length];
double M;
switch(mSpecification.getFilterType())
{
case TYPE_1_ODD_LENGTH_EVEN_ORDER_SYMMETRICAL:
M = ((double)length - 1.0) / 2.0;
for(int n = 0; n < length; n++)
{
double accumulator = frequencyResponse[0];
double frequency = TWO_PI * (n - M) / length;
for(int k = 1; k <= M; k++)
{
accumulator += 2.0 * frequencyResponse[k] * Math.cos(frequency * (double)k);
}
impulseResponse[n] = accumulator / (double)length;
}
break;
case TYPE_2_EVEN_LENGTH_ODD_ORDER_SYMMETRICAL:
double offset = (double)(length - 1) / 2.0;
for(int n = 0; n < length; n++)
{
double accumulator = frequencyResponse[0];
double frequency = TWO_PI * ((double)n - offset) / (double)length;
for(int k = 1; k < frequencyResponse.length; k++)
{
accumulator += 2.0 * frequencyResponse[k] * Math.cos(frequency * (double)k);
}
impulseResponse[n] = accumulator / (double)length;
}
break;
}
return impulseResponse;
}
/**
* Calculates the frequency response of this filter at the specified cosine of the frequency
*
* Implements Oppenheim/Schafer Discrete Time Signal Processing, 3e, 2016, equation 116a to
* determine the actual response of the specified frequency cosine using Lagrange
* interpolation of the polynomial formed at the extremal index set.
*
* @param cosineOfFrequency to calculate the frequency response
* @return actual frequency response for the specified frequency with the current set of
* extreaml indices
*/
public double getFrequencyResponse(double cosineOfFrequency)
{
BigDecimal numerator = BigDecimal.ZERO;
BigDecimal denominator = BigDecimal.ZERO;
for(int k = 0; k < mExtremalIndices.size() - 1; k++)
{
double cosineDelta = cosineOfFrequency - mGrid.getCosineFrequencyGrid()[mExtremalIndices.get(k)];
//If this frequency is close to one of the polynomial points, use the point as the response value
if(Math.abs(cosineDelta) < FREQUENCY_PROXIMITY_THRESHOLD)
{
return mIdealFrequencyResponse[k];
}
else
{
BigDecimal dkOverCosineDelta = mD[k].divide(BigDecimal.valueOf(cosineDelta), 25, RoundingMode.HALF_UP);
numerator = numerator.add(dkOverCosineDelta.multiply(BigDecimal.valueOf(mIdealFrequencyResponse[k])));
denominator = denominator.add(dkOverCosineDelta);
}
}
if(denominator.doubleValue() == 0.0)
{
int a = 0;
}
return numerator.divide(denominator, 25, RoundingMode.HALF_UP).doubleValue();
}
/**
* Calculated Delta value
*
* @return delta value
*/
public double getDelta()
{
return mDelta;
}
/**
* Creates initial array of extremal frequency indices spaced at intervals of the grid frequency
* set for all frequencies where a frequency band weighting is specified.
*/
private List<Integer> getInitialExtremalIndices()
{
int count = mSpecification.getExtremaCount();
int density = mSpecification.getGridDensity();
List<Integer> extremalIndices = new ArrayList<>();
for(int i = 0; i < count; i++)
{
extremalIndices.add(i * density);
}
return extremalIndices;
}
/**
* Calculates the actual frequency response of a dense frequency grid using the polynomial
* represented by the extremal indices.
*/
private void calculateGridFrequencyResponse()
{
mLog.debug("\tCalculating B");
BigDecimal[] b = calculateB();
mLog.debug("\tCalculating Delta");
calculateDelta(b);
mLog.debug("\tCalculating C");
calculateC();
mLog.debug("\tCalculating D");
calculateD(b);
mLog.debug("\tUpdating Grid Frequency Response");
updateGridFrequencyResponse();
}
/**
* Updates the frequency response of frequencies specified in the dense frequency grid
*/
private void updateGridFrequencyResponse()
{
mGridFrequencyResponse = new double[mGrid.getSize()];
double[] gridFrequencyCosines = mGrid.getCosineFrequencyGrid();
for(int i = 0; i < mGridFrequencyResponse.length; i++)
{
//TODO: parallelize this part!!!!
mGridFrequencyResponse[i] = getFrequencyResponse(gridFrequencyCosines[i]);
if(i % 1000 == 0)
{
mLog.debug("\t\tUpdating Frequency Response - index:" + i + " of " +
mGridFrequencyResponse.length + " Value:" + mGridFrequencyResponse[i]);
}
}
}
/**
* Calculates the value of b across the set of extremal indices.
*
* Implements Oppenheim/Schafer Discrete Time Signal Processing, 3e, 2016, equation 115
*
* @return the array of b values of length L+2
*/
private BigDecimal[] calculateB()
{
int length = mExtremalIndices.size();
BigDecimal[] b = new BigDecimal[length];
for(int k = 0; k < length; k++)
{
b[k] = BigDecimal.ONE;
double xk = mGrid.getCosineFrequencyGrid()[mExtremalIndices.get(k)];
for(int i = 0; i < length; i++)
{
if(i != k)
{
double xi = mGrid.getCosineFrequencyGrid()[mExtremalIndices.get(i)];
double denominator = xk - xi;
if(Math.abs(denominator) < 0.00001)
{
denominator = 0.00001;
}
b[k] = b[k].multiply(BigDecimal.valueOf(1.0 / denominator));
}
}
}
return b;
}
/**
* Calculates the value of delta that represents the maximum ripple for the current set of
* extremal indices (L+2)
*
* Implements Oppenheim/Schafer Discrete Time Signal Processing, 3e, 2016, equation 114
*
* @param b calculated for the grid from equation 115
*/
private void calculateDelta(BigDecimal[] b)
{
BigDecimal numerator = BigDecimal.ZERO;
BigDecimal denominator = BigDecimal.ZERO;
double sign = 1.0;
for(int k = 0; k < b.length; k++)
{
if(k < mExtremalIndices.size())
{
int extremalIndex = mExtremalIndices.get(k);
numerator = numerator.add(b[k].multiply(BigDecimal.valueOf(mGrid.getDesiredResponse()[extremalIndex])));
denominator = denominator.add(b[k].multiply(BigDecimal.valueOf(sign / mGrid.getWeight()[extremalIndex])));
sign = -sign;
}
else
{
mLog.error("Something went wrong -- the length of b exceeds the set of extremal indices");
}
}
mDelta = numerator.divide(denominator, 1023, BigDecimal.ROUND_HALF_DOWN).doubleValue();
}
/**
* Calculates the ideal frequency response (C) of the current extremal index set. This reference
* is used to determine the level of error in the set of actual frequency responses calculated
* from this set of extremal indices to determine convergence.
*
* Implements Oppenheim/Schafer Discrete Time Signal Processing, 3e, 2016, equation 116b
*/
private void calculateC()
{
int length = mSpecification.getExtremaCount() - 1;
mIdealFrequencyResponse = new double[length];
double sign = 1.0;
for(int k = 0; k < length; k++)
{
if(k < mExtremalIndices.size())
{
int index = mExtremalIndices.get(k);
mIdealFrequencyResponse[k] = mGrid.getDesiredResponse()[index] -
(sign * mDelta / mGrid.getWeight()[index]);
sign = -sign;
}
}
}
/**
* Calculates the set of D values for the current extremal index set.
*
* Implements Oppenheim/Schafer Discrete Time Signal Processing, 3e, 2016, equation 116c
*/
private void calculateD(BigDecimal[] b)
{
int length = mExtremalIndices.size() - 1;
mD = new BigDecimal[length];
for(int k = 0; k < length; k++)
{
//Note: extremalCosines array is one index longer than d array, so we use length as
//a pointer to the final (L+2) extremalCosine index
mD[k] = b[k].multiply(BigDecimal.valueOf((mGrid.getCosineFrequencyGrid()[mExtremalIndices.get(k)] -
mGrid.getCosineFrequencyGrid()[mExtremalIndices.get(length)])));
}
}
/**
* Calculates the weighted error between the specification desired frequency response and the
* actual frequency response of the current extremal index set across the frequencies in the
* dense frequency grid.
*
* Implements Oppenheim/Schafer Discrete Time Signal Processing, 3e, 2016, equation 112
*/
public void calculateGridError()
{
int length = mGridFrequencyResponse.length;
mGridErrors = new double[length];
for(int i = 0; i < length; i++)
{
mGridErrors[i] = mGrid.getWeight()[i] *
(mGrid.getDesiredResponse()[i] - mGridFrequencyResponse[i]);
}
}
/**
* Identifies the indexes of the extremal error values using the Alternation theorem
*
* @throws FilterDesignException if the full set of extremal indices can't be identified
*/
private void findExtremalIndices() throws FilterDesignException
{
mExtremalIndices.clear();
//Check for extremum at error index 0
if(((mGridErrors[0] > 0.0 && mGridErrors[0] > mGridErrors[1]) ||
(mGridErrors[0] < 0.0 && mGridErrors[0] < mGridErrors[1])) &&
isGTEDelta(mGridErrors[0]))
{
mExtremalIndices.add(0);
}
//Check for extrema in middle error indices )
for(int x = 1; x < mGridErrors.length - 1; x++)
{
if(((mGridErrors[x] > 0.0 && (mGridErrors[x - 1] <= mGridErrors[x] && mGridErrors[x] > mGridErrors[x + 1])) ||
(mGridErrors[x] < 0.0 && (mGridErrors[x - 1] >= mGridErrors[x] && mGridErrors[x] < mGridErrors[x + 1]))) &&
isGTEDelta(mGridErrors[x]))
{
mExtremalIndices.add(x);
}
}
//Check for extremum at final error index
int last = mGridErrors.length - 1;
if(((mGridErrors[last] > 0.0 && (mGridErrors[last] > mGridErrors[last - 1])) ||
(mGridErrors[last] < 0.0 && (mGridErrors[last] < mGridErrors[last - 1]))) &&
isGTEDelta(mGridErrors[last]))
{
mExtremalIndices.add(last);
}
//Ensure we have the minimum of extremals before we continue
if(mExtremalIndices.size() < mSpecification.getExtremaCount())
{
throw new FilterDesignException("Couldn't find the minimum extremal frequencies in "
+ "error set");
}
//Enforce alternation theory -- only one extremal for each transition about the zero axis
List<Integer> indicesToRemove = new ArrayList<>();
Iterator<Integer> it = mExtremalIndices.iterator();
Integer current = it.next();
Integer next;
boolean positiveAxis = mGridErrors[current] > 0.0;
while(it.hasNext())
{
next = it.next();
//Check for consecutive (redundant) indices on same side of zero axis - retain largest
if(!(positiveAxis ^ (mGridErrors[next] > 0.0)))
{
if(Math.abs(mGridErrors[next]) <= Math.abs(mGridErrors[current]))
{
//Remove next error index that is less than current index and on
it.remove();
next = current;
}
else
{
//Since we can't remove the current index with the iterator, queue for later removal
indicesToRemove.add(current);
}
}
else
{
positiveAxis = !positiveAxis;
}
current = next;
}
//Remove redundant extremal indices that couldn't be removed via the iterator
mExtremalIndices.removeAll(indicesToRemove);
//Truncate the list to one larger than needed by removing excess tailing indices
while(mExtremalIndices.size() > mSpecification.getExtremaCount())
{
mExtremalIndices.remove(mExtremalIndices.size() - 1);
}
//If we have one too many indices, delete the smaller of the first or last
if(mExtremalIndices.size() > mSpecification.getExtremaCount())
{
int lastIndex = mExtremalIndices.size() - 1;
if(Math.abs(mGridErrors[mExtremalIndices.get(0)]) >
Math.abs(mGridErrors[mExtremalIndices.get(lastIndex)]))
{
mExtremalIndices.remove(lastIndex);
}
else
{
mExtremalIndices.remove(0);
}
}
//Detect if we have too few indices
if(mExtremalIndices.size() < mSpecification.getExtremaCount())
{
throw new FilterDesignException("Couldn't find the minimum extremal frequencies in "
+ "error set");
}
}
/**
* Indicates if the absolute value of the argument is greater than or equal to the delta
* value with accuracy to 14 digits of precision. This avoids rounding errors at 15 digits or
* precision or greater.
*
* @param value to evaluate
* @return true if the absolute value is greater than or equal to the absolute delta value
*/
private boolean isGTEDelta(double value)
{
return Math.abs(value) - Math.abs(mDelta) > -1.0e-5;
}
/**
* Checks for convergence of the frequency response of the current set of extremal indices to
* the filter specification by comparing the maximum absolute error value against the delta
* value to determine if these two values are within the convergence threshold.
*/
public void checkConvergence()
{
double maximum = Math.abs(mGridErrors[mExtremalIndices.get(0)]);
for(int i = 1; i < mExtremalIndices.size(); i++)
{
double current = Math.abs(mGridErrors[mExtremalIndices.get(i)]);
if(current > maximum)
{
maximum = current;
}
}
double convergence = maximum - Math.abs(mDelta);
mConverged = convergence < CONVERGENCE_THRESHOLD;
}
/**
* Coverts/casts the double array to a float array
*/
private static float[] convertToFloatArray(double[] samples)
{
float[] converted = new float[samples.length];
for(int x = 0; x < samples.length; x++)
{
converted[x] = (float)samples[x];
}
return converted;
}
/**
* Resamples the designed filter at half of the filter length at evenly spaced intervals of the
* frequency spectrum from 0 to PI radians.
*
* @return frequency response of the filter sampled the desired filter length and order
*/
private double[] resample()
{
int length = mSpecification.getFilterLength();
if(length % 2 == 0)
{
length--;
}
double half = (double)length / 2.0;
double[] resampled = new double[(int)Math.ceil(half)];
for(int x = 0; x < resampled.length; x++)
{
resampled[x] = getFrequencyResponse(Math.cos(Math.PI * (double)x / half));
}
return resampled;
}
/**
* Applies Parks/McClelland filter type correction to the final frequency response array
*
* @param frequencyResponse of the resampled filter design from 0 <> ~ PI radians
* @return corrected frequency response
*/
private double[] correctFrequencyResponse(double[] frequencyResponse)
{
double filterLength = mSpecification.getFilterLength();
switch(mSpecification.getFilterType())
{
case TYPE_1_ODD_LENGTH_EVEN_ORDER_SYMMETRICAL:
//No correction needed
break;
case TYPE_2_EVEN_LENGTH_ODD_ORDER_SYMMETRICAL:
//No correction needed
break;
case TYPE_3_ODD_LENGTH_EVEN_ORDER_ANTI_SYMMETRICAL:
for(int x = 0; x < frequencyResponse.length; x++)
{
double frequencyRadians = Math.PI * (((double)x) / filterLength);
frequencyResponse[x] *= Math.sin(TWO_PI * frequencyRadians);
}
break;
case TYPE_4_EVEN_LENGTH_ODD_ORDER_ANTI_SYMMETRICAL:
for(int x = 0; x < frequencyResponse.length; x++)
{
double frequencyRadians = Math.PI * (((double)x) / filterLength);
frequencyResponse[x] *= Math.sin(Math.PI * frequencyRadians);
}
break;
default:
break;
}
return frequencyResponse;
}
}

View File

@@ -0,0 +1,686 @@
/*******************************************************************************
* sdrtrunk
* Copyright (C) 2014-2017 Dennis Sheirer
*
* This program is free software: you can redistribute it and/or modify
* it under the terms of the GNU General Public License as published by
* the Free Software Foundation, either version 3 of the License, or
* (at your option) any later version.
*
* This program is distributed in the hope that it will be useful,
* but WITHOUT ANY WARRANTY; without even the implied warranty of
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
* GNU General Public License for more details.
*
* You should have received a copy of the GNU General Public License
* along with this program. If not, see <http://www.gnu.org/licenses/>
*
******************************************************************************/
package io.github.dsheirer.dsp.filter.fir.remez;
import io.github.dsheirer.dsp.filter.design.FilterDesignException;
import io.github.dsheirer.dsp.filter.fir.FIRFilterSpecification;
import org.apache.commons.math3.analysis.polynomials.PolynomialFunctionLagrangeForm;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import java.math.BigDecimal;
import java.text.DecimalFormat;
import java.util.ArrayList;
import java.util.Iterator;
import java.util.List;
public class RemezFIRFilterDesignerWithLagrange
{
private final static Logger mLog = LoggerFactory.getLogger(RemezFIRFilterDesignerWithLagrange.class);
private static final double CONVERGENCE_THRESHOLD = 0.0001;
public static final int MAXIMUM_ITERATION_COUNT = 40;
public static final double TWO_PI = 2.0 * Math.PI;
private static final DecimalFormat DECIMAL_FORMATTER = new DecimalFormat("0.00000000");
private FIRFilterSpecification mSpecification;
private Grid mGrid;
private List<Integer> mExtremalIndices;
private double[] mD;
private double[] mGridErrors;
private double[] mGridFrequencyResponse;
private double[] mIdealFrequencyResponse;
private PolynomialFunctionLagrangeForm mLagrangeInterpolator;
private double mDelta;
private boolean mConverged;
/**
* Linear FIR filter designer using the Parks-McClelland Remez exchange algorithm.
*
* Construct a filter specification using the builder methods in the FIRFilterSpecification
* class and construct an instance of this class with the specification. Check the isValid()
* method after construction to see if the filter design was successful. If successful, use the
* getFrequencyResponse() and getImpulseResponse() methods to access the filter taps as the
* specified length.
*
* If the filter cannot be designed from the specification the isValid() method will return
* false and any attempts to access the getFrequencyResponse() or getImpulseResponse() methods
* will throw a FilterDesignException.
*
* @param specification that defines a Type 1-4 linear phase FIR Filter
*/
public RemezFIRFilterDesignerWithLagrange(FIRFilterSpecification specification) throws FilterDesignException
{
mSpecification = specification;
design();
}
/**
* Designs the filter according to the specification
*/
private void design() throws FilterDesignException
{
mGrid = new Grid(mSpecification);
mExtremalIndices = getInitialExtremalIndices();
int iterationCount = 0;
do
{
calculateGridFrequencyResponse();
calculateGridError();
findExtremalIndices();
checkConvergence();
iterationCount++;
}
while(!mConverged && iterationCount < MAXIMUM_ITERATION_COUNT);
if(mConverged)
{
//Update frequency response using the final set of extremal indices
calculateGridFrequencyResponse();
}
else
{
throw new FilterDesignException("Unable to design filter after [" + iterationCount + "] iterations");
}
}
/**
* Indicates if the filter was successfully created from the filter specification. Check this
* method before accessing frequency response or filter response, otherwise a FilterDesignException
* is thrown.
*
* @return true if the filter was successfully designed.
*/
public boolean isValid()
{
return mConverged;
}
/**
* Frequency response of the designed filter with double precision
*
* @return half ( length / 2 ) of filter frequency response sampled at 0.0 - PI radians
* @throws FilterDesignException if the specified filter cannot be designed
*/
public double[] getFrequencyResponse() throws FilterDesignException
{
if(!mConverged)
{
throw new FilterDesignException("Can't create filter from specification - failed "
+ "to converge ");
}
//Resample the final polynomial at the desired filter length
double[] resampled = resample();
//Apply Parks/McClelland frequency response corrections according to the filter type
return correctFrequencyResponse(resampled);
}
/**
* Impulse response of the designed filter with floating point precision.
*
* @return filter impulse response
* @throws FilterDesignException if the specified filter cannot be designed
*/
public float[] getImpulseResponse() throws FilterDesignException
{
return convertToFloatArray(getImpulseResponseDoubles());
}
/**
* Impulse response of the designed filter with double precision
*/
public double[] getImpulseResponseDoubles() throws FilterDesignException
{
double[] frequencyResponse = getFrequencyResponse();
int length = mSpecification.getFilterLength();
double[] impulseResponse = new double[length];
double M;
switch(mSpecification.getFilterType())
{
case TYPE_1_ODD_LENGTH_EVEN_ORDER_SYMMETRICAL:
M = ((double)length - 1.0) / 2.0;
for(int n = 0; n < length; n++)
{
double accumulator = frequencyResponse[0];
double frequency = TWO_PI * (n - M) / length;
for(int k = 1; k <= M; k++)
{
accumulator += 2.0 * frequencyResponse[k] * Math.cos(frequency * (double)k);
}
impulseResponse[n] = accumulator / (double)length;
}
break;
case TYPE_2_EVEN_LENGTH_ODD_ORDER_SYMMETRICAL:
double offset = (double)(length - 1) / 2.0;
for(int n = 0; n < length; n++)
{
double accumulator = frequencyResponse[0];
double frequency = TWO_PI * ((double)n - offset) / (double)length;
for(int k = 1; k < frequencyResponse.length; k++)
{
accumulator += 2.0 * frequencyResponse[k] * Math.cos(frequency * (double)k);
}
impulseResponse[n] = accumulator / (double)length;
}
break;
}
return impulseResponse;
}
/**
* Calculates the frequency response of this filter at the specified cosine of the frequency
*
* Implements Oppenheim/Schafer Discrete Time Signal Processing, 3e, 2016, equation 116a to
* determine the actual response of the specified frequency cosine using Lagrange
* interpolation of the polynomial formed at the extremal index set.
*
* @param cosineOfFrequency to calculate the frequency response
* @return actual frequency response for the specified frequency with the current set of
* extreaml indices
*/
public double getFrequencyResponse(double cosineOfFrequency)
{
if(mLagrangeInterpolator == null)
{
throw new IllegalStateException("Lagrange interpolator is not setup - can't calculate frequency response");
}
return mLagrangeInterpolator.value(cosineOfFrequency);
// double numerator = 0.0;
// double denominator = 0.0;
//
// for( int k = 0; k < mExtremalIndices.size() - 1; k++ )
// {
// double cosineDelta = cosineOfFrequency - mGrid.getCosineFrequencyGrid()[ mExtremalIndices.get( k ) ];
//
// //If this frequency is close to one of the polynomial points, use the polynomial point for the response
// if( Math.abs( cosineDelta ) < 1.0e-7 )
// {
// return mIdealFrequencyResponse[ k ];
// }
// else
// {
// double dkOverCosineDelta = mD[ k ] / cosineDelta;
//
// numerator += dkOverCosineDelta * mIdealFrequencyResponse[ k ];
// denominator += dkOverCosineDelta;
// }
// }
//
// return numerator / denominator;
}
/**
* Calculated Delta value
*
* @return delta value
*/
public double getDelta()
{
return mDelta;
}
/**
* Creates initial array of extremal frequency indices spaced at intervals of the grid frequency
* set for all frequencies where a frequency band weighting is specified.
*/
private List<Integer> getInitialExtremalIndices()
{
int count = mSpecification.getExtremaCount();
int density = mSpecification.getGridDensity();
List<Integer> extremalIndices = new ArrayList<>();
for(int i = 0; i < count; i++)
{
extremalIndices.add(i * density);
}
return extremalIndices;
}
/**
* Calculates the actual frequency response of a dense frequency grid using the polynomial
* represented by the extremal indices.
*/
private void calculateGridFrequencyResponse()
{
BigDecimal[] b = calculateB();
calculateDelta(b);
calculateC();
//Setup the lagrange polynomial so that we can determine the frequency response at the grid frequencies
//where mExtremalIndices are the polynomial points, the subset of cosine frequency grid identified by the
//extremal indices is the x value set and mIdealFrequencyResponse is the y axis value set
double[] xAxis = new double[mIdealFrequencyResponse.length];
for(int x = 0; x < mExtremalIndices.size() - 1; x++)
{
int index = mExtremalIndices.get(x);
xAxis[x] = mGrid.getCosineFrequencyGrid()[index];
}
mLagrangeInterpolator = new PolynomialFunctionLagrangeForm(xAxis, mIdealFrequencyResponse);
updateGridFrequencyResponse();
}
/**
* Updates the frequency response of frequencies specified in the dense frequency grid
*/
private void updateGridFrequencyResponse()
{
mGridFrequencyResponse = new double[mGrid.getSize()];
double[] gridFrequencyCosines = mGrid.getCosineFrequencyGrid();
for(int i = 0; i < mGridFrequencyResponse.length; i++)
{
mGridFrequencyResponse[i] = getFrequencyResponse(gridFrequencyCosines[i]);
}
}
/**
* Calculates the value of b across the set of extremal indices.
*
* Implements Oppenheim/Schafer Discrete Time Signal Processing, 3e, 2016, equation 115
*
* @return the array of b values of length L+2
*/
private BigDecimal[] calculateB()
{
int length = mExtremalIndices.size();
BigDecimal[] b = new BigDecimal[length];
for(int k = 0; k < length; k++)
{
b[k] = BigDecimal.ONE;
double xk = mGrid.getCosineFrequencyGrid()[mExtremalIndices.get(k)];
for(int i = 0; i < length; i++)
{
if(i != k)
{
double xi = mGrid.getCosineFrequencyGrid()[mExtremalIndices.get(i)];
double denominator = xk - xi;
if(Math.abs(denominator) < 0.00001)
{
denominator = 0.00001;
}
b[k] = b[k].multiply(BigDecimal.valueOf(1.0 / denominator));
// b[ k ] *= 1.0 / denominator;
}
}
}
return b;
}
/**
* Calculates the value of delta that represents the maximum ripple for the current set of
* extremal indices (L+2)
*
* Implements Oppenheim/Schafer Discrete Time Signal Processing, 3e, 2016, equation 114
*
* @param b calculated for the grid from equation 115
*/
private void calculateDelta(BigDecimal[] b)
{
BigDecimal numerator = BigDecimal.ZERO;
BigDecimal denominator = BigDecimal.ZERO;
double sign = 1.0;
for(int k = 0; k < b.length; k++)
{
if(k < mExtremalIndices.size())
{
int extremalIndex = mExtremalIndices.get(k);
numerator = numerator.add(b[k].multiply(BigDecimal.valueOf(mGrid.getDesiredResponse()[extremalIndex])));
denominator = denominator.add(b[k].multiply(BigDecimal.valueOf(sign / mGrid.getWeight()[extremalIndex])));
sign = -sign;
}
else
{
mLog.error("Something went wrong -- the length of b exceeds the set of extremal indices");
}
}
//Set the scale to the maximum scale we could see for a primitive decimal
mDelta = numerator.divide(denominator, 1023, BigDecimal.ROUND_HALF_DOWN).doubleValue();
}
/**
* Calculates the ideal frequency response (C) of the current extremal index set. This reference
* is used to determine the level of error in the set of actual frequency responses calculated
* from this set of extremal indices to determine convergence.
*
* Implements Oppenheim/Schafer Discrete Time Signal Processing, 3e, 2016, equation 116b
*/
private void calculateC()
{
int length = mSpecification.getExtremaCount() - 1;
mIdealFrequencyResponse = new double[length];
double sign = 1.0;
for(int k = 0; k < length; k++)
{
if(k < mExtremalIndices.size())
{
int index = mExtremalIndices.get(k);
mIdealFrequencyResponse[k] = mGrid.getDesiredResponse()[index] -
(sign * mDelta / mGrid.getWeight()[index]);
sign = -sign;
}
}
}
/**
* Calculates the weighted error between the specification desired frequency response and the
* actual frequency response of the current extremal index set across the frequencies in the
* dense frequency grid.
*
* Implements Oppenheim/Schafer Discrete Time Signal Processing, 3e, 2016, equation 112
*/
public void calculateGridError()
{
int length = mGridFrequencyResponse.length;
mGridErrors = new double[length];
for(int i = 0; i < length; i++)
{
mGridErrors[i] = mGrid.getWeight()[i] * (mGrid.getDesiredResponse()[i] - mGridFrequencyResponse[i]);
}
}
/**
* Identifies the indexes of the extremal error values using the Alternation theorem
*
* @throws FilterDesignException if the full set of extremal indices can't be identified
*/
private void findExtremalIndices() throws FilterDesignException
{
mExtremalIndices.clear();
//Check for extremum at error index 0
if(((mGridErrors[0] > 0.0 && mGridErrors[0] > mGridErrors[1]) ||
(mGridErrors[0] < 0.0 && mGridErrors[0] < mGridErrors[1])) &&
isGTEDelta(mGridErrors[0]))
{
mExtremalIndices.add(0);
}
//Check for extrema in middle error indices )
for(int x = 1; x < mGridErrors.length - 1; x++)
{
if(((mGridErrors[x] > 0.0 && (mGridErrors[x - 1] <= mGridErrors[x] && mGridErrors[x] > mGridErrors[x + 1])) ||
(mGridErrors[x] < 0.0 && (mGridErrors[x - 1] >= mGridErrors[x] && mGridErrors[x] < mGridErrors[x + 1]))) &&
isGTEDelta(mGridErrors[x]))
{
mExtremalIndices.add(x);
}
}
//Check for extremum at final error index
int last = mGridErrors.length - 1;
if(((mGridErrors[last] > 0.0 && (mGridErrors[last] > mGridErrors[last - 1])) ||
(mGridErrors[last] < 0.0 && (mGridErrors[last] < mGridErrors[last - 1]))) &&
isGTEDelta(mGridErrors[last]))
{
mExtremalIndices.add(last);
}
//Ensure we have the minimum of extremals before we continue
if(mExtremalIndices.size() < mSpecification.getExtremaCount())
{
throw new FilterDesignException("Couldn't find the minimum extremal frequencies in "
+ "error set before checking for alternation theory - found:" + mExtremalIndices.size() +
" required:" + mSpecification.getExtremaCount());
}
//Enforce alternation theory -- only one extremal for each transition about the zero axis
List<Integer> indicesToRemove = new ArrayList<>();
Iterator<Integer> it = mExtremalIndices.iterator();
Integer current = it.next();
Integer next;
boolean positiveAxis = mGridErrors[current] > 0.0;
while(it.hasNext())
{
next = it.next();
//Check for consecutive (redundant) indices on same side of zero axis - retain largest
if(!(positiveAxis ^ (mGridErrors[next] > 0.0)))
{
if(Math.abs(mGridErrors[next]) <= Math.abs(mGridErrors[current]))
{
//Remove next error index that is less than current index and on
it.remove();
next = current;
}
else
{
//Since we can't remove the current index with the iterator, queue for later removal
indicesToRemove.add(current);
}
}
else
{
positiveAxis = !positiveAxis;
}
current = next;
}
//Remove redundant extremal indices that couldn't be removed via the iterator
mExtremalIndices.removeAll(indicesToRemove);
//Truncate the list to one larger than needed by removing excess tailing indices
while(mExtremalIndices.size() > mSpecification.getExtremaCount())
{
mExtremalIndices.remove(mExtremalIndices.size() - 1);
}
//If we have one too many indices, delete the smaller of the first or last
if(mExtremalIndices.size() > mSpecification.getExtremaCount())
{
int lastIndex = mExtremalIndices.size() - 1;
if(Math.abs(mGridErrors[mExtremalIndices.get(0)]) >
Math.abs(mGridErrors[mExtremalIndices.get(lastIndex)]))
{
mExtremalIndices.remove(lastIndex);
}
else
{
mExtremalIndices.remove(0);
}
}
//Detect if we have too few indices
if(mExtremalIndices.size() < mSpecification.getExtremaCount())
{
throw new FilterDesignException("Couldn't find the minimum extremal frequencies in "
+ "error set - found:" + mExtremalIndices.size() + " required:" + mSpecification.getExtremaCount());
}
}
/**
* Indicates if the absolute value of the argument is greater than or equal to the delta
* value with accuracy to 14 digits of precision. This avoids rounding errors at 15 digits or
* precision or greater.
*
* @param value to evaluate
* @return true if the absolute value is greater than or equal to the absolute delta value
*/
private boolean isGTEDelta(double value)
{
return Math.abs(value) - Math.abs(mDelta) > -1.0e-5;
}
/**
* Checks for convergence of the frequency response of the current set of extremal indices to
* the filter specification by comparing the maximum absolute error value against the delta
* value to determine if these two values are within the convergence threshold.
*/
public void checkConvergence()
{
double maximum = Math.abs(mGridErrors[mExtremalIndices.get(0)]);
for(int i = 1; i < mExtremalIndices.size(); i++)
{
double current = Math.abs(mGridErrors[mExtremalIndices.get(i)]);
if(current > maximum)
{
maximum = current;
}
}
double convergence = maximum - Math.abs(mDelta);
mLog.debug("Convergence: " + convergence);
mConverged = convergence < CONVERGENCE_THRESHOLD;
}
/**
* Coverts/casts the double array to a float array
*/
private static float[] convertToFloatArray(double[] samples)
{
float[] converted = new float[samples.length];
for(int x = 0; x < samples.length; x++)
{
converted[x] = (float)samples[x];
}
return converted;
}
/**
* Resamples the designed filter at half of the filter length at evenly spaced intervals of the
* frequency spectrum from 0 to PI radians.
*
* @return frequency response of the filter sampled the desired filter length and order
*/
private double[] resample()
{
int length = mSpecification.getFilterLength();
if(length % 2 == 0)
{
length--;
}
double half = (double)length / 2.0;
double[] resampled = new double[(int)Math.ceil(half)];
for(int x = 0; x < resampled.length; x++)
{
resampled[x] = getFrequencyResponse(Math.cos(Math.PI * (double)x / half));
}
return resampled;
}
/**
* Applies Parks/McClelland filter type correction to the final frequency response array
*
* @param frequencyResponse of the resampled filter design from 0 <> ~ PI radians
* @return corrected frequency response
*/
private double[] correctFrequencyResponse(double[] frequencyResponse)
{
double filterLength = mSpecification.getFilterLength();
switch(mSpecification.getFilterType())
{
case TYPE_1_ODD_LENGTH_EVEN_ORDER_SYMMETRICAL:
//No correction needed
break;
case TYPE_2_EVEN_LENGTH_ODD_ORDER_SYMMETRICAL:
//No correction needed
break;
case TYPE_3_ODD_LENGTH_EVEN_ORDER_ANTI_SYMMETRICAL:
for(int x = 0; x < frequencyResponse.length; x++)
{
double frequencyRadians = Math.PI * (((double)x) / filterLength);
frequencyResponse[x] *= Math.sin(TWO_PI * frequencyRadians);
}
break;
case TYPE_4_EVEN_LENGTH_ODD_ORDER_ANTI_SYMMETRICAL:
for(int x = 0; x < frequencyResponse.length; x++)
{
double frequencyRadians = Math.PI * (((double)x) / filterLength);
frequencyResponse[x] *= Math.sin(Math.PI * frequencyRadians);
}
break;
default:
break;
}
return frequencyResponse;
}
}

View File

@@ -0,0 +1,106 @@
/*******************************************************************************
* sdr-trunk
* Copyright (C) 2014-2018 Dennis Sheirer
*
* This program is free software: you can redistribute it and/or modify it under the terms of the GNU General Public
* License as published by the Free Software Foundation, either version 3 of the License, or (at your option) any
* later version.
*
* This program is distributed in the hope that it will be useful, but WITHOUT ANY WARRANTY; without even the implied
* warranty of MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU General Public License for more details.
*
* You should have received a copy of the GNU General Public License along with this program.
* If not, see <http://www.gnu.org/licenses/>
*
******************************************************************************/
package io.github.dsheirer.dsp.filter.halfband.complex;
import io.github.dsheirer.dsp.filter.halfband.real.HalfBandFilter;
import io.github.dsheirer.sample.buffer.ReusableComplexBuffer;
public class ComplexHalfBandFilter
{
private HalfBandFilter mIFilter;
private HalfBandFilter mQFilter;
private boolean mDecimate;
private boolean mDecimateFlag;
/**
* @param coefficients
* @param gain
* @param decimate
*/
public ComplexHalfBandFilter(float[] coefficients, float gain, boolean decimate)
{
mIFilter = new HalfBandFilter(coefficients, gain);
mQFilter = new HalfBandFilter(coefficients, gain);
mDecimate = decimate;
}
public void dispose()
{
mIFilter.dispose();
mQFilter.dispose();
}
public ReusableComplexBuffer filter(ReusableComplexBuffer buffer)
{
throw new IllegalStateException("This filter has to be updated to work with reusable buffers");
// if(mDecimate)
// {
// float[] samples = buffer.getSamples();
//
// int half = samples.length / 2;
//
// float[] decimated;
//
// if(half % 2 == 0 || mDecimateFlag)
// {
// decimated = new float[half];
// }
// else
// {
// /* If inbound buffer size is odd-length, then we have to
// * adjust when the first operation is non-decimation, since
// * that will produce an outbound buffer 1 sample larger */
// decimated = new float[half + 1];
// }
//
// int decimatedPointer = 0;
//
// for(int x = 0; x < samples.length; x += 2)
// {
// /* Insert the sample but don't filter */
// if(mDecimateFlag)
// {
// mIFilter.insert(samples[x]);
// mQFilter.insert(samples[x + 1]);
// }
// else
// {
// decimated[decimatedPointer++] = mIFilter.filter(samples[x]);
// decimated[decimatedPointer++] = mQFilter.filter(samples[x + 1]);
// }
//
// /* Toggle the decimation flag for every sample */
// mDecimateFlag = !mDecimateFlag;
// }
//
// //return buffer
// }
// else
// {
// /* Non Decimate - filter each of the values and return the buffer */
// float[] samples = buffer.getSamples();
//
// for(int x = 0; x < samples.length; x += 2)
// {
// samples[x] = mIFilter.filter(samples[x]);
// samples[x + 1] = mQFilter.filter(samples[x + 1]);
// }
//
// //return buffer
// }
}
}

View File

@@ -1,117 +0,0 @@
package io.github.dsheirer.dsp.filter.halfband.complex;
import io.github.dsheirer.dsp.filter.halfband.real.HalfBandFilter;
import io.github.dsheirer.sample.Listener;
import io.github.dsheirer.sample.complex.ComplexBuffer;
public class HalfBandFilter_CB_CB implements Listener<ComplexBuffer>
{
private HalfBandFilter mIFilter;
private HalfBandFilter mQFilter;
private boolean mDecimate;
private boolean mDecimateFlag;
private Listener<ComplexBuffer> mListener;
/**
*
* @param coefficients
* @param gain
* @param decimate
*/
public HalfBandFilter_CB_CB( float[] coefficients, float gain, boolean decimate )
{
mIFilter = new HalfBandFilter( coefficients, gain );
mQFilter = new HalfBandFilter( coefficients, gain );
mDecimate = decimate;
}
public void dispose()
{
mIFilter.dispose();
mQFilter.dispose();
mListener = null;
}
@Override
public void receive( ComplexBuffer buffer )
{
if( mListener != null )
{
if( mDecimate )
{
float[] samples = buffer.getSamples();
int half = samples.length / 2;
float[] decimated;
if( half % 2 == 0 || mDecimateFlag )
{
decimated = new float[ half ];
}
else
{
/* If inbound buffer size is odd-length, then we have to
* adjust when the first operation is non-decimation, since
* that will produce an outbound buffer 1 sample larger */
decimated = new float[ half + 1 ];
}
int decimatedPointer = 0;
for( int x = 0; x < samples.length; x += 2 )
{
/* Insert the sample but don't filter */
if( mDecimateFlag )
{
mIFilter.insert( samples[ x ] );
mQFilter.insert( samples[ x + 1 ] );
}
else
{
decimated[ decimatedPointer++ ] = mIFilter.filter( samples[ x ] );
decimated[ decimatedPointer++ ] = mQFilter.filter( samples[ x + 1 ] );
}
/* Toggle the decimation flag for every sample */
mDecimateFlag = !mDecimateFlag;
}
if( mListener != null )
{
mListener.receive( new ComplexBuffer( decimated ) );
}
}
else
{
/* Non Decimate - filter each of the values and return the buffer */
float[] samples = buffer.getSamples();
for( int x = 0; x < samples.length; x += 2 )
{
samples[ x ] = mIFilter.filter( samples[ x ] );
samples[ x + 1 ] = mQFilter.filter( samples[ x + 1 ] );
}
if( mListener != null )
{
mListener.receive( buffer );
}
}
}
}
public void setListener( Listener<ComplexBuffer> listener )
{
mListener = listener;
}
public void removeListener()
{
mListener = null;
}
}

View File

@@ -1,162 +1,188 @@
/*******************************************************************************
* SDR Trunk
* Copyright (C) 2014,2015 Dennis Sheirer
*
*
* This program is free software: you can redistribute it and/or modify
* it under the terms of the GNU General Public License as published by
* the Free Software Foundation, either version 3 of the License, or
* (at your option) any later version.
*
*
* This program is distributed in the hope that it will be useful,
* but WITHOUT ANY WARRANTY; without even the implied warranty of
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
* GNU General Public License for more details.
*
*
* You should have received a copy of the GNU General Public License
* along with this program. If not, see <http://www.gnu.org/licenses/>
******************************************************************************/
package io.github.dsheirer.dsp.filter.halfband.real;
import io.github.dsheirer.sample.buffer.ReusableBuffer;
import io.github.dsheirer.sample.buffer.ReusableBufferQueue;
import org.apache.commons.lang3.Validate;
public class HalfBandFilter
{
private float[] mBuffer;
private float[] mBuffer;
private int mBufferSize = 1; //Temporary initial value
private int mBufferPointer = 0;
private float mGain;
private float[] mCoefficients;
private int[][] mIndexMap;
private int mCenterCoefficient;
private int mCenterCoefficientMapIndex;
private int mBufferPointer = 0;
/**
* Half-Band filter for processing real float samples.
*
* Takes advantage of the 0-valued FIR half-band coefficents inherent
* in the half-band filter and does not calculate those coefficients.
*
* This reduces the workload to (tap-size - 1) / 4 + 1 calculations per sample.
*
* @param filter - filter coefficients
* @param gain - gain multiplier. Use 1.0 for unity/no gain
*/
public HalfBandFilter( float[] coefficients, float gain )
{
mCoefficients = coefficients;
mGain = gain;
mBufferSize = mCoefficients.length;
mBuffer = new float[ mBufferSize ];
generateIndexMap( mCoefficients.length );
}
private float mGain;
private float[] mCoefficients;
private int[][] mIndexMap;
public void dispose()
{
}
private int mCenterCoefficient;
private int mCenterCoefficientMapIndex;
/**
* Inserts the sample into the buffer without calculating a filtered value
*/
public void insert( float sample )
{
mBuffer[ mBufferPointer ] = sample;
private ReusableBufferQueue mReusableBufferQueue = new ReusableBufferQueue("HalfBandFilter");
mBufferPointer++;
mBufferPointer = mBufferPointer % mBufferSize;
}
/**
* Half-Band filter for processing real float samples.
*
* Takes advantage of the 0-valued FIR half-band coefficents inherent
* in the half-band filter and does not calculate those coefficients.
*
* This reduces the workload to (tap-size - 1) / 4 + 1 calculations per sample.
*
* @param filter - filter coefficients
* @param gain - gain multiplier. Use 1.0 for unity/no gain
*/
public HalfBandFilter(float[] coefficients, float gain)
{
//TODO: update this to use SIMD structures with 2 float arrays and a toggle operation
/**
* Calculate the filtered value by applying the coefficients against
* the complex samples in mBuffer
*/
public float filter( float sample )
{
insert( sample );
float accumulator = 0;
//Start with the center tap value
accumulator += mCoefficients[ mCenterCoefficient ] *
mBuffer[ mIndexMap[ mBufferPointer ][ mCenterCoefficientMapIndex ] ];
//For the remaining coefficients, add the symmetric samples, oldest and newest
//first, then multiply by the single coefficient
for( int x = 0; x < mCenterCoefficientMapIndex; x += 2 )
{
accumulator += mCoefficients[ x ] *
( mBuffer[ mIndexMap[ mBufferPointer ][ x ] ] +
mBuffer[ mIndexMap[ mBufferPointer ][ x + 1 ] ] );
}
mCoefficients = coefficients;
mGain = gain;
return accumulator * mGain;
}
/**
* Creates an n X (n + 1 / 2) index map enabling quick access to the
* circular buffer samples.
*
* As the buffer shifts right with each subsequent sample, we have to move
* the index pointers with it, for efficient access of the samples.
*
* The first array index value in the index map corresponds to the current
* buffer pointer location.
*
* The second array index value points to the samples that should be
* multiplied by the coefficients as follows:
*
* 0 = center tap sample, to be multiplied by center coefficient
*
* 0 = sample( 1 )
* 1 = sample( size - 1 )
*
* Indexes 0 and 1 will be multiplied by coefficient( 0 ).
*
* Subsequent indexes 3, 4, etc, point to the oldest and newest samples that
* correspond to the matching ( 3 ) coefficient index.
*
* @param odd-sized number of filter taps (ie coefficients) and buffer
*/
private void generateIndexMap( int size )
{
//Ensure we have an odd size
Validate.isTrue(size % 2 == 1);
mBufferSize = mCoefficients.length;
mBuffer = new float[mBufferSize];
int mapWidth = ( ( size + 1 ) / 2 ) + 1;
//Set center tap index for coefficients array
mCenterCoefficient = ( size - 1 ) / 2;
mCenterCoefficientMapIndex = mCenterCoefficient + 1;
generateIndexMap(mCoefficients.length);
}
mIndexMap = new int[ size ][ mapWidth ];
public HalfBandFilter(float[] coefficients, float gain, boolean decimate)
{
this(coefficients, gain);
//Setup the first row, buffer pointer index 0, as a starting point
for( int x = 0; x < mapWidth - 2; x += 2 )
{
mIndexMap[ 0 ][ x ] = x;
mIndexMap[ 0 ][ x + 1 ] = size - 1 - x;
}
throw new IllegalStateException("This filter must be updated to support non-decimate option");
}
//Place center tap index in last element
mIndexMap[ 0 ][ mCenterCoefficientMapIndex ] = mCenterCoefficient;
//For each subsequent row, increment the previous row's value by 1,
//subtracting size as needed, to keep the values between 0 and size - 1
for( int x = 1; x < size; x++ )
{
for( int y = 0; y < mapWidth; y++ )
{
mIndexMap[ x ][ y ] = mIndexMap[ x - 1 ][ y ] + 1;
if( mIndexMap[ x ][ y ] >= size )
{
mIndexMap[ x ][ y ] -= size;
}
}
}
}
public void dispose()
{
}
/**
* Inserts the sample into the buffer without calculating a filtered value
*/
public void insert(float sample)
{
mBuffer[mBufferPointer] = sample;
mBufferPointer++;
mBufferPointer = mBufferPointer % mBufferSize;
}
public ReusableBuffer filter(ReusableBuffer input)
{
float[] inputSamples = input.getSamples();
ReusableBuffer output = mReusableBufferQueue.getBuffer(inputSamples.length);
float[] outputSamples = output.getSamples();
for(int x = 0; x < inputSamples.length; x++)
{
outputSamples[x] = filter(inputSamples[x]);
}
input.decrementUserCount();
return output;
}
/**
* Calculate the filtered value by applying the coefficients against the samples in mBuffer
*/
public float filter(float sample)
{
insert(sample);
float accumulator = 0;
//Start with the center tap value
accumulator += mCoefficients[mCenterCoefficient] * mBuffer[mIndexMap[mBufferPointer][mCenterCoefficientMapIndex]];
//For the remaining coefficients, add the symmetric samples, oldest and newest
//first, then multiply by the single coefficient
for(int x = 0; x < mCenterCoefficientMapIndex; x += 2)
{
accumulator += mCoefficients[x] * (mBuffer[mIndexMap[mBufferPointer][x]] + mBuffer[mIndexMap[mBufferPointer][x + 1]]);
}
return accumulator * mGain;
}
/**
* Creates an n X (n + 1 / 2) index map enabling quick access to the
* circular buffer samples.
*
* As the buffer shifts right with each subsequent sample, we have to move
* the index pointers with it, for efficient access of the samples.
*
* The first array index value in the index map corresponds to the current
* buffer pointer location.
*
* The second array index value points to the samples that should be
* multiplied by the coefficients as follows:
*
* 0 = center tap sample, to be multiplied by center coefficient
*
* 0 = sample( 1 )
* 1 = sample( size - 1 )
*
* Indexes 0 and 1 will be multiplied by coefficient( 0 ).
*
* Subsequent indexes 3, 4, etc, point to the oldest and newest samples that
* correspond to the matching ( 3 ) coefficient index.
*
* @param odd-sized number of filter taps (ie coefficients) and buffer
*/
private void generateIndexMap(int size)
{
//Ensure we have an odd size
Validate.isTrue(size % 2 == 1);
int mapWidth = ((size + 1) / 2) + 1;
//Set center tap index for coefficients array
mCenterCoefficient = (size - 1) / 2;
mCenterCoefficientMapIndex = mCenterCoefficient + 1;
mIndexMap = new int[size][mapWidth];
//Setup the first row, buffer pointer index 0, as a starting point
for(int x = 0; x < mapWidth - 2; x += 2)
{
mIndexMap[0][x] = x;
mIndexMap[0][x + 1] = size - 1 - x;
}
//Place center tap index in last element
mIndexMap[0][mCenterCoefficientMapIndex] = mCenterCoefficient;
//For each subsequent row, increment the previous row's value by 1,
//subtracting size as needed, to keep the values between 0 and size - 1
for(int x = 1; x < size; x++)
{
for(int y = 0; y < mapWidth; y++)
{
mIndexMap[x][y] = mIndexMap[x - 1][y] + 1;
if(mIndexMap[x][y] >= size)
{
mIndexMap[x][y] -= size;
}
}
}
}
}

View File

@@ -1,117 +0,0 @@
package io.github.dsheirer.dsp.filter.halfband.real;
import io.github.dsheirer.sample.Listener;
import io.github.dsheirer.sample.real.RealBuffer;
public class HalfBandFilter_RB_RB extends HalfBandFilter implements Listener<RealBuffer>
{
private boolean mDecimate;
private boolean mDecimateFlag;
private Listener<RealBuffer> mListener;
/**
* Halfband filter for real buffers with optional decimation by two.
*
* If this filter is used in non-decimating mode, the filter will overwrite
* the inbound sample buffer with the filtered samples. This will cause
* issues if the buffers are being used across several processes are sharing
* the buffer, since this filter will modify the contents of the object, so
* consider making a copy of the buffer prior to feeding the buffer to this
* filter.
*
* If used in decimating mode, a new (half-length) buffer will be created
* for the filtered samples.
*
* @param coefficients - filter kernel
* @param gain - gain to apply to the outputs
* @param decimate - true for decimate by 2, or false for no decimation
*/
public HalfBandFilter_RB_RB( float[] coefficients, float gain, boolean decimate )
{
super( coefficients, gain );
mDecimate = decimate;
}
public void dispose()
{
super.dispose();
mListener = null;
}
@Override
public void receive( RealBuffer buffer )
{
if( mListener != null )
{
mListener.receive( filter( buffer ) );
}
}
public RealBuffer filter( RealBuffer buffer )
{
if( mDecimate )
{
float[] samples = buffer.getSamples();
int half = samples.length / 2;
float[] decimated;
if( half % 2 == 0 || mDecimateFlag )
{
decimated = new float[ half ];
}
else
{
/* If inbound buffer size is odd-length, then we have to
* adjust when the first operation is non-decimation, since
* that will produce an outbound buffer 1 sample larger */
decimated = new float[ half + 1 ];
}
int decimatedPointer = 0;
for( float sample: samples )
{
/* Insert the sample but don't filter */
if( mDecimateFlag )
{
insert( sample );
}
else
{
decimated[ decimatedPointer++ ] = filter( sample );
}
/* Toggle the decimation flag for every sample */
mDecimateFlag = !mDecimateFlag;
}
return new RealBuffer( decimated );
}
else
{
/* Non Decimate - filter each of the values and return the buffer */
float[] samples = buffer.getSamples();
for( int x = 0; x < samples.length; x++ )
{
samples[ x ] = filter( samples[ x ] );
}
return buffer;
}
}
public void setListener( Listener<RealBuffer> listener )
{
mListener = listener;
}
public void removeListener()
{
mListener = null;
}
}

Some files were not shown because too many files have changed in this diff Show More