diff --git a/.idea/dictionaries/denny.xml b/.idea/dictionaries/denny.xml
new file mode 100644
index 00000000..fc474e43
--- /dev/null
+++ b/.idea/dictionaries/denny.xml
@@ -0,0 +1,31 @@
+
+
+
+ bahroo
+ broadcastify
+ correlator
+ dinf
+ esnid
+ fleetsync
+ heterdyned
+ heterodynes
+ icecast
+ ident
+ nutall
+ overflowable
+ polyphase
+ remez
+ resampler
+ samplerate
+ sdrtrunk
+ sheirer
+ shoutcast
+ streamable
+ talkgroup
+ tgid
+ ultravox
+ wacn
+ xtea
+
+
+
\ No newline at end of file
diff --git a/.idea/libraries/imports.xml b/.idea/libraries/imports.xml
new file mode 100644
index 00000000..b90eef82
--- /dev/null
+++ b/.idea/libraries/imports.xml
@@ -0,0 +1,14 @@
+
+
+
+
+
+
+
+
+
+
+
+
+
+
\ No newline at end of file
diff --git a/.idea/misc.xml b/.idea/misc.xml
index 05483570..d5d79e0c 100644
--- a/.idea/misc.xml
+++ b/.idea/misc.xml
@@ -1,6 +1,6 @@
-
+
\ No newline at end of file
diff --git a/.idea/vcs.xml b/.idea/vcs.xml
index 94a25f7f..fef64bf1 100644
--- a/.idea/vcs.xml
+++ b/.idea/vcs.xml
@@ -1,5 +1,15 @@
+
+
+
+
+
+
+
+
+
+
diff --git a/copyright_header.txt b/copyright_header.txt
deleted file mode 100644
index 3eaea8b8..00000000
--- a/copyright_header.txt
+++ /dev/null
@@ -1,17 +0,0 @@
-/*******************************************************************************
- * SDR Trunk
- * Copyright (C) 2014-2016 Dennis Sheirer
- *
- * This program is free software: you can redistribute it and/or modify
- * it under the terms of the GNU General Public License as published by
- * the Free Software Foundation, either version 3 of the License, or
- * (at your option) any later version.
- *
- * This program is distributed in the hope that it will be useful,
- * but WITHOUT ANY WARRANTY; without even the implied warranty of
- * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
- * GNU General Public License for more details.
- *
- * You should have received a copy of the GNU General Public License
- * along with this program. If not, see
- ******************************************************************************/
diff --git a/imports/commons-math3-3.6.1.jar b/imports/commons-math3-3.6.1.jar
new file mode 100644
index 00000000..0ff582cf
Binary files /dev/null and b/imports/commons-math3-3.6.1.jar differ
diff --git a/sdrtrunk.iml b/sdrtrunk.iml
new file mode 100644
index 00000000..390626c0
--- /dev/null
+++ b/sdrtrunk.iml
@@ -0,0 +1,14 @@
+
+
+
+
+
+
+
+
+
+
+
+
+
+
\ No newline at end of file
diff --git a/src/main/java/io/github/dsheirer/audio/AudioManager.java b/src/main/java/io/github/dsheirer/audio/AudioManager.java
index c0f1d877..e48cf36a 100644
--- a/src/main/java/io/github/dsheirer/audio/AudioManager.java
+++ b/src/main/java/io/github/dsheirer/audio/AudioManager.java
@@ -24,6 +24,7 @@ import io.github.dsheirer.audio.output.StereoAudioOutput;
import io.github.dsheirer.properties.SystemProperties;
import io.github.dsheirer.sample.Broadcaster;
import io.github.dsheirer.sample.Listener;
+import io.github.dsheirer.sample.buffer.ReusableAudioPacket;
import io.github.dsheirer.source.mixer.MixerChannel;
import io.github.dsheirer.source.mixer.MixerChannelConfiguration;
import io.github.dsheirer.source.mixer.MixerManager;
@@ -43,7 +44,7 @@ import java.util.concurrent.LinkedTransferQueue;
import java.util.concurrent.ScheduledFuture;
import java.util.concurrent.TimeUnit;
-public class AudioManager implements Listener, IAudioController
+public class AudioManager implements Listener, IAudioController
{
private static final Logger mLog = LoggerFactory.getLogger(AudioManager.class);
@@ -58,7 +59,7 @@ public class AudioManager implements Listener, IAudioController
public static final AudioEvent CONFIGURATION_CHANGE_COMPLETE =
new AudioEvent(AudioEvent.Type.AUDIO_CONFIGURATION_CHANGE_COMPLETE, null);
- private LinkedTransferQueue mAudioPacketQueue = new LinkedTransferQueue<>();
+ private LinkedTransferQueue mAudioPacketQueue = new LinkedTransferQueue<>();
private Map mChannelConnectionMap = new HashMap<>();
private List mAudioOutputConnections = new ArrayList<>();
private AudioOutputConnection mLowestPriorityConnection;
@@ -170,7 +171,7 @@ public class AudioManager implements Listener, IAudioController
* Primary ingest point for audio produced by all decoding channels, for distribution to audio playback devices.
*/
@Override
- public synchronized void receive(AudioPacket packet)
+ public synchronized void receive(ReusableAudioPacket packet)
{
mAudioPacketQueue.add(packet);
}
@@ -362,7 +363,7 @@ public class AudioManager implements Listener, IAudioController
* @param audioPacket from a decoding channel source
* @return an audio output connection or null
*/
- private AudioOutputConnection getConnection(AudioPacket audioPacket)
+ private AudioOutputConnection getConnection(ReusableAudioPacket audioPacket)
{
int channelMetadataID = audioPacket.getMetadata().getMetadataID();
@@ -410,6 +411,8 @@ public class AudioManager implements Listener, IAudioController
public class AudioPacketProcessor implements Runnable
{
+ private List mPackets = new ArrayList<>();
+
@Override
public void run()
{
@@ -419,14 +422,12 @@ public class AudioManager implements Listener, IAudioController
if(mAudioPacketQueue != null)
{
- List packets = new ArrayList();
+ mAudioPacketQueue.drainTo(mPackets);
- mAudioPacketQueue.drainTo(packets);
-
- for(AudioPacket packet : packets)
+ for(ReusableAudioPacket packet : mPackets)
{
/* Don't process any packet's marked as do not monitor */
- if(!packet.getMetadata().isDoNotMonitor() && packet.getType() == AudioPacket.Type.AUDIO)
+ if(!packet.getMetadata().isDoNotMonitor() && packet.getType() == ReusableAudioPacket.Type.AUDIO)
{
AudioOutputConnection connection = getConnection(packet);
@@ -434,13 +435,29 @@ public class AudioManager implements Listener, IAudioController
{
connection.receive(packet);
}
+ else
+ {
+ packet.decrementUserCount();
+ }
+ }
+ else
+ {
+ packet.decrementUserCount();
}
}
+
+ mPackets.clear();
}
}
catch(Exception e)
{
mLog.error("Encountered error while processing audio packets", e);
+
+ while(mPackets.size() > 0)
+ {
+ ReusableAudioPacket audioPacket = mPackets.remove(0);
+ audioPacket.decrementUserCount();
+ }
}
}
}
@@ -463,7 +480,7 @@ public class AudioManager implements Listener, IAudioController
mAudioOutput = audioOutput;
}
- public void receive(AudioPacket packet)
+ public void receive(ReusableAudioPacket packet)
{
if(packet.hasMetadata() && packet.getMetadata().getMetadataID() == mChannelMetadataID)
{
diff --git a/src/main/java/io/github/dsheirer/audio/AudioModule.java b/src/main/java/io/github/dsheirer/audio/AudioModule.java
index 369dd8ed..b11f8cb0 100644
--- a/src/main/java/io/github/dsheirer/audio/AudioModule.java
+++ b/src/main/java/io/github/dsheirer/audio/AudioModule.java
@@ -1,19 +1,16 @@
/*******************************************************************************
- * sdrtrunk
- * Copyright (C) 2014-2017 Dennis Sheirer
+ * sdr-trunk
+ * Copyright (C) 2014-2018 Dennis Sheirer
*
- * This program is free software: you can redistribute it and/or modify
- * it under the terms of the GNU General Public License as published by
- * the Free Software Foundation, either version 3 of the License, or
- * (at your option) any later version.
+ * This program is free software: you can redistribute it and/or modify it under the terms of the GNU General Public
+ * License as published by the Free Software Foundation, either version 3 of the License, or (at your option) any
+ * later version.
*
- * This program is distributed in the hope that it will be useful,
- * but WITHOUT ANY WARRANTY; without even the implied warranty of
- * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
- * GNU General Public License for more details.
+ * This program is distributed in the hope that it will be useful, but WITHOUT ANY WARRANTY; without even the implied
+ * warranty of MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU General Public License for more details.
*
- * You should have received a copy of the GNU General Public License
- * along with this program. If not, see
+ * You should have received a copy of the GNU General Public License along with this program.
+ * If not, see
*
******************************************************************************/
package io.github.dsheirer.audio;
@@ -23,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
+public class AudioModule extends Module implements IAudioPacketProvider, IReusableBufferListener,
+ ISquelchStateListener, Listener
{
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 mAudioPacketListener;
- private PolyphaseFIRDecimatingFilter_RB mAudioDecimationFilter;
+ private Listener mAudioPacketListener;
private Metadata mMetadata;
+ /**
+ * Creates an Audio Module.
+ *
+ * @param metadata to use for audio packets produced by this audio module.
+ */
public AudioModule(Metadata metadata)
{
mMetadata = metadata;
-
- mAudioDecimationFilter = new PolyphaseFIRDecimatingFilter_RB(mDecimationCoefficients, 6, 2.0f);
- mAudioDecimationFilter.setListener(new Listener()
- {
- @Override
- public void receive(RealBuffer realBuffer)
- {
- if(mAudioPacketListener != null)
- {
- AudioPacket packet = new AudioPacket(realBuffer.getSamples(), mMetadata.copyOf());
- mAudioPacketListener.receive(packet);
- }
- }
- });
}
@Override
public void dispose()
{
- mSquelchStateListener = null;
mAudioPacketListener = null;
+ mSquelchStateListener = null;
}
@Override
@@ -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 getFilteredRealBufferListener()
- {
- return this;
- }
-
- @Override
- public void setAudioPacketListener(Listener listener)
+ public void setAudioPacketListener(Listener 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;
diff --git a/src/main/java/io/github/dsheirer/audio/AudioPacket.java b/src/main/java/io/github/dsheirer/audio/AudioPacket.java
deleted file mode 100644
index a57f2bdb..00000000
--- a/src/main/java/io/github/dsheirer/audio/AudioPacket.java
+++ /dev/null
@@ -1,78 +0,0 @@
-/*******************************************************************************
- * sdrtrunk
- * Copyright (C) 2014-2017 Dennis Sheirer
- *
- * This program is free software: you can redistribute it and/or modify
- * it under the terms of the GNU General Public License as published by
- * the Free Software Foundation, either version 3 of the License, or
- * (at your option) any later version.
- *
- * This program is distributed in the hope that it will be useful,
- * but WITHOUT ANY WARRANTY; without even the implied warranty of
- * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
- * GNU General Public License for more details.
- *
- * You should have received a copy of the GNU General Public License
- * along with this program. If not, see
- *
- ******************************************************************************/
-package io.github.dsheirer.audio;
-
-import io.github.dsheirer.channel.metadata.Metadata;
-import io.github.dsheirer.sample.real.RealBuffer;
-
-public class AudioPacket
-{
- private Type mType;
- private RealBuffer mAudioData;
- private Metadata mMetadata;
-
- public AudioPacket(Type type, Metadata metadata)
- {
- mType = type;
- mMetadata = metadata;
- }
-
- public AudioPacket(float[] audio, Metadata metadata)
- {
- this(Type.AUDIO, metadata);
-
- mAudioData = new RealBuffer(audio);
- }
-
- public boolean hasMetadata()
- {
- return mMetadata != null;
- }
-
- public Metadata getMetadata()
- {
- return mMetadata;
- }
-
- public void setMetadata(Metadata metadata)
- {
- mMetadata = metadata;
- }
-
- public Type getType()
- {
- return mType;
- }
-
- public RealBuffer getAudioBuffer()
- {
- return mAudioData;
- }
-
- public boolean hasAudioBuffer()
- {
- return mAudioData != null;
- }
-
- public enum Type
- {
- AUDIO,
- END;
- }
-}
diff --git a/src/main/java/io/github/dsheirer/audio/AudioPacketRateCounter.java b/src/main/java/io/github/dsheirer/audio/AudioPacketRateCounter.java
deleted file mode 100644
index fbc7ea0d..00000000
--- a/src/main/java/io/github/dsheirer/audio/AudioPacketRateCounter.java
+++ /dev/null
@@ -1,55 +0,0 @@
-package io.github.dsheirer.audio;
-
-import io.github.dsheirer.sample.Listener;
-import org.slf4j.Logger;
-import org.slf4j.LoggerFactory;
-
-public class AudioPacketRateCounter implements Listener
-{
- private final static Logger mLog =
- LoggerFactory.getLogger( AudioPacketRateCounter.class );
-
- private Listener mListener;
- private long mCount;
- private long mStartTime = -1;
-
- private String mName;
-
- public AudioPacketRateCounter()
- {
- mName = "UNNAMED";
- }
-
- public AudioPacketRateCounter( String name )
- {
- mName = name;
- }
-
- @Override
- public void receive( AudioPacket packet )
- {
- if( mStartTime == -1 )
- {
- mStartTime = System.currentTimeMillis();
- }
-
- mCount += packet.getAudioBuffer().getSamples().length;
-
- long elapsed = System.currentTimeMillis() - mStartTime;
-
- if( elapsed != 0 )
- {
- mLog.debug( "[" + mName + "] Audio Rate: " + ( mCount / elapsed ) + " kHz" );
- }
-
- if( mListener != null )
- {
- mListener.receive( packet );
- }
- }
-
- public void setListener( Listener listener )
- {
- mListener = listener;
- }
-}
diff --git a/src/main/java/io/github/dsheirer/audio/AudioUtils.java b/src/main/java/io/github/dsheirer/audio/AudioUtils.java
index 08b4d3d8..1eca2361 100644
--- a/src/main/java/io/github/dsheirer/audio/AudioUtils.java
+++ b/src/main/java/io/github/dsheirer/audio/AudioUtils.java
@@ -1,24 +1,22 @@
/*******************************************************************************
- * sdrtrunk
- * Copyright (C) 2014-2016 Dennis Sheirer
+ * sdr-trunk
+ * Copyright (C) 2014-2018 Dennis Sheirer
*
- * This program is free software: you can redistribute it and/or modify
- * it under the terms of the GNU General Public License as published by
- * the Free Software Foundation, either version 3 of the License, or
- * (at your option) any later version.
+ * This program is free software: you can redistribute it and/or modify it under the terms of the GNU General Public
+ * License as published by the Free Software Foundation, either version 3 of the License, or (at your option) any
+ * later version.
*
- * This program is distributed in the hope that it will be useful,
- * but WITHOUT ANY WARRANTY; without even the implied warranty of
- * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
- * GNU General Public License for more details.
+ * This program is distributed in the hope that it will be useful, but WITHOUT ANY WARRANTY; without even the implied
+ * warranty of MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU General Public License for more details.
*
- * You should have received a copy of the GNU General Public License
- * along with this program. If not, see
+ * You should have received a copy of the GNU General Public License along with this program.
+ * If not, see
*
******************************************************************************/
package io.github.dsheirer.audio;
import io.github.dsheirer.sample.ConversionUtils;
+import io.github.dsheirer.sample.buffer.ReusableAudioPacket;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
@@ -34,17 +32,18 @@ public class AudioUtils
/**
* Converts the audio packets into a byte array of 16-bit, little-endian audio samples
*/
- public static byte[] convertTo16BitSamples(List audioPackets)
+ public static byte[] convertTo16BitSamples(List audioPackets)
{
ByteArrayOutputStream stream = new ByteArrayOutputStream();
try
{
- for(AudioPacket packet: audioPackets)
+ for(ReusableAudioPacket audioPacket: audioPackets)
{
//Converting from 32-bit floats to signed 16-bit samples
- ByteBuffer buffer = ConversionUtils.convertToSigned16BitSamples(packet.getAudioBuffer().getSamples());
+ ByteBuffer buffer = ConversionUtils.convertToSigned16BitSamples(audioPacket.getAudioSamples());
stream.write(buffer.array());
+ audioPacket.decrementUserCount();
}
}
catch(IOException e)
diff --git a/src/main/java/io/github/dsheirer/audio/IAudioPacketListener.java b/src/main/java/io/github/dsheirer/audio/IAudioPacketListener.java
index 96833a73..76452704 100644
--- a/src/main/java/io/github/dsheirer/audio/IAudioPacketListener.java
+++ b/src/main/java/io/github/dsheirer/audio/IAudioPacketListener.java
@@ -1,8 +1,9 @@
package io.github.dsheirer.audio;
import io.github.dsheirer.sample.Listener;
+import io.github.dsheirer.sample.buffer.ReusableAudioPacket;
public interface IAudioPacketListener
{
- public Listener getAudioPacketListener();
+ public Listener getAudioPacketListener();
}
diff --git a/src/main/java/io/github/dsheirer/audio/IAudioPacketProvider.java b/src/main/java/io/github/dsheirer/audio/IAudioPacketProvider.java
index b6623cee..7fc00a58 100644
--- a/src/main/java/io/github/dsheirer/audio/IAudioPacketProvider.java
+++ b/src/main/java/io/github/dsheirer/audio/IAudioPacketProvider.java
@@ -1,9 +1,10 @@
package io.github.dsheirer.audio;
import io.github.dsheirer.sample.Listener;
+import io.github.dsheirer.sample.buffer.ReusableAudioPacket;
public interface IAudioPacketProvider
{
- public void setAudioPacketListener( Listener listener );
+ public void setAudioPacketListener( Listener listener );
public void removeAudioPacketListener();
}
diff --git a/src/main/java/io/github/dsheirer/audio/broadcast/BroadcastModel.java b/src/main/java/io/github/dsheirer/audio/broadcast/BroadcastModel.java
index fc7c9eac..986306fd 100644
--- a/src/main/java/io/github/dsheirer/audio/broadcast/BroadcastModel.java
+++ b/src/main/java/io/github/dsheirer/audio/broadcast/BroadcastModel.java
@@ -19,12 +19,12 @@
package io.github.dsheirer.audio.broadcast;
import io.github.dsheirer.alias.id.broadcast.BroadcastChannel;
-import io.github.dsheirer.audio.AudioPacket;
import io.github.dsheirer.channel.metadata.Metadata;
import io.github.dsheirer.icon.IconManager;
import io.github.dsheirer.properties.SystemProperties;
import io.github.dsheirer.sample.Broadcaster;
import io.github.dsheirer.sample.Listener;
+import io.github.dsheirer.sample.buffer.ReusableAudioPacket;
import io.github.dsheirer.util.ThreadPool;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
@@ -46,7 +46,7 @@ import java.util.function.Consumer;
import java.util.regex.Matcher;
import java.util.regex.Pattern;
-public class BroadcastModel extends AbstractTableModel implements Listener
+public class BroadcastModel extends AbstractTableModel implements Listener
{
private final static Logger mLog = LoggerFactory.getLogger(BroadcastModel.class);
@@ -276,7 +276,7 @@ public class BroadcastModel extends AbstractTableModel implements Listener
+public class StreamManager implements Listener
{
private final static Logger mLog = LoggerFactory.getLogger(StreamManager.class);
private static final long MAXIMUM_RECORDER_LIFESPAN_MILLIS = 30000; //30 seconds
@@ -115,7 +115,7 @@ public class StreamManager implements Listener
}
@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
{
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
if(recorder != null)
{
+ audioPacket.incrementUserCount();
recorder.receive(audioPacket);
}
}
@@ -140,12 +141,13 @@ public class StreamManager implements Listener
{
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.decrementUserCount();
}
/**
diff --git a/src/main/java/io/github/dsheirer/audio/convert/IAudioConverter.java b/src/main/java/io/github/dsheirer/audio/convert/IAudioConverter.java
index fd738f13..2af24280 100644
--- a/src/main/java/io/github/dsheirer/audio/convert/IAudioConverter.java
+++ b/src/main/java/io/github/dsheirer/audio/convert/IAudioConverter.java
@@ -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 audioPackets);
+ public byte[] convert(List audioPackets);
/**
* Finalizes audio conversion by fully converting any partial frames left in the buffer and returning the
diff --git a/src/main/java/io/github/dsheirer/audio/convert/MP3AudioConverter.java b/src/main/java/io/github/dsheirer/audio/convert/MP3AudioConverter.java
index 1a8c86cb..704f1313 100644
--- a/src/main/java/io/github/dsheirer/audio/convert/MP3AudioConverter.java
+++ b/src/main/java/io/github/dsheirer/audio/convert/MP3AudioConverter.java
@@ -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 audioPackets)
+ public byte[] convert(List audioPackets)
{
mMP3Stream.reset();
diff --git a/src/main/java/io/github/dsheirer/audio/convert/MP3SilenceGenerator.java b/src/main/java/io/github/dsheirer/audio/convert/MP3SilenceGenerator.java
index e9e429f3..6329db42 100644
--- a/src/main/java/io/github/dsheirer/audio/convert/MP3SilenceGenerator.java
+++ b/src/main/java/io/github/dsheirer/audio/convert/MP3SilenceGenerator.java
@@ -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 silencePackets = new ArrayList<>();
+ ReusableAudioPacket silencePacket = mAudioPacketQueue.getBuffer(length);
+ Arrays.fill(silencePacket.getAudioSamples(), 0.0f);
+
+ List silencePackets = new ArrayList<>();
silencePackets.add(silencePacket);
byte[] frameData = mGenerator.convert(silencePackets);
diff --git a/src/main/java/io/github/dsheirer/audio/invert/AudioAdapter.java b/src/main/java/io/github/dsheirer/audio/invert/AudioAdapter.java
index 090c002c..d3e1d269 100644
--- a/src/main/java/io/github/dsheirer/audio/invert/AudioAdapter.java
+++ b/src/main/java/io/github/dsheirer/audio/invert/AudioAdapter.java
@@ -20,6 +20,7 @@ package io.github.dsheirer.audio.invert;
import io.github.dsheirer.dsp.filter.FilterFactory;
import io.github.dsheirer.dsp.filter.FloatFIRFilter;
import io.github.dsheirer.dsp.filter.Window.WindowType;
+import io.github.dsheirer.dsp.mixer.IOscillator;
import io.github.dsheirer.dsp.mixer.Oscillator;
/**
@@ -29,7 +30,7 @@ import io.github.dsheirer.dsp.mixer.Oscillator;
public class AudioAdapter
{
private static int mGain = 5;
- private Oscillator mSineWaveGenerator;
+ private IOscillator mSineWaveGenerator;
private FloatFIRFilter mPostInversionFilter;
private FloatFIRFilter mAudioHighPassFilter;
private AudioType mAudioType = AudioType.NORMAL;
@@ -88,7 +89,7 @@ public class AudioAdapter
default:
retVal = (float)( mPostInversionFilter.get(
mAudioHighPassFilter.get( sample ) *
- mSineWaveGenerator.getFloat() ) );
+ mSineWaveGenerator.inphase() ) );
mSineWaveGenerator.rotate();
break;
diff --git a/src/main/java/io/github/dsheirer/audio/invert/AudioInverter.java b/src/main/java/io/github/dsheirer/audio/invert/AudioInverter.java
index a892b23c..63b6b6d9 100644
--- a/src/main/java/io/github/dsheirer/audio/invert/AudioInverter.java
+++ b/src/main/java/io/github/dsheirer/audio/invert/AudioInverter.java
@@ -20,6 +20,7 @@ package io.github.dsheirer.audio.invert;
import io.github.dsheirer.audio.InversionFrequency;
import io.github.dsheirer.dsp.filter.Filters;
import io.github.dsheirer.dsp.filter.FloatFIRFilter;
+import io.github.dsheirer.dsp.mixer.IOscillator;
import io.github.dsheirer.dsp.mixer.Oscillator;
import io.github.dsheirer.sample.real.RealSampleListener;
@@ -29,7 +30,7 @@ import io.github.dsheirer.sample.real.RealSampleListener;
*/
public class AudioInverter implements RealSampleListener
{
- private Oscillator mSineWaveGenerator;
+ private IOscillator mSineWaveGenerator;
private FloatFIRFilter mPostInversionLowPassFilter;
private RealSampleListener mListener;
@@ -61,7 +62,7 @@ public class AudioInverter implements RealSampleListener
if( mPostInversionLowPassFilter != null )
{
mPostInversionLowPassFilter.receive(
- sample * mSineWaveGenerator.getFloat() );
+ sample * mSineWaveGenerator.inphase() );
mSineWaveGenerator.rotate();
}
diff --git a/src/main/java/io/github/dsheirer/audio/output/AudioOutput.java b/src/main/java/io/github/dsheirer/audio/output/AudioOutput.java
index 0742543a..fa0af9e6 100644
--- a/src/main/java/io/github/dsheirer/audio/output/AudioOutput.java
+++ b/src/main/java/io/github/dsheirer/audio/output/AudioOutput.java
@@ -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, LineListener
+public abstract class AudioOutput implements Listener, LineListener
{
private final static Logger mLog = LoggerFactory.getLogger(AudioOutput.class);
- private LinkedTransferQueue mBuffer = new LinkedTransferQueue<>();
+ private LinkedTransferQueue mBuffer = new LinkedTransferQueue<>();
private int mBufferStartThreshold;
private int mBufferStopThreshold;
@@ -190,7 +190,7 @@ public abstract class AudioOutput implements Listener, 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, LineListener
}
@Override
- public void receive(AudioPacket packet)
+ public void receive(ReusableAudioPacket packet)
{
if(mCanProcessAudio)
{
@@ -281,11 +281,17 @@ public abstract class AudioOutput implements Listener, LineListener
mBuffer.add(packet);
}
+ else
+ {
+ packet.decrementUserCount();
+ }
}
public class BufferProcessor implements Runnable
{
private AtomicBoolean mProcessing = new AtomicBoolean();
+ private List mAudioPackets = new ArrayList();
+
public BufferProcessor()
{
@@ -300,13 +306,11 @@ public abstract class AudioOutput implements Listener, LineListener
* processor can run at any given time */
if(mProcessing.compareAndSet(false, true))
{
- List packets = new ArrayList();
+ 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, LineListener
updateTimestamp();
}
+ else
+ {
+ packet.decrementUserCount();
+ }
}
+ mAudioPackets.clear();
+
checkStop();
mProcessing.set(false);
diff --git a/src/main/java/io/github/dsheirer/audio/output/MonoAudioOutput.java b/src/main/java/io/github/dsheirer/audio/output/MonoAudioOutput.java
index aedbc62c..b1722b5f 100644
--- a/src/main/java/io/github/dsheirer/audio/output/MonoAudioOutput.java
+++ b/src/main/java/io/github/dsheirer/audio/output/MonoAudioOutput.java
@@ -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;
}
}
diff --git a/src/main/java/io/github/dsheirer/audio/output/StereoAudioOutput.java b/src/main/java/io/github/dsheirer/audio/output/StereoAudioOutput.java
index 96134621..7a7cc828 100644
--- a/src/main/java/io/github/dsheirer/audio/output/StereoAudioOutput.java
+++ b/src/main/java/io/github/dsheirer/audio/output/StereoAudioOutput.java
@@ -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;
}
}
diff --git a/src/main/java/io/github/dsheirer/audio/squelch/ManualSquelchController.java b/src/main/java/io/github/dsheirer/audio/squelch/ManualSquelchController.java
deleted file mode 100644
index b0e93ac4..00000000
--- a/src/main/java/io/github/dsheirer/audio/squelch/ManualSquelchController.java
+++ /dev/null
@@ -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
- ******************************************************************************/
-
-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
- }
-}
diff --git a/src/main/java/io/github/dsheirer/audio/squelch/SquelchController.java b/src/main/java/io/github/dsheirer/audio/squelch/SquelchController.java
deleted file mode 100644
index 43c88005..00000000
--- a/src/main/java/io/github/dsheirer/audio/squelch/SquelchController.java
+++ /dev/null
@@ -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
- ******************************************************************************/
-
-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, RealSampleListener
-{
- private Broadcaster mSquelchStateBroadcaster = new Broadcaster<>();
- private Listener 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 listener )
- {
- if( listener != null )
- {
- mSquelchStateBroadcaster.addListener( listener );
-
- /* Send current squelch state to the listener */
- listener.receive( mSquelchState );
- }
- }
-
- public void removeSquelchStateListener( Listener listener )
- {
- mSquelchStateBroadcaster.removeListener( listener );
- }
-
- public void setAudioListener( Listener listener )
- {
- mAudioListener = listener;
- }
-
- public void removeAudioListener( Listener listener )
- {
- mAudioListener = null;
- }
-}
diff --git a/src/main/java/io/github/dsheirer/audio/squelch/TrunkingSquelchController.java b/src/main/java/io/github/dsheirer/audio/squelch/TrunkingSquelchController.java
deleted file mode 100644
index fbb3e843..00000000
--- a/src/main/java/io/github/dsheirer/audio/squelch/TrunkingSquelchController.java
+++ /dev/null
@@ -1,55 +0,0 @@
-/*******************************************************************************
- * SDR Trunk
- * Copyright (C) 2015 Dennis Sheirer
- *
- * This program is free software: you can redistribute it and/or modify
- * it under the terms of the GNU General Public License as published by
- * the Free Software Foundation, either version 3 of the License, or
- * (at your option) any later version.
- *
- * This program is distributed in the hope that it will be useful,
- * but WITHOUT ANY WARRANTY; without even the implied warranty of
- * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
- * GNU General Public License for more details.
- *
- * You should have received a copy of the GNU General Public License
- * along with this program. If not, see
- ******************************************************************************/
-
-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;
- }
- }
-}
diff --git a/src/main/java/io/github/dsheirer/bits/IBinarySymbolProcessor.java b/src/main/java/io/github/dsheirer/bits/IBinarySymbolProcessor.java
new file mode 100644
index 00000000..a46185ad
--- /dev/null
+++ b/src/main/java/io/github/dsheirer/bits/IBinarySymbolProcessor.java
@@ -0,0 +1,21 @@
+/*******************************************************************************
+ * sdr-trunk
+ * Copyright (C) 2014-2018 Dennis Sheirer
+ *
+ * This program is free software: you can redistribute it and/or modify it under the terms of the GNU General Public
+ * License as published by the Free Software Foundation, either version 3 of the License, or (at your option) any
+ * later version.
+ *
+ * This program is distributed in the hope that it will be useful, but WITHOUT ANY WARRANTY; without even the implied
+ * warranty of MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU General Public License for more details.
+ *
+ * You should have received a copy of the GNU General Public License along with this program.
+ * If not, see
+ *
+ ******************************************************************************/
+package io.github.dsheirer.bits;
+
+public interface IBinarySymbolProcessor
+{
+ void receive(boolean symbol);
+}
diff --git a/src/main/java/io/github/dsheirer/bits/ISyncDetectListener.java b/src/main/java/io/github/dsheirer/bits/ISyncDetectListener.java
deleted file mode 100644
index bbe93440..00000000
--- a/src/main/java/io/github/dsheirer/bits/ISyncDetectListener.java
+++ /dev/null
@@ -1,6 +0,0 @@
-package io.github.dsheirer.bits;
-
-public interface ISyncDetectListener
-{
- public void syncDetected();
-}
diff --git a/src/main/java/io/github/dsheirer/bits/MessageFramer.java b/src/main/java/io/github/dsheirer/bits/MessageFramer.java
index 79596cae..27bdcabe 100644
--- a/src/main/java/io/github/dsheirer/bits/MessageFramer.java
+++ b/src/main/java/io/github/dsheirer/bits/MessageFramer.java
@@ -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
+ * sdr-trunk
+ * Copyright (C) 2014-2018 Dennis Sheirer
+ *
+ * This program is free software: you can redistribute it and/or modify it under the terms of the GNU General Public
+ * License as published by the Free Software Foundation, either version 3 of the License, or (at your option) any
+ * later version.
+ *
+ * This program is distributed in the hope that it will be useful, but WITHOUT ANY WARRANTY; without even the implied
+ * warranty of MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU General Public License for more details.
+ *
+ * You should have received a copy of the GNU General Public License along with this program.
+ * If not, see
+ *
******************************************************************************/
package io.github.dsheirer.bits;
-import io.github.dsheirer.dsp.symbol.SyncDetectListener;
+import io.github.dsheirer.dsp.symbol.ISyncDetectListener;
import io.github.dsheirer.dsp.symbol.SyncDetectProvider;
import io.github.dsheirer.sample.Broadcaster;
import io.github.dsheirer.sample.Listener;
import java.util.ArrayList;
import java.util.Iterator;
+import java.util.List;
/**
* MessageFramer - processes bitsets looking for a sync pattern within
* the bits, and then extracts the message, including the sync
* pattern, for a total bit length of messageLength.
- *
+ *
* Will extract multiple messages simultaneously, for each sync pattern that is
* encountered within the bitset bit stream.
*/
-public class MessageFramer implements Listener,
- SyncDetectProvider
+public class MessageFramer implements IBinarySymbolProcessor, Listener, SyncDetectProvider
{
- private boolean[] mSyncPattern;
- private int mMessageLength;
- private SyncDetectListener mSyncDetectListener;
- private Broadcaster mBroadcaster =
- new Broadcaster();
- private ArrayList mMessageAssemblers =
- new ArrayList();
+ private boolean[] mSyncPattern;
+ private int mMessageLength;
+ private ISyncDetectListener mSyncDetectListener;
+ private Broadcaster mBroadcaster = new Broadcaster();
+ private List mMessageAssemblers = new ArrayList();
+ private List mCompletedMessageAssemblers = new ArrayList();
+ private SyncPatternMatcher mMatcher;
- private ArrayList mCompletedMessageAssemblers =
- new ArrayList();
-
- private BinaryMessage mPreviousBuffer = null;
-
- private SyncPatternMatcher mMatcher;
-
- public MessageFramer( boolean[] syncPattern, int messageLength )
+ public MessageFramer(boolean[] syncPattern, int messageLength)
{
mSyncPattern = syncPattern;
- mMatcher = new SyncPatternMatcher( syncPattern );
+ mMatcher = new SyncPatternMatcher(syncPattern);
mMessageLength = messageLength;
}
-
+
public void reset()
{
- for( MessageAssembler assembler: mMessageAssemblers )
- {
- assembler.dispose();
- }
-
+ for(MessageAssembler assembler : mMessageAssemblers)
+ {
+ assembler.dispose();
+ }
+
mMessageAssemblers.clear();
- }
-
- public void dispose()
- {
- mBroadcaster.dispose();
- mCompletedMessageAssemblers.clear();
- mMessageAssemblers.clear();
}
- @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 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,
*/
public void flush()
{
- for( MessageAssembler assembler: mMessageAssemblers )
+ for(MessageAssembler assembler : mMessageAssemblers)
{
assembler.flush();
}
}
-
- @Override
- public void setSyncDetectListener( SyncDetectListener listener )
+
+ @Override
+ public void setSyncDetectListener(ISyncDetectListener listener)
{
- mSyncDetectListener = listener;
+ mSyncDetectListener = listener;
}
-
+
/**
* Allow a message listener to register with this framer to receive
* all framed messages
*/
- public void addMessageListener( Listener listener )
+ public void addMessageListener(Listener listener)
{
- mBroadcaster.addListener( listener );
+ mBroadcaster.addListener(listener);
}
-
- public void removeMessageListener( Listener listener )
+
+ public void removeMessageListener(Listener listener)
{
- mBroadcaster.removeListener( listener );
+ mBroadcaster.removeListener(listener);
}
/*
* Adds a message assembler to receive bits from the bit stream
*/
- private void addMessageAssembler( MessageAssembler assembler )
+ private void addMessageAssembler(MessageAssembler assembler)
{
- mMessageAssemblers.add( assembler );
+ mMessageAssemblers.add(assembler);
}
- @SuppressWarnings( "unused" )
- private void removeMessageAssembler( MessageAssembler assembler )
+ @SuppressWarnings("unused")
+ private void removeMessageAssembler(MessageAssembler assembler)
{
- mMessageAssemblers.remove( assembler );
+ mMessageAssemblers.remove(assembler);
}
/**
@@ -166,7 +163,7 @@ public class MessageFramer implements Listener,
* identified sync pattern, and every bit thereafter. Once the accumulated
* bits equal the message length, the message is sent and the assembler
* flags itself as complete.
- *
+ *
* By design, multiple message assemblers can exist at the same time, each
* assembling different, overlapping potential messages
*/
@@ -174,46 +171,46 @@ public class MessageFramer implements Listener,
{
BinaryMessage mMessage;
boolean mComplete = false;
-
- MessageAssembler( int messageLength )
+
+ MessageAssembler(int messageLength)
{
- mMessage = new BinaryMessage( messageLength );
+ mMessage = new BinaryMessage(messageLength);
}
-
- MessageAssembler( int messageLength, boolean[] initialFill )
+
+ MessageAssembler(int messageLength, boolean[] initialFill)
{
- this( messageLength );
-
+ this(messageLength);
+
/* Pre-load the message with the sync pattern */
- for( int x = 0; x < initialFill.length; x++ )
+ for(int x = 0; x < initialFill.length; x++)
{
- receive( initialFill[ x ] );
+ receive(initialFill[x]);
}
}
-
+
public void dispose()
{
- mMessage = null;
+ mMessage = null;
}
@Override
/**
* Receives one bit at a time, and assembles them into a message
*/
- public void receive( Boolean bit )
+ public void receive(Boolean bit)
{
try
{
- mMessage.add( bit );
+ mMessage.add(bit);
}
- catch( BitSetFullException e )
+ catch(BitSetFullException e)
{
e.printStackTrace();
}
-
+
/* Once our message is complete (ie full), send it to all registered
* message listeners, and set complete flag so for auto-removal */
- if( mMessage.isFull() )
+ if(mMessage.isFull())
{
mComplete = true;
flush();
@@ -226,7 +223,7 @@ public class MessageFramer implements Listener,
*/
public void flush()
{
- mBroadcaster.receive( mMessage );
+ mBroadcaster.receive(mMessage);
mComplete = true;
}
diff --git a/src/main/java/io/github/dsheirer/bits/SoftSyncDetector.java b/src/main/java/io/github/dsheirer/bits/SoftSyncDetector.java
index 1a285f70..c2748f83 100644
--- a/src/main/java/io/github/dsheirer/bits/SoftSyncDetector.java
+++ b/src/main/java/io/github/dsheirer/bits/SoftSyncDetector.java
@@ -1,5 +1,7 @@
package io.github.dsheirer.bits;
+import io.github.dsheirer.dsp.symbol.ISyncDetectListener;
+
public class SoftSyncDetector implements ISyncProcessor
{
private ISyncDetectListener mListener;
diff --git a/src/main/java/io/github/dsheirer/bits/SyncDetector.java b/src/main/java/io/github/dsheirer/bits/SyncDetector.java
index fd8e2091..23aa6be0 100644
--- a/src/main/java/io/github/dsheirer/bits/SyncDetector.java
+++ b/src/main/java/io/github/dsheirer/bits/SyncDetector.java
@@ -1,5 +1,7 @@
package io.github.dsheirer.bits;
+import io.github.dsheirer.dsp.symbol.ISyncDetectListener;
+
public class SyncDetector implements ISyncProcessor
{
private long mPattern;
diff --git a/src/main/java/io/github/dsheirer/bits/SyncPattern.java b/src/main/java/io/github/dsheirer/bits/SyncPattern.java
index 5ef8f4be..8f589bbb 100644
--- a/src/main/java/io/github/dsheirer/bits/SyncPattern.java
+++ b/src/main/java/io/github/dsheirer/bits/SyncPattern.java
@@ -29,7 +29,7 @@ public enum SyncPattern
false, false, false, false //0000 0x0
} ),
- /* Revs (0x0A) and Sync (0x23EB) = 10101 0010001111101011 */
+ /* Revs (0x0A) and Sync (0x23EB) = 0101 000 1 000 11111 0 1 0 11 */
FLEETSYNC2( new boolean[]
{
false, true, false, true, false, //End of bit revs
diff --git a/src/main/java/io/github/dsheirer/bits/SyncPatternMatcher.java b/src/main/java/io/github/dsheirer/bits/SyncPatternMatcher.java
index 3748fa4e..7adf426f 100644
--- a/src/main/java/io/github/dsheirer/bits/SyncPatternMatcher.java
+++ b/src/main/java/io/github/dsheirer/bits/SyncPatternMatcher.java
@@ -1,17 +1,17 @@
/*******************************************************************************
* SDR Trunk
* Copyright (C) 2014 Dennis Sheirer
- *
+ *
* This program is free software: you can redistribute it and/or modify
* it under the terms of the GNU General Public License as published by
* the Free Software Foundation, either version 3 of the License, or
* (at your option) any later version.
- *
+ *
* This program is distributed in the hope that it will be useful,
* but WITHOUT ANY WARRANTY; without even the implied warranty of
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
* GNU General Public License for more details.
- *
+ *
* You should have received a copy of the GNU General Public License
* along with this program. If not, see
******************************************************************************/
@@ -20,113 +20,112 @@ package io.github.dsheirer.bits;
/**
* Sync pattern matcher.
- *
+ *
* Note: works for sync patterns up to 63 bits (integer size - 1 ) long.
* If a 64 bit or larger sync pattern is applied, then the wrapping feature
* of the Long.rotate methods will wrap the MSB around to the LSB and
* corrupt the matching value.
- *
*/
public class SyncPatternMatcher
{
- private long mBits = 0;
- private long mMask = 0;
- private long mSync = 0;
- private boolean mSoftMode = false;
- private int mSoftModeErrorThreshold = 0;
-
- public SyncPatternMatcher( boolean[] syncPattern )
- {
- //Setup a bit mask of all ones the length of the sync pattern
- mMask = (long)( ( Math.pow( 2, syncPattern.length ) ) - 1 );
-
- //Convert the sync bits into a long value for comparison
- for( int x = 0; x < syncPattern.length; x++ )
- {
- if( syncPattern[ x ] )
- {
- mSync += 1l << ( syncPattern.length - 1 - x );
- }
- }
- }
-
- public SyncPatternMatcher( long sync )
- {
- mSync = sync;
- mMask = getMask( sync );
- }
+ private long mBits = 0;
+ private long mMask = 0;
+ private long mSync = 0;
+ private boolean mSoftMode = false;
+ private int mSoftModeErrorThreshold = 0;
- /**
- * Constructs the sync pattern matcher and defines the soft mode maximum
- * number of bit errors to allow in the pattern match.
- *
- * @param sync pattern
- * @param softModeErrorThreshold - maximum bit errors (ie pattern mismatch
- * positions) when considering a sync match
- */
- public SyncPatternMatcher( long sync, int softModeErrorThreshold )
- {
- this( sync );
-
- mSoftModeErrorThreshold = softModeErrorThreshold;
- }
+ public SyncPatternMatcher(boolean[] syncPattern)
+ {
+ //Setup a bit mask of all ones the length of the sync pattern
+ mMask = (long)((Math.pow(2, syncPattern.length)) - 1);
- /**
- * Generates an all ones mask corresponding to the sync pattern
- */
- private static long getMask( long sync )
- {
- long mask = sync;
-
- for( int x = 1; x < 64; x *= 2 )
- {
- mask |= ( mask >> x );
- }
-
- return mask;
- }
-
-
- public void receive( boolean bit )
- {
- //Left shift the previous value
- mBits = Long.rotateLeft( mBits, 1 );
-
- //Apply the mask to erase the previous MSB
- mBits &= mMask;
-
- //Add in the new bit
- if( bit )
- {
- mBits += 1;
- }
- }
+ //Convert the sync bits into a long value for comparison
+ for(int x = 0; x < syncPattern.length; x++)
+ {
+ if(syncPattern[x])
+ {
+ mSync += 1l << (syncPattern.length - 1 - x);
+ }
+ }
+ }
- /**
- * Indicates if the most recently received bit sequence matches the
- * sync pattern
- */
- public boolean matches()
- {
- if( mSoftMode )
- {
- long difference = mBits ^ mSync;
-
- return difference == 0 ||
- Long.bitCount( difference ) <= mSoftModeErrorThreshold;
- }
- else
- {
- return ( mBits == mSync );
- }
- }
+ public SyncPatternMatcher(long sync)
+ {
+ mSync = sync;
+ mMask = getMask(sync);
+ }
- /**
- * Sets the sync pattern matcher in soft mode which allows up to the soft
- * mode threshold number of bit errors when considering a sync pattern match
- */
- public void setSoftMode( boolean softMode )
- {
- mSoftMode = softMode;
- }
+ /**
+ * Constructs the sync pattern matcher and defines the soft mode maximum
+ * number of bit errors to allow in the pattern match.
+ *
+ * @param sync pattern
+ * @param softModeErrorThreshold - maximum bit errors (ie pattern mismatch
+ * positions) when considering a sync match
+ */
+ public SyncPatternMatcher(long sync, int softModeErrorThreshold)
+ {
+ this(sync);
+
+ mSoftModeErrorThreshold = softModeErrorThreshold;
+ }
+
+ /**
+ * Generates an all ones mask corresponding to the sync pattern
+ */
+ private static long getMask(long sync)
+ {
+ long mask = sync;
+
+ for(int x = 1; x < 64; x *= 2)
+ {
+ mask |= (mask >> x);
+ }
+
+ return mask;
+ }
+
+
+ public void receive(boolean bit)
+ {
+ //Left shift the previous value
+ mBits = Long.rotateLeft(mBits, 1);
+
+ //Apply the mask to erase the previous MSB
+ mBits &= mMask;
+
+ //Add in the new bit
+ if(bit)
+ {
+ mBits += 1;
+ }
+ }
+
+ /**
+ * Indicates if the most recently received bit sequence matches the
+ * sync pattern
+ */
+ public boolean matches()
+ {
+ if(mSoftMode)
+ {
+ long difference = mBits ^ mSync;
+
+ return difference == 0 ||
+ Long.bitCount(difference) <= mSoftModeErrorThreshold;
+ }
+ else
+ {
+ return (mBits == mSync);
+ }
+ }
+
+ /**
+ * Sets the sync pattern matcher in soft mode which allows up to the soft
+ * mode threshold number of bit errors when considering a sync pattern match
+ */
+ public void setSoftMode(boolean softMode)
+ {
+ mSoftMode = softMode;
+ }
}
diff --git a/src/main/java/io/github/dsheirer/buffer/BooleanAveragingBuffer.java b/src/main/java/io/github/dsheirer/buffer/BooleanAveragingBuffer.java
index 5117972a..9d37d2e3 100644
--- a/src/main/java/io/github/dsheirer/buffer/BooleanAveragingBuffer.java
+++ b/src/main/java/io/github/dsheirer/buffer/BooleanAveragingBuffer.java
@@ -1,107 +1,89 @@
/*******************************************************************************
* SDR Trunk
* Copyright (C) 2014 Dennis Sheirer
- *
+ *
* This program is free software: you can redistribute it and/or modify
* it under the terms of the GNU General Public License as published by
* the Free Software Foundation, either version 3 of the License, or
* (at your option) any later version.
- *
+ *
* This program is distributed in the hope that it will be useful,
* but WITHOUT ANY WARRANTY; without even the implied warranty of
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
* GNU General Public License for more details.
- *
+ *
* You should have received a copy of the GNU General Public License
* along with this program. If not, see
******************************************************************************/
package io.github.dsheirer.buffer;
-import java.util.Arrays;
-
-
/**
* Circular Buffer - implements a circular buffer with a method to get the
* current head of the buffer, and put a new value in its place at the same
* time
- *
+ *
* Also provides averaging over the elements in the buffer
*/
-public class BooleanAveragingBuffer {
+public class BooleanAveragingBuffer
+{
+ private boolean[] mBuffer;
+ private int mBufferPointer;
+ private int mThreshold;
+ private int mTrueCount;
- private boolean[] mBuffer;
- private int mBufferPointer;
-
- public BooleanAveragingBuffer( int length )
- {
- mBuffer = new boolean[ length ];
-
- //Preload the array with false values
- Arrays.fill( mBuffer, false );
- }
-
- public boolean get( boolean newValue )
- {
- //Fetch current boolean value
- boolean retVal = mBuffer[ mBufferPointer ];
+ public BooleanAveragingBuffer(int length)
+ {
+ mBuffer = new boolean[length];
+ mThreshold = length / 2;
+ }
- //Load the new value into the buffer
- put( newValue );
-
- return retVal;
- }
+ public boolean get(boolean newValue)
+ {
+ //Fetch current boolean value
+ boolean retVal = mBuffer[mBufferPointer];
- /**
- * Loads the newValue into this buffer and adjusts the buffer pointer
- * to prepare for the next get/put cycle
- */
- private void put( boolean newValue )
- {
- //Store the new value to the buffer
- mBuffer[ mBufferPointer ] = newValue;
-
- //Increment the buffer pointer
- mBufferPointer++;
+ //Load the new value into the buffer
+ put(newValue);
- //Wrap the buffer pointer around to 0 when necessary
- if( mBufferPointer >= mBuffer.length )
- {
- mBufferPointer = 0;
- }
- }
+ return retVal;
+ }
- /**
- * Loads the newValue into the buffer, calculates the average
- * and returns that average from this method
- *
- * This effectively performs low-pass filtering
- */
- public boolean getAverage( boolean newValue )
- {
- //Load the new value into the buffer
- put( newValue );
-
- int trueCount = 0;
-
- for( int x = 0; x < mBuffer.length; x++ )
- {
- if( mBuffer[ x ] )
- {
- trueCount++;
- }
- }
+ /**
+ * Loads the newValue into this buffer and adjusts the buffer pointer
+ * to prepare for the next get/put cycle
+ */
+ private void put(boolean newValue)
+ {
+ //Store the new value to the buffer
+ mBuffer[mBufferPointer] = newValue;
- /**
- * If the number of true values in the buffer is
- * more than half, return true, otherwise, false
- */
- if( trueCount > (int)( mBuffer.length / 2 ) )
- {
- return true;
- }
- else
- {
- return false;
- }
- }
+ //Increment the buffer pointer
+ mBufferPointer++;
+
+ mBufferPointer = mBufferPointer % mBuffer.length;
+ }
+
+ /**
+ * Loads the newValue into the buffer, calculates the average
+ * and returns that average from this method
+ *
+ * This effectively performs low-pass filtering
+ */
+ public boolean getAverage(boolean newValue)
+ {
+ //Load the new value into the buffer
+ put(newValue);
+
+ mTrueCount = 0;
+
+ for(int x = 0; x < mBuffer.length; x++)
+ {
+ if(mBuffer[x])
+ {
+ mTrueCount++;
+ }
+ }
+
+ return mTrueCount > mThreshold;
+ }
}
diff --git a/src/main/java/io/github/dsheirer/buffer/CircularBuffer.java b/src/main/java/io/github/dsheirer/buffer/CircularBuffer.java
index fd92b870..86ff4b0e 100644
--- a/src/main/java/io/github/dsheirer/buffer/CircularBuffer.java
+++ b/src/main/java/io/github/dsheirer/buffer/CircularBuffer.java
@@ -1,3 +1,18 @@
+/*******************************************************************************
+ * sdr-trunk
+ * Copyright (C) 2014-2018 Dennis Sheirer
+ *
+ * This program is free software: you can redistribute it and/or modify it under the terms of the GNU General Public
+ * License as published by the Free Software Foundation, either version 3 of the License, or (at your option) any
+ * later version.
+ *
+ * This program is distributed in the hope that it will be useful, but WITHOUT ANY WARRANTY; without even the implied
+ * warranty of MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU General Public License for more details.
+ *
+ * You should have received a copy of the GNU General Public License along with this program.
+ * If not, see
+ *
+ ******************************************************************************/
package io.github.dsheirer.buffer;
import io.github.dsheirer.sample.Listener;
@@ -8,87 +23,86 @@ import java.util.List;
public class CircularBuffer implements Listener
{
- private ArrayList mElements;
- private Listener mListener;
- private int mPointer;
- private int mSize;
+ private List mElements;
+ private Listener mListener;
+ private int mPointer;
+ private int mSize;
- /**
- * Circular Buffer - stores elements in an array list. Once the buffer is
- * full, new samples replace older samples and older samples are broadcast
- * to an optional listener.
- *
- * @param size - size of buffer
- */
- public CircularBuffer( int size )
- {
- mSize = size;
- mElements = new ArrayList();
- }
+ /**
+ * Circular Buffer - stores elements in an array list. Once the buffer is
+ * full, new samples replace older samples and older samples are broadcast
+ * to an optional listener.
+ *
+ * @param size - size of buffer
+ */
+ public CircularBuffer(int size)
+ {
+ mSize = size;
+ mElements = new ArrayList();
+ }
- /**
- * Returns the element stored at index. Note: the index argument refers to
- * the internal storage index and does not indicate a temporal relationship
- * among elements. For example, if you get indexes 1, 2, and 3, you may be
- * getting ( oldest - 1, oldest, newest ) elements, if the internal pointer
- * is currently pointing to and will insert the next element at index 4.
- *
- * @param index - element index
- *
- * @return element or null
- */
- public T get( int index )
- {
- if( index < mElements.size() )
- {
- return mElements.get( index );
- }
-
- return null;
- }
-
- public int getSize()
- {
- return mSize;
- }
-
- public List getElements()
- {
- return Collections.unmodifiableList( mElements );
- }
-
- @Override
- public void receive( T element )
- {
- T previous = null;
-
- if( mElements.size() > mPointer )
- {
- previous = mElements.get( mPointer );
-
- mElements.set( mPointer, element );
- }
- else
- {
- mElements.add( element );
- }
-
- mPointer++;
-
- if( mPointer >= mSize )
- {
- mPointer = 0;
- }
-
- if( previous != null && mListener != null )
- {
- mListener.receive( previous );
- }
- }
+ /**
+ * Returns the element stored at index. Note: the index argument refers to
+ * the internal storage index and does not indicate a temporal relationship
+ * among elements. For example, if you get indexes 1, 2, and 3, you may be
+ * getting ( oldest - 1, oldest, newest ) elements, if the internal pointer
+ * is currently pointing to and will insert the next element at index 4.
+ *
+ * @param index - element index
+ * @return element or null
+ */
+ public T get(int index)
+ {
+ if(index < mElements.size())
+ {
+ return mElements.get(index);
+ }
- public void setListener( Listener listener )
- {
- mListener = listener;
- }
+ return null;
+ }
+
+ public int getSize()
+ {
+ return mSize;
+ }
+
+ public List getElements()
+ {
+ return Collections.unmodifiableList(mElements);
+ }
+
+ @Override
+ public void receive(T element)
+ {
+ T previous = null;
+
+ if(mElements.size() > mPointer)
+ {
+ previous = mElements.get(mPointer);
+
+ mElements.set(mPointer, element);
+ }
+ else
+ {
+ mElements.add(element);
+ }
+
+ mPointer++;
+
+ if(mPointer >= mSize)
+ {
+ mPointer = 0;
+ }
+
+ if(previous != null && mListener != null)
+ {
+ mListener.receive(previous);
+ }
+ }
+
+ public void setListener(Listener listener)
+ {
+ mListener = listener;
+ }
}
diff --git a/src/main/java/io/github/dsheirer/buffer/RealCircularBuffer.java b/src/main/java/io/github/dsheirer/buffer/RealCircularBuffer.java
new file mode 100644
index 00000000..f4274c8a
--- /dev/null
+++ b/src/main/java/io/github/dsheirer/buffer/RealCircularBuffer.java
@@ -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
+ *
+ ******************************************************************************/
+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;
+ }
+}
diff --git a/src/main/java/io/github/dsheirer/channel/metadata/ChannelMetadataViewer.java b/src/main/java/io/github/dsheirer/channel/metadata/ChannelMetadataViewer.java
index f121508e..d0f3fd23 100644
--- a/src/main/java/io/github/dsheirer/channel/metadata/ChannelMetadataViewer.java
+++ b/src/main/java/io/github/dsheirer/channel/metadata/ChannelMetadataViewer.java
@@ -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();
diff --git a/src/main/java/io/github/dsheirer/channel/state/AlwaysUnsquelchedDecoderState.java b/src/main/java/io/github/dsheirer/channel/state/AlwaysUnsquelchedDecoderState.java
index bd2432fb..1d3795e8 100644
--- a/src/main/java/io/github/dsheirer/channel/state/AlwaysUnsquelchedDecoderState.java
+++ b/src/main/java/io/github/dsheirer/channel/state/AlwaysUnsquelchedDecoderState.java
@@ -25,8 +25,6 @@ import io.github.dsheirer.module.decode.DecoderType;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
-import java.util.concurrent.ScheduledExecutorService;
-
/**
* Basic decoder channel state - provides the minimum channel state functionality
* to support an always un-squelched audio decoder.
@@ -94,7 +92,7 @@ public class AlwaysUnsquelchedDecoderState extends DecoderState
}
@Override
- public void start(ScheduledExecutorService executor)
+ public void start()
{
broadcast(new AttributeChangeRequest(Attribute.PRIMARY_ADDRESS_TO, NO_SQUELCH));
broadcast(new DecoderStateEvent(this, Event.ALWAYS_UNSQUELCH, State.IDLE));
diff --git a/src/main/java/io/github/dsheirer/channel/state/ChannelState.java b/src/main/java/io/github/dsheirer/channel/state/ChannelState.java
index ae83ec65..f2d069d5 100644
--- a/src/main/java/io/github/dsheirer/channel/state/ChannelState.java
+++ b/src/main/java/io/github/dsheirer/channel/state/ChannelState.java
@@ -20,8 +20,6 @@ package io.github.dsheirer.channel.state;
import io.github.dsheirer.audio.squelch.ISquelchStateProvider;
import io.github.dsheirer.audio.squelch.SquelchState;
-import io.github.dsheirer.channel.heartbeat.Heartbeat;
-import io.github.dsheirer.channel.heartbeat.IHeartbeatListener;
import io.github.dsheirer.channel.metadata.Attribute;
import io.github.dsheirer.channel.metadata.AttributeChangeRequest;
import io.github.dsheirer.channel.metadata.IAttributeChangeRequestListener;
@@ -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(Attribute.CHANNEL_STATE, mState));
@@ -160,14 +158,14 @@ public class ChannelState extends Module implements ICallEventProvider, IDecoder
}
@Override
- public Listener getFrequencyChangeListener()
+ public Listener getSourceEventListener()
{
- if(mFrequencyChangeListener == null)
+ if(mSourceChangeListener == null)
{
- mFrequencyChangeListener = new FrequencyChangeListener();
+ mSourceChangeListener = new SourceChangeListener();
}
- return mFrequencyChangeListener;
+ return mSourceChangeListener;
}
private boolean isStandardChannel()
@@ -520,14 +518,14 @@ public class ChannelState extends Module implements ICallEventProvider, IDecoder
/**
* Listener to receive frequency change events and rebroadcast them to the decoder states
*/
- public class FrequencyChangeListener implements Listener
+ public class SourceChangeListener implements Listener
{
@Override
- public void receive(FrequencyChangeEvent frequencyChangeEvent)
+ public void receive(SourceEvent sourceEvent)
{
- if(frequencyChangeEvent.getEvent() == NOTIFICATION_FREQUENCY_CHANGE)
+ if(sourceEvent.getEvent() == NOTIFICATION_FREQUENCY_CHANGE)
{
- long frequency = frequencyChangeEvent.getValue().longValue();
+ long frequency = sourceEvent.getValue().longValue();
broadcast(new DecoderStateEvent(this, Event.SOURCE_FREQUENCY, getState(), frequency));
}
diff --git a/src/main/java/io/github/dsheirer/channel/traffic/TrafficChannelManager.java b/src/main/java/io/github/dsheirer/channel/traffic/TrafficChannelManager.java
index efd9e52c..80721e1a 100644
--- a/src/main/java/io/github/dsheirer/channel/traffic/TrafficChannelManager.java
+++ b/src/main/java/io/github/dsheirer/channel/traffic/TrafficChannelManager.java
@@ -1,19 +1,16 @@
/*******************************************************************************
- * sdrtrunk
+ * sdr-trunk
* Copyright (C) 2014-2017 Dennis Sheirer
*
- * This program is free software: you can redistribute it and/or modify
- * it under the terms of the GNU General Public License as published by
- * the Free Software Foundation, either version 3 of the License, or
- * (at your option) any later version.
+ * This program is free software: you can redistribute it and/or modify it under the terms of the GNU General Public
+ * License as published by the Free Software Foundation, either version 3 of the License, or (at your option) any
+ * later version.
*
- * This program is distributed in the hope that it will be useful,
- * but WITHOUT ANY WARRANTY; without even the implied warranty of
- * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
- * GNU General Public License for more details.
+ * This program is distributed in the hope that it will be useful, but WITHOUT ANY WARRANTY; without even the implied
+ * warranty of MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU General Public License for more details.
*
- * You should have received a copy of the GNU General Public License
- * along with this program. If not, see
+ * You should have received a copy of the GNU General Public License along with this program.
+ * If not, see
*
******************************************************************************/
package io.github.dsheirer.channel.traffic;
@@ -29,13 +26,12 @@ import io.github.dsheirer.controller.channel.TrafficChannelEvent;
import io.github.dsheirer.module.Module;
import io.github.dsheirer.module.decode.config.DecodeConfiguration;
import io.github.dsheirer.module.decode.event.CallEvent;
+import io.github.dsheirer.module.decode.event.CallEvent.CallEventType;
import io.github.dsheirer.module.decode.event.ICallEventProvider;
-import io.github.dsheirer.module.log.config.EventLogConfiguration;
import io.github.dsheirer.record.config.RecordConfiguration;
import io.github.dsheirer.sample.Listener;
import io.github.dsheirer.source.config.SourceConfigTuner;
-import io.github.dsheirer.source.tuner.TunerChannel;
-import io.github.dsheirer.source.tuner.TunerChannel.Type;
+import io.github.dsheirer.source.tuner.channel.TunerChannel;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
@@ -43,7 +39,6 @@ import java.util.ArrayList;
import java.util.List;
import java.util.Map;
import java.util.concurrent.ConcurrentHashMap;
-import java.util.concurrent.ScheduledExecutorService;
public class TrafficChannelManager extends Module implements ICallEventProvider, IDecoderStateEventListener
{
@@ -61,7 +56,6 @@ public class TrafficChannelManager extends Module implements ICallEventProvider,
private ChannelModel mChannelModel;
private DecodeConfiguration mDecodeConfiguration;
- private EventLogConfiguration mEventLogConfiguration;
private RecordConfiguration mRecordConfiguration;
private String mSystem;
private String mSite;
@@ -75,7 +69,6 @@ public class TrafficChannelManager extends Module implements ICallEventProvider,
* @param channelModel containing channels currently in use
* @param decodeConfiguration - decoder configuration to use for each
* traffic channel allocation.
- * @param eventLogConfiguration - logging configuration to use for traffic channels
* @param recordConfiguration - recording options for each traffic channel
* @param system label to use to describe the system
* @param site label to use to describe the site
@@ -85,7 +78,6 @@ public class TrafficChannelManager extends Module implements ICallEventProvider,
*/
public TrafficChannelManager(ChannelModel channelModel,
DecodeConfiguration decodeConfiguration,
- EventLogConfiguration eventLogConfiguration,
RecordConfiguration recordConfiguration,
String system,
String site,
@@ -94,7 +86,6 @@ public class TrafficChannelManager extends Module implements ICallEventProvider,
{
mChannelModel = channelModel;
mDecodeConfiguration = decodeConfiguration;
- mEventLogConfiguration = eventLogConfiguration;
mRecordConfiguration = recordConfiguration;
mSystem = system;
mSite = site;
@@ -150,8 +141,6 @@ public class TrafficChannelManager extends Module implements ICallEventProvider,
channel.setDecodeConfiguration(mDecodeConfiguration);
- channel.setEventLogConfiguration(mEventLogConfiguration);
-
channel.setRecordConfiguration(mRecordConfiguration);
channel.setAliasListName(mAliasListName);
@@ -161,7 +150,7 @@ public class TrafficChannelManager extends Module implements ICallEventProvider,
mTrafficChannelPool.add(channel);
}
- /* If we have a configured channel, update metadata */
+ /* If we have a configured channel, update metadata */
if(channel != null)
{
channel.setSourceConfiguration(new SourceConfigTuner(tunerChannel));
@@ -184,7 +173,7 @@ public class TrafficChannelManager extends Module implements ICallEventProvider,
{
CallEvent callEvent = event.getCallEvent();
- /* Check for duplicate events and suppress */
+ /* Check for duplicate events and suppress */
synchronized(mTrafficChannelsInUse)
{
if(mTrafficChannelsInUse.containsKey(callEvent.getChannel()))
@@ -196,8 +185,8 @@ public class TrafficChannelManager extends Module implements ICallEventProvider,
if(frequency > 0)
{
- Channel channel = getChannel(callEvent.getChannel(), new TunerChannel(Type.TRAFFIC,
- frequency, mDecodeConfiguration.getDecoderType().getChannelBandwidth()));
+ Channel channel = getChannel(callEvent.getChannel(), new TunerChannel(frequency,
+ mDecodeConfiguration.getDecoderType().getChannelBandwidth()));
if(channel != null)
{
@@ -213,7 +202,7 @@ public class TrafficChannelManager extends Module implements ICallEventProvider,
}
else
{
- callEvent.setCallEventType(CallEvent.CallEventType.CALL_DETECT);
+ callEvent.setCallEventType(CallEventType.CALL_DETECT);
String details = callEvent.getDetails();
@@ -230,7 +219,7 @@ public class TrafficChannelManager extends Module implements ICallEventProvider,
}
else
{
- callEvent.setCallEventType(CallEvent.CallEventType.CALL_DETECT);
+ callEvent.setCallEventType(CallEventType.CALL_DETECT);
String details = callEvent.getDetails();
@@ -247,7 +236,7 @@ public class TrafficChannelManager extends Module implements ICallEventProvider,
}
else
{
- callEvent.setCallEventType(CallEvent.CallEventType.CALL_DETECT);
+ callEvent.setCallEventType(CallEventType.CALL_DETECT);
String details = callEvent.getDetails();
@@ -337,7 +326,7 @@ public class TrafficChannelManager extends Module implements ICallEventProvider,
}
@Override
- public void start(ScheduledExecutorService executor)
+ public void start()
{
}
@@ -348,7 +337,7 @@ public class TrafficChannelManager extends Module implements ICallEventProvider,
{
List channels = new ArrayList<>();
- /* Copy the keyset so we don't get concurrent modification of the map */
+ /* Copy the keyset so we don't get concurrent modification of the map */
channels.addAll(mTrafficChannelsInUse.keySet());
for(String channel : channels)
@@ -399,4 +388,4 @@ public class TrafficChannelManager extends Module implements ICallEventProvider,
}
}
}
-}
+}
\ No newline at end of file
diff --git a/src/main/java/io/github/dsheirer/controller/channel/Channel.java b/src/main/java/io/github/dsheirer/controller/channel/Channel.java
index 4927f68d..f19e56d7 100644
--- a/src/main/java/io/github/dsheirer/controller/channel/Channel.java
+++ b/src/main/java/io/github/dsheirer/controller/channel/Channel.java
@@ -31,18 +31,16 @@ import io.github.dsheirer.module.log.config.EventLogConfiguration;
import io.github.dsheirer.record.RecorderType;
import io.github.dsheirer.record.config.RecordConfiguration;
import io.github.dsheirer.sample.Listener;
+import io.github.dsheirer.source.SourceEvent;
import io.github.dsheirer.source.SourceType;
import io.github.dsheirer.source.config.SourceConfigFactory;
import io.github.dsheirer.source.config.SourceConfigRecording;
import io.github.dsheirer.source.config.SourceConfigTuner;
import io.github.dsheirer.source.config.SourceConfiguration;
-import io.github.dsheirer.source.tuner.TunerChannel;
-import io.github.dsheirer.source.tuner.TunerChannel.Type;
-import io.github.dsheirer.source.tuner.frequency.FrequencyChangeEvent;
-import io.github.dsheirer.source.tuner.frequency.FrequencyChangeEvent.Event;
+import io.github.dsheirer.source.tuner.channel.TunerChannel;
@JacksonXmlRootElement(localName = "channel")
-public class Channel extends Configuration implements Listener
+public class Channel extends Configuration implements Listener
{
// Standard channels are persisted and traffic channels are temporary
public enum ChannelType
@@ -490,15 +488,14 @@ public class Channel extends Configuration implements Listener mProcessingChains = new HashMap<>();
- private List> mAudioPacketListeners = new CopyOnWriteArrayList<>();
+ private List> mAudioPacketListeners = new CopyOnWriteArrayList<>();
private List> 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 listener : mAudioPacketListeners)
+ for(Listener 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 listener)
+ public void addAudioPacketListener(Listener listener)
{
mAudioPacketListeners.add(listener);
}
@@ -375,7 +375,7 @@ public class ChannelProcessingManager implements ChannelEventListener
/**
* Removes a message listener.
*/
- public void removeAudioPacketListener(Listener listener)
+ public void removeAudioPacketListener(Listener listener)
{
mAudioPacketListeners.remove(listener);
}
diff --git a/src/main/java/io/github/dsheirer/dsp/NRZDecoder.java b/src/main/java/io/github/dsheirer/dsp/NRZDecoder.java
index 2c1e9e68..a93e9c01 100644
--- a/src/main/java/io/github/dsheirer/dsp/NRZDecoder.java
+++ b/src/main/java/io/github/dsheirer/dsp/NRZDecoder.java
@@ -1,77 +1,76 @@
/*******************************************************************************
- * SDR Trunk
- * Copyright (C) 2014 Dennis Sheirer
- *
- * This program is free software: you can redistribute it and/or modify
- * it under the terms of the GNU General Public License as published by
- * the Free Software Foundation, either version 3 of the License, or
- * (at your option) any later version.
- *
- * This program is distributed in the hope that it will be useful,
- * but WITHOUT ANY WARRANTY; without even the implied warranty of
- * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
- * GNU General Public License for more details.
- *
- * You should have received a copy of the GNU General Public License
- * along with this program. If not, see
+ * sdr-trunk
+ * Copyright (C) 2014-2018 Dennis Sheirer
+ *
+ * This program is free software: you can redistribute it and/or modify it under the terms of the GNU General Public
+ * License as published by the Free Software Foundation, either version 3 of the License, or (at your option) any
+ * later version.
+ *
+ * This program is distributed in the hope that it will be useful, but WITHOUT ANY WARRANTY; without even the implied
+ * warranty of MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU General Public License for more details.
+ *
+ * You should have received a copy of the GNU General Public License along with this program.
+ * If not, see
+ *
******************************************************************************/
package io.github.dsheirer.dsp;
+import io.github.dsheirer.bits.IBinarySymbolProcessor;
import io.github.dsheirer.sample.Listener;
-public class NRZDecoder implements Listener
+public class NRZDecoder implements IBinarySymbolProcessor
{
- public final static boolean MODE_NORMAL = true;
- public final static boolean MODE_INVERTED = false;
-
- private Listener mListener;
- private boolean mMode = MODE_NORMAL;
- private boolean mPrevious;
+ public final static boolean MODE_NORMAL = true;
+ public final static boolean MODE_INVERTED = false;
- /**
- * Non Return To Zero (NRZ) and NRZ Inverted (NRZ-I) Decoder.
- *
- * Use default constructor for NRZ output. Use the alternate constructor
- * with the MODE_INVERTED argument for NRZ-I output.
- *
- * Performs XOR of incoming bit with previous output and sends the result
- * to the registered listener.
- */
- public NRZDecoder( boolean mode )
- {
- mMode = mode;
- }
-
- public NRZDecoder()
- {
- this( MODE_NORMAL );
- }
-
- public void dispose()
- {
- mListener = null;
- }
+ private IBinarySymbolProcessor mListener;
+ private boolean mMode = MODE_NORMAL;
+ private boolean mPrevious;
- @Override
- public void receive( Boolean bit )
+ /**
+ * Non Return To Zero (NRZ) and NRZ Inverted (NRZ-I) Decoder.
+ *
+ * Use default constructor for NRZ output. Use the alternate constructor
+ * with the MODE_INVERTED argument for NRZ-I output.
+ *
+ * Performs XOR of incoming bit with previous output and sends the result
+ * to the registered listener.
+ */
+ public NRZDecoder(boolean mode)
{
- boolean result = mPrevious ^ bit;
-
- if( mListener != null )
- {
- mListener.receive( mMode ? result : !result );
- }
-
- mPrevious = result;
+ mMode = mode;
}
- public void setListener( Listener listener )
- {
- mListener = listener;
- }
-
- public void removeListener( Listener listener )
- {
- mListener = null;
- }
+ public NRZDecoder()
+ {
+ this(MODE_NORMAL);
+ }
+
+ public void dispose()
+ {
+ mListener = null;
+ }
+
+ @Override
+ public void receive(boolean symbol)
+ {
+ boolean result = mPrevious ^ symbol;
+
+ if(mListener != null)
+ {
+ mListener.receive(mMode ? result : !result);
+ }
+
+ mPrevious = result;
+ }
+
+ public void setListener(IBinarySymbolProcessor listener)
+ {
+ mListener = listener;
+ }
+
+ public void removeListener(Listener listener)
+ {
+ mListener = null;
+ }
}
diff --git a/src/main/java/io/github/dsheirer/dsp/afsk/AFSK1200Decoder.java b/src/main/java/io/github/dsheirer/dsp/afsk/AFSK1200Decoder.java
new file mode 100644
index 00000000..ca72ac66
--- /dev/null
+++ b/src/main/java/io/github/dsheirer/dsp/afsk/AFSK1200Decoder.java
@@ -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
+ *
+ ******************************************************************************/
+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
+{
+ 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
+ {
+ @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;
+ }
+ }
+}
diff --git a/src/main/java/io/github/dsheirer/dsp/afsk/AFSK1200DecoderInstrumented.java b/src/main/java/io/github/dsheirer/dsp/afsk/AFSK1200DecoderInstrumented.java
new file mode 100644
index 00000000..dd0e444f
--- /dev/null
+++ b/src/main/java/io/github/dsheirer/dsp/afsk/AFSK1200DecoderInstrumented.java
@@ -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
+ *
+ ******************************************************************************/
+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 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 listener)
+ {
+ mSymbolBroadcaster.addListener(listener);
+ }
+}
diff --git a/src/main/java/io/github/dsheirer/dsp/afsk/AFSKSampleBuffer.java b/src/main/java/io/github/dsheirer/dsp/afsk/AFSKSampleBuffer.java
new file mode 100644
index 00000000..674e7734
--- /dev/null
+++ b/src/main/java/io/github/dsheirer/dsp/afsk/AFSKSampleBuffer.java
@@ -0,0 +1,121 @@
+/*******************************************************************************
+ * sdr-trunk
+ * Copyright (C) 2014-2018 Dennis Sheirer
+ *
+ * This program is free software: you can redistribute it and/or modify it under the terms of the GNU General Public
+ * License as published by the Free Software Foundation, either version 3 of the License, or (at your option) any
+ * later version.
+ *
+ * This program is distributed in the hope that it will be useful, but WITHOUT ANY WARRANTY; without even the implied
+ * warranty of MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU General Public License for more details.
+ *
+ * You should have received a copy of the GNU General Public License along with this program.
+ * If not, see
+ *
+ ******************************************************************************/
+package io.github.dsheirer.dsp.afsk;
+
+import org.slf4j.Logger;
+import org.slf4j.LoggerFactory;
+
+public class AFSKSampleBuffer
+{
+ private final static Logger mLog = LoggerFactory.getLogger(AFSKSampleBuffer.class);
+
+ private final static float SYMBOL_SAMPLE_THRESHOLD = 1.0f;
+ protected boolean[] mDelayLine;
+ protected int mDelayLinePointer = 0;
+
+ private int mHalfIntegerSamplesPerSymbol;
+ private int mPositiveSampleDecisionCount;
+ protected int mTwiceSamplesPerSymbol;
+ protected int mSymbolStart;
+ protected int mSymbolEnd;
+ protected int mSamplesPerSymbol;
+ protected float mSymbolSamplingPoint;
+ protected float mSymbolTimingGain;
+
+ /**
+ * Buffer to store complex sample data and produce interpolated samples.
+ * @param samplesPerSymbol
+ */
+ public AFSKSampleBuffer(int samplesPerSymbol, float symbolTimingGain)
+ {
+ mSamplesPerSymbol = samplesPerSymbol;
+ mSymbolTimingGain = symbolTimingGain;
+
+ mHalfIntegerSamplesPerSymbol = samplesPerSymbol / 2;
+ mSymbolSamplingPoint = samplesPerSymbol;
+ mTwiceSamplesPerSymbol = 2 * samplesPerSymbol;
+ mDelayLine = new boolean[2 * mTwiceSamplesPerSymbol];
+ }
+
+ /**
+ * Stores the sample in the sample buffer to support making symbol decisions.
+ *
+ * Note: use the hasSymbol() method to determine if sufficient samples have been collected to make a symbol
+ * determination.
+ *
+ * @param sample to store in the sample buffer
+ */
+ public void receive(boolean sample)
+ {
+ mSymbolSamplingPoint--;
+
+ mDelayLine[mDelayLinePointer] = sample;
+ mDelayLine[mDelayLinePointer + mTwiceSamplesPerSymbol] = sample;
+
+ //Increment pointer and keep pointer in bounds
+ mDelayLinePointer++;
+ mDelayLinePointer = mDelayLinePointer % mTwiceSamplesPerSymbol;
+ }
+
+ /**
+ * Adjusts samples per symbol and symbol timing counters and increments the sample counter to collect another
+ * symbol.
+ *
+ * @param symbolTimingError from a symbol timing error detector
+ */
+ public void resetAndAdjust(float symbolTimingError)
+ {
+ mSymbolSamplingPoint += (mSamplesPerSymbol + (symbolTimingError * mSymbolTimingGain));
+ }
+
+ public void setTimingGain(float gain)
+ {
+ mSymbolTimingGain = gain;
+ }
+
+ /**
+ * Indicates if this buffer has accumulated enough samples to represent a full symbol
+ */
+ public boolean hasSymbol()
+ {
+ return mSymbolSamplingPoint < SYMBOL_SAMPLE_THRESHOLD;
+ }
+
+ /**
+ * Calculates the symbol as a majority decision where the number of positive/true samples are greater than half
+ * of the samples per symbol. Symbol decision is based on the samples collected over the preceeding 1.0 to 2.0
+ * symbol periods.
+ *
+ * @return a true symbol if the majority of the samples were true or positive, otherwise a false symbol.
+ */
+ public boolean getSymbol()
+ {
+ mPositiveSampleDecisionCount = 0;
+
+ mSymbolStart = mDelayLinePointer;
+ mSymbolEnd = mDelayLinePointer + mSamplesPerSymbol - 1;
+
+ for(int x = mSymbolStart; x <= mSymbolEnd; x++)
+ {
+ if(mDelayLine[x])
+ {
+ mPositiveSampleDecisionCount++;
+ }
+ }
+
+ return mPositiveSampleDecisionCount > mHalfIntegerSamplesPerSymbol;
+ }
+}
diff --git a/src/main/java/io/github/dsheirer/dsp/afsk/AFSKSampleBufferInstrumented.java b/src/main/java/io/github/dsheirer/dsp/afsk/AFSKSampleBufferInstrumented.java
new file mode 100644
index 00000000..714e7065
--- /dev/null
+++ b/src/main/java/io/github/dsheirer/dsp/afsk/AFSKSampleBufferInstrumented.java
@@ -0,0 +1,90 @@
+/*******************************************************************************
+ * sdr-trunk
+ * Copyright (C) 2014-2018 Dennis Sheirer
+ *
+ * This program is free software: you can redistribute it and/or modify it under the terms of the GNU General Public
+ * License as published by the Free Software Foundation, either version 3 of the License, or (at your option) any
+ * later version.
+ *
+ * This program is distributed in the hope that it will be useful, but WITHOUT ANY WARRANTY; without even the implied
+ * warranty of MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU General Public License for more details.
+ *
+ * You should have received a copy of the GNU General Public License along with this program.
+ * If not, see
+ *
+ ******************************************************************************/
+package io.github.dsheirer.dsp.afsk;
+
+public class AFSKSampleBufferInstrumented extends AFSKSampleBuffer
+{
+ public boolean mLastSymbol;
+
+ /**
+ * Buffer to store complex sample data and produce interpolated samples.
+ *
+ * @param samplesPerSymbol
+ * @param symbolTimingGain
+ */
+ public AFSKSampleBufferInstrumented(int samplesPerSymbol, float symbolTimingGain)
+ {
+ super(samplesPerSymbol, symbolTimingGain);
+ }
+
+ /**
+ * Sample delay buffer containing boolean (ie positive or negative) samples.
+ */
+ public boolean[] getDelayLine()
+ {
+ return mDelayLine;
+ }
+
+ /**
+ * Current delay buffer pointer which points to where the next sample will be stored and/or the oldest sample in
+ * the buffer.
+ */
+ public int getDelayLinePointer()
+ {
+ return mDelayLinePointer;
+ }
+
+ public int getDelayLineSecondPointer()
+ {
+ return mDelayLinePointer + mTwiceSamplesPerSymbol;
+ }
+
+ /**
+ * Current symbol timing gain value
+ * @return
+ */
+ public float getSymbolTimingGain()
+ {
+ return mSymbolTimingGain;
+ }
+
+ public float getSamplesPerSymbol()
+ {
+ return mSamplesPerSymbol;
+ }
+
+ @Override
+ public boolean getSymbol()
+ {
+ mLastSymbol = super.getSymbol();
+ return mLastSymbol;
+ }
+
+ public boolean getLastSymbol()
+ {
+ return mLastSymbol;
+ }
+
+ public int getSymbolStart()
+ {
+ return mSymbolStart;
+ }
+
+ public int getSymbolEnd()
+ {
+ return mSymbolEnd;
+ }
+}
diff --git a/src/main/java/io/github/dsheirer/dsp/afsk/AFSKTest.java b/src/main/java/io/github/dsheirer/dsp/afsk/AFSKTest.java
new file mode 100644
index 00000000..3e215f0c
--- /dev/null
+++ b/src/main/java/io/github/dsheirer/dsp/afsk/AFSKTest.java
@@ -0,0 +1,122 @@
+/*******************************************************************************
+ * sdr-trunk
+ * Copyright (C) 2014-2018 Dennis Sheirer
+ *
+ * This program is free software: you can redistribute it and/or modify it under the terms of the GNU General Public
+ * License as published by the Free Software Foundation, either version 3 of the License, or (at your option) any
+ * later version.
+ *
+ * This program is distributed in the hope that it will be useful, but WITHOUT ANY WARRANTY; without even the implied
+ * warranty of MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU General Public License for more details.
+ *
+ * You should have received a copy of the GNU General Public License along with this program.
+ * If not, see
+ *
+ ******************************************************************************/
+package io.github.dsheirer.dsp.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++;
+ }
+ }
+}
diff --git a/src/main/java/io/github/dsheirer/dsp/afsk/AFSKTimingErrorDetector.java b/src/main/java/io/github/dsheirer/dsp/afsk/AFSKTimingErrorDetector.java
new file mode 100644
index 00000000..92447773
--- /dev/null
+++ b/src/main/java/io/github/dsheirer/dsp/afsk/AFSKTimingErrorDetector.java
@@ -0,0 +1,103 @@
+/*******************************************************************************
+ * sdr-trunk
+ * Copyright (C) 2014-2018 Dennis Sheirer
+ *
+ * This program is free software: you can redistribute it and/or modify it under the terms of the GNU General Public
+ * License as published by the Free Software Foundation, either version 3 of the License, or (at your option) any
+ * later version.
+ *
+ * This program is distributed in the hope that it will be useful, but WITHOUT ANY WARRANTY; without even the implied
+ * warranty of MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU General Public License for more details.
+ *
+ * You should have received a copy of the GNU General Public License along with this program.
+ * If not, see
+ *
+ ******************************************************************************/
+package io.github.dsheirer.dsp.afsk;
+
+/**
+ * Zero crossing error detector. Provides symbol timing error correction values by detecting zero-crossings within
+ * each symbol period. Ideally there should be only 0 or 1 zero crossings within each symbol period. However, when
+ * there is a slight DC offset in the demodulated samples, there can be up to 2 zero crossings within a symbol
+ * period.
+ *
+ * This detector will only provide error corrections when there are 1 or 2 zero crossings detected. If the number
+ * of zero crossings is greater than 2, then it is assumed that there is no signal present and the returned timing
+ * error value is zero. This detector provides an error correction value relative to (approximately) the middle of
+ * a symbol period and therefore continuously attempts to align zero crossings to the middle of the period.
+ *
+ * Since this detector does NOT use interpolation, the error signal will contain minor levels of symbol timing
+ * jitter. Error signal values are reported as integrals of the sampling period.
+ */
+public class AFSKTimingErrorDetector
+{
+ protected boolean[] mBuffer;
+ protected int mDetectedZeroCrossing;
+ protected float mSymbolMiddle;
+ private int mZeroCrossingCount;
+
+ /**
+ * Constructs the detector for the specified samples per symbol.
+ *
+ * @param samplesPerSymbol
+ */
+ public AFSKTimingErrorDetector(int samplesPerSymbol)
+ {
+ mBuffer = new boolean[samplesPerSymbol];
+ mSymbolMiddle = (float)(samplesPerSymbol - 1) / 2.0f;
+ }
+
+ /**
+ * Stores the sample in the buffer. Use the getError() method once enough samples have been processed to determine
+ * the error for a symbol period.
+ *
+ * @param sample
+ */
+ public void receive(boolean sample)
+ {
+ //Shift all samples left and store new sample at the end
+ System.arraycopy(mBuffer, 1, mBuffer, 0, mBuffer.length - 1);
+ mBuffer[mBuffer.length - 1] = sample;
+ }
+
+ /**
+ * Determines the symbol timing error for the samples processed thus far. This method should be invoked once per
+ * symbol period, triggered via an external samples per symbol tracking process.
+ *
+ * @return symbol timing error calculated as the distance between the approximate symbol center and the next closest
+ * zero crossing index. An error value of zero is returned if the number of zero crossings is less than one or
+ * greater than two.
+ */
+ public int getError()
+ {
+ mZeroCrossingCount = 0;
+
+ for(int x = 0; x < mBuffer.length - 1; x++)
+ {
+ if(mBuffer[x] ^ mBuffer[x + 1])
+ {
+ mDetectedZeroCrossing = x + 1;
+ mZeroCrossingCount++;
+
+ if(mZeroCrossingCount > 1)
+ {
+ return 0; //Too many crossings
+ }
+ }
+ }
+
+ if(mZeroCrossingCount == 1)
+ {
+ if(mDetectedZeroCrossing > mSymbolMiddle)
+ {
+ return -(mBuffer.length - mDetectedZeroCrossing);
+ }
+ else
+ {
+ return mDetectedZeroCrossing + 1;
+ }
+ }
+
+ return 0;
+ }
+}
diff --git a/src/main/java/io/github/dsheirer/dsp/afsk/AFSKTimingErrorDetectorInstrumented.java b/src/main/java/io/github/dsheirer/dsp/afsk/AFSKTimingErrorDetectorInstrumented.java
new file mode 100644
index 00000000..9824f654
--- /dev/null
+++ b/src/main/java/io/github/dsheirer/dsp/afsk/AFSKTimingErrorDetectorInstrumented.java
@@ -0,0 +1,53 @@
+/*******************************************************************************
+ * sdr-trunk
+ * Copyright (C) 2014-2018 Dennis Sheirer
+ *
+ * This program is free software: you can redistribute it and/or modify it under the terms of the GNU General Public
+ * License as published by the Free Software Foundation, either version 3 of the License, or (at your option) any
+ * later version.
+ *
+ * This program is distributed in the hope that it will be useful, but WITHOUT ANY WARRANTY; without even the implied
+ * warranty of MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU General Public License for more details.
+ *
+ * You should have received a copy of the GNU General Public License along with this program.
+ * If not, see
+ *
+ ******************************************************************************/
+package io.github.dsheirer.dsp.afsk;
+
+import javafx.beans.property.SimpleIntegerProperty;
+
+public class AFSKTimingErrorDetectorInstrumented extends AFSKTimingErrorDetector
+{
+ public SimpleIntegerProperty timingError = new SimpleIntegerProperty();
+
+ /**
+ * Constructs the detector for the specified samples per symbol.
+ *
+ * @param samplesPerSymbol
+ */
+ public AFSKTimingErrorDetectorInstrumented(int samplesPerSymbol)
+ {
+ super(samplesPerSymbol);
+ }
+
+ @Override
+ public int getError()
+ {
+ int error = super.getError();
+
+ timingError.setValue(error);
+
+ return error;
+ }
+
+ public int getDetectedZeroCrossing()
+ {
+ return mDetectedZeroCrossing;
+ }
+
+ public boolean[] getBuffer()
+ {
+ return mBuffer;
+ }
+}
diff --git a/src/main/java/io/github/dsheirer/dsp/am/AMDemodulator.java b/src/main/java/io/github/dsheirer/dsp/am/AMDemodulator.java
index 045d748f..0ca45fb3 100644
--- a/src/main/java/io/github/dsheirer/dsp/am/AMDemodulator.java
+++ b/src/main/java/io/github/dsheirer/dsp/am/AMDemodulator.java
@@ -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
+ *
+ ******************************************************************************/
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;
- }
}
diff --git a/src/main/java/io/github/dsheirer/dsp/am/AMDemodulator_CB.java b/src/main/java/io/github/dsheirer/dsp/am/AMDemodulator_CB.java
deleted file mode 100644
index 4a22ba6b..00000000
--- a/src/main/java/io/github/dsheirer/dsp/am/AMDemodulator_CB.java
+++ /dev/null
@@ -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
- ******************************************************************************/
-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
-{
- private Listener 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 listener )
- {
- mListener = listener;
- }
-
- public void removeListener()
- {
- mListener = null;
- }
-
- public void dispose()
- {
- mListener = null;
- }
-}
diff --git a/src/main/java/io/github/dsheirer/dsp/filter/ComplexFilter.java b/src/main/java/io/github/dsheirer/dsp/filter/ComplexFilter.java
deleted file mode 100644
index 59b9abb9..00000000
--- a/src/main/java/io/github/dsheirer/dsp/filter/ComplexFilter.java
+++ /dev/null
@@ -1,55 +0,0 @@
-/*******************************************************************************
- * SDR Trunk
- * Copyright (C) 2014 Dennis Sheirer
- *
- * This program is free software: you can redistribute it and/or modify
- * it under the terms of the GNU General Public License as published by
- * the Free Software Foundation, either version 3 of the License, or
- * (at your option) any later version.
- *
- * This program is distributed in the hope that it will be useful,
- * but WITHOUT ANY WARRANTY; without even the implied warranty of
- * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
- * GNU General Public License for more details.
- *
- * You should have received a copy of the GNU General Public License
- * along with this program. If not, see
- ******************************************************************************/
-package io.github.dsheirer.dsp.filter;
-
-import io.github.dsheirer.sample.Listener;
-import io.github.dsheirer.sample.complex.Complex;
-
-@Deprecated
-public abstract class ComplexFilter implements Listener
-{
- private Listener mListener;
-
- public void setListener( Listener listener )
- {
- mListener = listener;
- }
-
- public void dispose()
- {
- mListener = null;
- }
-
- public Listener getListener()
- {
- return mListener;
- }
-
- public boolean hasListener()
- {
- return mListener != null;
- }
-
- protected void send( Complex sample )
- {
- if( mListener != null )
- {
- mListener.receive( sample );
- }
- }
-}
diff --git a/src/main/java/io/github/dsheirer/dsp/filter/FilterFactory.java b/src/main/java/io/github/dsheirer/dsp/filter/FilterFactory.java
index 074448f4..989372fb 100644
--- a/src/main/java/io/github/dsheirer/dsp/filter/FilterFactory.java
+++ b/src/main/java/io/github/dsheirer/dsp/filter/FilterFactory.java
@@ -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 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 getFactors(int value)
{
Set factors = new TreeSet();
-
- /* 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));
+ }
}
diff --git a/src/main/java/io/github/dsheirer/dsp/filter/Window.java b/src/main/java/io/github/dsheirer/dsp/filter/Window.java
index 5ca75acd..a97f1453 100644
--- a/src/main/java/io/github/dsheirer/dsp/filter/Window.java
+++ b/src/main/java/io/github/dsheirer/dsp/filter/Window.java
@@ -1,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
+ * 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
+ *
******************************************************************************/
+
+/*
+ * Kaiser Window design methods: getKaiser(), getKaiserBeta() and getBesselZerothOrder() with minor naming changes were
+ * adopted from:
+ *
+ * https://github.com/rjeschke/neetutils-base/blob/master/src/main/java/com/github/rjeschke/neetutils/audio/FIRUtils.java
+ *
+ * Copyright (C) 2012 René Jeschke
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
package io.github.dsheirer.dsp.filter;
+import org.slf4j.Logger;
+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));
+ }
}
diff --git a/src/main/java/io/github/dsheirer/dsp/filter/channelizer/AbstractComplexPolyphaseChannelizer.java b/src/main/java/io/github/dsheirer/dsp/filter/channelizer/AbstractComplexPolyphaseChannelizer.java
new file mode 100644
index 00000000..102f145b
--- /dev/null
+++ b/src/main/java/io/github/dsheirer/dsp/filter/channelizer/AbstractComplexPolyphaseChannelizer.java
@@ -0,0 +1,169 @@
+/*******************************************************************************
+ * sdrtrunk
+ * Copyright (C) 2014-2017 Dennis Sheirer
+ *
+ * This program is free software: you can redistribute it and/or modify
+ * it under the terms of the GNU General Public License as published by
+ * the Free Software Foundation, either version 3 of the License, or
+ * (at your option) any later version.
+ *
+ * This program is distributed in the hope that it will be useful,
+ * but WITHOUT ANY WARRANTY; without even the implied warranty of
+ * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
+ * GNU General Public License for more details.
+ *
+ * You should have received a copy of the GNU General Public License
+ * along with this program. If not, see
+ *
+ ******************************************************************************/
+package 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, ISourceEventListener
+{
+ private final static Logger mLog = LoggerFactory.getLogger(AbstractComplexPolyphaseChannelizer.class);
+ private ReusableChannelResultsBufferQueue mBufferQueue = new ReusableChannelResultsBufferQueue("AbstractComplexPolyphaseChannelizer");
+ private Broadcaster mSourceChangeBroadcaster = new Broadcaster();
+ private List 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 getSourceEventListener()
+ {
+ return mSourceChangeBroadcaster;
+ }
+}
diff --git a/src/main/java/io/github/dsheirer/dsp/filter/channelizer/ChannelCalculator.java b/src/main/java/io/github/dsheirer/dsp/filter/channelizer/ChannelCalculator.java
new file mode 100644
index 00000000..2fb541f0
--- /dev/null
+++ b/src/main/java/io/github/dsheirer/dsp/filter/channelizer/ChannelCalculator.java
@@ -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
+ * *********************************************************************************************************************
+ ******************************************************************************/
+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 getChannelIndexes(TunerChannel tunerChannel) throws IllegalArgumentException
+ {
+ if(tunerChannel.getMinFrequency() < getMinimumFrequency() ||
+ tunerChannel.getMaxFrequency() > getMaximumFrequency())
+ {
+ throw new IllegalArgumentException("Requested channel cannot be provided by this " +
+ "channelizer. Requested channel [" + tunerChannel.getMinFrequency() + " - " +
+ tunerChannel.getMaxFrequency() + "] exceeds current channelizer frequency range [" +
+ getMinimumFrequency() + " - " + getMaximumFrequency() + "]");
+ }
+
+ int minIndex = getIndexForFrequency(tunerChannel.getMinFrequency(), IndexBoundaryPolicy.ADJUST_POSITIVE);
+ int maxIndex = getIndexForFrequency(tunerChannel.getMaxFrequency(), IndexBoundaryPolicy.ADJUST_NEGATIVE);
+
+ //Detect the unsourceable situation where the wrap around index would be included in both the minimum index
+ //and the maximum index, meaning that the tuner channel bandwidth is nearly the same as the input sample rate.
+ if(minIndex == getWrapAroundIndex() && maxIndex == getWrapAroundIndex())
+ {
+ throw new IllegalArgumentException("Requested tuner channel cannot be provided. Requested bandwidth is " +
+ "within two channel bandwidths of the sample rate.");
+ }
+
+ List indexes = new ArrayList<>();
+
+ indexes.add(minIndex);
+
+ if(minIndex != maxIndex)
+ {
+ //Ensure our min/max indexes are within the correct bounds 0 <> N-1 to safeguard the while loop below
+ if(minIndex < 0 || minIndex >= getChannelCount() || maxIndex < 0 || maxIndex >= getChannelCount())
+ {
+ throw new IllegalArgumentException("Something went wrong while calculating the min and max polyphase " +
+ "channel indexes for tuner channel: " + tunerChannel);
+ }
+
+ int pointer = minIndex + 1;
+
+ if(pointer >= getChannelCount())
+ {
+ pointer -= getChannelCount();
+ }
+
+ while(pointer != maxIndex)
+ {
+ indexes.add(pointer);
+
+ pointer++;
+
+ if(pointer >= getChannelCount())
+ {
+ pointer -= getChannelCount();
+ }
+ }
+
+ indexes.add(maxIndex);
+ }
+
+ return indexes;
+ }
+
+ /**
+ * Calculates the index for the specified frequency. Certain frequencies can exist as both the maximum frequency
+ * of one channel and the minimum frequency of an adjacent channel. The index boundary policy identifies your
+ * choice to select the higher frequency channel index (ADJUST_POSITIVE) or the lower frequency channel
+ * index (ADJUST_NEGATIVE).
+ *
+ * @param frequency to determine channel index for
+ * @param indexBoundaryPolicy to determine the behavior when the frequency overlaps two channel boundaries
+ * @return index for the frequency
+ */
+ public int getIndexForFrequency(long frequency, IndexBoundaryPolicy indexBoundaryPolicy)
+ {
+ double offset = frequency - getCenterFrequency();
+
+ if(Math.abs(offset) < getHalfChannelBandwidth())
+ {
+ return 0; //Center index
+ }
+
+ if(offset > 0)
+ {
+ offset += getHalfChannelBandwidth();
+ }
+ else
+ {
+ offset -= getHalfChannelBandwidth();
+ }
+
+ int indexOffset = (int)(offset / getChannelBandwidth());
+
+ if(indexOffset < 0)
+ {
+ indexOffset += getChannelCount();
+ }
+
+ if(indexBoundaryPolicy == IndexBoundaryPolicy.ADJUST_POSITIVE &&
+ isOverlapFrequency(frequency, indexOffset, indexOffset + 1))
+ {
+ indexOffset = normalize(indexOffset + 1);
+ }
+ else if(indexBoundaryPolicy == IndexBoundaryPolicy.ADJUST_NEGATIVE &&
+ isOverlapFrequency(frequency, indexOffset - 1, indexOffset))
+ {
+ indexOffset = normalize(indexOffset - 1);
+ }
+
+ return indexOffset;
+ }
+
+ /**
+ * Indicates if the frequency is an overlapping frequency between channel index 1 and channel index 2 where the
+ * maximum frequency of index 1 is the same as the minimum frequency of index 2 and both of these frequencies are
+ * the same as the frequency argument.
+ *
+ * Note: both index 1 and index 2 will be normalized to the range of 0 <> N-1 prior to determining overlap
+ *
+ * @param frequency to test for overlap between index 1 and 2
+ * @param index1 which is the lower channel index located next to index 2.
+ * @param index2 which is the higher channel index located next to index 1.
+ * @return true if frequency = index1 maximum frequency = index2 minimum frequency
+ */
+ public boolean isOverlapFrequency(long frequency, int index1, int index2)
+ {
+ index1 = normalize(index1);
+ index2 = normalize(index2);
+
+ //Check that these two indexes are side by side
+ int delta = normalize(index2 - index1);
+
+ if(delta != 1)
+ {
+ return false;
+ }
+
+ long index1MaximumFrequency = (long)getIndexMaximumFrequency(index1, IndexBoundaryPolicy.ADJUST_POSITIVE);
+ long index2MinimumFrequency = (long)getIndexMinimumFrequency(index2, IndexBoundaryPolicy.ADJUST_NEGATIVE);
+
+ if(index1 == getWrapAroundIndex())
+ {
+ index1MaximumFrequency = (long)getIndexMaximumFrequency(index1, IndexBoundaryPolicy.ADJUST_NEGATIVE);
+ }
+
+ if(index2 == getWrapAroundIndex())
+ {
+ index2MinimumFrequency = (long)getIndexMinimumFrequency(index2, IndexBoundaryPolicy.ADJUST_POSITIVE);
+ }
+
+ return frequency == index1MaximumFrequency && frequency == index2MinimumFrequency;
+ }
+
+ /**
+ * Normalizes the index to the range of 0 <> N-1 channel indexes>
+ */
+ private int normalize(int index)
+ {
+ while(index < 0)
+ {
+ index += getChannelCount();
+ }
+
+ while(index >= getChannelCount())
+ {
+ index -= getChannelCount();
+ }
+
+ return index;
+ }
+
+ /**
+ * Calculates the center frequency for the channel identified by the index in the channel results array.
+ *
+ * @param index to determine the channel center frequency
+ * @return center frequency for the indexed channel
+ */
+ public double getIndexCenterFrequency(int index, IndexBoundaryPolicy indexBoundaryPolicy)
+ {
+ if(index < 0 || index >= getChannelCount())
+ {
+ throw new IllegalArgumentException("Illegal channel index [" + 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 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");
+ }
+}
diff --git a/src/main/java/io/github/dsheirer/dsp/filter/channelizer/ComplexPolyphaseChannelizerM2.java b/src/main/java/io/github/dsheirer/dsp/filter/channelizer/ComplexPolyphaseChannelizerM2.java
new file mode 100644
index 00000000..7b547713
--- /dev/null
+++ b/src/main/java/io/github/dsheirer/dsp/filter/channelizer/ComplexPolyphaseChannelizerM2.java
@@ -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
+ *
+ ******************************************************************************/
+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
+ {
+ 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>()
+ {
+ @Override
+ public void receive(List 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 clearQueue()
+ {
+ Collection buffersToDispose = super.clearQueue();
+
+ for(ReusableChannelResultsBuffer buffer: buffersToDispose)
+ {
+ buffer.clearUserCount();
+ }
+
+ return buffersToDispose;
+ }
+ }
+}
diff --git a/src/main/java/io/github/dsheirer/dsp/filter/channelizer/ContinuousBufferProcessor.java b/src/main/java/io/github/dsheirer/dsp/filter/channelizer/ContinuousBufferProcessor.java
new file mode 100644
index 00000000..0ee7e268
--- /dev/null
+++ b/src/main/java/io/github/dsheirer/dsp/filter/channelizer/ContinuousBufferProcessor.java
@@ -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
+ *
+ ******************************************************************************/
+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 implements Listener
+{
+ private final static Logger mLog = LoggerFactory.getLogger(ContinuousBufferProcessor.class);
+
+ private OverflowableTransferQueue mQueue;
+ private Listener> 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 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> 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 clearQueue()
+ {
+ Collection 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 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);
+ }
+ }
+ }
+}
diff --git a/src/main/java/io/github/dsheirer/dsp/filter/channelizer/ContinuousReusableBufferProcessor.java b/src/main/java/io/github/dsheirer/dsp/filter/channelizer/ContinuousReusableBufferProcessor.java
new file mode 100644
index 00000000..0439fb11
--- /dev/null
+++ b/src/main/java/io/github/dsheirer/dsp/filter/channelizer/ContinuousReusableBufferProcessor.java
@@ -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
+ *
+ ******************************************************************************/
+package io.github.dsheirer.dsp.filter.channelizer;
+
+import io.github.dsheirer.sample.buffer.OverflowableReusableBufferTransferQueue;
+import io.github.dsheirer.sample.buffer.ReusableBuffer;
+
+public class ContinuousReusableBufferProcessor extends ContinuousBufferProcessor
+{
+ /**
+ * 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(maximumSize, resetThreshold));
+ }
+}
diff --git a/src/main/java/io/github/dsheirer/dsp/filter/channelizer/PolyphaseChannelManager.java b/src/main/java/io/github/dsheirer/dsp/filter/channelizer/PolyphaseChannelManager.java
new file mode 100644
index 00000000..ed5bf2d8
--- /dev/null
+++ b/src/main/java/io/github/dsheirer/dsp/filter/channelizer/PolyphaseChannelManager.java
@@ -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
+ *
+ ******************************************************************************/
+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 mSourceEventBroadcaster = new Broadcaster<>();
+ private IReusableComplexBufferProvider mReusableBufferProvider;
+ private List mChannelSources = new CopyOnWriteArrayList<>();
+ private ChannelCalculator mChannelCalculator;
+ private ComplexPolyphaseChannelizerM2 mPolyphaseChannelizer;
+ private ChannelSourceEventListener mChannelSourceEventListener = new ChannelSourceEventListener();
+ private BufferSourceEventMonitor mBufferSourceEventMonitor = new BufferSourceEventMonitor();
+ private ContinuousBufferProcessor mBufferProcessor;
+ private Map 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 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 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 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 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 getTunerChannels()
+ {
+ SortedSet tunerChannels = new TreeSet<>();
+
+ for(PolyphaseChannelSource channelSource: mChannelSources)
+ {
+ tunerChannels.add(channelSource.getTunerChannel());
+ }
+
+ return tunerChannels;
+ }
+
+ /**
+ * Count of currently sourced tuner channels
+ */
+ public int getTunerChannelCount()
+ {
+ return mChannelSources.size();
+ }
+
+ /**
+ * Adds the listener to receive source events
+ */
+ public void addSourceEventListener(Listener listener)
+ {
+ mSourceEventBroadcaster.addListener(listener);
+ }
+
+ /**
+ * Removes the listener from receiving source events
+ */
+ public void removeSourceEventListener(Listener listener)
+ {
+ mSourceEventBroadcaster.removeListener(listener);
+ }
+
+ /**
+ * 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
+ {
+ @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>
+ {
+ private Queue mQueuedSourceEvents = new ConcurrentLinkedQueue<>();
+
+ /**
+ * Queues the source event for deferred execution on the buffer processing thread.
+ * @param event that affects configuration of the channelizer (frequency or sample rate change events)
+ */
+ public void receive(SourceEvent event)
+ {
+ mQueuedSourceEvents.offer(event);
+ }
+
+ @Override
+ public void receive(List 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();
+ }
+ }
+ }
+ }
+}
\ No newline at end of file
diff --git a/src/main/java/io/github/dsheirer/dsp/filter/channelizer/PolyphaseChannelResultsBuffer.java b/src/main/java/io/github/dsheirer/dsp/filter/channelizer/PolyphaseChannelResultsBuffer.java
new file mode 100644
index 00000000..ce46030c
--- /dev/null
+++ b/src/main/java/io/github/dsheirer/dsp/filter/channelizer/PolyphaseChannelResultsBuffer.java
@@ -0,0 +1,171 @@
+/*******************************************************************************
+ * sdr-trunk
+ * Copyright (C) 2014-2018 Dennis Sheirer
+ *
+ * This program is free software: you can redistribute it and/or modify it under the terms of the GNU General Public
+ * License as published by the Free Software Foundation, either version 3 of the License, or (at your option) any
+ * later version.
+ *
+ * This program is distributed in the hope that it will be useful, but WITHOUT ANY WARRANTY; without even the implied
+ * warranty of MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU General Public License for more details.
+ *
+ * You should have received a copy of the GNU General Public License along with this program.
+ * If not, see
+ *
+ ******************************************************************************/
+package io.github.dsheirer.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;
+ }
+}
diff --git a/src/main/java/io/github/dsheirer/dsp/filter/channelizer/PolyphaseChannelSource.java b/src/main/java/io/github/dsheirer/dsp/filter/channelizer/PolyphaseChannelSource.java
new file mode 100644
index 00000000..222e0690
--- /dev/null
+++ b/src/main/java/io/github/dsheirer/dsp/filter/channelizer/PolyphaseChannelSource.java
@@ -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
+ *
+ ******************************************************************************/
+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 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 listener)
+ {
+ mReusableComplexBufferAssembler.setListener(listener);
+ }
+
+ /**
+ * Removes the listener from receiving complex sample buffers from this channel source
+ */
+ @Override
+ public void removeListener(Listener 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());
+ }
+ }
+}
diff --git a/src/main/java/io/github/dsheirer/dsp/filter/channelizer/SampleTimestampManager.java b/src/main/java/io/github/dsheirer/dsp/filter/channelizer/SampleTimestampManager.java
new file mode 100644
index 00000000..38c8a943
--- /dev/null
+++ b/src/main/java/io/github/dsheirer/dsp/filter/channelizer/SampleTimestampManager.java
@@ -0,0 +1,83 @@
+/*******************************************************************************
+ * sdr-trunk
+ * Copyright (C) 2014-2018 Dennis Sheirer
+ *
+ * This program is free software: you can redistribute it and/or modify it under the terms of the GNU General Public
+ * License as published by the Free Software Foundation, either version 3 of the License, or (at your option) any
+ * later version.
+ *
+ * This program is distributed in the hope that it will be useful, but WITHOUT ANY WARRANTY; without even the implied
+ * warranty of MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU General Public License for more details.
+ *
+ * You should have received a copy of the GNU General Public License along with this program.
+ * If not, see
+ *
+ ******************************************************************************/
+package io.github.dsheirer.dsp.filter.channelizer;
+
+public class SampleTimestampManager
+{
+ private long mReferenceTimestamp = System.currentTimeMillis();
+ private double mSamplesPerMilliSecond;
+ private long mSampleCount;
+
+ /**
+ * Tracks the number of samples produced relative to a reference timestamp to derive a current reference timestamp
+ * for use in generating sample buffers.
+ *
+ * @param sampleRate per second
+ */
+ public SampleTimestampManager(double sampleRate)
+ {
+ }
+
+ /**
+ * Updates the internal reference timestamp and resets the sample counter to zero.
+ *
+ * @param referenceTimestamp in milli-seconds since epoch
+ */
+ public void setReferenceTimestamp(long referenceTimestamp)
+ {
+ mReferenceTimestamp = referenceTimestamp;
+ mSampleCount = 0;
+ }
+
+ /**
+ * Updates the sample rate that is used in calculating the reference timestamp
+ *
+ * @param sampleRate
+ */
+ public void setSampleRate(double sampleRate)
+ {
+ //Convert to samples per milli-second for compatibility with reference timestamp
+ mSamplesPerMilliSecond = sampleRate / 1000.0d;
+ }
+
+ /**
+ * Increments the internal count of dispatched samples to use in calculating an accurate reference timestamp.
+ */
+ public void increment()
+ {
+ mSampleCount++;
+ }
+
+ /**
+ * Increments the internal count of dispatched samples to use in calculating an accurate reference timestamp.
+ *
+ * @param value to add to the sample count
+ */
+ public void increment(int value)
+ {
+ mSampleCount += value;
+ }
+
+ public long getCurrentTimestamp()
+ {
+ if(mSampleCount > 0)
+ {
+ return mReferenceTimestamp + (long)((double)mSampleCount / mSamplesPerMilliSecond);
+ }
+
+ return mReferenceTimestamp;
+ }
+}
diff --git a/src/main/java/io/github/dsheirer/dsp/filter/channelizer/TwoChannelSynthesizerM2.java b/src/main/java/io/github/dsheirer/dsp/filter/channelizer/TwoChannelSynthesizerM2.java
new file mode 100644
index 00000000..a8bbc02c
--- /dev/null
+++ b/src/main/java/io/github/dsheirer/dsp/filter/channelizer/TwoChannelSynthesizerM2.java
@@ -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
+ *
+ ******************************************************************************/
+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;
+ }
+}
diff --git a/src/main/java/io/github/dsheirer/dsp/filter/channelizer/output/ChannelOutputProcessor.java b/src/main/java/io/github/dsheirer/dsp/filter/channelizer/output/ChannelOutputProcessor.java
new file mode 100644
index 00000000..599832fb
--- /dev/null
+++ b/src/main/java/io/github/dsheirer/dsp/filter/channelizer/output/ChannelOutputProcessor.java
@@ -0,0 +1,161 @@
+/*******************************************************************************
+ * sdrtrunk
+ * Copyright (C) 2014-2017 Dennis Sheirer
+ *
+ * This program is free software: you can redistribute it and/or modify
+ * it under the terms of the GNU General Public License as published by
+ * the Free Software Foundation, either version 3 of the License, or
+ * (at your option) any later version.
+ *
+ * This program is distributed in the hope that it will be useful,
+ * but WITHOUT ANY WARRANTY; without even the implied warranty of
+ * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
+ * GNU General Public License for more details.
+ *
+ * You should have received a copy of the GNU General Public License
+ * along with this program. If not, see
+ *
+ ******************************************************************************/
+package 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 mChannelResultsQueue;
+ private List 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 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;
+ }
+}
diff --git a/src/main/java/io/github/dsheirer/dsp/filter/channelizer/output/IPolyphaseChannelOutputProcessor.java b/src/main/java/io/github/dsheirer/dsp/filter/channelizer/output/IPolyphaseChannelOutputProcessor.java
new file mode 100644
index 00000000..70eb7c4a
--- /dev/null
+++ b/src/main/java/io/github/dsheirer/dsp/filter/channelizer/output/IPolyphaseChannelOutputProcessor.java
@@ -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
+ *
+ ******************************************************************************/
+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 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();
+}
diff --git a/src/main/java/io/github/dsheirer/dsp/filter/channelizer/output/OneChannelOutputProcessor.java b/src/main/java/io/github/dsheirer/dsp/filter/channelizer/output/OneChannelOutputProcessor.java
new file mode 100644
index 00000000..ece7eedd
--- /dev/null
+++ b/src/main/java/io/github/dsheirer/dsp/filter/channelizer/output/OneChannelOutputProcessor.java
@@ -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
+ *
+ ******************************************************************************/
+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 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 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 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();
+ }
+ }
+}
diff --git a/src/main/java/io/github/dsheirer/dsp/filter/channelizer/output/TwoChannelOutputProcessor.java b/src/main/java/io/github/dsheirer/dsp/filter/channelizer/output/TwoChannelOutputProcessor.java
new file mode 100644
index 00000000..34cca3a1
--- /dev/null
+++ b/src/main/java/io/github/dsheirer/dsp/filter/channelizer/output/TwoChannelOutputProcessor.java
@@ -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
+ *
+ ******************************************************************************/
+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 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 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 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 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);
+ }
+}
diff --git a/src/main/java/io/github/dsheirer/dsp/filter/cic/ComplexPrimeCICDecimate.java b/src/main/java/io/github/dsheirer/dsp/filter/cic/ComplexPrimeCICDecimate.java
index 2ad5d95e..54c2db59 100644
--- a/src/main/java/io/github/dsheirer/dsp/filter/cic/ComplexPrimeCICDecimate.java
+++ b/src/main/java/io/github/dsheirer/dsp/filter/cic/ComplexPrimeCICDecimate.java
@@ -1,19 +1,16 @@
/*******************************************************************************
- * sdrtrunk
- * Copyright (C) 2014-2017 Dennis Sheirer
+ * sdr-trunk
+ * Copyright (C) 2014-2018 Dennis Sheirer
*
- * This program is free software: you can redistribute it and/or modify
- * it under the terms of the GNU General Public License as published by
- * the Free Software Foundation, either version 3 of the License, or
- * (at your option) any later version.
+ * This program is free software: you can redistribute it and/or modify it under the terms of the GNU General Public
+ * License as published by the Free Software Foundation, either version 3 of the License, or (at your option) any
+ * later version.
*
- * This program is distributed in the hope that it will be useful,
- * but WITHOUT ANY WARRANTY; without even the implied warranty of
- * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
- * GNU General Public License for more details.
+ * This program is distributed in the hope that it will be useful, but WITHOUT ANY WARRANTY; without even the implied
+ * warranty of MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU General Public License for more details.
*
- * You should have received a copy of the GNU General Public License
- * along with this program. If not, see
+ * You should have received a copy of the GNU General Public License along with this program.
+ * If not, see
*
******************************************************************************/
package io.github.dsheirer.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
+public class ComplexPrimeCICDecimate implements Listener
{
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
}
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
/**
* Adds a listener to receive the output of this CIC decimation filter
*/
- public void setListener(Listener listener)
+ public void setListener(Listener listener)
{
mOutput.setListener(listener);
}
@@ -166,7 +163,7 @@ public class ComplexPrimeCICDecimate implements Listener
* 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
mFirstDecimatingStage.receive(samples[x], samples[x + 1]);
}
}
+
+ buffer.decrementUserCount();
}
/**
@@ -289,11 +288,11 @@ public class ComplexPrimeCICDecimate implements Listener
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
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 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()
+ {
+ @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
@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 listener)
+ public void setListener(Listener listener)
{
- mHalfBandFilter.setListener(listener);
+ mReusableComplexBufferListener = listener;
}
- /**
- * Removes the listener from receiving output samples
- */
public void removeListener()
{
- mHalfBandFilter.removeListener();
+ mReusableComplexBufferListener = null;
}
}
}
diff --git a/src/main/java/io/github/dsheirer/dsp/filter/cic/RealPrimeCICDecimate.java b/src/main/java/io/github/dsheirer/dsp/filter/cic/RealPrimeCICDecimate.java
index 2b1b2b82..87b444b7 100644
--- a/src/main/java/io/github/dsheirer/dsp/filter/cic/RealPrimeCICDecimate.java
+++ b/src/main/java/io/github/dsheirer/dsp/filter/cic/RealPrimeCICDecimate.java
@@ -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
******************************************************************************/
@@ -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 mDecimatingStages =
- new ArrayList();
-
- 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 mDecimatingStages = new ArrayList();
+ private DecimatingStage mFirstDecimatingStage;
+ private Output mOutput;
- mOutputBufferSize = outputBufferSize;
+ private int mOutputBufferSize = 2048;
- List 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 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 getPrimeFactors( int decimation )
- {
- ArrayList stages = new ArrayList();
-
- 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 mStages = new ArrayList();
- 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 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 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 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 getPrimeFactors(int decimation)
+ {
+ ArrayList stages = new ArrayList();
+
+ 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 mStages = new ArrayList();
+ 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 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()
+ {
+ @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 listener)
+ {
+ mReusableBufferListener = listener;
+ }
+
+ /**
+ * Removes the listener from receiving output samples
+ */
public void removeListener()
{
- mHalfBandFilter.removeListener();
+ mReusableBufferListener = null;
}
- }
+ }
}
diff --git a/src/main/java/io/github/dsheirer/dsp/filter/correction/IQCorrectionFilter.java b/src/main/java/io/github/dsheirer/dsp/filter/correction/IQCorrectionFilter.java
new file mode 100644
index 00000000..03224bc2
--- /dev/null
+++ b/src/main/java/io/github/dsheirer/dsp/filter/correction/IQCorrectionFilter.java
@@ -0,0 +1,58 @@
+/*******************************************************************************
+ * sdrtrunk
+ * Copyright (C) 2014-2017 Dennis Sheirer
+ *
+ * This program is free software: you can redistribute it and/or modify
+ * it under the terms of the GNU General Public License as published by
+ * the Free Software Foundation, either version 3 of the License, or
+ * (at your option) any later version.
+ *
+ * This program is distributed in the hope that it will be useful,
+ * but WITHOUT ANY WARRANTY; without even the implied warranty of
+ * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
+ * GNU General Public License for more details.
+ *
+ * You should have received a copy of the GNU General Public License
+ * along with this program. If not, see
+ *
+ ******************************************************************************/
+package 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;
+ }
+}
diff --git a/src/main/java/io/github/dsheirer/dsp/filter/dc/BufferDCFilter.java b/src/main/java/io/github/dsheirer/dsp/filter/dc/BufferDCFilter.java
new file mode 100644
index 00000000..d9f2d25c
--- /dev/null
+++ b/src/main/java/io/github/dsheirer/dsp/filter/dc/BufferDCFilter.java
@@ -0,0 +1,98 @@
+/*******************************************************************************
+ * sdr-trunk
+ * Copyright (C) 2014-2018 Dennis Sheirer
+ *
+ * This program is free software: you can redistribute it and/or modify it under the terms of the GNU General Public
+ * License as published by the Free Software Foundation, either version 3 of the License, or (at your option) any
+ * later version.
+ *
+ * This program is distributed in the hope that it will be useful, but WITHOUT ANY WARRANTY; without even the implied
+ * warranty of MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU General Public License for more details.
+ *
+ * You should have received a copy of the GNU General Public License along with this program.
+ * If not, see
+ *
+ ******************************************************************************/
+package io.github.dsheirer.dsp.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;
+ }
+}
diff --git a/src/main/java/io/github/dsheirer/dsp/filter/dc/DCRemovalFilter.java b/src/main/java/io/github/dsheirer/dsp/filter/dc/DCRemovalFilter.java
index d265b8e6..4f7f2be4 100644
--- a/src/main/java/io/github/dsheirer/dsp/filter/dc/DCRemovalFilter.java
+++ b/src/main/java/io/github/dsheirer/dsp/filter/dc/DCRemovalFilter.java
@@ -1,41 +1,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
+ * sdr-trunk
+ * Copyright (C) 2014-2018 Dennis Sheirer
+ *
+ * This program is free software: you can redistribute it and/or modify it under the terms of the GNU General Public
+ * License as published by the Free Software Foundation, either version 3 of the License, or (at your option) any
+ * later version.
+ *
+ * This program is distributed in the hope that it will be useful, but WITHOUT ANY WARRANTY; without even the implied
+ * warranty of MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU General Public License for more details.
+ *
+ * You should have received a copy of the GNU General Public License along with this program.
+ * If not, see
+ *
******************************************************************************/
package io.github.dsheirer.dsp.filter.dc;
+import io.github.dsheirer.sample.buffer.ReusableBuffer;
+import io.github.dsheirer.sample.buffer.ReusableBufferQueue;
+
+/**
+ * DC removal filter for removing any DC offset that may be present when a signal is not aligned within the
+ * demodulated channel.
+ */
public class DCRemovalFilter
{
- protected float mAverage;
- protected float mRatio;
+ private ReusableBufferQueue mReusableBufferQueue = new ReusableBufferQueue("DC Removal Filter");
+ protected float mAverage;
+ protected float mRatio;
- public DCRemovalFilter( float ratio )
- {
- mRatio = ratio;
- }
+ /**
+ * Creates a DC removal filter.
+ *
+ * @param ratio
+ */
+ public DCRemovalFilter(float ratio)
+ {
+ mRatio = ratio;
+ }
- public void reset()
- {
- mAverage = 0.0f;
- }
+ /**
+ * Resets the running average to zero.
+ */
+ public void reset()
+ {
+ mAverage = 0.0f;
+ }
- public float filter( float sample )
- {
- float filtered = sample - mAverage;
- mAverage += mRatio * filtered;
- return filtered;
- }
+ /**
+ * Filters the sample and returns a filtered (ie DC-removed) sample.
+ * @param sample to filter
+ * @return filtered sample
+ */
+ public float filter(float sample)
+ {
+ float filtered = sample - mAverage;
+ mAverage += mRatio * filtered;
+ return filtered;
+ }
+
+ 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;
+ }
}
\ No newline at end of file
diff --git a/src/main/java/io/github/dsheirer/dsp/filter/dc/DCRemovalFilter_CB.java b/src/main/java/io/github/dsheirer/dsp/filter/dc/DCRemovalFilter_CB.java
deleted file mode 100644
index 28deb686..00000000
--- a/src/main/java/io/github/dsheirer/dsp/filter/dc/DCRemovalFilter_CB.java
+++ /dev/null
@@ -1,71 +0,0 @@
-/*******************************************************************************
- * SDR Trunk
- * Copyright (C) 2014 Dennis Sheirer
- *
- * This program is free software: you can redistribute it and/or modify
- * it under the terms of the GNU General Public License as published by
- * the Free Software Foundation, either version 3 of the License, or
- * (at your option) any later version.
- *
- * This program is distributed in the hope that it will be useful,
- * but WITHOUT ANY WARRANTY; without even the implied warranty of
- * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
- * GNU General Public License for more details.
- *
- * You should have received a copy of the GNU General Public License
- * along with this program. If not, see
- ******************************************************************************/
-package io.github.dsheirer.dsp.filter.dc;
-
-import io.github.dsheirer.sample.Listener;
-import io.github.dsheirer.sample.complex.ComplexBuffer;
-
-public class DCRemovalFilter_CB implements Listener
-{
- private Listener mListener;
- private DCRemovalFilter mIFilter;
- private DCRemovalFilter mQFilter;
-
- public DCRemovalFilter_CB( float ratio )
- {
- mIFilter = new DCRemovalFilter( ratio );
- mQFilter = new DCRemovalFilter( ratio );
- }
-
- public void dispose()
- {
- mListener = null;
- }
-
- public float[] filter( float[] samples )
- {
- for( int x = 0; x < samples.length; x += 2 )
- {
- samples[ x ] = mIFilter.filter( samples[ x ] );
- samples[ x + 1 ] = mQFilter.filter( samples[ x + 1 ] );
- }
-
- return samples;
- }
-
- public ComplexBuffer filter( ComplexBuffer buffer )
- {
- filter( buffer.getSamples() );
-
- return buffer;
- }
-
- @Override
- public void receive( ComplexBuffer buffer )
- {
- if( mListener != null )
- {
- mListener.receive( filter( buffer ) );
- }
- }
-
- public void setListener( Listener listener )
- {
- mListener = listener;
- }
-}
diff --git a/src/main/java/io/github/dsheirer/dsp/filter/dc/DCRemovalFilter_RB.java b/src/main/java/io/github/dsheirer/dsp/filter/dc/DCRemovalFilter_RB.java
deleted file mode 100644
index 74892dae..00000000
--- a/src/main/java/io/github/dsheirer/dsp/filter/dc/DCRemovalFilter_RB.java
+++ /dev/null
@@ -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
-{
- protected Listener mListener;
-
- public DCRemovalFilter_RB( float ratio )
- {
- super( ratio );
- }
-
- public void setListener( Listener 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 ) );
- }
- }
-}
diff --git a/src/main/java/io/github/dsheirer/dsp/filter/dc/DCRemovalFilter_R_R.java b/src/main/java/io/github/dsheirer/dsp/filter/dc/DCRemovalFilter_R_R.java
deleted file mode 100644
index ee5c8c53..00000000
--- a/src/main/java/io/github/dsheirer/dsp/filter/dc/DCRemovalFilter_R_R.java
+++ /dev/null
@@ -1,54 +0,0 @@
-/*******************************************************************************
- * SDR Trunk
- * Copyright (C) 2014 Dennis Sheirer
- *
- * This program is free software: you can redistribute it and/or modify
- * it under the terms of the GNU General Public License as published by
- * the Free Software Foundation, either version 3 of the License, or
- * (at your option) any later version.
- *
- * This program is distributed in the hope that it will be useful,
- * but WITHOUT ANY WARRANTY; without even the implied warranty of
- * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
- * GNU General Public License for more details.
- *
- * You should have received a copy of the GNU General Public License
- * along with this program. If not, see
- ******************************************************************************/
-package io.github.dsheirer.dsp.filter.dc;
-
-import io.github.dsheirer.sample.real.RealSampleListener;
-
-public class DCRemovalFilter_R_R extends DCRemovalFilter implements RealSampleListener
-{
- RealSampleListener mListener;
-
- public DCRemovalFilter_R_R( float ratio )
- {
- super( ratio );
- }
-
- public void dispose()
- {
- mListener = null;
- }
-
- @Override
- public void receive( float sample )
- {
- if( mListener != null )
- {
- mListener.receive( filter( sample ) );
- }
- }
-
- public void setListener( RealSampleListener listener )
- {
- mListener = listener;
- }
-
- public void removeListener()
- {
- mListener = null;
- }
-}
diff --git a/src/main/java/io/github/dsheirer/dsp/filter/dc/DCRemovalFilter_RealBuffer.java b/src/main/java/io/github/dsheirer/dsp/filter/dc/DCRemovalFilter_RealBuffer.java
deleted file mode 100644
index f401aaf4..00000000
--- a/src/main/java/io/github/dsheirer/dsp/filter/dc/DCRemovalFilter_RealBuffer.java
+++ /dev/null
@@ -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
-{
- private Listener 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 listener )
- {
- mListener = listener;
- }
-
- public void removeListener()
- {
- mListener = null;
- }
-}
diff --git a/src/main/java/io/github/dsheirer/dsp/filter/dc/IIRSinglePoleDCRemovalFilter.java b/src/main/java/io/github/dsheirer/dsp/filter/dc/IIRSinglePoleDCRemovalFilter.java
index 83a59eae..d11d8035 100644
--- a/src/main/java/io/github/dsheirer/dsp/filter/dc/IIRSinglePoleDCRemovalFilter.java
+++ b/src/main/java/io/github/dsheirer/dsp/filter/dc/IIRSinglePoleDCRemovalFilter.java
@@ -1,45 +1,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
+ *
+ ******************************************************************************/
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;
+ }
}
diff --git a/src/main/java/io/github/dsheirer/dsp/filter/dc/IIRSinglePoleDCRemovalFilter_RB.java b/src/main/java/io/github/dsheirer/dsp/filter/dc/IIRSinglePoleDCRemovalFilter_RB.java
deleted file mode 100644
index 974374c0..00000000
--- a/src/main/java/io/github/dsheirer/dsp/filter/dc/IIRSinglePoleDCRemovalFilter_RB.java
+++ /dev/null
@@ -1,69 +0,0 @@
-package io.github.dsheirer.dsp.filter.dc;
-
-import io.github.dsheirer.sample.Listener;
-import io.github.dsheirer.sample.real.RealBuffer;
-
-public class IIRSinglePoleDCRemovalFilter_RB implements Listener
-{
- private float mAlpha;
- private float mPreviousInput = 0.0f;
- private float mPreviousOutput = 0.0f;
- private Listener mListener;
-
- /**
- * IIR single-pole DC removal filter, as described by J M de Freitas in
- * 29Jan2007 paper at:
- *
- * http://www.mathworks.com/matlabcentral/fileexchange/downloads/72134/download
- *
- * @param alpha 0.0 - 1.0 float - the closer alpha is to unity, the closer
- * the cutoff frequency is to DC.
- */
- public IIRSinglePoleDCRemovalFilter_RB( float alpha )
- {
- mAlpha = alpha;
- }
-
- @Override
- public void receive( RealBuffer buffer )
- {
- if( mListener != null )
- {
- mListener.receive( filter( buffer ) );
- }
- }
-
- public RealBuffer filter( RealBuffer buffer )
- {
- float[] samples = buffer.getSamples();
-
- for( int x = 0; x < samples.length; x++ )
- {
- samples[ x ] = filter( samples[ x ] );
- }
-
- return buffer;
- }
-
- public float filter( float sample )
- {
- float currentOutput = ( sample - mPreviousInput ) +
- ( mAlpha * mPreviousOutput );
-
- mPreviousInput = sample;
- mPreviousOutput = currentOutput;
-
- return currentOutput;
- }
-
- public void reset()
- {
- mPreviousInput = 0.0f;
- mPreviousOutput = 0.0f;
- }
-
- public void setListener( Listener listener )
- {
- mListener = listener;
- }
-}
diff --git a/src/main/java/io/github/dsheirer/dsp/filter/design/FilterView.java b/src/main/java/io/github/dsheirer/dsp/filter/design/FilterView.java
index 591da465..f0ee3b78 100644
--- a/src/main/java/io/github/dsheirer/dsp/filter/design/FilterView.java
+++ b/src/main/java/io/github/dsheirer/dsp/filter/design/FilterView.java
@@ -1,3 +1,18 @@
+/*******************************************************************************
+ * sdr-trunk
+ * Copyright (C) 2014-2018 Dennis Sheirer
+ *
+ * This program is free software: you can redistribute it and/or modify it under the terms of the GNU General Public
+ * License as published by the Free Software Foundation, either version 3 of the License, or (at your option) any
+ * later version.
+ *
+ * This program is distributed in the hope that it will be useful, but WITHOUT ANY WARRANTY; without even the implied
+ * warranty of MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU General Public License for more details.
+ *
+ * You should have received a copy of the GNU General Public License along with this program.
+ * If not, see
+ *
+ ******************************************************************************/
package io.github.dsheirer.dsp.filter.design;
import io.github.dsheirer.gui.control.CurveFittedAreaChart;
@@ -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);
}
diff --git a/src/main/java/io/github/dsheirer/dsp/filter/design/FilterViewer.java b/src/main/java/io/github/dsheirer/dsp/filter/design/FilterViewer.java
index 480463ee..9cf838eb 100644
--- a/src/main/java/io/github/dsheirer/dsp/filter/design/FilterViewer.java
+++ b/src/main/java/io/github/dsheirer/dsp/filter/design/FilterViewer.java
@@ -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;
}
}
diff --git a/src/main/java/io/github/dsheirer/dsp/filter/fir/FIRFilterSpecification.java b/src/main/java/io/github/dsheirer/dsp/filter/fir/FIRFilterSpecification.java
index 661038df..6fe1294e 100644
--- a/src/main/java/io/github/dsheirer/dsp/filter/fir/FIRFilterSpecification.java
+++ b/src/main/java/io/github/dsheirer/dsp/filter/fir/FIRFilterSpecification.java
@@ -1,6 +1,9 @@
package io.github.dsheirer.dsp.filter.fir;
import io.github.dsheirer.dsp.filter.fir.remez.FIRLinearPhaseFilterType;
+import org.apache.commons.lang3.Validate;
+import org.slf4j.Logger;
+import org.slf4j.LoggerFactory;
import java.text.DecimalFormat;
import java.util.ArrayList;
@@ -9,1123 +12,1125 @@ import java.util.List;
public class FIRFilterSpecification
{
- private static final DecimalFormat DECIMAL_FORMATTER = new DecimalFormat( "0.00000" );
-
- private FIRLinearPhaseFilterType mRemezFilterType;
- private int mOrder;
- private int mGridDensity = 16;
- private List mFrequencyBands = new ArrayList<>();
-
- /**
- * Constructs a Remez filter specification. Use one of the filter builders to construct a Remez
- * filter specification.
- *
- * @param order of the filter
- * @param frequencyBands containing the start and stop values for each frequency band normalized
- * to the sample rate where each frequency value is in the range (0.0 <> 0.5)
- * @param amplitudes of each frequency band
- * @param weights of the band ripples normalized to the largest ripple value
- */
- private FIRFilterSpecification( FIRLinearPhaseFilterType type, int order, int gridDensity )
- {
- mRemezFilterType = type;
- mOrder = order;
- mGridDensity = gridDensity;
- }
-
- public String toString()
- {
- StringBuilder sb = new StringBuilder();
- sb.append( "Type: " );
- sb.append( mRemezFilterType.name() );
- sb.append( " Order: " );
- sb.append( mOrder );
- sb.append( " Extrema: " );
- sb.append( getExtremaCount() );
- sb.append( " Grid Density: " );
- sb.append( mGridDensity );
- sb.append( " Grid Size: " );
- sb.append( getGridSize() );
-
- for ( FrequencyBand band : mFrequencyBands )
- {
- sb.append( "\n" );
- sb.append( band.toString() );
- }
-
- return sb.toString();
- }
-
- public void addFrequencyBand( FrequencyBand band )
- {
- mFrequencyBands.add( band );
-
- updateGridSize();
- }
-
- public void addFrequencyBands( Collection bands )
- {
- mFrequencyBands.addAll( bands );
-
- updateGridSize();
- }
-
- public List getFrequencyBands()
- {
- return mFrequencyBands;
- }
-
- /**
- * Returns the total bandwidth of the defined frequency bands in the specification.
- *
- * @return total bandwidth -- should be less than or equal to 0.5
- */
- public double getTotalBandwidth()
- {
- double bandwidth = 0.0;
-
- for ( FrequencyBand band : mFrequencyBands )
- {
- bandwidth += band.getBandWidth();
- }
-
- return bandwidth;
- }
-
- /**
- * Creates a Low-Pass FIR filter specification builder that allows you to define the filter
- * parameters and create a filter specification.
- */
- public static LowPassBuilder lowPassBuilder()
- {
- return new LowPassBuilder();
- }
-
- /**
- * Creates a High-Pass FIR filter specification builder that allows you to define the filter
- * parameters and create a filter specification.
- */
- public static HighPassBuilder highPassBuilder()
- {
- return new HighPassBuilder();
- }
-
- /**
- * Creates a Band-Pass FIR filter specification builder that allows you to define the filter
- * parameters and create a filter specification.
- */
- public static BandPassBuilder bandPassBuilder()
- {
- return new BandPassBuilder();
- }
-
- public FIRLinearPhaseFilterType getFilterType()
- {
- return mRemezFilterType;
- }
-
- public boolean isSymmetrical()
- {
- return mRemezFilterType == FIRLinearPhaseFilterType.TYPE_1_ODD_LENGTH_EVEN_ORDER_SYMMETRICAL ||
- mRemezFilterType == FIRLinearPhaseFilterType.TYPE_2_EVEN_LENGTH_ODD_ORDER_SYMMETRICAL;
- }
-
- public boolean isAntiSymmetrical()
- {
- return mRemezFilterType == FIRLinearPhaseFilterType.TYPE_3_ODD_LENGTH_EVEN_ORDER_ANTI_SYMMETRICAL ||
- mRemezFilterType == FIRLinearPhaseFilterType.TYPE_4_EVEN_LENGTH_ODD_ORDER_ANTI_SYMMETRICAL;
- }
-
- public int getOrder()
- {
- return mOrder;
- }
-
- public boolean isEvenOrder()
- {
- return mOrder % 2 == 0;
- }
-
- public boolean isOddOrder()
- {
- return mOrder % 2 == 1;
- }
-
- public int getFilterLength()
- {
- return mOrder + 1;
- }
-
- public int getBandCount()
- {
- return mFrequencyBands.size();
- }
-
- /**
- * Grid density setting. Default value is 16.
- */
- public int getGridDensity()
- {
- return mGridDensity;
- }
-
- /**
- * Sets the grid density setting. Default value is 16.
- */
- public void setGridDensity( int density )
- {
- mGridDensity = density;
-
- updateGridSize();
- }
-
- /**
- * Maximum number of alternations/extrema (L + 2 or M + 2)
- */
- public int getExtremaCount()
- {
- return getHalfFilterOrder() + 2;
- }
-
- /**
- * Half of the filter order.
- *
- * This value is referred to as L or M, depending on the source.
- */
- public int getHalfFilterOrder()
- {
- switch ( mRemezFilterType )
- {
- case TYPE_1_ODD_LENGTH_EVEN_ORDER_SYMMETRICAL:
- return ( mOrder ) / 2;
- case TYPE_2_EVEN_LENGTH_ODD_ORDER_SYMMETRICAL:
- return ( mOrder - 1 ) / 2;
- case TYPE_3_ODD_LENGTH_EVEN_ORDER_ANTI_SYMMETRICAL:
- return ( mOrder - 2 ) / 2;
- case TYPE_4_EVEN_LENGTH_ODD_ORDER_ANTI_SYMMETRICAL:
- return ( mOrder - 1 ) / 2;
- default:
- return 0;
- }
- }
-
- /**
- * Determines the max band ripple from across the frequency bands in this specification.
- */
- public double getMaxBandAmplitude()
- {
- double maxRippleAmplitude = 0.0;
-
- for ( FrequencyBand band : mFrequencyBands )
- {
- double bandRippleAmplitude = band.getRippleAmplitude();
-
- if ( bandRippleAmplitude > maxRippleAmplitude )
- {
- maxRippleAmplitude = bandRippleAmplitude;
- }
- }
-
- return maxRippleAmplitude;
- }
-
- public int getNominalGridSize()
- {
- return ( getExtremaCount() - 1 ) * getGridDensity() + 1;
- }
-
- public int getGridSize()
- {
- int gridSize = 0;
-
- for ( FrequencyBand band : mFrequencyBands )
- {
- gridSize += band.getGridSize();
- }
-
- return gridSize;
- }
-
- private void updateGridSize()
- {
- if ( !mFrequencyBands.isEmpty() )
- {
- int gridSize = getNominalGridSize();
-
- double totalBandwidth = getTotalBandwidth();
-
- for ( FrequencyBand band : mFrequencyBands )
- {
- band.setGridSize( gridSize, totalBandwidth );
- }
- }
- }
-
- // /**
- // * Determines the grid size needed to represent the percentage of weighted
- // frequency bands
- // * over the entire frequency band.
- // */
- // public int getGridSize()
- // {
- // int gridSize = 0;
- //
- // int maxGridSize = getMaxGridSize();
- //
- // //Add the proportional index counts from each weighted band
- // for( FrequencyBand band: mFrequencyBands )
- // {
- // gridSize += band.getBandGridSize( maxGridSize );
- // }
- //
- // if( getSymmetry() == Symmetry.NEGATIVE )
- // {
- // gridSize--;
- // }
- //
- // return gridSize;
- // }
-
- /**
- * Dense frequency spacing for extrema count * grid density over a frequency range of 0.0 - 1.0
- */
- public double getGridFrequencyInterval()
- {
- return getTotalBandwidth() / (double) ( ( getGridSize() - mFrequencyBands.size() ) );
- }
-
- // /**
- // * ''' FIR order estimator (lowpass, highpass, bandpass, mulitiband).
- // *
- // * (n, fo, ao, w) = remezord (f, a, dev) (n, fo, ao, w) = remezord (f, a,
- // * dev, fs)
- // *
- // * (n, fo, ao, w) = remezord (f, a, dev) finds the approximate order,
- // * normalized frequency band edges, frequency band amplitudes, and weights
- // * that meet input specifications f, a, and dev, to use with the remez
- // * command.
- // *
- // * f is a sequence of frequency band edges (between 0 and Fs/2, where Fs
- // is
- // * the sampling frequency), and a is a sequence specifying the desired
- // * amplitude on the bands defined by f. The length of f is twice the
- // length
- // * of a, minus 2. The desired function is piecewise constant.
- // *
- // * dev is a sequence the same size as a that specifies the maximum
- // allowable
- // * deviation or ripples between the frequency response and the desired
- // * amplitude of the output filter, for each band.
- // *
- // * Use remez with the resulting order n, frequency sequence fo, amplitude
- // * response sequence ao, and weights w to design the filter b which
- // * approximately meets the specifications given by remezord input
- // parameters
- // * f, a, and dev:
- // *
- // * b = remez (n, fo, ao, w)
- // *
- // * (n, fo, ao, w) = remezord (f, a, dev, Fs) specifies a sampling
- // frequency
- // * Fs.
- // *
- // * Fs defaults to 2 Hz, implying a Nyquist frequency of 1 Hz. You can
- // * therefore specify band edges scaled to a particular applications
- // sampling
- // * frequency.
- // *
- // * In some cases remezord underestimates the order n. If the filter does
- // not
- // * meet the specifications, try a higher order such as n+1 or n+2. '''
- // *
- // * @param fcuts
- // * @param mags
- // * @param devs
- // * @param fsamp
- // */
- // // TODO: rename to calculateFilterOrder()
- // public static FIRFilterSpecification remezOrd( int[] fcutoffs,
- // double[] mags, double[] devs, int fsamp )
- // {
- // // def remezord (fcuts, mags, devs, fsamp = 2):
- //
- // double[] fcuts = new double[ fcutoffs.length ];
- //
- // for ( int x = 0; x < fcuts.length; x++ )
- // {
- // fcuts[ x ] = (double) fcutoffs[ x ] / (double) fsamp;
- // }
- //
- // int nf = fcuts.length;
- // int nm = mags.length;
- // int nd = devs.length;
- // int nbands = nm;
- //
- // if ( mags.length != devs.length )
- // {
- // throw new IllegalArgumentException(
- // "Length of magnitudes and deviations must be equal" );
- // }
- //
- // if ( fcuts.length != ( 2 * ( mags.length - 1 ) ) )
- // {
- // throw new IllegalArgumentException(
- // "Length of frequency band edges must be 2 * mags length - 2" );
- // }
- //
- // for ( int x = 0; x < mags.length; x++ )
- // {
- // if ( mags[ x ] != 0 )
- // {
- // devs[ x ] = devs[ x ] / mags[ x ];
- // }
- // }
- //
- // // Separate the passband and stopband edges
- // double[] f1 = new double[ fcuts.length / 2 ];
- // double[] f2 = new double[ fcuts.length / 2 ];
- //
- // for ( int x = 0; x < fcuts.length / 2; x++ )
- // {
- // f1[ x ] = fcuts[ 2 * x ];
- // f2[ x ] = fcuts[ 2 * x + 1 ];
- // }
- //
- // int n = 0;
- //
- // double min_delta = 2.0;
- //
- // for ( int x = 0; x < f1.length; x++ )
- // {
- // if ( f2[ x ] - f1[ x ] < min_delta )
- // {
- // n = x;
- // min_delta = f2[ x ] - f1[ x ];
- // }
- // }
- //
- // double l;
- //
- // if ( nbands == 2 )
- // {
- // // lowpass or highpass case (use formula)
- // l = lporder( f1[ n ], f2[ n ], devs[ 0 ], devs[ 1 ] );
- // } else
- // {
- // // bandpass or multipass case
- // // try different lowpasses and take the worst one that
- // // goes through the BP specs
- // l = 0;
- //
- // for ( int x = 1; x < nbands - 1; x++ )
- // {
- // double l1 = lporder( f1[ x - 1 ], f2[ x - 1 ], devs[ x ],
- // devs[ x - 1 ] );
- // double l2 = lporder( f1[ x ], f2[ x ], devs[ x ],
- // devs[ x + 1 ] );
- // l = Math.max( l1, l2 );
- // }
- // }
- //
- // n = (int) ( Math.ceil( l ) - 1 ); // need order, not length for remez
- //
- // // // cook up remez compatible result
- // double[] ff = new double[ fcuts.length + 2 ];
- // ff[ 0 ] = 0.0;
- // ff[ ff.length - 1 ] = 1.0;
- //
- // System.arraycopy( fcuts, 0, ff, 1, fcuts.length );
- //
- // for ( int x = 1; x < ff.length - 1; x++ )
- // {
- // ff[ x ] *= 2;
- // }
- //
- // double[] aa = new double[ mags.length * 2 ];
- //
- // for ( int x = 0; x < mags.length; x++ )
- // {
- // aa[ 2 * x ] = mags[ x ];
- // aa[ 2 * x + 1 ] = mags[ x ];
- // }
- //
- // double max_dev = 0;
- //
- // for ( double dev : devs )
- // {
- // if ( dev > max_dev )
- // {
- // max_dev = dev;
- // }
- // }
- //
- // double[] wts = new double[ devs.length ];
- //
- // for ( int x = 0; x < devs.length; x++ )
- // {
- // wts[ x ] = max_dev / devs[ x ];
- // }
- //
- // return null;
- // // return new RemezFilterSpecification( RemezFilterType.BANDPASS, n, ff,
- // // aa, wts, 16);
- // }
-
- /**
- * Low-pass FIR filter specification builder
- */
- public static class LowPassBuilder
- {
- private int mSampleRate;
- private int mOrder;
- private int mGridDensity = 16;
- private int mPassBandEndFrequency;
- private int mStopBandStartFrequency;
- private double mPassBandRipple;
- private double mStopBandRipple;
- private double mPassBandAmplitude = 1.0;
- private double mStopBandAmplitude = 0.0;
-
- public LowPassBuilder()
- {
- }
-
- public LowPassBuilder sampleRate( int sampleRateHz )
- {
- mSampleRate = sampleRateHz;
- return this;
- }
-
- /**
- * Sets the filter order. If the order is not specified, it will be calculated from the
- * other parameters.
- */
- public LowPassBuilder order( int order )
- {
- mOrder = order;
- return this;
- }
-
- public LowPassBuilder gridDensity( int density )
- {
- mGridDensity = density;
- return this;
- }
-
- public LowPassBuilder passBandCutoff( int frequencyHz )
- {
- mPassBandEndFrequency = frequencyHz;
- return this;
- }
-
- public LowPassBuilder stopBandStart( int frequencyHz )
- {
- mStopBandStartFrequency = frequencyHz;
- return this;
- }
-
- public LowPassBuilder passBandRipple( double rippleDb )
- {
- mPassBandRipple = rippleDb;
- return this;
- }
-
- public LowPassBuilder stopBandRipple( double rippleDb )
- {
- mStopBandRipple = rippleDb;
- return this;
- }
-
- public LowPassBuilder passBandAmplitude( double amplitude )
- {
- mPassBandAmplitude = amplitude;
- return this;
- }
-
- public LowPassBuilder stopBandAmplitude( double amplitude )
- {
- mStopBandAmplitude = amplitude;
- return this;
- }
-
- public FIRFilterSpecification build()
- {
- if ( mOrder < 6 )
- {
- mOrder = estimateFilterOrder( mSampleRate, mPassBandEndFrequency,
- mStopBandStartFrequency, mPassBandRipple, mStopBandRipple );
- }
-
- FIRLinearPhaseFilterType type = ( mOrder % 2 == 0
- ? FIRLinearPhaseFilterType.TYPE_1_ODD_LENGTH_EVEN_ORDER_SYMMETRICAL
- : FIRLinearPhaseFilterType.TYPE_2_EVEN_LENGTH_ODD_ORDER_SYMMETRICAL );
-
- FIRFilterSpecification spec = new FIRFilterSpecification( type, mOrder, mGridDensity );
-
- FrequencyBand passBand = new FrequencyBand( mSampleRate, 0, mPassBandEndFrequency,
- mPassBandAmplitude, mPassBandRipple );
- spec.addFrequencyBand( passBand );
-
- FrequencyBand stopBand = new FrequencyBand( mSampleRate, mStopBandStartFrequency,
- mSampleRate / 2, mStopBandAmplitude, mStopBandRipple );
- spec.addFrequencyBand( stopBand );
-
- return spec;
- }
- }
-
- /**
- * Remez high-pass FIR filter specification builder
- */
- public static class HighPassBuilder
- {
- private int mSampleRate;
- private int mOrder = -1;
- private int mGridDensity = 16;
- private int mPassBandStartFrequency;
- private int mStopBandEndFrequency;
- private double mPassBandRipple;
- private double mStopBandRipple;
- private double mPassBandAmplitude = 1.0;
- private double mStopBandAmplitude = 0.0;
-
- public HighPassBuilder()
- {
- }
-
- public HighPassBuilder sampleRate( int sampleRateHz )
- {
- mSampleRate = sampleRateHz;
- return this;
- }
-
- /**
- * Sets the filter order. If the order is not specified, it will be calculated from the
- * other parameters.
- */
- public HighPassBuilder order( int order )
- {
- mOrder = order;
- return this;
- }
-
- public HighPassBuilder gridDensity( int density )
- {
- mGridDensity = density;
- return this;
- }
-
- public HighPassBuilder passBandStart( int frequencyHz )
- {
- mPassBandStartFrequency = frequencyHz;
- return this;
- }
-
- public HighPassBuilder stopBandCutoff( int frequencyHz )
- {
- mStopBandEndFrequency = frequencyHz;
- return this;
- }
-
- public HighPassBuilder passBandRipple( double rippleDb )
- {
- mPassBandRipple = rippleDb;
- return this;
- }
-
- public HighPassBuilder stopBandRipple( double rippleDb )
- {
- mStopBandRipple = rippleDb;
- return this;
- }
-
- public HighPassBuilder passBandAmplitude( double amplitude )
- {
- mPassBandAmplitude = amplitude;
- return this;
- }
-
- public HighPassBuilder stopBandAmplitude( double amplitude )
- {
- mStopBandAmplitude = amplitude;
- return this;
- }
-
- public FIRFilterSpecification build()
- {
- if ( mOrder < 6 )
- {
- mOrder = estimateFilterOrder( mSampleRate, mStopBandEndFrequency,
- mPassBandStartFrequency, mPassBandRipple, mStopBandRipple );
- }
-
- // High pass is a Type 1 filter -- ensure order is even
- if ( mOrder % 2 == 1 )
- {
- mOrder++;
- }
-
- FIRFilterSpecification spec = new FIRFilterSpecification(
- FIRLinearPhaseFilterType.TYPE_1_ODD_LENGTH_EVEN_ORDER_SYMMETRICAL, mOrder,
- mGridDensity );
-
- FrequencyBand stopBand = new FrequencyBand( mSampleRate, 0, mStopBandEndFrequency,
- mStopBandAmplitude, mStopBandRipple );
- spec.addFrequencyBand( stopBand );
-
- FrequencyBand passBand = new FrequencyBand( mSampleRate, mPassBandStartFrequency,
- mSampleRate / 2, mPassBandAmplitude, mPassBandRipple );
- spec.addFrequencyBand( passBand );
-
- return spec;
- }
- }
-
- /**
- * Band-pass or band-stop FIR filter specification builder
- */
- public static class BandPassBuilder
- {
- private int mOrder;
- private int mGridDensity = 16;
- private int mSampleRate;
- private int mStopFrequency1;
- private int mPassFrequencyBegin;
- private int mPassFrequencyEnd;
- private int mStopFrequency2;
- private double mStopRipple = 0.01;
- private double mPassRipple = 0.01;
- private double mStopAmplitude = 0.0;
- private double mPassAmplitude = 1.0;
-
- public BandPassBuilder()
- {
- }
-
- /**
- * Sets the filter order. If the order is not specified, it will be calculated from the
- * other parameters. If you specify and odd-length order, it will be corrected (+1) to make
- * it an even order number.
- */
- public BandPassBuilder order( int order )
- {
- mOrder = order;
- return this;
- }
-
- /**
- * Sets the frequency grid density. Default is 16.
- */
- public BandPassBuilder gridDensity( int density )
- {
- mGridDensity = density;
- return this;
- }
-
- /**
- * Sets the filter sample rate in hertz
- */
- public BandPassBuilder sampleRate( int sampleRate )
- {
- mSampleRate = sampleRate;
- return this;
- }
-
- /**
- * Specifies the ending frequency of the first stop band
- */
- public BandPassBuilder stopFrequency1( int stopFrequency )
- {
- mStopFrequency1 = stopFrequency;
- return this;
- }
-
- /**
- * Specifies the beginning frequency of the second stop band
- */
- public BandPassBuilder stopFrequency2( int stopFrequency )
- {
- mStopFrequency2 = stopFrequency;
- return this;
- }
-
- /**
- * Specifies the pass band beginning frequency
- */
- public BandPassBuilder passFrequencyBegin( int passFrequency )
- {
- mPassFrequencyBegin = passFrequency;
- return this;
- }
-
- /**
- * Specifies the pass band ending frequency
- */
- public BandPassBuilder passFrequencyEnd( int passFrequency )
- {
- mPassFrequencyEnd = passFrequency;
- return this;
- }
-
- /**
- * Specifies the ripple in dB for both stop bands
- */
- public BandPassBuilder stopRipple( double rippleDb )
- {
- mStopRipple = rippleDb;
- return this;
- }
-
- /**
- * Specifies the ripple in dB for the pass band
- */
- public BandPassBuilder passRipple( double rippleDb )
- {
- mPassRipple = rippleDb;
- return this;
- }
-
- /**
- * Specifies the amplitude of the stop bands. Default is 0.0
- */
- public BandPassBuilder stopAmplitude( double amplitude )
- {
- mStopAmplitude = amplitude;
- return this;
- }
-
- /**
- * Specifies the amplitude of the pass band. Default is 1.0
- */
- public BandPassBuilder passAmplitude( double amplitude )
- {
- mPassAmplitude = amplitude;
- return this;
- }
-
- /**
- * Validates the frequency bands to test for overlap and completeness.
- */
- private void validateFrequencyBands()
- {
- if ( mStopFrequency1 >= mPassFrequencyBegin )
- {
- throw new IllegalArgumentException( "Stop band 1 ending frequency ["
- + mStopFrequency1 + "] must be less than the pass band start frequency ["
- + mPassFrequencyBegin + "]" );
- }
-
- if ( mPassFrequencyBegin >= mPassFrequencyEnd )
- {
- throw new IllegalArgumentException( "Pass band begin frequency ["
- + mPassFrequencyBegin + "] must be less than the pass band end frequency ["
- + mPassFrequencyEnd + "]" );
- }
-
- if ( mPassFrequencyEnd >= mStopFrequency2 )
- {
- throw new IllegalArgumentException( "Pass band end frequency [" + mPassFrequencyEnd
- + "] must be less than stop band 2 beginning frequency [" + mStopFrequency2
- + "]" );
- }
-
- if ( mStopFrequency2 >= ( mSampleRate / 2 ) )
- {
- throw new IllegalArgumentException( "Stop band 2 beginning frequency ["
- + mStopFrequency2 + "] must be less than half of the sample rate ["
- + ( mSampleRate / 2 ) + "]" );
- }
- }
-
- public FIRFilterSpecification build()
- {
- validateFrequencyBands();
-
- FIRLinearPhaseFilterType type = FIRLinearPhaseFilterType.TYPE_1_ODD_LENGTH_EVEN_ORDER_SYMMETRICAL;
-
- if ( mOrder < 10 )
- {
- mOrder = estimateBandPassOrder( mSampleRate, mPassFrequencyBegin, mPassFrequencyEnd,
- mPassRipple, mStopRipple );
- }
-
- // Ensure even order since we're designing a Type 1 filter
- if ( mOrder % 2 == 1 )
- {
- mOrder++;
- }
-
- FIRFilterSpecification spec = new FIRFilterSpecification( type, mOrder, mGridDensity );
-
- spec.addFrequencyBand( new FrequencyBand( mSampleRate, 0, mStopFrequency1,
- mStopAmplitude, mStopRipple ) );
- spec.addFrequencyBand( new FrequencyBand( mSampleRate, mPassFrequencyBegin,
- mPassFrequencyEnd, mPassAmplitude, mPassRipple ) );
- spec.addFrequencyBand( new FrequencyBand( mSampleRate, mStopFrequency2,
- (int) ( mSampleRate / 2 ), mStopAmplitude, mStopRipple ) );
-
- return spec;
- }
- }
-
- // /**
- // * Converts the set of band amplitude values into a set of weights
- // * normalized to the largest band amplitude value
- // */
- // private static double[] getBandWeights( double... amplitudes )
- // {
- // double maximum = 0.0;
- //
- // for ( double amplitude : amplitudes )
- // {
- // if ( amplitude > maximum )
- // {
- // maximum = amplitude;
- // }
- // }
- //
- // double[] weights = new double[ amplitudes.length ];
- //
- // for ( int x = 0; x < amplitudes.length; x++ )
- // {
- // weights[ x ] = maximum / amplitudes[ x ];
- // }
- //
- // return weights;
- // }
-
- /**
- * Calculates filter order from filter length
- */
- private static int getFilterOrder( double length )
- {
- return (int) ( Math.ceil( length ) - 1 );
- }
-
- /**
- * Estimates filter length for a two-band (high or low pass) FIR filter.
- *
- * Note: this estimator doesn't work well if the transition frequency is near 0 or sampleRate/2
- *
- * From Herrmann et al (1973), Practical design rules for optimum finite impulse response
- * filters. Bell System Technical J., 52, 769-99
- *
- * @param sampleRate in hertz
- * @param frequency1 in hertz
- * @param frequency2 in hertz
- * @param passBandRipple pass band ripple in dB
- * @param stopBandRipple stop band ripple in dB
- *
- * @return estimated filter length
- */
- private static int estimateFilterOrder( int sampleRate, int frequency1, int frequency2,
- double passBandRipple, double stopBandRipple )
- {
- double df = Math.abs( frequency2 - frequency1 ) / (double) sampleRate;
-
- double ddp = Math
- .log10( stopBandRipple <= passBandRipple ? passBandRipple : stopBandRipple );
- double dds = Math
- .log10( stopBandRipple <= passBandRipple ? stopBandRipple : passBandRipple );
-
- double a1 = 5.309e-3;
- double a2 = 7.114e-2;
- double a3 = -4.761e-1;
- double a4 = -2.66e-3;
- double a5 = -5.941e-1;
- double a6 = -4.278e-1;
-
- double b1 = 11.01217;
- double b2 = 0.5124401;
-
- double t1 = a1 * ddp * ddp;
- double t2 = a2 * ddp;
- double t3 = a4 * ddp * ddp;
- double t4 = a5 * ddp;
-
- double dinf = ( ( t1 + t2 + a3 ) * dds ) + ( t3 + t4 + a6 );
- double ff = b1 + b2 * ( ddp - dds );
- double n = dinf / df - ff * df + 1.0;
-
- return (int) Math.ceil( n );
- }
-
- /**
- * Estimates band pass filter length;
- *
- * @param passBandStart frequency normalized to sample rate
- * @param passBandEnd frequency normalized to sample rate
- * @param passBandRippleDb
- * @param stopBandRippleDb
- */
- public static int estimateBandPassOrder( int sampleRate, int passBandStart, int passBandEnd,
- double passBandRippleDb, double stopBandRippleDb )
- {
- double df = (double) Math.abs( passBandEnd - passBandStart ) / (double) sampleRate;
- double ddp = (double) Math.log10( passBandRippleDb );
- double dds = (double) Math.log10( stopBandRippleDb );
-
- double a1 = 0.01201;
- double a2 = 0.09664;
- double a3 = -0.51325;
- double a4 = 0.00203;
- double a5 = -0.57054;
- double a6 = -0.44314;
-
- double t1 = a1 * ddp * ddp;
- double t2 = a2 * ddp;
- double t3 = a4 * ddp * ddp;
- double t4 = a5 * ddp;
-
- double cinf = dds * ( t1 + t2 + a3 ) + t3 + t4 + a6;
- double ginf = -14.6f * (double) Math.log10( passBandRippleDb / stopBandRippleDb ) - 16.9;
- double n = cinf / df + ginf * df + 1.0;
-
- return (int) Math.ceil( n );
- }
-
- /**
- * Frequency band
- */
- public static class FrequencyBand
- {
- private int mGridSize;
- private double mStart;
- private double mEnd;
- private double mAmplitude;
- private double mRippleDB;
-
- /**
- * Constructs a frequency band where the start and end frequencies are normalized to a
- * sample rate of 1.
- *
- * @param start edge of the frequency band (0.0 - 1.0)
- * @param end edge of the frequency band (0.0 - 1.0)
- * @param amplitude of the frequency band on the unity scale (0.0 - 1.0)
- * @param ripple of the frequency band specified as db ripple
- */
- public FrequencyBand( double start, double end, double amplitude, double ripple )
- {
- assert ( 0.0 <= mStart );
- assert ( mStart < mEnd );
- assert ( mEnd <= 1.0 );
- assert ( 0.0 <= amplitude && amplitude <= 1.0 );
- assert ( 0.0 <= ripple );
-
- mStart = start;
- mEnd = end;
- mAmplitude = amplitude;
- mRippleDB = ripple;
- }
-
- /**
- * Constructs a frequency band for the sample rate and start and end frequencies.
- *
- * @param sampleRate in hertz
- * @param start edge of the frequency band in hertz
- * @param end edge of the frequency band in hertz
- * @param amplitude of the frequency band relative to unity (0.0 - 1.0)
- * @param ripple of the frequency band specified as db ripple
- */
- public FrequencyBand( int sampleRate, int start, int end, double amplitude, double ripple )
- {
- this( (double) start / (double) sampleRate, (double) end / (double) sampleRate,
- amplitude, ripple );
- }
-
- public int getGridSize()
- {
- return mGridSize;
- }
-
- public void setGridSize( int totalGridSize, double totalBandwidth )
- {
- mGridSize = (int) Math
- .ceil( (double) totalGridSize * ( getBandWidth() / totalBandwidth ) );
- }
-
- public String toString()
- {
- StringBuilder sb = new StringBuilder();
-
- sb.append( "Band Start: " );
- sb.append( mStart );
- sb.append( " End:" );
- sb.append( mEnd );
- sb.append( " Amplitude: " );
- sb.append( mAmplitude );
- sb.append( " Ripple dB: " );
- sb.append( mRippleDB );
- sb.append( " Ripple Amplitude: " );
- sb.append( DECIMAL_FORMATTER.format( getRippleAmplitude() ) );
- sb.append( " Grid Size: " );
- sb.append( mGridSize );
-
- return sb.toString();
- }
-
- /**
- * Start frequency edge for this band normalized to 1 Hz.
- */
- public double getStart()
- {
- return mStart;
- }
-
- /**
- * End frequency edge for this band normalized to 1 Hz.
- */
- public double getEnd()
- {
- return mEnd;
- }
-
- /**
- * Desired amplitude response for this frequency band
- *
- * @return amplitude (0.0 - 1.0)
- */
- public double getAmplitude()
- {
- return mAmplitude;
- }
-
- /**
- * Frequency band ripple in decibels
- */
- public double getRippleDB()
- {
- return mRippleDB;
- }
-
- /**
- * Frequency band ripple as an amplitude value
- */
- public double getRippleAmplitude()
- {
- return ( Math.pow( 10.0, ( mRippleDB / 20 ) ) - 1 )
- / ( Math.pow( 10.0, ( mRippleDB / 20 ) ) + 1 );
- }
-
- /**
- * Calculates the weighting of this frequency band's ripple relative to the max ripple for a
- * filter specification.
- *
- * @param maxRippleAmplitude for the filter specification
- *
- * @return weight of this frequency band
- */
- public double getWeight( double maxRippleAmplitude )
- {
- return 1.0 / ( getRippleAmplitude() / maxRippleAmplitude );
- }
-
- /**
- * Double-sided bandwidth of this frequency band
- */
- public double getBandWidth()
- {
- return mEnd - mStart;
- }
- }
+ private final static Logger mLog = LoggerFactory.getLogger(FIRFilterSpecification.class);
+
+ private static final double PERFECT_RECONSTRUCTION_GAIN_AT_BAND_EDGE = -6.020599842071533; //0.5 magnitude
+ private static final DecimalFormat DECIMAL_FORMATTER = new DecimalFormat("0.00000");
+
+ private FIRLinearPhaseFilterType mRemezFilterType;
+ private int mOrder;
+ private int mGridDensity = 16;
+ private List mFrequencyBands = new ArrayList<>();
+
+ /**
+ * Constructs a Remez filter specification. Use one of the filter builders to construct a Remez
+ * filter specification.
+ *
+ * @param type of filter
+ * @param order of the filter
+ * @param gridDensity to use when calculating the coefficients
+ */
+ protected FIRFilterSpecification(FIRLinearPhaseFilterType type, int order, int gridDensity)
+ {
+ mRemezFilterType = type;
+ mOrder = order;
+ mGridDensity = gridDensity;
+ }
+
+ public String toString()
+ {
+ StringBuilder sb = new StringBuilder();
+ sb.append("Type: ");
+ sb.append(mRemezFilterType.name());
+ sb.append(" Order: ");
+ sb.append(mOrder);
+ sb.append(" Extrema: ");
+ sb.append(getExtremaCount());
+ sb.append(" Grid Density: ");
+ sb.append(mGridDensity);
+ sb.append(" Grid Size: ");
+ sb.append(getGridSize());
+
+ for(FrequencyBand band : mFrequencyBands)
+ {
+ sb.append("\n");
+ sb.append(band.toString());
+ }
+
+ return sb.toString();
+ }
+
+ public void addFrequencyBand(FrequencyBand band)
+ {
+ mFrequencyBands.add(band);
+
+ updateGridSize();
+ }
+
+ public void addFrequencyBands(Collection bands)
+ {
+ mFrequencyBands.addAll(bands);
+
+ updateGridSize();
+ }
+
+ /**
+ * Removes all frequency bands from this filter specification
+ */
+ public void clearFrequencyBands()
+ {
+ mFrequencyBands.clear();
+ }
+
+ public List getFrequencyBands()
+ {
+ return mFrequencyBands;
+ }
+
+ /**
+ * Returns the total bandwidth of the defined frequency bands in the specification.
+ *
+ * @return total bandwidth -- should be less than or equal to 0.5
+ */
+ public double getTotalBandwidth()
+ {
+ double bandwidth = 0.0;
+
+ for(FrequencyBand band : mFrequencyBands)
+ {
+ bandwidth += band.getBandWidth();
+ }
+
+ return bandwidth;
+ }
+
+ /**
+ * Creates a Low-Pass FIR filter specification builder that allows you to define the filter
+ * parameters and create a filter specification.
+ */
+ public static LowPassBuilder lowPassBuilder()
+ {
+ return new LowPassBuilder();
+ }
+
+ /**
+ * Creates a High-Pass FIR filter specification builder that allows you to define the filter
+ * parameters and create a filter specification.
+ */
+ public static HighPassBuilder highPassBuilder()
+ {
+ return new HighPassBuilder();
+ }
+
+ /**
+ * Creates a Band-Pass FIR filter specification builder that allows you to define the filter
+ * parameters and create a filter specification.
+ */
+ public static BandPassBuilder bandPassBuilder()
+ {
+ return new BandPassBuilder();
+ }
+
+ /**
+ * Creates a Polyphase TunerChannelizer filter specification builder that allows you to define the filter parameters and
+ * create a filter specification
+ */
+ public static ChannelizerBuilder channelizerBuilder()
+ {
+ return new ChannelizerBuilder();
+ }
+
+ public FIRLinearPhaseFilterType getFilterType()
+ {
+ return mRemezFilterType;
+ }
+
+ public boolean isSymmetrical()
+ {
+ return mRemezFilterType == FIRLinearPhaseFilterType.TYPE_1_ODD_LENGTH_EVEN_ORDER_SYMMETRICAL ||
+ mRemezFilterType == FIRLinearPhaseFilterType.TYPE_2_EVEN_LENGTH_ODD_ORDER_SYMMETRICAL;
+ }
+
+ public boolean isAntiSymmetrical()
+ {
+ return mRemezFilterType == FIRLinearPhaseFilterType.TYPE_3_ODD_LENGTH_EVEN_ORDER_ANTI_SYMMETRICAL ||
+ mRemezFilterType == FIRLinearPhaseFilterType.TYPE_4_EVEN_LENGTH_ODD_ORDER_ANTI_SYMMETRICAL;
+ }
+
+ public int getOrder()
+ {
+ return mOrder;
+ }
+
+ public boolean isEvenOrder()
+ {
+ return mOrder % 2 == 0;
+ }
+
+ public boolean isOddOrder()
+ {
+ return mOrder % 2 == 1;
+ }
+
+ public int getFilterLength()
+ {
+ return mOrder + 1;
+ }
+
+ public int getBandCount()
+ {
+ return mFrequencyBands.size();
+ }
+
+ /**
+ * Grid density setting. Default value is 16.
+ */
+ public int getGridDensity()
+ {
+ return mGridDensity;
+ }
+
+ /**
+ * Sets the grid density setting. Default value is 16.
+ */
+ public void setGridDensity(int density)
+ {
+ mGridDensity = density;
+
+ updateGridSize();
+ }
+
+ /**
+ * Maximum number of alternations/extrema (L + 2 or M + 2)
+ */
+ public int getExtremaCount()
+ {
+ return getHalfFilterOrder() + 2;
+ }
+
+ /**
+ * Half of the filter order.
+ *
+ * This value is referred to as L or M, depending on the source.
+ */
+ public int getHalfFilterOrder()
+ {
+ switch(mRemezFilterType)
+ {
+ case TYPE_1_ODD_LENGTH_EVEN_ORDER_SYMMETRICAL:
+ return (mOrder) / 2;
+ case TYPE_2_EVEN_LENGTH_ODD_ORDER_SYMMETRICAL:
+ return (mOrder - 1) / 2;
+ case TYPE_3_ODD_LENGTH_EVEN_ORDER_ANTI_SYMMETRICAL:
+ return (mOrder - 2) / 2;
+ case TYPE_4_EVEN_LENGTH_ODD_ORDER_ANTI_SYMMETRICAL:
+ return (mOrder - 1) / 2;
+ default:
+ return 0;
+ }
+ }
+
+ /**
+ * Determines the max band ripple from across the frequency bands in this specification.
+ */
+ public double getMaxBandAmplitude()
+ {
+ double maxRippleAmplitude = 0.0;
+
+ for(FrequencyBand band : mFrequencyBands)
+ {
+ double bandRippleAmplitude = band.getRippleAmplitude();
+
+ if(bandRippleAmplitude > maxRippleAmplitude)
+ {
+ maxRippleAmplitude = bandRippleAmplitude;
+ }
+ }
+
+ return maxRippleAmplitude;
+ }
+
+ public int getNominalGridSize()
+ {
+ return (getExtremaCount() - 1) * getGridDensity() + 1;
+ }
+
+ public int getGridSize()
+ {
+ int gridSize = 0;
+
+ for(FrequencyBand band : mFrequencyBands)
+ {
+ gridSize += band.getGridSize();
+ }
+
+ return gridSize;
+ }
+
+ private void updateGridSize()
+ {
+ if(!mFrequencyBands.isEmpty())
+ {
+ int gridSize = getNominalGridSize();
+
+ double totalBandwidth = getTotalBandwidth();
+
+ for(FrequencyBand band : mFrequencyBands)
+ {
+ band.setGridSize(gridSize, totalBandwidth);
+ }
+ }
+ }
+
+ /**
+ * Dense frequency spacing for extrema count * grid density over a frequency range of 0.0 - 1.0
+ */
+ public double getGridFrequencyInterval()
+ {
+ return getTotalBandwidth() / (double)((getGridSize() - mFrequencyBands.size()));
+ }
+
+ /**
+ * Low-pass FIR filter specification builder
+ */
+ public static class LowPassBuilder
+ {
+ private double mSampleRate;
+ private int mOrder;
+ private Boolean mOddLength;
+ private int mGridDensity = 16;
+ private int mPassBandEndFrequency;
+ private int mStopBandStartFrequency;
+ private double mPassBandRipple;
+ private double mStopBandRipple;
+ private double mPassBandAmplitude = 1.0;
+ private double mStopBandAmplitude = 0.0;
+
+ public LowPassBuilder()
+ {
+ }
+
+ public LowPassBuilder sampleRate(double sampleRateHz)
+ {
+ mSampleRate = sampleRateHz;
+ return this;
+ }
+
+ /**
+ * Sets the filter order. If the order is not specified, it will be calculated from the
+ * other parameters.
+ */
+ public LowPassBuilder order(int order)
+ {
+ mOrder = order;
+ return this;
+ }
+
+ /**
+ * Indicates if the filters should be an odd-length (even order) filter. This will override any requested
+ * filter order if it is set, otherwise the filter length will be determined from the filter order.
+ */
+ public LowPassBuilder oddLength(boolean oddLength)
+ {
+ mOddLength = oddLength;
+ return this;
+ }
+
+ public LowPassBuilder gridDensity(int density)
+ {
+ mGridDensity = density;
+ return this;
+ }
+
+ public LowPassBuilder passBandCutoff(int frequencyHz)
+ {
+ mPassBandEndFrequency = frequencyHz;
+ return this;
+ }
+
+ public LowPassBuilder stopBandStart(int frequencyHz)
+ {
+ mStopBandStartFrequency = frequencyHz;
+ return this;
+ }
+
+ public LowPassBuilder passBandRipple(double rippleDb)
+ {
+ mPassBandRipple = rippleDb;
+ return this;
+ }
+
+ public LowPassBuilder stopBandRipple(double rippleDb)
+ {
+ mStopBandRipple = rippleDb;
+ return this;
+ }
+
+ public LowPassBuilder passBandAmplitude(double amplitude)
+ {
+ mPassBandAmplitude = amplitude;
+ return this;
+ }
+
+ public LowPassBuilder stopBandAmplitude(double amplitude)
+ {
+ mStopBandAmplitude = amplitude;
+ return this;
+ }
+
+ public FIRFilterSpecification build()
+ {
+ if(mOrder < 6)
+ {
+ mOrder = estimateFilterOrder(mSampleRate, mPassBandEndFrequency, mStopBandStartFrequency,
+ mPassBandRipple, mStopBandRipple);
+ }
+
+ FIRLinearPhaseFilterType type = null;
+
+ if(mOddLength != null)
+ {
+ if(mOddLength)
+ {
+ type = FIRLinearPhaseFilterType.TYPE_1_ODD_LENGTH_EVEN_ORDER_SYMMETRICAL;
+ //Force order to even
+ mOrder += mOrder % 2;
+ }
+ else
+ {
+ type = FIRLinearPhaseFilterType.TYPE_2_EVEN_LENGTH_ODD_ORDER_SYMMETRICAL;
+ //Force order to odd
+ mOrder += (mOrder % 2 == 0 ? 1 : 0);
+ }
+ }
+ else
+ {
+ type = (mOrder % 2 == 0 ? FIRLinearPhaseFilterType.TYPE_1_ODD_LENGTH_EVEN_ORDER_SYMMETRICAL
+ : FIRLinearPhaseFilterType.TYPE_2_EVEN_LENGTH_ODD_ORDER_SYMMETRICAL);
+ }
+
+ FIRFilterSpecification spec = new FIRFilterSpecification(type, mOrder, mGridDensity);
+
+ FrequencyBand passBand = new FrequencyBand(mSampleRate, 0, mPassBandEndFrequency,
+ mPassBandAmplitude, mPassBandRipple);
+ spec.addFrequencyBand(passBand);
+
+ FrequencyBand stopBand = new FrequencyBand(mSampleRate, mStopBandStartFrequency,
+ (int)(mSampleRate / 2), mStopBandAmplitude, mStopBandRipple);
+ spec.addFrequencyBand(stopBand);
+
+ return spec;
+ }
+ }
+
+ /**
+ * Remez high-pass FIR filter specification builder
+ */
+ public static class HighPassBuilder
+ {
+ private double mSampleRate;
+ private int mOrder = -1;
+ private int mGridDensity = 16;
+ private int mPassBandStartFrequency;
+ private int mStopBandEndFrequency;
+ private double mPassBandRipple;
+ private double mStopBandRipple;
+ private double mPassBandAmplitude = 1.0;
+ private double mStopBandAmplitude = 0.0;
+
+ public HighPassBuilder()
+ {
+ }
+
+ public HighPassBuilder sampleRate(double sampleRateHz)
+ {
+ mSampleRate = sampleRateHz;
+ return this;
+ }
+
+ /**
+ * Sets the filter order. If the order is not specified, it will be calculated from the
+ * other parameters.
+ */
+ public HighPassBuilder order(int order)
+ {
+ mOrder = order;
+ return this;
+ }
+
+ public HighPassBuilder gridDensity(int density)
+ {
+ mGridDensity = density;
+ return this;
+ }
+
+ public HighPassBuilder passBandStart(int frequencyHz)
+ {
+ mPassBandStartFrequency = frequencyHz;
+ return this;
+ }
+
+ public HighPassBuilder stopBandCutoff(int frequencyHz)
+ {
+ mStopBandEndFrequency = frequencyHz;
+ return this;
+ }
+
+ public HighPassBuilder passBandRipple(double rippleDb)
+ {
+ mPassBandRipple = rippleDb;
+ return this;
+ }
+
+ public HighPassBuilder stopBandRipple(double rippleDb)
+ {
+ mStopBandRipple = rippleDb;
+ return this;
+ }
+
+ public HighPassBuilder passBandAmplitude(double amplitude)
+ {
+ mPassBandAmplitude = amplitude;
+ return this;
+ }
+
+ public HighPassBuilder stopBandAmplitude(double amplitude)
+ {
+ mStopBandAmplitude = amplitude;
+ return this;
+ }
+
+ public FIRFilterSpecification build()
+ {
+ if(mOrder < 6)
+ {
+ mOrder = estimateFilterOrder(mSampleRate, mStopBandEndFrequency, mPassBandStartFrequency,
+ mPassBandRipple, mStopBandRipple);
+ }
+
+ // High pass is a Type 1 filter -- ensure order is even
+ if(mOrder % 2 == 1)
+ {
+ mOrder++;
+ }
+
+ FIRFilterSpecification spec = new FIRFilterSpecification(
+ FIRLinearPhaseFilterType.TYPE_1_ODD_LENGTH_EVEN_ORDER_SYMMETRICAL, mOrder,
+ mGridDensity);
+
+ FrequencyBand stopBand = new FrequencyBand(mSampleRate, 0, mStopBandEndFrequency,
+ mStopBandAmplitude, mStopBandRipple);
+ spec.addFrequencyBand(stopBand);
+
+ FrequencyBand passBand = new FrequencyBand(mSampleRate, mPassBandStartFrequency,
+ (int)(mSampleRate / 2), mPassBandAmplitude, mPassBandRipple);
+ spec.addFrequencyBand(passBand);
+
+ return spec;
+ }
+ }
+
+ /**
+ * Band-pass or band-stop FIR filter specification builder
+ */
+ public static class BandPassBuilder
+ {
+ private int mOrder;
+ private int mGridDensity = 16;
+ private double mSampleRate;
+ private int mStopFrequency1;
+ private int mPassFrequencyBegin;
+ private int mPassFrequencyEnd;
+ private int mStopFrequency2;
+ private double mStopRipple = 0.01;
+ private double mPassRipple = 0.01;
+ private double mStopAmplitude = 0.0;
+ private double mPassAmplitude = 1.0;
+
+ public BandPassBuilder()
+ {
+ }
+
+ /**
+ * Sets the filter order. If the order is not specified, it will be calculated from the
+ * other parameters. If you specify and odd-length order, it will be corrected (+1) to make
+ * it an even order number.
+ */
+ public BandPassBuilder order(int order)
+ {
+ mOrder = order;
+ return this;
+ }
+
+ /**
+ * Sets the frequency grid density. Default is 16.
+ */
+ public BandPassBuilder gridDensity(int density)
+ {
+ mGridDensity = density;
+ return this;
+ }
+
+ /**
+ * Sets the filter sample rate in hertz
+ */
+ public BandPassBuilder sampleRate(double sampleRate)
+ {
+ mSampleRate = sampleRate;
+ return this;
+ }
+
+ /**
+ * Specifies the ending frequency of the first stop band
+ */
+ public BandPassBuilder stopFrequency1(int stopFrequency)
+ {
+ mStopFrequency1 = stopFrequency;
+ return this;
+ }
+
+ /**
+ * Specifies the beginning frequency of the second stop band
+ */
+ public BandPassBuilder stopFrequency2(int stopFrequency)
+ {
+ mStopFrequency2 = stopFrequency;
+ return this;
+ }
+
+ /**
+ * Specifies the pass band beginning frequency
+ */
+ public BandPassBuilder passFrequencyBegin(int passFrequency)
+ {
+ mPassFrequencyBegin = passFrequency;
+ return this;
+ }
+
+ /**
+ * Specifies the pass band ending frequency
+ */
+ public BandPassBuilder passFrequencyEnd(int passFrequency)
+ {
+ mPassFrequencyEnd = passFrequency;
+ return this;
+ }
+
+ /**
+ * Specifies the ripple in dB for both stop bands
+ */
+ public BandPassBuilder stopRipple(double rippleDb)
+ {
+ mStopRipple = rippleDb;
+ return this;
+ }
+
+ /**
+ * Specifies the ripple in dB for the pass band
+ */
+ public BandPassBuilder passRipple(double rippleDb)
+ {
+ mPassRipple = rippleDb;
+ return this;
+ }
+
+ /**
+ * Specifies the amplitude of the stop bands. Default is 0.0
+ */
+ public BandPassBuilder stopAmplitude(double amplitude)
+ {
+ mStopAmplitude = amplitude;
+ return this;
+ }
+
+ /**
+ * Specifies the amplitude of the pass band. Default is 1.0
+ */
+ public BandPassBuilder passAmplitude(double amplitude)
+ {
+ mPassAmplitude = amplitude;
+ return this;
+ }
+
+ /**
+ * Validates the frequency bands to test for overlap and completeness.
+ */
+ private void validateFrequencyBands()
+ {
+ if(mStopFrequency1 >= mPassFrequencyBegin)
+ {
+ throw new IllegalArgumentException("Stop band 1 ending frequency ["
+ + mStopFrequency1 + "] must be less than the pass band start frequency ["
+ + mPassFrequencyBegin + "]");
+ }
+
+ if(mPassFrequencyBegin >= mPassFrequencyEnd)
+ {
+ throw new IllegalArgumentException("Pass band begin frequency ["
+ + mPassFrequencyBegin + "] must be less than the pass band end frequency ["
+ + mPassFrequencyEnd + "]");
+ }
+
+ if(mPassFrequencyEnd >= mStopFrequency2)
+ {
+ throw new IllegalArgumentException("Pass band end frequency [" + mPassFrequencyEnd
+ + "] must be less than stop band 2 beginning frequency [" + mStopFrequency2 + "]");
+ }
+
+ if(mStopFrequency2 >= (mSampleRate / 2))
+ {
+ throw new IllegalArgumentException("Stop band 2 beginning frequency [" + mStopFrequency2 +
+ "] must be less than half of the sample rate [" + (mSampleRate / 2) + "]");
+ }
+ }
+
+ public FIRFilterSpecification build()
+ {
+ validateFrequencyBands();
+
+ FIRLinearPhaseFilterType type = FIRLinearPhaseFilterType.TYPE_1_ODD_LENGTH_EVEN_ORDER_SYMMETRICAL;
+
+ if(mOrder < 10)
+ {
+ mOrder = estimateBandPassOrder(mSampleRate, mPassFrequencyBegin, mPassFrequencyEnd,
+ mPassRipple, mStopRipple);
+ }
+
+ // Ensure even order since we're designing a Type 1 filter
+ if(mOrder % 2 == 1)
+ {
+ mOrder++;
+ }
+
+ FIRFilterSpecification spec = new FIRFilterSpecification(type, mOrder, mGridDensity);
+
+ spec.addFrequencyBand(new FrequencyBand(mSampleRate, 0, mStopFrequency1,
+ mStopAmplitude, mStopRipple));
+ spec.addFrequencyBand(new FrequencyBand(mSampleRate, mPassFrequencyBegin,
+ mPassFrequencyEnd, mPassAmplitude, mPassRipple));
+ spec.addFrequencyBand(new FrequencyBand(mSampleRate, mStopFrequency2,
+ (int)(mSampleRate / 2), mStopAmplitude, mStopRipple));
+
+ return spec;
+ }
+ }
+
+ /**
+ * Builder for creating polyphase channelizer low-pass prototype filter.
+ */
+ public static class ChannelizerBuilder
+ {
+ private int mGridDensity = 16;
+ private double mSampleRate;
+ private int mChannelCount;
+ private int mChannelBandwidth;
+ private int mTapsPerChannel;
+ private double mStopRipple = 0.001;
+ private double mPassRipple = 0.01;
+ private double mAlpha = 0.2;
+
+ public ChannelizerBuilder()
+ {
+ }
+
+ /**
+ * Sets the filter sample rate in hertz
+ */
+ public ChannelizerBuilder sampleRate(double sampleRate)
+ {
+ mSampleRate = sampleRate;
+ return this;
+ }
+
+ /**
+ * Number of Channels
+ * @param channels - must be a multiple of 2
+ * @return
+ */
+ public ChannelizerBuilder channels(int channels)
+ {
+ Validate.isTrue(channels % 2 == 0, "Channel count must be a multiple of 2");
+ mChannelCount = channels;
+ return this;
+ }
+
+ /**
+ * Sets the channel bandwidth in hertz
+ */
+ public ChannelizerBuilder channelBandwidth(int channelBandwidth)
+ {
+ mChannelBandwidth = channelBandwidth;
+ return this;
+ }
+
+ /**
+ * Number of filter taps per channel. Note: total filter length will be channels * tapsPerChannel
+ */
+ public ChannelizerBuilder tapsPerChannel(int tapsPerChannel)
+ {
+ mTapsPerChannel = tapsPerChannel;
+ return this;
+ }
+
+ /**
+ * Sets the frequency grid density. Default is 16.
+ */
+ public ChannelizerBuilder gridDensity(int density)
+ {
+ mGridDensity = density;
+ return this;
+ }
+
+ /**
+ * Specifies the ripple in dB for both stop bands
+ */
+ public ChannelizerBuilder stopRipple(double rippleDb)
+ {
+ mStopRipple = rippleDb;
+ return this;
+ }
+
+ /**
+ * Specifies the ripple in dB for the pass band
+ */
+ public ChannelizerBuilder passRipple(double rippleDb)
+ {
+ mPassRipple = rippleDb;
+ return this;
+ }
+
+ /**
+ * Rolloff factor for the start band and stop band.
+ * @param alpha - value in range 0.0 - 1.0
+ */
+ public ChannelizerBuilder alpha(double alpha)
+ {
+ Validate.isTrue(0.0 <= alpha && alpha <= 1.0);
+ mAlpha = alpha;
+
+ return this;
+ }
+
+ public FIRFilterSpecification build()
+ {
+ FIRLinearPhaseFilterType type = FIRLinearPhaseFilterType.TYPE_2_EVEN_LENGTH_ODD_ORDER_SYMMETRICAL;
+
+ int passFrequencyEnd = (int)(mChannelBandwidth * (1.0 - mAlpha));
+ int stopFrequencyBegin = (int)(mChannelBandwidth * (1.0 + mAlpha));
+
+ int estimatedOrder = estimateFilterOrder(mSampleRate, passFrequencyEnd, stopFrequencyBegin, mPassRipple,
+ mStopRipple);
+
+ // Ensure odd order since we're designing a Type 2 filter
+ if(estimatedOrder % 2 == 0)
+ {
+ estimatedOrder++;
+ }
+
+ int order = mChannelCount * mTapsPerChannel - 1;
+
+ mLog.debug("Order:" + order + " Sample Rate:" + mSampleRate + " Estimated Order:" + estimatedOrder + " Requested Order:" + order);
+
+ FIRFilterSpecification spec = new FIRFilterSpecification(type, order, mGridDensity);
+
+ double bandEdge = (double)mChannelBandwidth / mSampleRate;
+ double passEnd = bandEdge * (1.0 - mAlpha);
+ double stopStart = bandEdge * (1.0 + mAlpha);
+
+ mLog.debug("Pass End:" + passEnd + " Band Edge:" + bandEdge + " Stop Begin:" + stopStart);
+ mLog.debug("Pass End:" + (int)(mSampleRate * passEnd) +
+ " Band Edge:" + (int)(mSampleRate * bandEdge) +
+ " Stop Begin:" + (int)(mSampleRate * stopStart));
+
+ FrequencyBand passBand = new FrequencyBand(0.0, passEnd, 1.0, mPassRipple);
+ FrequencyBand edgeBand = new FrequencyBand(bandEdge, bandEdge, 0.5, mPassRipple);
+ edgeBand.setWeight(8.0 * edgeBand.getWeight(mPassRipple));
+ FrequencyBand stopBand = new FrequencyBand(stopStart, 0.5, 0.0, mStopRipple);
+
+ spec.addFrequencyBand(passBand);
+ spec.addFrequencyBand(edgeBand);
+ spec.addFrequencyBand(stopBand);
+
+ mLog.debug(spec.toString());
+ return spec;
+ }
+ }
+
+ // /**
+ // * Converts the set of band amplitude values into a set of weights
+ // * normalized to the largest band amplitude value
+ // */
+ // private static double[] getBandWeights( double... amplitudes )
+ // {
+ // double maximum = 0.0;
+ //
+ // for ( double amplitude : amplitudes )
+ // {
+ // if ( amplitude > maximum )
+ // {
+ // maximum = amplitude;
+ // }
+ // }
+ //
+ // double[] weights = new double[ amplitudes.length ];
+ //
+ // for ( int x = 0; x < amplitudes.length; x++ )
+ // {
+ // weights[ x ] = maximum / amplitudes[ x ];
+ // }
+ //
+ // return weights;
+ // }
+
+ /**
+ * Calculates filter order from filter length
+ */
+ private static int getFilterOrder(double length)
+ {
+ return (int)(Math.ceil(length) - 1);
+ }
+
+ /**
+ * Estimates filter length for a two-band (high or low pass) FIR filter.
+ *
+ * Note: this estimator doesn't work well if the transition frequency is near 0 or sampleRate/2
+ *
+ * From Herrmann et al (1973), Practical design rules for optimum finite impulse response
+ * filters. Bell System Technical J., 52, 769-99
+ *
+ * @param sampleRate in hertz
+ * @param frequency1 in hertz
+ * @param frequency2 in hertz
+ * @param passBandRipple pass band ripple in dB
+ * @param stopBandRipple stop band ripple in dB
+ * @return estimated filter length
+ */
+ public static int estimateFilterOrder(double sampleRate, int frequency1, int frequency2,
+ double passBandRipple, double stopBandRipple)
+ {
+ double df = Math.abs(frequency2 - frequency1) / sampleRate;
+
+ double ddp = Math.log10(stopBandRipple <= passBandRipple ? passBandRipple : stopBandRipple);
+ double dds = Math.log10(stopBandRipple <= passBandRipple ? stopBandRipple : passBandRipple);
+
+ double a1 = 5.309e-3;
+ double a2 = 7.114e-2;
+ double a3 = -4.761e-1;
+ double a4 = -2.66e-3;
+ double a5 = -5.941e-1;
+ double a6 = -4.278e-1;
+
+ double b1 = 11.01217;
+ double b2 = 0.5124401;
+
+ double t1 = a1 * ddp * ddp;
+ double t2 = a2 * ddp;
+ double t3 = a4 * ddp * ddp;
+ double t4 = a5 * ddp;
+
+ double dinf = ((t1 + t2 + a3) * dds) + (t3 + t4 + a6);
+ double ff = b1 + b2 * (ddp - dds);
+ double n = dinf / df - ff * df + 1.0;
+
+ return (int)Math.ceil(n);
+ }
+
+ /**
+ * Estimates band pass filter length;
+ *
+ * @param passBandStart frequency normalized to sample rate
+ * @param passBandEnd frequency normalized to sample rate
+ * @param passBandRippleDb
+ * @param stopBandRippleDb
+ */
+ public static int estimateBandPassOrder(double sampleRate, int passBandStart, int passBandEnd,
+ double passBandRippleDb, double stopBandRippleDb)
+ {
+ double df = (double)Math.abs(passBandEnd - passBandStart) / sampleRate;
+ double ddp = Math.log10(passBandRippleDb);
+ double dds = Math.log10(stopBandRippleDb);
+
+ double a1 = 0.01201;
+ double a2 = 0.09664;
+ double a3 = -0.51325;
+ double a4 = 0.00203;
+ double a5 = -0.57054;
+ double a6 = -0.44314;
+
+ double t1 = a1 * ddp * ddp;
+ double t2 = a2 * ddp;
+ double t3 = a4 * ddp * ddp;
+ double t4 = a5 * ddp;
+
+ double cinf = dds * (t1 + t2 + a3) + t3 + t4 + a6;
+ double ginf = -14.6f * (double)Math.log10(passBandRippleDb / stopBandRippleDb) - 16.9;
+ double n = cinf / df + ginf * df + 1.0;
+
+ return (int)Math.ceil(n);
+ }
+
+ /**
+ * Frequency band
+ */
+ public static class FrequencyBand
+ {
+ private int mGridSize;
+ private double mStart;
+ private double mEnd;
+ private double mAmplitude;
+ private double mRippleDB;
+ private Double mWeight;
+
+ /**
+ * Constructs a frequency band where the start and end frequencies are normalized to a
+ * sample rate of 1.
+ *
+ * @param start edge of the frequency band (0.0 - 1.0)
+ * @param end edge of the frequency band (0.0 - 1.0)
+ * @param amplitude of the frequency band on the unity scale (0.0 - 1.0)
+ * @param ripple of the frequency band specified as db ripple
+ */
+ public FrequencyBand(double start, double end, double amplitude, double ripple)
+ {
+ assert (0.0 <= mStart);
+ assert (mStart <= mEnd);
+ assert (mEnd <= 1.0);
+ assert (0.0 <= amplitude && amplitude <= 1.0);
+ assert (0.0 <= ripple);
+
+ mStart = start;
+ mEnd = end;
+ mAmplitude = amplitude;
+ mRippleDB = ripple;
+ }
+
+ /**
+ * Constructs a frequency band for the sample rate and start and end frequencies.
+ *
+ * @param sampleRate in hertz
+ * @param start edge of the frequency band in hertz
+ * @param end edge of the frequency band in hertz
+ * @param amplitude of the frequency band relative to unity (0.0 - 1.0)
+ * @param ripple of the frequency band specified as db ripple
+ */
+ public FrequencyBand(double sampleRate, int start, int end, double amplitude, double ripple)
+ {
+ this((double)start / sampleRate, (double)end / sampleRate,
+ amplitude, ripple);
+ }
+
+ public FrequencyBand(double sampleRate, int start, int end, double amplitude, double ripple, double weight)
+ {
+ this(sampleRate, start, end, amplitude, ripple);
+ mWeight = weight;
+ }
+
+ /**
+ * Explicitly sets the weight of this frequency band.
+ */
+ public void setWeight(double weight)
+ {
+ mWeight = weight;
+ }
+
+ public int getGridSize()
+ {
+ return mGridSize;
+ }
+
+ public void setGridSize(int totalGridSize, double totalBandwidth)
+ {
+ mGridSize = Math.max(1, (int)Math.ceil((double)totalGridSize * (getBandWidth() / totalBandwidth)));
+ }
+
+ public String toString()
+ {
+ StringBuilder sb = new StringBuilder();
+
+ sb.append("Band Start: ");
+ sb.append(mStart);
+ sb.append(" End:");
+ sb.append(mEnd);
+ sb.append(" Amplitude: ");
+ sb.append(mAmplitude);
+ sb.append(" Ripple dB: ");
+ sb.append(mRippleDB);
+ sb.append(" Ripple Amplitude: ");
+ sb.append(DECIMAL_FORMATTER.format(getRippleAmplitude()));
+ sb.append(" Grid Size: ");
+ sb.append(mGridSize);
+ sb.append(" Weight: ");
+ sb.append(getWeight(mRippleDB));
+
+ return sb.toString();
+ }
+
+ /**
+ * Start frequency edge for this band normalized to 1 Hz.
+ */
+ public double getStart()
+ {
+ return mStart;
+ }
+
+ /**
+ * End frequency edge for this band normalized to 1 Hz.
+ */
+ public double getEnd()
+ {
+ return mEnd;
+ }
+
+ /**
+ * Desired amplitude response for this frequency band
+ *
+ * @return amplitude (0.0 - 1.0)
+ */
+ public double getAmplitude()
+ {
+ return mAmplitude;
+ }
+
+ /**
+ * Frequency band ripple in decibels
+ */
+ public double getRippleDB()
+ {
+ return mRippleDB;
+ }
+
+ /**
+ * Frequency band ripple as an amplitude value
+ */
+ public double getRippleAmplitude()
+ {
+ return (Math.pow(10.0, (mRippleDB / 20)) - 1)
+ / (Math.pow(10.0, (mRippleDB / 20)) + 1);
+ }
+
+ /**
+ * Calculates the weighting of this frequency band's ripple relative to the max ripple for a
+ * filter specification.
+ *
+ * @param maxRippleAmplitude for the filter specification
+ * @return weight of this frequency band
+ */
+ public double getWeight(double maxRippleAmplitude)
+ {
+ if(mWeight == null)
+ {
+ return 1.0 / (getRippleAmplitude() / maxRippleAmplitude);
+ }
+
+ return mWeight;
+ }
+
+ /**
+ * Double-sided bandwidth of this frequency band
+ */
+ public double getBandWidth()
+ {
+ return mEnd - mStart;
+ }
+ }
}
diff --git a/src/main/java/io/github/dsheirer/dsp/filter/fir/complex/ComplexFIRFilter.java b/src/main/java/io/github/dsheirer/dsp/filter/fir/complex/ComplexFIRFilter.java
deleted file mode 100644
index 6ac9a164..00000000
--- a/src/main/java/io/github/dsheirer/dsp/filter/fir/complex/ComplexFIRFilter.java
+++ /dev/null
@@ -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
- *
- ******************************************************************************/
-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();
- }
-}
diff --git a/src/main/java/io/github/dsheirer/dsp/filter/fir/complex/ComplexFIRFilter2.java b/src/main/java/io/github/dsheirer/dsp/filter/fir/complex/ComplexFIRFilter2.java
new file mode 100644
index 00000000..7c618c00
--- /dev/null
+++ b/src/main/java/io/github/dsheirer/dsp/filter/fir/complex/ComplexFIRFilter2.java
@@ -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
+ *
+ ******************************************************************************/
+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();
+ }
+}
diff --git a/src/main/java/io/github/dsheirer/dsp/filter/fir/complex/ComplexFIRFilter_CB_CB.java b/src/main/java/io/github/dsheirer/dsp/filter/fir/complex/ComplexFIRFilter_CB_CB.java
deleted file mode 100644
index d864d2e2..00000000
--- a/src/main/java/io/github/dsheirer/dsp/filter/fir/complex/ComplexFIRFilter_CB_CB.java
+++ /dev/null
@@ -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
-{
- private Listener