mirror of
https://github.com/DSheirer/sdrtrunk.git
synced 2026-11-12 16:47:31 -05:00
Merges from master branch.
This commit is contained in:
@@ -32,8 +32,6 @@ import io.github.dsheirer.sample.real.RealBuffer;
|
||||
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
|
||||
@@ -117,7 +115,7 @@ public class AudioModule extends Module implements IAudioPacketProvider, IFilter
|
||||
}
|
||||
|
||||
@Override
|
||||
public void start(ScheduledExecutorService executor)
|
||||
public void start()
|
||||
{
|
||||
/* No start operations provided */
|
||||
}
|
||||
|
||||
@@ -140,7 +140,7 @@ public class StreamManager implements Listener<AudioPacket>
|
||||
{
|
||||
AudioRecorder recorder = BroadcastFactory.getAudioRecorder(getTemporaryRecordingPath(),
|
||||
mBroadcastFormat);
|
||||
recorder.start(ThreadPool.SCHEDULED);
|
||||
recorder.start();
|
||||
recorder.receive(audioPacket);
|
||||
mStreamRecorders.put(channelMetadataID, recorder);
|
||||
}
|
||||
|
||||
@@ -1,118 +1,124 @@
|
||||
/*******************************************************************************
|
||||
* SDR Trunk
|
||||
* Copyright (C) 2014 Dennis Sheirer
|
||||
*
|
||||
* This program is free software: you can redistribute it and/or modify
|
||||
* it under the terms of the GNU General Public License as published by
|
||||
* the Free Software Foundation, either version 3 of the License, or
|
||||
* (at your option) any later version.
|
||||
*
|
||||
* This program is distributed in the hope that it will be useful,
|
||||
* but WITHOUT ANY WARRANTY; without even the implied warranty of
|
||||
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
|
||||
* GNU General Public License for more details.
|
||||
*
|
||||
* You should have received a copy of the GNU General Public License
|
||||
* along with this program. If not, see <http://www.gnu.org/licenses/>
|
||||
* sdrtrunk
|
||||
* Copyright (C) 2014-2017 Dennis Sheirer
|
||||
*
|
||||
* This program is free software: you can redistribute it and/or modify
|
||||
* it under the terms of the GNU General Public License as published by
|
||||
* the Free Software Foundation, either version 3 of the License, or
|
||||
* (at your option) any later version.
|
||||
*
|
||||
* This program is distributed in the hope that it will be useful,
|
||||
* but WITHOUT ANY WARRANTY; without even the implied warranty of
|
||||
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
|
||||
* GNU General Public License for more details.
|
||||
*
|
||||
* You should have received a copy of the GNU General Public License
|
||||
* along with this program. If not, see <http://www.gnu.org/licenses/>
|
||||
*
|
||||
******************************************************************************/
|
||||
package io.github.dsheirer.buffer;
|
||||
|
||||
import java.util.Arrays;
|
||||
|
||||
/**
|
||||
* Circular sample buffer - allocates a buffer and stores samples in a circular
|
||||
* fashion, overwriting older samples with newly arrived samples. Initially
|
||||
* fills buffer with 0-valued samples
|
||||
*
|
||||
* Can be used as a delay-type buffer, to delay samples by the 'size' amount
|
||||
* @author denny
|
||||
* 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 FloatCircularBuffer
|
||||
{
|
||||
float[] mBuffer;
|
||||
int mBufferPointer = 0;
|
||||
|
||||
public FloatCircularBuffer( int size )
|
||||
{
|
||||
mBuffer = new float[ size ];
|
||||
|
||||
Arrays.fill( mBuffer, (float)0.0 );
|
||||
}
|
||||
|
||||
public int getSize()
|
||||
{
|
||||
return mBuffer.length;
|
||||
}
|
||||
|
||||
public float[] getBuffer()
|
||||
{
|
||||
return mBuffer;
|
||||
}
|
||||
float[] mBuffer;
|
||||
int mBufferPointer = 0;
|
||||
|
||||
/**
|
||||
* Puts the new value into the buffer and returns the oldest buffer value
|
||||
* that it replaced
|
||||
* @param newValue
|
||||
* @return
|
||||
*/
|
||||
public float get( float newValue )
|
||||
{
|
||||
float oldestSample = mBuffer[ mBufferPointer ];
|
||||
public FloatCircularBuffer(int size)
|
||||
{
|
||||
mBuffer = new float[size];
|
||||
}
|
||||
|
||||
mBuffer[ mBufferPointer ] = newValue;
|
||||
|
||||
mBufferPointer++;
|
||||
|
||||
if( mBufferPointer >= mBuffer.length )
|
||||
{
|
||||
mBufferPointer = 0;
|
||||
}
|
||||
|
||||
return oldestSample;
|
||||
}
|
||||
public int getSize()
|
||||
{
|
||||
return mBuffer.length;
|
||||
}
|
||||
|
||||
/**
|
||||
* Convenience method to put a new value in the buffer, while discarding
|
||||
* the old value
|
||||
*/
|
||||
public void put( float newValue )
|
||||
{
|
||||
get( newValue );
|
||||
}
|
||||
public float[] getBuffer()
|
||||
{
|
||||
return mBuffer;
|
||||
}
|
||||
|
||||
/**
|
||||
* Returns an array of arraySize, loaded with sampleCount of the
|
||||
* oldest samples, loaded into the array starting at index 0.
|
||||
*
|
||||
* This is useful for fetching an array filled half way with samples to
|
||||
* use in FFT processing.
|
||||
* @param arraySize - size of the array (must be larger than sampleCount)
|
||||
* @param sampleCount - number of the oldest samples in the buffer to load
|
||||
* @return
|
||||
*/
|
||||
public float[] get( int arraySize, int sampleCount )
|
||||
{
|
||||
float[] samples = new float[ arraySize ];
|
||||
/**
|
||||
* 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];
|
||||
|
||||
if( arraySize > sampleCount )
|
||||
{
|
||||
int tempCounter = mBufferPointer;
|
||||
put(newValue);
|
||||
|
||||
for( int x = 0; x < sampleCount; x++ )
|
||||
{
|
||||
samples[ x ] = mBuffer[ tempCounter ];
|
||||
|
||||
tempCounter++;
|
||||
|
||||
if( tempCounter > mBuffer.length )
|
||||
{
|
||||
tempCounter = 0;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
return samples;
|
||||
}
|
||||
|
||||
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;
|
||||
}
|
||||
}
|
||||
|
||||
@@ -25,8 +25,6 @@ import io.github.dsheirer.module.decode.DecoderType;
|
||||
import org.slf4j.Logger;
|
||||
import org.slf4j.LoggerFactory;
|
||||
|
||||
import java.util.concurrent.ScheduledExecutorService;
|
||||
|
||||
/**
|
||||
* Basic decoder channel state - provides the minimum channel state functionality
|
||||
* to support an always un-squelched audio decoder.
|
||||
@@ -94,7 +92,7 @@ public class AlwaysUnsquelchedDecoderState extends DecoderState
|
||||
}
|
||||
|
||||
@Override
|
||||
public void start(ScheduledExecutorService executor)
|
||||
public void start()
|
||||
{
|
||||
broadcast(new AttributeChangeRequest<String>(Attribute.PRIMARY_ADDRESS_TO, NO_SQUELCH));
|
||||
broadcast(new DecoderStateEvent(this, Event.ALWAYS_UNSQUELCH, State.IDLE));
|
||||
|
||||
@@ -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;
|
||||
@@ -36,17 +34,17 @@ import io.github.dsheirer.module.decode.event.CallEvent;
|
||||
import io.github.dsheirer.module.decode.event.ICallEventProvider;
|
||||
import io.github.dsheirer.sample.Listener;
|
||||
import io.github.dsheirer.sample.real.IOverflowListener;
|
||||
import io.github.dsheirer.source.tuner.frequency.FrequencyChangeEvent;
|
||||
import io.github.dsheirer.source.tuner.frequency.IFrequencyChangeListener;
|
||||
import io.github.dsheirer.source.ISourceEventListener;
|
||||
import io.github.dsheirer.source.SourceEvent;
|
||||
import io.github.dsheirer.source.heartbeat.Heartbeat;
|
||||
import io.github.dsheirer.source.heartbeat.IHeartbeatListener;
|
||||
import org.slf4j.Logger;
|
||||
import org.slf4j.LoggerFactory;
|
||||
|
||||
import java.util.concurrent.ScheduledExecutorService;
|
||||
|
||||
import static io.github.dsheirer.source.tuner.frequency.FrequencyChangeEvent.Event.NOTIFICATION_FREQUENCY_CHANGE;
|
||||
import static io.github.dsheirer.source.SourceEvent.Event.NOTIFICATION_FREQUENCY_CHANGE;
|
||||
|
||||
public class ChannelState extends Module implements ICallEventProvider, IDecoderStateEventListener,
|
||||
IDecoderStateEventProvider, IFrequencyChangeListener, IHeartbeatListener, ISquelchStateProvider,
|
||||
IDecoderStateEventProvider, ISourceEventListener, IHeartbeatListener, ISquelchStateProvider,
|
||||
IAttributeChangeRequestListener, IOverflowListener
|
||||
{
|
||||
private final static Logger mLog = LoggerFactory.getLogger(ChannelState.class);
|
||||
@@ -64,7 +62,7 @@ public class ChannelState extends Module implements ICallEventProvider, IDecoder
|
||||
private ChannelType mChannelType;
|
||||
private TrafficChannelManager mTrafficChannelEndListener;
|
||||
private CallEvent mTrafficChannelCallEvent;
|
||||
private FrequencyChangeListener mFrequencyChangeListener;
|
||||
private SourceChangeListener mSourceChangeListener;
|
||||
|
||||
private boolean mSquelchLocked = false;
|
||||
private boolean mSelected = false;
|
||||
@@ -124,7 +122,7 @@ public class ChannelState extends Module implements ICallEventProvider, IDecoder
|
||||
}
|
||||
|
||||
@Override
|
||||
public void start(ScheduledExecutorService executor)
|
||||
public void start()
|
||||
{
|
||||
mMutableMetadata.receive(new AttributeChangeRequest<State>(Attribute.CHANNEL_STATE, mState));
|
||||
|
||||
@@ -160,14 +158,14 @@ public class ChannelState extends Module implements ICallEventProvider, IDecoder
|
||||
}
|
||||
|
||||
@Override
|
||||
public Listener<FrequencyChangeEvent> getFrequencyChangeListener()
|
||||
public Listener<SourceEvent> getSourceEventListener()
|
||||
{
|
||||
if(mFrequencyChangeListener == null)
|
||||
if(mSourceChangeListener == null)
|
||||
{
|
||||
mFrequencyChangeListener = new FrequencyChangeListener();
|
||||
mSourceChangeListener = new SourceChangeListener();
|
||||
}
|
||||
|
||||
return mFrequencyChangeListener;
|
||||
return mSourceChangeListener;
|
||||
}
|
||||
|
||||
private boolean isStandardChannel()
|
||||
@@ -520,14 +518,14 @@ public class ChannelState extends Module implements ICallEventProvider, IDecoder
|
||||
/**
|
||||
* Listener to receive frequency change events and rebroadcast them to the decoder states
|
||||
*/
|
||||
public class FrequencyChangeListener implements Listener<FrequencyChangeEvent>
|
||||
public class SourceChangeListener implements Listener<SourceEvent>
|
||||
{
|
||||
@Override
|
||||
public void receive(FrequencyChangeEvent frequencyChangeEvent)
|
||||
public void receive(SourceEvent sourceEvent)
|
||||
{
|
||||
if(frequencyChangeEvent.getEvent() == NOTIFICATION_FREQUENCY_CHANGE)
|
||||
if(sourceEvent.getEvent() == NOTIFICATION_FREQUENCY_CHANGE)
|
||||
{
|
||||
long frequency = frequencyChangeEvent.getValue().longValue();
|
||||
long frequency = sourceEvent.getValue().longValue();
|
||||
|
||||
broadcast(new DecoderStateEvent(this, Event.SOURCE_FREQUENCY, getState(), frequency));
|
||||
}
|
||||
|
||||
@@ -1,19 +1,16 @@
|
||||
/*******************************************************************************
|
||||
* sdrtrunk
|
||||
* sdr-trunk
|
||||
* Copyright (C) 2014-2017 Dennis Sheirer
|
||||
*
|
||||
* This program is free software: you can redistribute it and/or modify
|
||||
* it under the terms of the GNU General Public License as published by
|
||||
* the Free Software Foundation, either version 3 of the License, or
|
||||
* (at your option) any later version.
|
||||
* This program is free software: you can redistribute it and/or modify it under the terms of the GNU General Public
|
||||
* License as published by the Free Software Foundation, either version 3 of the License, or (at your option) any
|
||||
* later version.
|
||||
*
|
||||
* This program is distributed in the hope that it will be useful,
|
||||
* but WITHOUT ANY WARRANTY; without even the implied warranty of
|
||||
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
|
||||
* GNU General Public License for more details.
|
||||
* This program is distributed in the hope that it will be useful, but WITHOUT ANY WARRANTY; without even the implied
|
||||
* warranty of MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU General Public License for more details.
|
||||
*
|
||||
* You should have received a copy of the GNU General Public License
|
||||
* along with this program. If not, see <http://www.gnu.org/licenses/>
|
||||
* You should have received a copy of the GNU General Public License along with this program.
|
||||
* If not, see <http://www.gnu.org/licenses/>
|
||||
*
|
||||
******************************************************************************/
|
||||
package io.github.dsheirer.channel.traffic;
|
||||
@@ -29,13 +26,12 @@ import io.github.dsheirer.controller.channel.TrafficChannelEvent;
|
||||
import io.github.dsheirer.module.Module;
|
||||
import io.github.dsheirer.module.decode.config.DecodeConfiguration;
|
||||
import io.github.dsheirer.module.decode.event.CallEvent;
|
||||
import io.github.dsheirer.module.decode.event.CallEvent.CallEventType;
|
||||
import io.github.dsheirer.module.decode.event.ICallEventProvider;
|
||||
import io.github.dsheirer.module.log.config.EventLogConfiguration;
|
||||
import io.github.dsheirer.record.config.RecordConfiguration;
|
||||
import io.github.dsheirer.sample.Listener;
|
||||
import io.github.dsheirer.source.config.SourceConfigTuner;
|
||||
import io.github.dsheirer.source.tuner.TunerChannel;
|
||||
import io.github.dsheirer.source.tuner.TunerChannel.Type;
|
||||
import io.github.dsheirer.source.tuner.channel.TunerChannel;
|
||||
import org.slf4j.Logger;
|
||||
import org.slf4j.LoggerFactory;
|
||||
|
||||
@@ -43,7 +39,6 @@ import java.util.ArrayList;
|
||||
import java.util.List;
|
||||
import java.util.Map;
|
||||
import java.util.concurrent.ConcurrentHashMap;
|
||||
import java.util.concurrent.ScheduledExecutorService;
|
||||
|
||||
public class TrafficChannelManager extends Module implements ICallEventProvider, IDecoderStateEventListener
|
||||
{
|
||||
@@ -61,7 +56,6 @@ public class TrafficChannelManager extends Module implements ICallEventProvider,
|
||||
|
||||
private ChannelModel mChannelModel;
|
||||
private DecodeConfiguration mDecodeConfiguration;
|
||||
private EventLogConfiguration mEventLogConfiguration;
|
||||
private RecordConfiguration mRecordConfiguration;
|
||||
private String mSystem;
|
||||
private String mSite;
|
||||
@@ -75,7 +69,6 @@ public class TrafficChannelManager extends Module implements ICallEventProvider,
|
||||
* @param channelModel containing channels currently in use
|
||||
* @param decodeConfiguration - decoder configuration to use for each
|
||||
* traffic channel allocation.
|
||||
* @param eventLogConfiguration - logging configuration to use for traffic channels
|
||||
* @param recordConfiguration - recording options for each traffic channel
|
||||
* @param system label to use to describe the system
|
||||
* @param site label to use to describe the site
|
||||
@@ -85,7 +78,6 @@ public class TrafficChannelManager extends Module implements ICallEventProvider,
|
||||
*/
|
||||
public TrafficChannelManager(ChannelModel channelModel,
|
||||
DecodeConfiguration decodeConfiguration,
|
||||
EventLogConfiguration eventLogConfiguration,
|
||||
RecordConfiguration recordConfiguration,
|
||||
String system,
|
||||
String site,
|
||||
@@ -94,7 +86,6 @@ public class TrafficChannelManager extends Module implements ICallEventProvider,
|
||||
{
|
||||
mChannelModel = channelModel;
|
||||
mDecodeConfiguration = decodeConfiguration;
|
||||
mEventLogConfiguration = eventLogConfiguration;
|
||||
mRecordConfiguration = recordConfiguration;
|
||||
mSystem = system;
|
||||
mSite = site;
|
||||
@@ -150,8 +141,6 @@ public class TrafficChannelManager extends Module implements ICallEventProvider,
|
||||
|
||||
channel.setDecodeConfiguration(mDecodeConfiguration);
|
||||
|
||||
channel.setEventLogConfiguration(mEventLogConfiguration);
|
||||
|
||||
channel.setRecordConfiguration(mRecordConfiguration);
|
||||
|
||||
channel.setAliasListName(mAliasListName);
|
||||
@@ -161,7 +150,7 @@ public class TrafficChannelManager extends Module implements ICallEventProvider,
|
||||
mTrafficChannelPool.add(channel);
|
||||
}
|
||||
|
||||
/* If we have a configured channel, update metadata */
|
||||
/* If we have a configured channel, update metadata */
|
||||
if(channel != null)
|
||||
{
|
||||
channel.setSourceConfiguration(new SourceConfigTuner(tunerChannel));
|
||||
@@ -184,7 +173,7 @@ public class TrafficChannelManager extends Module implements ICallEventProvider,
|
||||
{
|
||||
CallEvent callEvent = event.getCallEvent();
|
||||
|
||||
/* Check for duplicate events and suppress */
|
||||
/* Check for duplicate events and suppress */
|
||||
synchronized(mTrafficChannelsInUse)
|
||||
{
|
||||
if(mTrafficChannelsInUse.containsKey(callEvent.getChannel()))
|
||||
@@ -196,8 +185,8 @@ public class TrafficChannelManager extends Module implements ICallEventProvider,
|
||||
|
||||
if(frequency > 0)
|
||||
{
|
||||
Channel channel = getChannel(callEvent.getChannel(), new TunerChannel(Type.TRAFFIC,
|
||||
frequency, mDecodeConfiguration.getDecoderType().getChannelBandwidth()));
|
||||
Channel channel = getChannel(callEvent.getChannel(), new TunerChannel(frequency,
|
||||
mDecodeConfiguration.getDecoderType().getChannelBandwidth()));
|
||||
|
||||
if(channel != null)
|
||||
{
|
||||
@@ -213,7 +202,7 @@ public class TrafficChannelManager extends Module implements ICallEventProvider,
|
||||
}
|
||||
else
|
||||
{
|
||||
callEvent.setCallEventType(CallEvent.CallEventType.CALL_DETECT);
|
||||
callEvent.setCallEventType(CallEventType.CALL_DETECT);
|
||||
|
||||
String details = callEvent.getDetails();
|
||||
|
||||
@@ -230,7 +219,7 @@ public class TrafficChannelManager extends Module implements ICallEventProvider,
|
||||
}
|
||||
else
|
||||
{
|
||||
callEvent.setCallEventType(CallEvent.CallEventType.CALL_DETECT);
|
||||
callEvent.setCallEventType(CallEventType.CALL_DETECT);
|
||||
|
||||
String details = callEvent.getDetails();
|
||||
|
||||
@@ -247,7 +236,7 @@ public class TrafficChannelManager extends Module implements ICallEventProvider,
|
||||
}
|
||||
else
|
||||
{
|
||||
callEvent.setCallEventType(CallEvent.CallEventType.CALL_DETECT);
|
||||
callEvent.setCallEventType(CallEventType.CALL_DETECT);
|
||||
|
||||
String details = callEvent.getDetails();
|
||||
|
||||
@@ -337,7 +326,7 @@ public class TrafficChannelManager extends Module implements ICallEventProvider,
|
||||
}
|
||||
|
||||
@Override
|
||||
public void start(ScheduledExecutorService executor)
|
||||
public void start()
|
||||
{
|
||||
}
|
||||
|
||||
@@ -348,7 +337,7 @@ public class TrafficChannelManager extends Module implements ICallEventProvider,
|
||||
{
|
||||
List<String> channels = new ArrayList<>();
|
||||
|
||||
/* Copy the keyset so we don't get concurrent modification of the map */
|
||||
/* Copy the keyset so we don't get concurrent modification of the map */
|
||||
channels.addAll(mTrafficChannelsInUse.keySet());
|
||||
|
||||
for(String channel : channels)
|
||||
@@ -399,4 +388,4 @@ public class TrafficChannelManager extends Module implements ICallEventProvider,
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -31,18 +31,16 @@ import io.github.dsheirer.module.log.config.EventLogConfiguration;
|
||||
import io.github.dsheirer.record.RecorderType;
|
||||
import io.github.dsheirer.record.config.RecordConfiguration;
|
||||
import io.github.dsheirer.sample.Listener;
|
||||
import io.github.dsheirer.source.SourceEvent;
|
||||
import io.github.dsheirer.source.SourceType;
|
||||
import io.github.dsheirer.source.config.SourceConfigFactory;
|
||||
import io.github.dsheirer.source.config.SourceConfigRecording;
|
||||
import io.github.dsheirer.source.config.SourceConfigTuner;
|
||||
import io.github.dsheirer.source.config.SourceConfiguration;
|
||||
import io.github.dsheirer.source.tuner.TunerChannel;
|
||||
import io.github.dsheirer.source.tuner.TunerChannel.Type;
|
||||
import io.github.dsheirer.source.tuner.frequency.FrequencyChangeEvent;
|
||||
import io.github.dsheirer.source.tuner.frequency.FrequencyChangeEvent.Event;
|
||||
import io.github.dsheirer.source.tuner.channel.TunerChannel;
|
||||
|
||||
@JacksonXmlRootElement(localName = "channel")
|
||||
public class Channel extends Configuration implements Listener<FrequencyChangeEvent>
|
||||
public class Channel extends Configuration implements Listener<SourceEvent>
|
||||
{
|
||||
// Standard channels are persisted and traffic channels are temporary
|
||||
public enum ChannelType {STANDARD, TRAFFIC}
|
||||
@@ -51,12 +49,9 @@ public class Channel extends Configuration implements Listener<FrequencyChangeEv
|
||||
private static int UNIQUE_ID = 0;
|
||||
|
||||
private DecodeConfiguration mDecodeConfiguration = DecoderFactory.getDefaultDecodeConfiguration();
|
||||
private AuxDecodeConfiguration mAuxDecodeConfiguration =
|
||||
new AuxDecodeConfiguration();
|
||||
private SourceConfiguration mSourceConfiguration =
|
||||
SourceConfigFactory.getDefaultSourceConfiguration();
|
||||
private EventLogConfiguration mEventLogConfiguration =
|
||||
new EventLogConfiguration();
|
||||
private AuxDecodeConfiguration mAuxDecodeConfiguration = new AuxDecodeConfiguration();
|
||||
private SourceConfiguration mSourceConfiguration = SourceConfigFactory.getDefaultSourceConfiguration();
|
||||
private EventLogConfiguration mEventLogConfiguration = new EventLogConfiguration();
|
||||
private RecordConfiguration mRecordConfiguration = new RecordConfiguration();
|
||||
|
||||
private String mAliasListName;
|
||||
@@ -423,15 +418,14 @@ public class Channel extends Configuration implements Listener<FrequencyChangeEv
|
||||
{
|
||||
SourceConfigTuner config = (SourceConfigTuner)mSourceConfiguration;
|
||||
|
||||
mTunerChannel = new TunerChannel(Type.LOCKED, config.getFrequency(),
|
||||
mTunerChannel = new TunerChannel(config.getFrequency(),
|
||||
mDecodeConfiguration.getDecoderType().getChannelBandwidth());
|
||||
}
|
||||
else if(mSourceConfiguration.getSourceType() == SourceType.RECORDING)
|
||||
{
|
||||
SourceConfigRecording config =
|
||||
(SourceConfigRecording)mSourceConfiguration;
|
||||
SourceConfigRecording config = (SourceConfigRecording)mSourceConfiguration;
|
||||
|
||||
mTunerChannel = new TunerChannel(Type.LOCKED, config.getFrequency(),
|
||||
mTunerChannel = new TunerChannel(config.getFrequency(),
|
||||
mDecodeConfiguration.getDecoderType().getChannelBandwidth());
|
||||
}
|
||||
}
|
||||
@@ -457,9 +451,9 @@ public class Channel extends Configuration implements Listener<FrequencyChangeEv
|
||||
* be used to visually show in the spectral display.
|
||||
*/
|
||||
@Override
|
||||
public void receive(FrequencyChangeEvent event)
|
||||
public void receive(SourceEvent event)
|
||||
{
|
||||
if(event.getEvent() == Event.NOTIFICATION_CHANNEL_FREQUENCY_CORRECTION_CHANGE)
|
||||
if(event.getEvent() == SourceEvent.Event.NOTIFICATION_CHANNEL_FREQUENCY_CORRECTION_CHANGE)
|
||||
{
|
||||
mChannelFrequencyCorrection = event.getValue().intValue();
|
||||
}
|
||||
|
||||
@@ -26,18 +26,21 @@ 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;
|
||||
|
||||
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)
|
||||
|
||||
/**
|
||||
* Generates coefficients for a unity-gain, windowed low-pass filter
|
||||
@@ -48,43 +51,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(int 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 +104,7 @@ public class FilterFactory
|
||||
* @param coefficients
|
||||
* @return
|
||||
*/
|
||||
private static float[] normalize(float[] coefficients)
|
||||
public static float[] normalize(float[] coefficients)
|
||||
{
|
||||
float accumulator = 0;
|
||||
|
||||
@@ -128,17 +137,15 @@ public class FilterFactory
|
||||
* @param length
|
||||
* @return
|
||||
*/
|
||||
public static float[] getUnityResponseArray(int sampleRate,
|
||||
long frequency,
|
||||
int length)
|
||||
public static float[] getUnityResponseArray(int 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 +154,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 +168,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 +201,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(int 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 +220,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(int 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 +254,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 +312,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 +323,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(int sampleRate, long pass, long stop, int attenuation)
|
||||
{
|
||||
double frequency = ((double) stop - (double) pass) / (double) sampleRate;
|
||||
double frequency = ((double)stop - (double)pass) / (double)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 +348,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 +358,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 +376,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 +386,7 @@ public class FilterFactory
|
||||
{
|
||||
rates = new int[2];
|
||||
|
||||
int stage2 = (int) (decimation / stage1);
|
||||
int stage2 = (int)(decimation / stage1);
|
||||
|
||||
if(stage1 > stage2)
|
||||
{
|
||||
@@ -465,8 +404,7 @@ public class FilterFactory
|
||||
}
|
||||
|
||||
/**
|
||||
* Finds the factor that is closest to the desired factor, from an ordered
|
||||
* list of factors.
|
||||
* Finds the factor that is closest to the desired factor, from an ordered list of factors.
|
||||
*/
|
||||
private static int findClosest(int desiredFactor, Set<Integer> factors)
|
||||
{
|
||||
@@ -488,9 +426,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 +435,11 @@ public class FilterFactory
|
||||
private static Set<Integer> getFactors(int value)
|
||||
{
|
||||
Set<Integer> factors = new TreeSet<Integer>();
|
||||
|
||||
|
||||
/* 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 +451,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 +462,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,14 +495,11 @@ 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 */
|
||||
if(taps % 2 == 0)
|
||||
{
|
||||
@@ -599,7 +516,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 +537,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 +562,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
|
||||
*
|
||||
* 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(int samplesPerSymbol,
|
||||
int symbols,
|
||||
float alpha)
|
||||
public static float[] getRootRaisedCosine(int samplesPerSymbol, int symbolCount, float alpha)
|
||||
{
|
||||
int taps = samplesPerSymbol * symbols + 1;
|
||||
int taps = samplesPerSymbol * symbolCount;
|
||||
|
||||
float scale = 0;
|
||||
|
||||
@@ -678,10 +592,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 / (float) samplesPerSymbol;
|
||||
float x2 = 4.0f * alpha * index / (float) 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 +604,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 / (float) 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 +625,11 @@ public class FilterFactory
|
||||
x3 = (1.0f - alpha) * x1;
|
||||
x2 = (1.0f + alpha) * x1;
|
||||
|
||||
numerator = (float) (Math.sin(x2) * (1.0f + alpha) * Math.PI -
|
||||
Math.cos(x3) * ((1.0 - alpha) * Math.PI *
|
||||
(double) samplesPerSymbol) / (4 * 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 +649,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 +666,248 @@ 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;
|
||||
}
|
||||
|
||||
/**
|
||||
* 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
|
||||
*/
|
||||
public static float[] getSincChannelizer(double channelBandwidth, int channels, int tapsPerChannel,
|
||||
Window.WindowType windowType, boolean logResults) throws FilterDesignException
|
||||
{
|
||||
int currentTapsPerChannel = tapsPerChannel;
|
||||
int sampleRate = (int)(channelBandwidth * (double)channels * 2.0);
|
||||
int filterLength = (channels * currentTapsPerChannel) - 1;
|
||||
|
||||
double cutoffFrequency = channelBandwidth / (double)sampleRate;
|
||||
double bandEdge = cutoffFrequency;
|
||||
double increment = cutoffFrequency * 0.1;
|
||||
|
||||
//Get an initial filter and band edge frequency response using the channel bandwidth as a cutoff
|
||||
float[] taps = FilterFactory.getSinc(cutoffFrequency, filterLength, windowType);
|
||||
double response = FilterFactory.evaluate(taps, bandEdge);
|
||||
|
||||
//Set cutoff adjustment threshold - we'll test cutoff frequencies to around 1 hertz resolution
|
||||
double incrementThreshold = 1.0 / (double)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) &&
|
||||
(cutoffFrequency + increment <= bandEdge))
|
||||
{
|
||||
float[] higherCutoffTaps = FilterFactory.getSinc(cutoffFrequency + increment, filterLength, windowType);
|
||||
double higherCutoffResponse = FilterFactory.evaluate(higherCutoffTaps, bandEdge);
|
||||
|
||||
if(matchesObjective(higherCutoffResponse, PERFECT_RECONSTRUCTION_GAIN_AT_BAND_EDGE))
|
||||
{
|
||||
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))
|
||||
{
|
||||
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));
|
||||
}
|
||||
|
||||
filterLength = channels * currentTapsPerChannel - 1;
|
||||
cutoffFrequency = ((double)channelBandwidth / (double)sampleRate);
|
||||
increment = cutoffFrequency * 0.1;
|
||||
}
|
||||
|
||||
taps = FilterFactory.getSinc(cutoffFrequency, filterLength, windowType);
|
||||
response = FilterFactory.evaluate(taps, bandEdge);
|
||||
}
|
||||
}
|
||||
|
||||
//This will probably never happen, but ...
|
||||
if(!matchesObjective(response, PERFECT_RECONSTRUCTION_GAIN_AT_BAND_EDGE))
|
||||
{
|
||||
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: " + windowType.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 1.0 OBJECTIVE: " + PERFECT_RECONSTRUCTION_GAIN_AT_BAND_EDGE);
|
||||
mLog.debug("Attenuation at 1.00 Channels: " + evaluate(taps, bandEdge * 1.00));
|
||||
mLog.debug("Attenuation at 1.25 Channels: " + evaluate(taps, bandEdge * 1.25));
|
||||
mLog.debug("Attenuation at 1.50 Channels: " + evaluate(taps, bandEdge * 1.50));
|
||||
mLog.debug("Attenuation at 1.75 Channels: " + evaluate(taps, bandEdge * 1.75));
|
||||
mLog.debug("Attenuation at 2.00 Channels: " + evaluate(taps, bandEdge * 2.00));
|
||||
}
|
||||
|
||||
return extendedTaps;
|
||||
}
|
||||
|
||||
/**
|
||||
* 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);
|
||||
|
||||
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)
|
||||
{
|
||||
return Math.abs(a - objective) < 0.0000000001;
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1,234 +1,439 @@
|
||||
/*******************************************************************************
|
||||
* SDR Trunk
|
||||
* Copyright (C) 2014 Dennis Sheirer
|
||||
*
|
||||
* This program is free software: you can redistribute it and/or modify
|
||||
* it under the terms of the GNU General Public License as published by
|
||||
* the Free Software Foundation, either version 3 of the License, or
|
||||
* (at your option) any later version.
|
||||
*
|
||||
* This program is distributed in the hope that it will be useful,
|
||||
* but WITHOUT ANY WARRANTY; without even the implied warranty of
|
||||
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
|
||||
* GNU General Public License for more details.
|
||||
*
|
||||
* You should have received a copy of the GNU General Public License
|
||||
* along with this program. If not, see <http://www.gnu.org/licenses/>
|
||||
* sdrtrunk
|
||||
* Copyright (C) 2014-2017 Dennis Sheirer
|
||||
*
|
||||
* This program is free software: you can redistribute it and/or modify
|
||||
* it under the terms of the GNU General Public License as published by
|
||||
* the Free Software Foundation, either version 3 of the License, or
|
||||
* (at your option) any later version.
|
||||
*
|
||||
* This program is distributed in the hope that it will be useful,
|
||||
* but WITHOUT ANY WARRANTY; without even the implied warranty of
|
||||
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
|
||||
* GNU General Public License for more details.
|
||||
*
|
||||
* You should have received a copy of the GNU General Public License
|
||||
* along with this program. If not, see <http://www.gnu.org/licenses/>
|
||||
*
|
||||
******************************************************************************/
|
||||
package io.github.dsheirer.dsp.filter;
|
||||
|
||||
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 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;
|
||||
}
|
||||
|
||||
|
||||
private 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;
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* 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"),
|
||||
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;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -0,0 +1,144 @@
|
||||
/*******************************************************************************
|
||||
* sdrtrunk
|
||||
* Copyright (C) 2014-2017 Dennis Sheirer
|
||||
*
|
||||
* This program is free software: you can redistribute it and/or modify
|
||||
* it under the terms of the GNU General Public License as published by
|
||||
* the Free Software Foundation, either version 3 of the License, or
|
||||
* (at your option) any later version.
|
||||
*
|
||||
* This program is distributed in the hope that it will be useful,
|
||||
* but WITHOUT ANY WARRANTY; without even the implied warranty of
|
||||
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
|
||||
* GNU General Public License for more details.
|
||||
*
|
||||
* You should have received a copy of the GNU General Public License
|
||||
* along with this program. If not, see <http://www.gnu.org/licenses/>
|
||||
*
|
||||
******************************************************************************/
|
||||
package io.github.dsheirer.dsp.filter.channelizer;
|
||||
|
||||
import io.github.dsheirer.sample.Broadcaster;
|
||||
import io.github.dsheirer.sample.Listener;
|
||||
import io.github.dsheirer.sample.complex.ComplexBuffer;
|
||||
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<ComplexBuffer>, ISourceEventListener
|
||||
{
|
||||
private final static Logger mLog = LoggerFactory.getLogger(AbstractComplexPolyphaseChannelizer.class);
|
||||
|
||||
private Broadcaster<SourceEvent> mSourceChangeBroadcaster = new Broadcaster();
|
||||
private List<PolyphaseChannelSource> mChannels = new CopyOnWriteArrayList<>();
|
||||
private double mSampleRate;
|
||||
private int mChannelCount;
|
||||
private double mChannelSampleRate;
|
||||
|
||||
/**
|
||||
* Complex sample polyphase channelizer
|
||||
*
|
||||
* @param channelCount
|
||||
*/
|
||||
public AbstractComplexPolyphaseChannelizer(double sampleRate, int channelCount)
|
||||
{
|
||||
mChannelCount = channelCount;
|
||||
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;
|
||||
}
|
||||
|
||||
/**
|
||||
* 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 channels containing an array of I/Q samples per channel
|
||||
*/
|
||||
protected void dispatch(float[] channels)
|
||||
{
|
||||
for(PolyphaseChannelSource channel : mChannels)
|
||||
{
|
||||
channel.receiveChannelResults(channels);
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* 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();
|
||||
}
|
||||
|
||||
@Override
|
||||
public Listener<SourceEvent> getSourceEventListener()
|
||||
{
|
||||
return mSourceChangeBroadcaster;
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,586 @@
|
||||
/*******************************************************************************
|
||||
* *********************************************************************************************************************
|
||||
* sdr-trunk
|
||||
* Copyright (C) 2014-2017 Dennis Sheirer
|
||||
*
|
||||
* This program is free software: you can redistribute it and/or modify it under the terms of the GNU General Public
|
||||
* License as published by the Free Software Foundation, either version 3 of the License, or (at your option) any
|
||||
* later version.
|
||||
*
|
||||
* This program is distributed in the hope that it will be useful, but WITHOUT ANY WARRANTY; without even the implied
|
||||
* warranty of MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU General Public License for more details.
|
||||
*
|
||||
* You should have received a copy of the GNU General Public License along with this program.
|
||||
* If not, see <http://www.gnu.org/licenses/>
|
||||
* *********************************************************************************************************************
|
||||
******************************************************************************/
|
||||
package io.github.dsheirer.dsp.filter.channelizer;
|
||||
|
||||
import io.github.dsheirer.source.tuner.channel.TunerChannel;
|
||||
import org.slf4j.Logger;
|
||||
import org.slf4j.LoggerFactory;
|
||||
|
||||
import java.text.DecimalFormat;
|
||||
import java.util.ArrayList;
|
||||
import java.util.List;
|
||||
|
||||
public class ChannelCalculator
|
||||
{
|
||||
private final static Logger mLog = LoggerFactory.getLogger(ChannelCalculator.class);
|
||||
|
||||
/**
|
||||
* Policy to determine desired corrective action for methods that calculate an index for a frequency or a frequency
|
||||
* from an index where the frequency may exist either at the overlapping boundary of two indexes, or in the middle
|
||||
* of the wrap-around (N/2) index.
|
||||
*/
|
||||
private 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 (double)getChannelBandwidth() * getOversampling();
|
||||
}
|
||||
|
||||
/**
|
||||
* PolyphaseChannelManager input minimum frequency
|
||||
* @return minimum frequency in hertz
|
||||
*/
|
||||
public long getMinimumFrequency()
|
||||
{
|
||||
return (long)(getCenterFrequency() - (getSampleRate() / 2.0));
|
||||
}
|
||||
|
||||
/**
|
||||
* PolyphaseChannelManager input maximum frequency
|
||||
* @return maximum frequency in hertz
|
||||
*/
|
||||
public long getMaximumFrequency()
|
||||
{
|
||||
return (long)(getCenterFrequency() + (getSampleRate() / 2.0));
|
||||
}
|
||||
|
||||
/**
|
||||
* Identifies the channelizer channel index(es) required to produce a tuner channel with the specified center
|
||||
* frequency and channel bandwidth.
|
||||
*
|
||||
* @param tunerChannel specifying the desired channel frequency and bandwidth
|
||||
*
|
||||
* @return an array of one or more polyphase channelizer index(es) to target in order to produce an output channel
|
||||
* for the desired frequency and bandwidth.
|
||||
*
|
||||
* @throws IllegalArgumentException if the tuner channel's minimum or maximum frequency is not contained
|
||||
* within the channelizer's minimum or maximum frequency range
|
||||
*/
|
||||
public List<Integer> getChannelIndexes(TunerChannel tunerChannel) throws IllegalArgumentException
|
||||
{
|
||||
if(tunerChannel.getMinFrequency() < getMinimumFrequency() ||
|
||||
tunerChannel.getMaxFrequency() > getMaximumFrequency())
|
||||
{
|
||||
throw new IllegalArgumentException("Requested channel cannot be provided by this " +
|
||||
"channelizer. Requested channel [" + tunerChannel.getMinFrequency() + " - " +
|
||||
tunerChannel.getMaxFrequency() + "] exceeds current channelizer frequency range [" +
|
||||
getMinimumFrequency() + " - " + getMaximumFrequency() + "]");
|
||||
}
|
||||
|
||||
int minIndex = getIndexForFrequency(tunerChannel.getMinFrequency(), IndexBoundaryPolicy.ADJUST_POSITIVE);
|
||||
int maxIndex = getIndexForFrequency(tunerChannel.getMaxFrequency(), IndexBoundaryPolicy.ADJUST_NEGATIVE);
|
||||
|
||||
//Detect the unsourceable situation where the wrap around index would be included in both the minimum index
|
||||
//and the maximum index, meaning that the tuner channel bandwidth is nearly the same as the input sample rate.
|
||||
if(minIndex == getWrapAroundIndex() && maxIndex == getWrapAroundIndex())
|
||||
{
|
||||
throw new IllegalArgumentException("Requested tuner channel cannot be provided. Requested bandwidth is " +
|
||||
"within two channel bandwidths of the sample rate.");
|
||||
}
|
||||
|
||||
List<Integer> indexes = new ArrayList<>();
|
||||
|
||||
indexes.add(minIndex);
|
||||
|
||||
if(minIndex != maxIndex)
|
||||
{
|
||||
//Ensure our min/max indexes are within the correct bounds 0 <> N-1 to safeguard the while loop below
|
||||
if(minIndex < 0 || minIndex >= getChannelCount() || maxIndex < 0 || maxIndex >= getChannelCount())
|
||||
{
|
||||
throw new IllegalArgumentException("Something went wrong while calculating the min and max polyphase " +
|
||||
"channel indexes for tuner channel: " + tunerChannel);
|
||||
}
|
||||
|
||||
int pointer = minIndex + 1;
|
||||
|
||||
if(pointer >= getChannelCount())
|
||||
{
|
||||
pointer -= getChannelCount();
|
||||
}
|
||||
|
||||
while(pointer != maxIndex)
|
||||
{
|
||||
indexes.add(pointer);
|
||||
|
||||
pointer++;
|
||||
|
||||
if(pointer >= getChannelCount())
|
||||
{
|
||||
pointer -= getChannelCount();
|
||||
}
|
||||
}
|
||||
|
||||
indexes.add(maxIndex);
|
||||
}
|
||||
|
||||
return indexes;
|
||||
}
|
||||
|
||||
/**
|
||||
* Calculates the index for the specified frequency. Certain frequencies can exist as both the maximum frequency
|
||||
* of one channel and the minimum frequency of an adjacent channel. The index boundary policy identifies your
|
||||
* choice to select the higher frequency channel index (ADJUST_POSITIVE) or the lower frequency channel
|
||||
* index (ADJUST_NEGATIVE).
|
||||
*
|
||||
* @param frequency to determine channel index for
|
||||
* @param indexBoundaryPolicy to determine the behavior when the frequency overlaps two channel boundaries
|
||||
* @return index for the frequency
|
||||
*/
|
||||
public int getIndexForFrequency(long frequency, IndexBoundaryPolicy indexBoundaryPolicy)
|
||||
{
|
||||
double offset = frequency - getCenterFrequency();
|
||||
|
||||
if(Math.abs(offset) < getHalfChannelBandwidth())
|
||||
{
|
||||
return 0; //Center index
|
||||
}
|
||||
|
||||
if(offset > 0)
|
||||
{
|
||||
offset += getHalfChannelBandwidth();
|
||||
}
|
||||
else
|
||||
{
|
||||
offset -= getHalfChannelBandwidth();
|
||||
}
|
||||
|
||||
int indexOffset = (int)(offset / getChannelBandwidth());
|
||||
|
||||
if(indexOffset < 0)
|
||||
{
|
||||
indexOffset += getChannelCount();
|
||||
}
|
||||
|
||||
if(indexBoundaryPolicy == IndexBoundaryPolicy.ADJUST_POSITIVE &&
|
||||
isOverlapFrequency(frequency, indexOffset, indexOffset + 1))
|
||||
{
|
||||
indexOffset = normalize(indexOffset + 1);
|
||||
}
|
||||
else if(indexBoundaryPolicy == IndexBoundaryPolicy.ADJUST_NEGATIVE &&
|
||||
isOverlapFrequency(frequency, indexOffset - 1, indexOffset))
|
||||
{
|
||||
indexOffset = normalize(indexOffset - 1);
|
||||
}
|
||||
|
||||
return indexOffset;
|
||||
}
|
||||
|
||||
/**
|
||||
* Indicates if the frequency is an overlapping frequency between channel index 1 and channel index 2 where the
|
||||
* maximum frequency of index 1 is the same as the minimum frequency of index 2 and both of these frequencies are
|
||||
* the same as the frequency argument.
|
||||
*
|
||||
* Note: both index 1 and index 2 will be normalized to the range of 0 <> N-1 prior to determining overlap
|
||||
*
|
||||
* @param frequency to test for overlap between index 1 and 2
|
||||
* @param index1 which is the lower channel index located next to index 2.
|
||||
* @param index2 which is the higher channel index located next to index 1.
|
||||
* @return true if frequency = index1 maximum frequency = index2 minimum frequency
|
||||
*/
|
||||
public boolean isOverlapFrequency(long frequency, int index1, int index2)
|
||||
{
|
||||
index1 = normalize(index1);
|
||||
index2 = normalize(index2);
|
||||
|
||||
//Check that these two indexes are side by side
|
||||
int delta = normalize(index2 - index1);
|
||||
|
||||
if(delta != 1)
|
||||
{
|
||||
return false;
|
||||
}
|
||||
|
||||
long index1MaximumFrequency = (long)getIndexMaximumFrequency(index1, IndexBoundaryPolicy.ADJUST_POSITIVE);
|
||||
long index2MinimumFrequency = (long)getIndexMinimumFrequency(index2, IndexBoundaryPolicy.ADJUST_NEGATIVE);
|
||||
|
||||
if(index1 == getWrapAroundIndex())
|
||||
{
|
||||
index1MaximumFrequency = (long)getIndexMaximumFrequency(index1, IndexBoundaryPolicy.ADJUST_NEGATIVE);
|
||||
}
|
||||
|
||||
if(index2 == getWrapAroundIndex())
|
||||
{
|
||||
index2MinimumFrequency = (long)getIndexMinimumFrequency(index2, IndexBoundaryPolicy.ADJUST_POSITIVE);
|
||||
}
|
||||
|
||||
return frequency == index1MaximumFrequency && frequency == index2MinimumFrequency;
|
||||
}
|
||||
|
||||
/**
|
||||
* Normalizes the index to the range of 0 <> N-1 channel indexes</>
|
||||
*/
|
||||
private int normalize(int index)
|
||||
{
|
||||
while(index < 0)
|
||||
{
|
||||
index += getChannelCount();
|
||||
}
|
||||
|
||||
while(index >= getChannelCount())
|
||||
{
|
||||
index -= getChannelCount();
|
||||
}
|
||||
|
||||
return index;
|
||||
}
|
||||
|
||||
/**
|
||||
* Calculates the center frequency for the channel identified by the index in the channel results array.
|
||||
*
|
||||
* @param index to determine the channel center frequency
|
||||
* @return center frequency for the indexed channel
|
||||
*/
|
||||
public double getIndexCenterFrequency(int index, IndexBoundaryPolicy indexBoundaryPolicy)
|
||||
{
|
||||
if(index < 0 || index >= getChannelCount())
|
||||
{
|
||||
throw new IllegalArgumentException("Illegal channel index");
|
||||
}
|
||||
|
||||
int wrapAroundIndex = getWrapAroundIndex();
|
||||
|
||||
if(index == wrapAroundIndex)
|
||||
{
|
||||
if(indexBoundaryPolicy == IndexBoundaryPolicy.ADJUST_POSITIVE)
|
||||
{
|
||||
return getCenterFrequency() + (index * getChannelBandwidth());
|
||||
}
|
||||
else
|
||||
{
|
||||
return getCenterFrequency() - (index * getChannelBandwidth());
|
||||
}
|
||||
}
|
||||
else if(index < wrapAroundIndex)
|
||||
{
|
||||
return getCenterFrequency() + (index * getChannelBandwidth());
|
||||
}
|
||||
else
|
||||
{
|
||||
return getCenterFrequency() - ((getChannelCount() - index) * getChannelBandwidth());
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Calculates the minimum frequency band edge for the channel identified by the index in the channel results
|
||||
* array.
|
||||
* @param index to calculate the minimum band edge frequency for the channel.
|
||||
* @param indexBoundaryPolicy for the wrap around index to indicate if this method should return the minimum
|
||||
* frequency of the upper spectrum half of the wrap around channel (ADJUST_POSITIVE) or the minimum frequency of the
|
||||
* lower spectrum half of the wrap around channel (ADJUST_NEGATIVE)
|
||||
* @return frequency of the minimum band edge
|
||||
*/
|
||||
public double getIndexMinimumFrequency(int index, IndexBoundaryPolicy indexBoundaryPolicy)
|
||||
{
|
||||
if(index < 0 || index >= getChannelCount())
|
||||
{
|
||||
throw new IllegalArgumentException("Can't determine minimum frequency - illegal channel index [" +
|
||||
index + "]");
|
||||
}
|
||||
|
||||
int wrapAroundIndex = getWrapAroundIndex();
|
||||
|
||||
if(index == wrapAroundIndex)
|
||||
{
|
||||
if(indexBoundaryPolicy == IndexBoundaryPolicy.ADJUST_POSITIVE)
|
||||
{
|
||||
return getCenterFrequency() + ((double)index * getChannelBandwidth()) - getHalfChannelBandwidth();
|
||||
}
|
||||
else
|
||||
{
|
||||
return getIndexCenterFrequency(index, indexBoundaryPolicy);
|
||||
}
|
||||
}
|
||||
else if(index <= wrapAroundIndex)
|
||||
{
|
||||
return getCenterFrequency() + ((double)index * getChannelBandwidth()) - getHalfChannelBandwidth();
|
||||
}
|
||||
else
|
||||
{
|
||||
return getCenterFrequency() - ((double)(getChannelCount() - index) * getChannelBandwidth()) -
|
||||
getHalfChannelBandwidth();
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Calculates the maximum frequency band edge for the channel identified by the index in the channel results
|
||||
* array.
|
||||
* @param index to calculate the maximum band edge frequency for the channel.
|
||||
* @param indexBoundaryPolicy for the wrap around index to indicate if this method should return the maximum
|
||||
* frequency of the upper spectrum half of the wrap around channel (ADJUST_POSITIVE) or the maximum frequency of the
|
||||
* lower spectrum half of the wrap around channel (ADJUST_NEGATIVE)
|
||||
* @return frequency of the maximum band edge
|
||||
*/
|
||||
public double getIndexMaximumFrequency(int index, IndexBoundaryPolicy indexBoundaryPolicy)
|
||||
{
|
||||
if(index < 0 || index >= getChannelCount())
|
||||
{
|
||||
throw new IllegalArgumentException("Can't determine maximum frequency - illegal channel index [" +
|
||||
index + "]");
|
||||
}
|
||||
|
||||
int wrapAroundIndex = getWrapAroundIndex();
|
||||
|
||||
if(index == wrapAroundIndex)
|
||||
{
|
||||
if(indexBoundaryPolicy == IndexBoundaryPolicy.ADJUST_POSITIVE)
|
||||
{
|
||||
return getIndexCenterFrequency(index, indexBoundaryPolicy);
|
||||
}
|
||||
else
|
||||
{
|
||||
return getCenterFrequency() - ((double)index * getChannelBandwidth() - getHalfChannelBandwidth());
|
||||
}
|
||||
}
|
||||
else if(index <= wrapAroundIndex)
|
||||
{
|
||||
return getCenterFrequency() + ((double)index * getChannelBandwidth()) + getHalfChannelBandwidth();
|
||||
}
|
||||
else
|
||||
{
|
||||
return getCenterFrequency() - ((double)(getChannelCount() - index) * getChannelBandwidth()) +
|
||||
getHalfChannelBandwidth();
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Calculates the center frequency for a channel that will be synthesized from the channel indexes.
|
||||
* @param indexes that are contiguous in the spectrum
|
||||
* @return center frequency in hertz
|
||||
*/
|
||||
public long getCenterFrequencyForIndexes(List<Integer> indexes)
|
||||
{
|
||||
if(indexes.isEmpty())
|
||||
{
|
||||
throw new IllegalArgumentException("Indexes cannot be empty");
|
||||
}
|
||||
|
||||
int middleIndex = indexes.get(indexes.size() / 2);
|
||||
|
||||
if(indexes.size() % 2 == 0)
|
||||
{
|
||||
//Even number of indexes
|
||||
return (long)getIndexMinimumFrequency(middleIndex, IndexBoundaryPolicy.ADJUST_POSITIVE);
|
||||
}
|
||||
else
|
||||
{
|
||||
//Odd number of indexes
|
||||
return (long)getIndexCenterFrequency(middleIndex, 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 ....");
|
||||
|
||||
int channelBandwidth = 12500;
|
||||
int sampleRate = 100000; //Airspy
|
||||
int channelCount = 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");
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,344 @@
|
||||
/*******************************************************************************
|
||||
* *********************************************************************************************************************
|
||||
* sdr-trunk
|
||||
* Copyright (C) 2014-2017 Dennis Sheirer
|
||||
*
|
||||
* This program is free software: you can redistribute it and/or modify it under the terms of the GNU General Public
|
||||
* License as published by the Free Software Foundation, either version 3 of the License, or (at your option) any
|
||||
* later version.
|
||||
*
|
||||
* This program is distributed in the hope that it will be useful, but WITHOUT ANY WARRANTY; without even the implied
|
||||
* warranty of MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU General Public License for more details.
|
||||
*
|
||||
* You should have received a copy of the GNU General Public License along with this program.
|
||||
* If not, see <http://www.gnu.org/licenses/>
|
||||
* *********************************************************************************************************************
|
||||
******************************************************************************/
|
||||
package io.github.dsheirer.dsp.filter.channelizer;
|
||||
|
||||
import io.github.dsheirer.sample.Listener;
|
||||
import io.github.dsheirer.sample.SampleType;
|
||||
import io.github.dsheirer.sample.complex.ComplexBuffer;
|
||||
import io.github.dsheirer.settings.SettingsManager;
|
||||
import io.github.dsheirer.source.ISourceEventProcessor;
|
||||
import io.github.dsheirer.source.SourceEvent;
|
||||
import io.github.dsheirer.source.SourceException;
|
||||
import io.github.dsheirer.source.tuner.channel.TunerChannel;
|
||||
import io.github.dsheirer.source.tuner.channel.TunerChannelSource;
|
||||
import io.github.dsheirer.source.tuner.configuration.TunerConfigurationModel;
|
||||
import io.github.dsheirer.source.tuner.test.TestTuner;
|
||||
import io.github.dsheirer.spectrum.DFTProcessor;
|
||||
import io.github.dsheirer.spectrum.DFTSize;
|
||||
import io.github.dsheirer.spectrum.SpectrumPanel;
|
||||
import io.github.dsheirer.spectrum.converter.ComplexDecibelConverter;
|
||||
import net.miginfocom.swing.MigLayout;
|
||||
import org.slf4j.Logger;
|
||||
import org.slf4j.LoggerFactory;
|
||||
|
||||
import javax.swing.JFrame;
|
||||
import javax.swing.JLabel;
|
||||
import javax.swing.JPanel;
|
||||
import javax.swing.JSpinner;
|
||||
import javax.swing.SpinnerNumberModel;
|
||||
import javax.swing.event.ChangeEvent;
|
||||
import javax.swing.event.ChangeListener;
|
||||
import java.awt.Dimension;
|
||||
import java.awt.EventQueue;
|
||||
|
||||
public class ChannelizerViewer extends JFrame
|
||||
{
|
||||
private final static Logger mLog = LoggerFactory.getLogger(ChannelizerViewer.class);
|
||||
|
||||
private static final int CHANNEL_BANDWIDTH = 12500;
|
||||
private static final int CHANNEL_FFT_FRAME_RATE = 20;
|
||||
|
||||
private SettingsManager mSettingsManager = new SettingsManager(new TunerConfigurationModel());
|
||||
private JPanel mPrimaryPanel;
|
||||
private JPanel mControlPanel;
|
||||
private PrimarySpectrumPanel mPrimarySpectrumPanel;
|
||||
private ChannelArrayPanel mChannelPanel;
|
||||
private int mChannelCount;
|
||||
private int mChannelsPerRow;
|
||||
private long mToneFrequency;
|
||||
private DFTSize mMainPanelDFTSize = DFTSize.FFT32768;
|
||||
private DFTSize mChannelPanelDFTSize = DFTSize.FFT01024;
|
||||
private TestTuner mTestTuner;
|
||||
|
||||
/**
|
||||
* GUI Test utility for researching polyphase channelizers.
|
||||
*
|
||||
* @param channelsPerRow
|
||||
*/
|
||||
public ChannelizerViewer(int channelsPerRow)
|
||||
{
|
||||
mTestTuner = new TestTuner();
|
||||
|
||||
mChannelCount = (int)(mTestTuner.getTunerController().getSampleRate() / CHANNEL_BANDWIDTH);
|
||||
mChannelsPerRow = channelsPerRow;
|
||||
|
||||
mToneFrequency = mTestTuner.getTunerController().getFrequency() + 19200;
|
||||
|
||||
mTestTuner.getTunerController().setToneFrequency(mToneFrequency);
|
||||
|
||||
init();
|
||||
}
|
||||
|
||||
private void init()
|
||||
{
|
||||
setTitle("TunerChannelizer Viewer");
|
||||
setSize(1000, 800);
|
||||
setDefaultCloseOperation(JFrame.EXIT_ON_CLOSE);
|
||||
setLayout(new MigLayout("insets 0 0 0 0", "[grow,fill]", "[grow,fill]"));
|
||||
setLocationRelativeTo(null);
|
||||
add(getPrimaryPanel());
|
||||
}
|
||||
|
||||
private JPanel getPrimaryPanel()
|
||||
{
|
||||
if(mPrimaryPanel == null)
|
||||
{
|
||||
mPrimaryPanel = new JPanel();
|
||||
mPrimaryPanel.setLayout(new MigLayout("insets 0 0 0 0", "[grow,fill]", "[][][grow,fill]"));
|
||||
mPrimaryPanel.add(getSpectrumPanel(), "wrap");
|
||||
mPrimaryPanel.add(getControlPanel(), "wrap");
|
||||
mPrimaryPanel.add(getChannelArrayPanel());
|
||||
}
|
||||
|
||||
return mPrimaryPanel;
|
||||
}
|
||||
|
||||
private PrimarySpectrumPanel getSpectrumPanel()
|
||||
{
|
||||
if(mPrimarySpectrumPanel == null)
|
||||
{
|
||||
mPrimarySpectrumPanel = new PrimarySpectrumPanel(mSettingsManager,
|
||||
mTestTuner.getTunerController().getSampleRate());
|
||||
mPrimarySpectrumPanel.setPreferredSize(new Dimension(1000, 200));
|
||||
mPrimarySpectrumPanel.setDFTSize(mMainPanelDFTSize);
|
||||
mTestTuner.getSourceManager().addComplexBufferListener(mPrimarySpectrumPanel);
|
||||
}
|
||||
|
||||
return mPrimarySpectrumPanel;
|
||||
}
|
||||
|
||||
private JPanel getControlPanel()
|
||||
{
|
||||
if(mControlPanel == null)
|
||||
{
|
||||
mControlPanel = new JPanel();
|
||||
mControlPanel.setLayout(new MigLayout("insets 0 0 0 0", "", ""));
|
||||
|
||||
mControlPanel.add(new JLabel("Tone:"), "align left");
|
||||
int maximumFrequencyOffset = (int)(mTestTuner.getTunerController().getSampleRate() / 2);
|
||||
long minimumFrequency = mTestTuner.getTunerController().getFrequency() - maximumFrequencyOffset;
|
||||
long maximumFrequency = mTestTuner.getTunerController().getFrequency() + maximumFrequencyOffset;
|
||||
|
||||
SpinnerNumberModel model = new SpinnerNumberModel(mToneFrequency, minimumFrequency, maximumFrequency,
|
||||
100 );
|
||||
|
||||
model.addChangeListener(new ChangeListener()
|
||||
{
|
||||
@Override
|
||||
public void stateChanged(ChangeEvent e)
|
||||
{
|
||||
int toneFrequency = model.getNumber().intValue();
|
||||
mTestTuner.getTunerController().setToneFrequency(toneFrequency);
|
||||
}
|
||||
});
|
||||
|
||||
JSpinner spinner = new JSpinner(model);
|
||||
|
||||
mControlPanel.add(spinner);
|
||||
mControlPanel.add(new JLabel("Hz"));
|
||||
|
||||
mControlPanel.add(new JLabel("Sample Rate: " + mTestTuner.getTunerController().getSampleRate() + " Hz"), "push,align right");
|
||||
|
||||
mControlPanel.add(new JLabel("Max: " + (mTestTuner.getTunerController().getSampleRate() / 2) + " Hz"), "push,align right");
|
||||
mControlPanel.add(new JLabel("Channels: " + mChannelCount), "push,align right");
|
||||
}
|
||||
|
||||
return mControlPanel;
|
||||
}
|
||||
|
||||
private ChannelArrayPanel getChannelArrayPanel()
|
||||
{
|
||||
if(mChannelPanel == null)
|
||||
{
|
||||
mChannelPanel = new ChannelArrayPanel();
|
||||
}
|
||||
|
||||
return mChannelPanel;
|
||||
}
|
||||
|
||||
public class ChannelArrayPanel extends JPanel
|
||||
{
|
||||
public ChannelArrayPanel()
|
||||
{
|
||||
int bufferSize = CHANNEL_BANDWIDTH / CHANNEL_FFT_FRAME_RATE;
|
||||
if(bufferSize % 2 == 1)
|
||||
{
|
||||
bufferSize++;
|
||||
}
|
||||
|
||||
init();
|
||||
}
|
||||
|
||||
private void init()
|
||||
{
|
||||
setLayout(new MigLayout("insets 0 0 0 0", "fill", "fill"));
|
||||
|
||||
double spectralBandwidth = mTestTuner.getTunerController().getSampleRate();
|
||||
double halfSpectralBandwidth = spectralBandwidth / 2.0;
|
||||
|
||||
int channelToLog = -1;
|
||||
|
||||
for(int x = 0; x < mChannelCount; x++)
|
||||
{
|
||||
//place the channels left and right of 10.0 MHz
|
||||
long frequency = (long)(mTestTuner.getTunerController().getFrequency() +
|
||||
((x * CHANNEL_BANDWIDTH) - halfSpectralBandwidth) + (CHANNEL_BANDWIDTH / 2));
|
||||
|
||||
mLog.debug("Channel " + x + " Frequency: " + frequency);
|
||||
|
||||
ChannelPanel channelPanel = new ChannelPanel(mSettingsManager, CHANNEL_BANDWIDTH * 2, frequency, CHANNEL_BANDWIDTH, (x == channelToLog));
|
||||
channelPanel.setDFTSize(mChannelPanelDFTSize);
|
||||
|
||||
if(x % mChannelsPerRow == mChannelsPerRow - 1)
|
||||
{
|
||||
add(channelPanel, "grow,push,wrap 2px");
|
||||
}
|
||||
else
|
||||
{
|
||||
add(channelPanel, "grow,push");
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
public class PrimarySpectrumPanel extends JPanel implements Listener<ComplexBuffer>, ISourceEventProcessor
|
||||
{
|
||||
private DFTProcessor mDFTProcessor = new DFTProcessor(SampleType.COMPLEX);
|
||||
private ComplexDecibelConverter mComplexDecibelConverter = new ComplexDecibelConverter();
|
||||
private SpectrumPanel mSpectrumPanel;
|
||||
|
||||
public PrimarySpectrumPanel(SettingsManager settingsManager, double sampleRate)
|
||||
{
|
||||
setLayout(new MigLayout("insets 0 0 0 0", "[grow,fill]", "[grow,fill]"));
|
||||
mSpectrumPanel = new SpectrumPanel(settingsManager);
|
||||
add(mSpectrumPanel);
|
||||
|
||||
mDFTProcessor.addConverter(mComplexDecibelConverter);
|
||||
mDFTProcessor.process(SourceEvent.sampleRateChange(sampleRate));
|
||||
mComplexDecibelConverter.addListener(mSpectrumPanel);
|
||||
}
|
||||
|
||||
public void setDFTSize(DFTSize dftSize)
|
||||
{
|
||||
mDFTProcessor.setDFTSize(dftSize);
|
||||
}
|
||||
|
||||
@Override
|
||||
public void receive(ComplexBuffer complexBuffer)
|
||||
{
|
||||
mDFTProcessor.receive(complexBuffer);
|
||||
}
|
||||
|
||||
@Override
|
||||
public void process(SourceEvent event) throws SourceException
|
||||
{
|
||||
mLog.debug("Source Event! Add handler support for this to channelizer viewer");
|
||||
}
|
||||
}
|
||||
|
||||
public class ChannelPanel extends JPanel implements Listener<ComplexBuffer>, ISourceEventProcessor
|
||||
{
|
||||
private TunerChannelSource mSource;
|
||||
private DFTProcessor mDFTProcessor = new DFTProcessor(SampleType.COMPLEX);
|
||||
private ComplexDecibelConverter mComplexDecibelConverter = new ComplexDecibelConverter();
|
||||
private SpectrumPanel mSpectrumPanel;
|
||||
private long mFrequency;
|
||||
|
||||
public ChannelPanel(SettingsManager settingsManager, double sampleRate, long frequency, int bandwidth, boolean enableLogging)
|
||||
{
|
||||
setLayout(new MigLayout("insets 0 0 0 0", "[grow,fill]", "[grow,fill]"));
|
||||
mSpectrumPanel = new SpectrumPanel(settingsManager);
|
||||
add(mSpectrumPanel);
|
||||
|
||||
mDFTProcessor.addConverter(mComplexDecibelConverter);
|
||||
mDFTProcessor.process(SourceEvent.sampleRateChange(sampleRate));
|
||||
|
||||
mComplexDecibelConverter.addListener(mSpectrumPanel);
|
||||
|
||||
mFrequency = frequency;
|
||||
|
||||
TunerChannel tunerChannel = new TunerChannel(frequency, bandwidth);
|
||||
mSource = mTestTuner.getSourceManager().getSource(tunerChannel);
|
||||
|
||||
if(mSource != null)
|
||||
{
|
||||
if(enableLogging)
|
||||
{
|
||||
mSource.setListener(new Listener<ComplexBuffer>()
|
||||
{
|
||||
@Override
|
||||
public void receive(ComplexBuffer complexBuffer)
|
||||
{
|
||||
mLog.debug("Sending samples to DFT processor: " + complexBuffer.getSamples().length);
|
||||
mDFTProcessor.receive(complexBuffer);
|
||||
}
|
||||
});
|
||||
}
|
||||
else
|
||||
{
|
||||
mSource.setListener(mDFTProcessor);
|
||||
}
|
||||
|
||||
mSource.start();
|
||||
}
|
||||
else
|
||||
{
|
||||
mLog.error("Couldn't get a source from the tuner for frequency: " + frequency);
|
||||
}
|
||||
}
|
||||
|
||||
public TunerChannelSource getSource()
|
||||
{
|
||||
return mSource;
|
||||
}
|
||||
|
||||
public void setDFTSize(DFTSize dftSize)
|
||||
{
|
||||
mDFTProcessor.setDFTSize(dftSize);
|
||||
}
|
||||
|
||||
@Override
|
||||
public void receive(ComplexBuffer complexBuffer)
|
||||
{
|
||||
mLog.debug("Got a buffer for the channel dft processor");
|
||||
mDFTProcessor.receive(complexBuffer);
|
||||
}
|
||||
|
||||
@Override
|
||||
public void process(SourceEvent event) throws SourceException
|
||||
{
|
||||
mLog.debug("Source Event! Add handler support for this to channelizer viewer");
|
||||
}
|
||||
}
|
||||
|
||||
public static void main(String[] args)
|
||||
{
|
||||
int channelsPerRow = 4;
|
||||
|
||||
final ChannelizerViewer frame = new ChannelizerViewer(channelsPerRow);
|
||||
|
||||
EventQueue.invokeLater(new Runnable()
|
||||
{
|
||||
@Override
|
||||
public void run()
|
||||
{
|
||||
frame.setVisible(true);
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
}
|
||||
@@ -0,0 +1,171 @@
|
||||
/*******************************************************************************
|
||||
* sdrtrunk
|
||||
* Copyright (C) 2014-2017 Dennis Sheirer
|
||||
*
|
||||
* This program is free software: you can redistribute it and/or modify
|
||||
* it under the terms of the GNU General Public License as published by
|
||||
* the Free Software Foundation, either version 3 of the License, or
|
||||
* (at your option) any later version.
|
||||
*
|
||||
* This program is distributed in the hope that it will be useful,
|
||||
* but WITHOUT ANY WARRANTY; without even the implied warranty of
|
||||
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
|
||||
* GNU General Public License for more details.
|
||||
*
|
||||
* You should have received a copy of the GNU General Public License
|
||||
* along with this program. If not, see <http://www.gnu.org/licenses/>
|
||||
*
|
||||
******************************************************************************/
|
||||
package io.github.dsheirer.dsp.filter.channelizer;
|
||||
|
||||
import io.github.dsheirer.dsp.filter.FilterFactory;
|
||||
import io.github.dsheirer.dsp.filter.Window;
|
||||
import io.github.dsheirer.dsp.filter.fir.real.RealFIRFilter;
|
||||
import io.github.dsheirer.dsp.mixer.Oscillator;
|
||||
import io.github.dsheirer.sample.complex.ComplexBuffer;
|
||||
import org.jtransforms.fft.FloatFFT_1D;
|
||||
import org.slf4j.Logger;
|
||||
import org.slf4j.LoggerFactory;
|
||||
|
||||
public class ComplexPolyphaseChannelizer extends AbstractComplexPolyphaseChannelizer
|
||||
{
|
||||
private final static Logger mLog = LoggerFactory.getLogger(ComplexPolyphaseChannelizer.class);
|
||||
|
||||
private RealFIRFilter[] mInphaseFilters;
|
||||
private RealFIRFilter[] mQuadratureFilters;
|
||||
private int mFilterPointer = 0;
|
||||
private float[] mFilteredSamples;
|
||||
private FloatFFT_1D mFFT;
|
||||
|
||||
/**
|
||||
* Maximally Decimated Polyphase Filter Bank (MDPFB) channelizer that divides the input frequency band into
|
||||
* equal bandwidth channels.
|
||||
*
|
||||
* @param coefficients of a prototype 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
|
||||
* filter 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 channelCount - number of channels to output.
|
||||
* @param channelSampleRate - channel sample rate (after decimation)
|
||||
*/
|
||||
public ComplexPolyphaseChannelizer(float[] coefficients, int channelCount, int channelSampleRate)
|
||||
{
|
||||
super(channelCount, channelSampleRate);
|
||||
|
||||
initFilters(coefficients);
|
||||
}
|
||||
|
||||
@Override
|
||||
public void receive(ComplexBuffer complexBuffer)
|
||||
{
|
||||
float[] samples = complexBuffer.getSamples();
|
||||
|
||||
for(int x = 0; x < samples.length; x += 2)
|
||||
{
|
||||
filter(samples[x], samples[x + 1]);
|
||||
}
|
||||
}
|
||||
|
||||
protected void filter(float inphase, float quadrature)
|
||||
{
|
||||
int index = (getChannelCount() - mFilterPointer - 1) * 2;
|
||||
|
||||
mFilteredSamples[index] = mInphaseFilters[mFilterPointer].filter(inphase);
|
||||
mFilteredSamples[index + 1] = mQuadratureFilters[mFilterPointer].filter(quadrature);
|
||||
|
||||
if(mFilterPointer == 0)
|
||||
{
|
||||
calculate();
|
||||
mFilterPointer = getChannelCount() - 1;
|
||||
}
|
||||
else
|
||||
{
|
||||
mFilterPointer--;
|
||||
}
|
||||
}
|
||||
|
||||
private void calculate()
|
||||
{
|
||||
float[] samples = new float[getChannelCount() * 2];
|
||||
System.arraycopy(mFilteredSamples, 0, samples, 0, mFilteredSamples.length);
|
||||
|
||||
//FFT is executed in-place where the output overwrites the input
|
||||
mFFT.complexForward(samples);
|
||||
|
||||
dispatch(samples);
|
||||
}
|
||||
|
||||
/**
|
||||
* Distributes the filter taps to each of the polyphase filters
|
||||
*
|
||||
* @param taps
|
||||
*/
|
||||
private void initFilters(float[] taps)
|
||||
{
|
||||
int tapCount = (int)Math.ceil((double)taps.length / (double)getChannelCount());
|
||||
|
||||
float[][] coefficients = new float[getChannelCount()][tapCount];
|
||||
|
||||
for(int tap = 0; tap < tapCount; tap++)
|
||||
{
|
||||
for(int filter = 0; filter < getChannelCount(); filter++)
|
||||
{
|
||||
int index = tap * getChannelCount() + filter;
|
||||
|
||||
if(index < taps.length)
|
||||
{
|
||||
coefficients[filter][tap] = taps[index];
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
mInphaseFilters = new RealFIRFilter[getChannelCount()];
|
||||
mQuadratureFilters = new RealFIRFilter[getChannelCount()];
|
||||
|
||||
for(int filter = 0; filter < getChannelCount(); filter++)
|
||||
{
|
||||
mInphaseFilters[filter] = new RealFIRFilter(coefficients[filter], 1.0f);
|
||||
mQuadratureFilters[filter] = new RealFIRFilter(coefficients[filter], 1.0f);
|
||||
}
|
||||
|
||||
mFilteredSamples = new float[getChannelCount() * 2];
|
||||
mFFT = new FloatFFT_1D(getChannelCount());
|
||||
mFilterPointer = getChannelCount() -1;
|
||||
}
|
||||
|
||||
public static void main(String[] args)
|
||||
{
|
||||
mLog.debug("Starting ...");
|
||||
|
||||
int symbolRate = 4800;
|
||||
int samplesPerSymbol = 2;
|
||||
int channels = 800;
|
||||
int channelBandwidth = 12500;
|
||||
int sampleRate = channels * channelBandwidth;
|
||||
|
||||
float[] taps = FilterFactory.getLowPass( 10000000, 12500, 5600, Window.WindowType.BLACKMAN_HARRIS_7 );
|
||||
|
||||
int channelCenter = (int)(12500 * 1.5);
|
||||
|
||||
StringBuilder sb = new StringBuilder();
|
||||
sb.append("\nPolyphase TunerChannelizer\n");
|
||||
sb.append("Sample Rate:" + sampleRate + " Channels:" + channels + " Channel Rate:" + channelBandwidth + "\n");
|
||||
sb.append("Tap Count:" + taps.length + "\n");
|
||||
sb.append("Channel:" + channelCenter);
|
||||
|
||||
mLog.debug(sb.toString());
|
||||
|
||||
ComplexPolyphaseChannelizer channelizer = new ComplexPolyphaseChannelizer(taps, 8, channelBandwidth);
|
||||
|
||||
Oscillator oscillator = new Oscillator(channelCenter, 2 * sampleRate);
|
||||
|
||||
for(int x = 0; x < 2000; x++)
|
||||
{
|
||||
channelizer.filter(oscillator.getComplex().inphase(), oscillator.getComplex().quadrature());
|
||||
oscillator.rotate();
|
||||
}
|
||||
|
||||
mLog.debug("Finished!");
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,349 @@
|
||||
/*******************************************************************************
|
||||
* sdrtrunk
|
||||
* Copyright (C) 2014-2017 Dennis Sheirer
|
||||
*
|
||||
* This program is free software: you can redistribute it and/or modify
|
||||
* it under the terms of the GNU General Public License as published by
|
||||
* the Free Software Foundation, either version 3 of the License, or
|
||||
* (at your option) any later version.
|
||||
*
|
||||
* This program is distributed in the hope that it will be useful,
|
||||
* but WITHOUT ANY WARRANTY; without even the implied warranty of
|
||||
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
|
||||
* GNU General Public License for more details.
|
||||
*
|
||||
* You should have received a copy of the GNU General Public License
|
||||
* along with this program. If not, see <http://www.gnu.org/licenses/>
|
||||
*
|
||||
******************************************************************************/
|
||||
package io.github.dsheirer.dsp.filter.channelizer;
|
||||
|
||||
import io.github.dsheirer.dsp.filter.FilterFactory;
|
||||
import io.github.dsheirer.dsp.filter.Window.WindowType;
|
||||
import io.github.dsheirer.dsp.filter.design.FilterDesignException;
|
||||
import io.github.dsheirer.sample.complex.ComplexBuffer;
|
||||
import org.jtransforms.fft.FloatFFT_1D;
|
||||
import org.slf4j.Logger;
|
||||
import org.slf4j.LoggerFactory;
|
||||
|
||||
public class ComplexPolyphaseChannelizerM2 extends AbstractComplexPolyphaseChannelizer
|
||||
{
|
||||
private final static Logger mLog = LoggerFactory.getLogger(ComplexPolyphaseChannelizerM2.class);
|
||||
|
||||
private static final int DEFAULT_MINIMUM_CHANNEL_BANDWIDTH = 12500;
|
||||
|
||||
private float[][] mSamples;
|
||||
private float[][] mCoefficients;
|
||||
private int[][] mSampleIndexMap;
|
||||
private int mColumnPointer;
|
||||
private int mRowPointer;
|
||||
private int mBlockSize;
|
||||
private FloatFFT_1D mFFT;
|
||||
|
||||
/**
|
||||
* Non-Maximally Decimated Polyphase Filter Bank (NMDPFB) channelizer that divides the input frequency band into
|
||||
* equal bandwidth channels that are each oversampled by 2x for output.
|
||||
*
|
||||
* @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
|
||||
* filter 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 oversamples each filter
|
||||
* output, this number must be even (divisible by the 2x oversample rate).
|
||||
*/
|
||||
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 oversample rate (2)");
|
||||
}
|
||||
|
||||
init(taps);
|
||||
}
|
||||
|
||||
/**
|
||||
* Non-Maximally Decimated Polyphase Filter Bank (NMDPFB) channelizer that divides the input frequency band into
|
||||
* equal bandwidth channels that are each oversampled by 2x for output.
|
||||
*
|
||||
* Note: this constructor uses a default minimum channel bandwidth of 12.5 kHz. The number of channels is calculate
|
||||
* from the sample rate and this default minimum channel bandwidth. The resulting per-channel bandwidth will be
|
||||
* between 12.5 and 25.0 kHz with the output per-channel sample rate of 2x the bandwidth.
|
||||
*
|
||||
* @param sampleRate to be channelized.
|
||||
* @param tapsPerFilter to use when designing the filter
|
||||
*/
|
||||
public ComplexPolyphaseChannelizerM2(double sampleRate, int tapsPerFilter) throws FilterDesignException
|
||||
{
|
||||
super(sampleRate, (int)(sampleRate / DEFAULT_MINIMUM_CHANNEL_BANDWIDTH));
|
||||
|
||||
float[] filterTaps = FilterFactory.getSincChannelizer(getChannelSampleRate(), getChannelCount(),
|
||||
tapsPerFilter, WindowType.BLACKMAN_HARRIS_7, true);
|
||||
|
||||
init(filterTaps);
|
||||
}
|
||||
|
||||
/**
|
||||
* 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.getSincChannelizer(getChannelSampleRate(), getChannelCount(),
|
||||
15, WindowType.BLACKMAN_HARRIS_7, true);
|
||||
|
||||
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(ComplexBuffer complexBuffer)
|
||||
{
|
||||
float[] samples = complexBuffer.getSamples();
|
||||
|
||||
int samplesPointer = 0;
|
||||
|
||||
while(samplesPointer < samples.length)
|
||||
{
|
||||
int lengthToCopy = mBlockSize - (mRowPointer % mBlockSize);
|
||||
|
||||
if((samplesPointer + lengthToCopy) > samples.length)
|
||||
{
|
||||
lengthToCopy = samples.length - samplesPointer;
|
||||
}
|
||||
|
||||
System.arraycopy(samples, samplesPointer, mSamples[mColumnPointer], mRowPointer, lengthToCopy);
|
||||
samplesPointer += lengthToCopy;
|
||||
mRowPointer += lengthToCopy;
|
||||
|
||||
if(mRowPointer % mBlockSize == 0)
|
||||
{
|
||||
process();
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* 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()
|
||||
{
|
||||
int sampleRowCount = getChannelCount() * 2;
|
||||
|
||||
float[] processed = new float[sampleRowCount];
|
||||
|
||||
|
||||
//Top of block processing
|
||||
if(mRowPointer >= sampleRowCount)
|
||||
{
|
||||
int[] sampleIndexMap = mSampleIndexMap[mColumnPointer];
|
||||
|
||||
for(int channel = 0; channel < getChannelCount(); channel++)
|
||||
{
|
||||
int outputIndex = 2 * (getChannelCount() - channel - 1);
|
||||
int sampleIndex = 2 * channel;
|
||||
|
||||
float[] filter = mCoefficients[channel];
|
||||
|
||||
processed[outputIndex] = filter((sampleIndex), mCoefficients[channel], sampleIndexMap);
|
||||
processed[outputIndex + 1] = filter((sampleIndex + 1), mCoefficients[channel], sampleIndexMap);
|
||||
}
|
||||
|
||||
mRowPointer = 0;
|
||||
mColumnPointer--;
|
||||
|
||||
if(mColumnPointer < 0)
|
||||
{
|
||||
mColumnPointer = mSamples.length - 1;
|
||||
}
|
||||
}
|
||||
//Mid-point processing
|
||||
else
|
||||
{
|
||||
int[] sampleIndexMap = mSampleIndexMap[mColumnPointer];
|
||||
|
||||
int half = getChannelCount() / 2;
|
||||
|
||||
for(int channel = 0; channel < half; channel++)
|
||||
{
|
||||
int outputIndex = 2 * (getChannelCount() - channel - 1);
|
||||
int sampleIndex = 2 * channel;
|
||||
int filterIndex = channel + half;
|
||||
|
||||
processed[outputIndex] = filter(sampleIndex, mCoefficients[filterIndex], sampleIndexMap);
|
||||
processed[outputIndex + 1] = filter(sampleIndex + 1, mCoefficients[filterIndex], sampleIndexMap);
|
||||
}
|
||||
|
||||
//The second half of the sample buffer was loaded using the previous column pointer - adjust for that now
|
||||
sampleIndexMap = mSampleIndexMap[(mColumnPointer == (mSamples.length - 1) ? 0 : mColumnPointer + 1)];
|
||||
|
||||
for(int channel = half; channel < getChannelCount(); channel++)
|
||||
{
|
||||
int outputIndex = 2 * (getChannelCount() - channel - 1);
|
||||
int sampleIndex = 2 * channel;
|
||||
int filterIndex = channel - half;
|
||||
|
||||
processed[outputIndex] = filter(sampleIndex, mCoefficients[filterIndex], sampleIndexMap);
|
||||
processed[outputIndex + 1] = filter(sampleIndex + 1, mCoefficients[filterIndex], sampleIndexMap);
|
||||
}
|
||||
}
|
||||
|
||||
//Rotate each of the channels to the correct phase using the IFFT
|
||||
mFFT.complexInverse(processed, true);
|
||||
|
||||
dispatch(processed);
|
||||
}
|
||||
|
||||
/**
|
||||
* Calculates the filtered output for polyphase filter arm.
|
||||
*
|
||||
* @param sampleRow index pointing to a row in the mSamples array to filter
|
||||
* @param coefficients to use in filtering the samples
|
||||
* @param sampleIndexMap to translate the column index offset based on the current sample column pointer
|
||||
* @return filtered sample for the row
|
||||
*/
|
||||
private float filter(int sampleRow, float[] coefficients, int[] sampleIndexMap)
|
||||
{
|
||||
float accumulator = 0.0f;
|
||||
|
||||
for(int column = 0; column < mSamples.length; column++)
|
||||
{
|
||||
accumulator += mSamples[sampleIndexMap[column]][sampleRow] * coefficients[column];
|
||||
}
|
||||
|
||||
return accumulator;
|
||||
}
|
||||
|
||||
|
||||
/**
|
||||
* Initializes the channelizer structures. Distributes the prototype filter coefficients to each of the polyphase
|
||||
* filters, sets up the sample buffer array and pointers and initializes the FFT.
|
||||
*
|
||||
* These structures resemble those described by Fred Harris in Multirate Signal Processing for Communications
|
||||
* Systems, p230-233. However, instead of swapping the input sample buffers and swapping the output sample array
|
||||
* on every other processing block, we simply swap the filter coefficient banks and leave the input sample buffer
|
||||
* and output array indexes aligned. However, the entire structure (sample buffer, coefficients and output array)
|
||||
* is inverted (row zero index is M-1 and M-1 is zero index) to facilitate easy loading of data into the input
|
||||
* sample buffer array using Java's efficient System.arrayCopy() utility.
|
||||
*
|
||||
* Note: the filter coefficients array is organized (row,column) for ease of accessing each filter arm row during
|
||||
* convolution. The samples array is organized (column,row) for ease/efficiency in loading each row of data. Also,
|
||||
* the input sample array implements a circular buffer where the mCoefficientsMap provides a lookup to each sample
|
||||
* index based on the current value of the mColumnPointer.
|
||||
*
|
||||
* @param coefficients of the prototype filter for this channelizer
|
||||
*/
|
||||
private void init(float[] coefficients)
|
||||
{
|
||||
int columnCount = (int)Math.ceil((double)coefficients.length / (double)getChannelCount());
|
||||
int filterRowCount = getChannelCount();
|
||||
int sampleRowCount = getChannelCount() * 2;
|
||||
|
||||
//Sample and filter coefficient arrays are specifically setup as column/row and row/column
|
||||
mSamples = new float[columnCount][sampleRowCount];
|
||||
mCoefficients = new float[filterRowCount][columnCount];
|
||||
|
||||
mColumnPointer = columnCount - 1;
|
||||
mBlockSize = getChannelCount();
|
||||
mRowPointer = mBlockSize;
|
||||
|
||||
//Setup the FFT
|
||||
mFFT = new FloatFFT_1D(getChannelCount());
|
||||
|
||||
//Setup the polyphase filters
|
||||
|
||||
//Setup the index map for the sample array circular buffer
|
||||
initIndexMap(columnCount);
|
||||
|
||||
int coefficientIndex = 0;
|
||||
|
||||
//Load filter columns left-2-right (samples are loaded in reverse column order, right-2-left, for easy convolution)
|
||||
for(int columnIndex = 0; columnIndex < columnCount; columnIndex++)
|
||||
{
|
||||
//Load filter rows bottom to top, opposite of sample row loading and sample output ordering
|
||||
for(int rowIndex = filterRowCount - 1; rowIndex >= 0; rowIndex--)
|
||||
{
|
||||
mCoefficients[rowIndex][columnIndex] = coefficients[coefficientIndex++];
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Generates a circular buffer index map to support lookup of the translated index based on the current column
|
||||
* pointer and the desired sample index.
|
||||
*/
|
||||
private void initIndexMap(int size)
|
||||
{
|
||||
mSampleIndexMap = new int[size][size];
|
||||
|
||||
for(int row = 0; row < size; row++)
|
||||
{
|
||||
for(int column = 0;column < size;column++)
|
||||
{
|
||||
int offset = row + column;
|
||||
|
||||
mSampleIndexMap[row][column] = ((offset < size) ? offset : offset - size);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
public static void main(String[] args)
|
||||
{
|
||||
mLog.debug("Starting ...");
|
||||
|
||||
int channelCount = 4;
|
||||
int channelBandwidth = 12500;
|
||||
int sampleRate = channelCount * channelBandwidth;
|
||||
int tapsPerFilter = 14;
|
||||
|
||||
int channelCenter = (int)(12500 * 1.5);
|
||||
|
||||
float[] taps = new float[16];
|
||||
for(int x = 0; x < taps.length; x++)
|
||||
{
|
||||
taps[x] = x;
|
||||
}
|
||||
float[] samples = new float[32];
|
||||
for(int x = 0; x < samples.length; x++)
|
||||
{
|
||||
samples[x] = x;
|
||||
}
|
||||
|
||||
try
|
||||
{
|
||||
// ComplexPolyphaseChannelizerM2 channelizer = new ComplexPolyphaseChannelizerM2(sampleRate, tapsPerFilter);
|
||||
ComplexPolyphaseChannelizerM2 channelizer = new ComplexPolyphaseChannelizerM2(taps,sampleRate, channelCount);
|
||||
|
||||
// Oscillator oscillator = new Oscillator(channelCenter, 2 * sampleRate);
|
||||
//
|
||||
// ComplexBuffer complexBuffer = oscillator.generateComplexBuffer(1000);
|
||||
ComplexBuffer complexBuffer = new ComplexBuffer(samples);
|
||||
|
||||
channelizer.receive(complexBuffer);
|
||||
}
|
||||
catch(Exception e)
|
||||
{
|
||||
mLog.error("Error", e);
|
||||
}
|
||||
|
||||
mLog.debug("Finished!");
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,363 @@
|
||||
/*******************************************************************************
|
||||
* sdr-trunk
|
||||
* Copyright (C) 2014-2017 Dennis Sheirer
|
||||
*
|
||||
* This program is free software: you can redistribute it and/or modify it under the terms of the GNU General Public
|
||||
* License as published by the Free Software Foundation, either version 3 of the License, or (at your option) any
|
||||
* later version.
|
||||
*
|
||||
* This program is distributed in the hope that it will be useful, but WITHOUT ANY WARRANTY; without even the implied
|
||||
* warranty of MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU General Public License for more details.
|
||||
*
|
||||
* You should have received a copy of the GNU General Public License along with this program.
|
||||
* If not, see <http://www.gnu.org/licenses/>
|
||||
*
|
||||
******************************************************************************/
|
||||
package io.github.dsheirer.dsp.filter.channelizer;
|
||||
|
||||
import io.github.dsheirer.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.Listener;
|
||||
import io.github.dsheirer.sample.complex.IComplexBufferProvider;
|
||||
import io.github.dsheirer.source.Source;
|
||||
import io.github.dsheirer.source.SourceEvent;
|
||||
import io.github.dsheirer.source.SourceException;
|
||||
import io.github.dsheirer.source.tuner.channel.TunerChannel;
|
||||
import org.slf4j.Logger;
|
||||
import org.slf4j.LoggerFactory;
|
||||
|
||||
import java.util.List;
|
||||
import java.util.concurrent.CopyOnWriteArrayList;
|
||||
|
||||
public class PolyphaseChannelManager implements Listener<SourceEvent>
|
||||
{
|
||||
private final static Logger mLog = LoggerFactory.getLogger(PolyphaseChannelManager.class);
|
||||
private static final int CHANNEL_BANDWIDTH = 12500;
|
||||
private static final double CHANNEL_OVERSAMPLING = 2.0;
|
||||
private static final int CHANNEL_SAMPLE_RATE = (int)(CHANNEL_BANDWIDTH * CHANNEL_OVERSAMPLING);
|
||||
private static final int POLYPHASE_FILTER_TAPS_PER_CHANNEL = 17;
|
||||
|
||||
private IComplexBufferProvider mComplexBufferProvider;
|
||||
private List<PolyphaseChannelSource> mChannelSources = new CopyOnWriteArrayList<>();
|
||||
private ChannelCalculator mChannelCalculator;
|
||||
private ComplexPolyphaseChannelizerM2 mPolyphaseChannelizer;
|
||||
private ChannelSourceEventListener mChannelSourceEventListener = new ChannelSourceEventListener();
|
||||
private ScheduledBufferProcessor mBufferProcessor;
|
||||
|
||||
/**
|
||||
* 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.
|
||||
*
|
||||
* 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.
|
||||
*
|
||||
* @param complexBufferProvider that supports register/deregister of this channel manager
|
||||
* @param frequency of the provided complex buffer samples
|
||||
* @param sampleRate of the provided complex buffer samples
|
||||
*/
|
||||
public PolyphaseChannelManager(IComplexBufferProvider complexBufferProvider, long frequency, int sampleRate)
|
||||
{
|
||||
if(complexBufferProvider == null)
|
||||
{
|
||||
throw new IllegalArgumentException("Complex buffer provider argument cannot be null");
|
||||
}
|
||||
|
||||
mComplexBufferProvider = complexBufferProvider;
|
||||
|
||||
//Ensure that sample rate is a multiple of channel sample rate since polyphase channelizer is M2
|
||||
if(sampleRate % (CHANNEL_SAMPLE_RATE) != 0)
|
||||
{
|
||||
throw new IllegalArgumentException("Tuner sample rate [" + sampleRate + "] must be an even multiple of " +
|
||||
CHANNEL_SAMPLE_RATE + " Hz");
|
||||
}
|
||||
|
||||
int channelCount = sampleRate / CHANNEL_BANDWIDTH;
|
||||
|
||||
mChannelCalculator = new ChannelCalculator(sampleRate, channelCount, frequency, CHANNEL_OVERSAMPLING);
|
||||
|
||||
mBufferProcessor = new ScheduledBufferProcessor(500, 100, 50, 50);
|
||||
|
||||
updateChannelizer(sampleRate, null);
|
||||
}
|
||||
|
||||
/**
|
||||
* 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 Source getChannel(TunerChannel tunerChannel)
|
||||
{
|
||||
PolyphaseChannelSource channelSource = null;
|
||||
|
||||
try
|
||||
{
|
||||
List<Integer> polyphaseIndexes = mChannelCalculator.getChannelIndexes(tunerChannel);
|
||||
|
||||
IPolyphaseChannelOutputProcessor outputProcessor = getOutputProcessor(polyphaseIndexes);
|
||||
|
||||
if(outputProcessor != null)
|
||||
{
|
||||
channelSource = new PolyphaseChannelSource(tunerChannel, outputProcessor, mChannelSourceEventListener,
|
||||
CHANNEL_SAMPLE_RATE);
|
||||
|
||||
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;
|
||||
}
|
||||
|
||||
/**
|
||||
* Creates a processor to process the channelizer channel indexes into a composite output stream providing
|
||||
* channelized complex sample buffers to a registered source listener.
|
||||
* @param indexes to target by the output processor
|
||||
* @return output processor compatible with the number of indexes to monitor
|
||||
*/
|
||||
private IPolyphaseChannelOutputProcessor getOutputProcessor(List<Integer> indexes)
|
||||
{
|
||||
switch(indexes.size())
|
||||
{
|
||||
case 1:
|
||||
return new OneChannelOutputProcessor(CHANNEL_SAMPLE_RATE, indexes);
|
||||
case 2:
|
||||
return new TwoChannelOutputProcessor(CHANNEL_SAMPLE_RATE, indexes);
|
||||
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)
|
||||
{
|
||||
mPolyphaseChannelizer.addChannel(channelSource);
|
||||
|
||||
if(mPolyphaseChannelizer.getRegisteredChannelCount() == 1)
|
||||
{
|
||||
mComplexBufferProvider.addComplexBufferListener(mBufferProcessor);
|
||||
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);
|
||||
|
||||
if(mPolyphaseChannelizer.getRegisteredChannelCount() == 0)
|
||||
{
|
||||
mComplexBufferProvider.removeComplexBufferListener(mBufferProcessor);
|
||||
mBufferProcessor.stop();
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Implements the Listener<SourceEvent> interface for receiving notifications of frequency and sample rate
|
||||
* changes from the tuner.
|
||||
*/
|
||||
@Override
|
||||
public void receive(SourceEvent sourceEvent)
|
||||
{
|
||||
switch(sourceEvent.getEvent())
|
||||
{
|
||||
case NOTIFICATION_FREQUENCY_CHANGE:
|
||||
//Update the index calculator and each of the output processors to update each channel frequency
|
||||
mChannelCalculator.setCenterFrequency(sourceEvent.getValue().longValue());
|
||||
updateOutputProcessors(sourceEvent);
|
||||
break;
|
||||
case NOTIFICATION_SAMPLE_RATE_CHANGE:
|
||||
//Update the channelizer (and the output processors) for the new sample rate
|
||||
updateChannelizer(sourceEvent.getValue().intValue(), sourceEvent);
|
||||
break;
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Creates or updates the channelizer to process the incoming sample rate and updates any channel processors.
|
||||
*
|
||||
* @param sampleRate to use for the channelizer
|
||||
* @param sourceEvent containing a notification of sample rate change (optional - can be null). This source
|
||||
* event will be passed to all output processors to ensure the change is fully promulgated before restarting the
|
||||
* buffer processor if it was already running.
|
||||
*/
|
||||
private void updateChannelizer(int sampleRate, SourceEvent sourceEvent)
|
||||
{
|
||||
mChannelCalculator.setSampleRate(sampleRate);
|
||||
|
||||
//Lock on the buffer processor to block any channel create/start/stop requests while we configure
|
||||
synchronized(mBufferProcessor)
|
||||
{
|
||||
//Get current processor state so we can restart it after we update the channelizer
|
||||
boolean bufferProcessorWasRunning = mBufferProcessor.isRunning();
|
||||
|
||||
//Stop will be ignored if it is not currently running
|
||||
mBufferProcessor.stop();
|
||||
|
||||
try
|
||||
{
|
||||
if(mPolyphaseChannelizer == null)
|
||||
{
|
||||
mPolyphaseChannelizer = new ComplexPolyphaseChannelizerM2(sampleRate,
|
||||
POLYPHASE_FILTER_TAPS_PER_CHANNEL);
|
||||
|
||||
mBufferProcessor.setListener(mPolyphaseChannelizer);
|
||||
}
|
||||
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);
|
||||
}
|
||||
|
||||
if(sourceEvent != null && sourceEvent.getEvent() == SourceEvent.Event.NOTIFICATION_SAMPLE_RATE_CHANGE)
|
||||
{
|
||||
updateOutputProcessors(sourceEvent);
|
||||
}
|
||||
|
||||
if(bufferProcessorWasRunning)
|
||||
{
|
||||
mBufferProcessor.start();
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* 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 that requires an update to the output processors
|
||||
*/
|
||||
private void updateOutputProcessors(SourceEvent sourceEvent)
|
||||
{
|
||||
for(PolyphaseChannelSource channelSource: mChannelSources)
|
||||
{
|
||||
updateOutputProcessor(channelSource);
|
||||
|
||||
//Notify the channel source that the frequency or sample rate has changed so that it can reset
|
||||
//any frequency correction value.
|
||||
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
|
||||
{
|
||||
List<Integer> indexes = mChannelCalculator.getChannelIndexes(channelSource.getTunerChannel());
|
||||
|
||||
long centerFrequency = mChannelCalculator.getCenterFrequencyForIndexes(indexes);
|
||||
|
||||
//If the indexes size is the same then update the current processor, otherwise create a new one
|
||||
if(channelSource.getPolyphaseChannelOutputProcessor().getInputChannelCount() == indexes.size())
|
||||
{
|
||||
channelSource.getPolyphaseChannelOutputProcessor().setPolyphaseChannelIndices(indexes);
|
||||
channelSource.setFrequency(centerFrequency);
|
||||
}
|
||||
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);
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Internal class for handling requests for start/stop sample stream from polyphase channel sources
|
||||
*/
|
||||
private class ChannelSourceEventListener implements Listener<SourceEvent>
|
||||
{
|
||||
@Override
|
||||
public void receive(SourceEvent sourceEvent)
|
||||
{
|
||||
switch(sourceEvent.getEvent())
|
||||
{
|
||||
case REQUEST_START_SAMPLE_STREAM:
|
||||
if(sourceEvent.hasSource() && sourceEvent.getSource() instanceof PolyphaseChannelSource)
|
||||
{
|
||||
startChannelSource((PolyphaseChannelSource)sourceEvent.getSource());
|
||||
}
|
||||
else
|
||||
{
|
||||
mLog.error("Request to start sample stream for unrecognized source: " +
|
||||
(sourceEvent.hasSource() ? sourceEvent.getSource().getClass() : "null source"));
|
||||
}
|
||||
break;
|
||||
case REQUEST_STOP_SAMPLE_STREAM:
|
||||
if(sourceEvent.hasSource() && sourceEvent.getSource() instanceof PolyphaseChannelSource)
|
||||
{
|
||||
stopChannelSource((PolyphaseChannelSource)sourceEvent.getSource());
|
||||
}
|
||||
else
|
||||
{
|
||||
mLog.error("Request to stop sample stream for unrecognized source: " +
|
||||
(sourceEvent.hasSource() ? sourceEvent.getSource().getClass() : "null source"));
|
||||
}
|
||||
break;
|
||||
default:
|
||||
mLog.error("Received unrecognized source event from polyphase channel source [" +
|
||||
sourceEvent.getEvent() + "]");
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,222 @@
|
||||
/*******************************************************************************
|
||||
* *********************************************************************************************************************
|
||||
* sdr-trunk
|
||||
* Copyright (C) 2014-2017 Dennis Sheirer
|
||||
*
|
||||
* This program is free software: you can redistribute it and/or modify it under the terms of the GNU General Public
|
||||
* License as published by the Free Software Foundation, either version 3 of the License, or (at your option) any
|
||||
* later version.
|
||||
*
|
||||
* This program is distributed in the hope that it will be useful, but WITHOUT ANY WARRANTY; without even the implied
|
||||
* warranty of MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU General Public License for more details.
|
||||
*
|
||||
* You should have received a copy of the GNU General Public License along with this program.
|
||||
* If not, see <http://www.gnu.org/licenses/>
|
||||
* *********************************************************************************************************************
|
||||
******************************************************************************/
|
||||
package io.github.dsheirer.dsp.filter.channelizer;
|
||||
|
||||
import io.github.dsheirer.dsp.filter.channelizer.output.IPolyphaseChannelOutputProcessor;
|
||||
import io.github.dsheirer.sample.Listener;
|
||||
import io.github.dsheirer.sample.complex.ComplexBuffer;
|
||||
import io.github.dsheirer.sample.complex.ComplexToComplexBufferAssembler;
|
||||
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 ComplexToComplexBufferAssembler mComplexBufferAssembler = new ComplexToComplexBufferAssembler(2500);
|
||||
private IPolyphaseChannelOutputProcessor mPolyphaseChannelOutputProcessor;
|
||||
private double mChannelSampleRate;
|
||||
private long mCenterFrequency;
|
||||
private long mFrequencyCorrection;
|
||||
|
||||
/**
|
||||
* 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
|
||||
*/
|
||||
public PolyphaseChannelSource(TunerChannel tunerChannel, IPolyphaseChannelOutputProcessor outputProcessor,
|
||||
Listener<SourceEvent> producerSourceEventListener, double channelSampleRate)
|
||||
{
|
||||
super(producerSourceEventListener, tunerChannel);
|
||||
mPolyphaseChannelOutputProcessor = outputProcessor;
|
||||
mChannelSampleRate = channelSampleRate;
|
||||
}
|
||||
|
||||
/**
|
||||
* Registers the listener to receive complex sample buffers from this channel source
|
||||
*/
|
||||
@Override
|
||||
public void setListener(final Listener<ComplexBuffer> listener)
|
||||
{
|
||||
mComplexBufferAssembler.setListener(listener);
|
||||
}
|
||||
|
||||
/**
|
||||
* Removes the listener from receiving complex sample buffers from this channel source
|
||||
*/
|
||||
@Override
|
||||
public void removeListener(Listener<ComplexBuffer> listener)
|
||||
{
|
||||
mComplexBufferAssembler.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(mComplexBufferAssembler);
|
||||
}
|
||||
|
||||
//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.
|
||||
mCenterFrequency = frequency;
|
||||
mFrequencyCorrection = 0;
|
||||
mPolyphaseChannelOutputProcessor.setFrequencyOffset(getFrequencyOffset());
|
||||
broadcastConsumerSourceEvent(SourceEvent.frequencyCorrectionChange(mFrequencyCorrection));
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Primary method for receiving channel results output from a polyphase channelizer. The results array 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 the polyphase channelizer output for a single complex sample period.
|
||||
*/
|
||||
public void receiveChannelResults(float[] channelResultsBuffer)
|
||||
{
|
||||
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()
|
||||
{
|
||||
mLog.info("Polyphase channel source received request to dispose");
|
||||
}
|
||||
|
||||
@Override
|
||||
public long getFrequencyCorrection()
|
||||
{
|
||||
return mFrequencyCorrection;
|
||||
}
|
||||
|
||||
/**
|
||||
* 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 setFrequencyCorrection(long value)
|
||||
{
|
||||
mFrequencyCorrection = value;
|
||||
updateFrequencyOffset();
|
||||
broadcastConsumerSourceEvent(SourceEvent.frequencyCorrectionChange(mFrequencyCorrection));
|
||||
}
|
||||
|
||||
/**
|
||||
* Sets the center frequency for the incoming sample stream channel results and resets frequency correction to zero.
|
||||
* @param frequency in hertz
|
||||
*/
|
||||
@Override
|
||||
protected void setFrequency(long frequency)
|
||||
{
|
||||
mCenterFrequency = frequency;
|
||||
setFrequencyCorrection(0);
|
||||
}
|
||||
|
||||
@Override
|
||||
protected void processSamples()
|
||||
{
|
||||
//Lock on the output processor so that it can't be changed out in the middle of processing
|
||||
synchronized(mPolyphaseChannelOutputProcessor)
|
||||
{
|
||||
mPolyphaseChannelOutputProcessor.processChannelResults(mComplexBufferAssembler);
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Calculates the frequency offset required to mix the incoming signal to the desired output baseband
|
||||
*/
|
||||
private long getFrequencyOffset()
|
||||
{
|
||||
return getTunerChannel().getFrequency() - mCenterFrequency + mFrequencyCorrection;
|
||||
}
|
||||
|
||||
/**
|
||||
* 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());
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,168 @@
|
||||
/*******************************************************************************
|
||||
* sdr-trunk
|
||||
* Copyright (C) 2014-2017 Dennis Sheirer
|
||||
*
|
||||
* This program is free software: you can redistribute it and/or modify it under the terms of the GNU General Public
|
||||
* License as published by the Free Software Foundation, either version 3 of the License, or (at your option) any
|
||||
* later version.
|
||||
*
|
||||
* This program is distributed in the hope that it will be useful, but WITHOUT ANY WARRANTY; without even the implied
|
||||
* warranty of MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU General Public License for more details.
|
||||
*
|
||||
* You should have received a copy of the GNU General Public License along with this program.
|
||||
* If not, see <http://www.gnu.org/licenses/>
|
||||
*
|
||||
******************************************************************************/
|
||||
package io.github.dsheirer.dsp.filter.channelizer;
|
||||
|
||||
import io.github.dsheirer.sample.Listener;
|
||||
import io.github.dsheirer.sample.OverflowableTransferQueue;
|
||||
import io.github.dsheirer.sample.real.IOverflowListener;
|
||||
import io.github.dsheirer.util.ThreadPool;
|
||||
import org.slf4j.Logger;
|
||||
import org.slf4j.LoggerFactory;
|
||||
|
||||
import java.util.ArrayList;
|
||||
import java.util.List;
|
||||
import java.util.concurrent.ScheduledFuture;
|
||||
import java.util.concurrent.TimeUnit;
|
||||
import java.util.concurrent.atomic.AtomicBoolean;
|
||||
|
||||
public class ScheduledBufferProcessor<E> implements Listener<E>
|
||||
{
|
||||
private final static Logger mLog = LoggerFactory.getLogger(ScheduledBufferProcessor.class);
|
||||
|
||||
private OverflowableTransferQueue<E> mQueue;
|
||||
private Listener<E> mListener;
|
||||
private ScheduledFuture<?> mScheduledFuture;
|
||||
private long mDistributionInterval;
|
||||
private int mMaxBuffersPerInterval;
|
||||
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
|
||||
* @param distributionInterval in milliseconds specifying how often the internal scheduled thread pool buffer
|
||||
* distributor should run
|
||||
* @param maxBuffersPerInterval maximum number of buffers to pull from the internal queue during each distribution
|
||||
* interval
|
||||
*/
|
||||
public ScheduledBufferProcessor(int maximumSize, int resetThreshold, long distributionInterval, int maxBuffersPerInterval)
|
||||
{
|
||||
mQueue = new OverflowableTransferQueue<>(maximumSize, resetThreshold);
|
||||
|
||||
mQueue.setOverflowListener(new IOverflowListener()
|
||||
{
|
||||
@Override
|
||||
public void sourceOverflow(boolean overflow)
|
||||
{
|
||||
mLog.error("Overflow state changed - overflow:" + overflow);
|
||||
}
|
||||
});
|
||||
mDistributionInterval = distributionInterval;
|
||||
mMaxBuffersPerInterval = maxBuffersPerInterval;
|
||||
}
|
||||
|
||||
/**
|
||||
* 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<E> listener)
|
||||
{
|
||||
mListener = listener;
|
||||
}
|
||||
|
||||
/**
|
||||
* Primary input method for adding buffers to this processor. Note: incoming buffers will be ignored if this
|
||||
* processor is in a stopped state. You must invoke start() to allow incoming buffers and initiate buffer
|
||||
* processing.
|
||||
*
|
||||
* @param e to enqueue for distribution to a registered listener
|
||||
*/
|
||||
@Override
|
||||
public void receive(E e)
|
||||
{
|
||||
mQueue.offer(e);
|
||||
}
|
||||
|
||||
/**
|
||||
* Starts this buffer processor and allows queuing of incoming buffers.
|
||||
*/
|
||||
public void start()
|
||||
{
|
||||
if(mRunning.compareAndSet(false, true))
|
||||
{
|
||||
mScheduledFuture = ThreadPool.SCHEDULED.scheduleAtFixedRate(new Processor(), 0, mDistributionInterval, 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;
|
||||
}
|
||||
|
||||
mQueue.clear();
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* 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
|
||||
{
|
||||
private List<E> mBuffers = new ArrayList<>();
|
||||
|
||||
@Override
|
||||
public void run()
|
||||
{
|
||||
mQueue.drainTo(mBuffers, mMaxBuffersPerInterval);
|
||||
|
||||
if(mListener != null)
|
||||
{
|
||||
for(E buffer: mBuffers)
|
||||
{
|
||||
mListener.receive(buffer);
|
||||
}
|
||||
}
|
||||
|
||||
mBuffers.clear();
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,170 @@
|
||||
/*******************************************************************************
|
||||
* sdrtrunk
|
||||
* Copyright (C) 2014-2017 Dennis Sheirer
|
||||
*
|
||||
* This program is free software: you can redistribute it and/or modify
|
||||
* it under the terms of the GNU General Public License as published by
|
||||
* the Free Software Foundation, either version 3 of the License, or
|
||||
* (at your option) any later version.
|
||||
*
|
||||
* This program is distributed in the hope that it will be useful,
|
||||
* but WITHOUT ANY WARRANTY; without even the implied warranty of
|
||||
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
|
||||
* GNU General Public License for more details.
|
||||
*
|
||||
* You should have received a copy of the GNU General Public License
|
||||
* along with this program. If not, see <http://www.gnu.org/licenses/>
|
||||
*
|
||||
******************************************************************************/
|
||||
package io.github.dsheirer.dsp.filter.channelizer.output;
|
||||
|
||||
import io.github.dsheirer.dsp.mixer.Oscillator;
|
||||
import io.github.dsheirer.sample.OverflowableTransferQueue;
|
||||
import io.github.dsheirer.sample.complex.Complex;
|
||||
import io.github.dsheirer.sample.complex.ComplexSampleListener;
|
||||
import io.github.dsheirer.sample.real.IOverflowListener;
|
||||
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 OverflowableTransferQueue<float[]> mChannelResultsQueue;
|
||||
private List<float[]> mChannelResultsToProcess = new ArrayList<>();
|
||||
private int mMaxResultsToProcess;
|
||||
|
||||
private int mInputChannelCount;
|
||||
//TODO: swap this out and use the LowPhaseNoiseOscillator
|
||||
private Oscillator 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;
|
||||
mFrequencyCorrectionMixer = new Oscillator(0, sampleRate);
|
||||
mMaxResultsToProcess = (int)(sampleRate / 10) * 2; //process at 100 millis interval, twice the expected inflow rate
|
||||
|
||||
mChannelResultsQueue = new OverflowableTransferQueue<>((int)(sampleRate * 3), (int)(sampleRate * 0.5));
|
||||
}
|
||||
|
||||
/**
|
||||
* 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);
|
||||
}
|
||||
|
||||
@Override
|
||||
public void receiveChannelResults(float[] channelResults)
|
||||
{
|
||||
mChannelResultsQueue.offer(channelResults);
|
||||
}
|
||||
|
||||
/**
|
||||
* Processes all enqueued polyphase channelizer results until the internal queue is empty
|
||||
* @param listener to receive the processed channel results
|
||||
*/
|
||||
@Override
|
||||
public void processChannelResults(ComplexSampleListener listener)
|
||||
{
|
||||
try
|
||||
{
|
||||
int count = mChannelResultsQueue.drainTo(mChannelResultsToProcess, mMaxResultsToProcess);
|
||||
|
||||
if(count > 0)
|
||||
{
|
||||
process(mChannelResultsToProcess, listener);
|
||||
}
|
||||
}
|
||||
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 listener to receive the channel complex sample
|
||||
*/
|
||||
public abstract void process(List<float[]> channelResults, ComplexSampleListener listener);
|
||||
|
||||
|
||||
/**
|
||||
* 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;
|
||||
}
|
||||
|
||||
/**
|
||||
* Provides frequency correction of the inphase component or simply returns the inphase argument value when no
|
||||
* frequency correction value has been specified.
|
||||
*
|
||||
* @param inphase value of the sample to translate/correct
|
||||
* @param quadrature value of the sample to translate/correct
|
||||
* @return corrected inphase value
|
||||
*/
|
||||
protected float getFrequencyCorrectedInphase(float inphase, float quadrature)
|
||||
{
|
||||
return Complex.multiplyInphase(inphase, quadrature, mFrequencyCorrectionMixer.inphase(),
|
||||
mFrequencyCorrectionMixer.quadrature());
|
||||
}
|
||||
|
||||
/**
|
||||
* Provides frequency correction of the quadrature component or simply returns the quadrature argument value when no
|
||||
* frequency correction value has been applied,
|
||||
*
|
||||
* @param inphase value of the sample to translate/correct
|
||||
* @param quadrature value of the sample to translate/correct
|
||||
* @return translated inphase value
|
||||
*/
|
||||
protected float getFrequencyCorrectedQuadrature(float inphase, float quadrature)
|
||||
{
|
||||
return Complex.multiplyQuadrature(inphase, quadrature, mFrequencyCorrectionMixer.inphase(),
|
||||
mFrequencyCorrectionMixer.quadrature());
|
||||
}
|
||||
|
||||
/**
|
||||
* Indicates if this channel has a frequency correction offset
|
||||
*/
|
||||
public boolean hasFrequencyCorrection()
|
||||
{
|
||||
return mFrequencyCorrectionEnabled;
|
||||
}
|
||||
|
||||
}
|
||||
@@ -0,0 +1,66 @@
|
||||
/*******************************************************************************
|
||||
* sdrtrunk
|
||||
* Copyright (C) 2014-2017 Dennis Sheirer
|
||||
*
|
||||
* This program is free software: you can redistribute it and/or modify
|
||||
* it under the terms of the GNU General Public License as published by
|
||||
* the Free Software Foundation, either version 3 of the License, or
|
||||
* (at your option) any later version.
|
||||
*
|
||||
* This program is distributed in the hope that it will be useful,
|
||||
* but WITHOUT ANY WARRANTY; without even the implied warranty of
|
||||
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
|
||||
* GNU General Public License for more details.
|
||||
*
|
||||
* You should have received a copy of the GNU General Public License
|
||||
* along with this program. If not, see <http://www.gnu.org/licenses/>
|
||||
*
|
||||
******************************************************************************/
|
||||
package io.github.dsheirer.dsp.filter.channelizer.output;
|
||||
|
||||
import io.github.dsheirer.sample.complex.ComplexSampleListener;
|
||||
import io.github.dsheirer.source.Source;
|
||||
|
||||
import java.util.List;
|
||||
|
||||
public interface IPolyphaseChannelOutputProcessor
|
||||
{
|
||||
/**
|
||||
* Receive and enqueue output results from the polyphase analysis channelizer
|
||||
* @param channelResults to enqueue
|
||||
*/
|
||||
void receiveChannelResults(float[] channelResults);
|
||||
|
||||
/**
|
||||
* Process the channel output channel results queue and deliver the output to the listener
|
||||
*
|
||||
* @param listener to receive the processed channel results
|
||||
*/
|
||||
void processChannelResults(ComplexSampleListener listener);
|
||||
|
||||
/**
|
||||
* Sets the desired frequency offset from center. The samples will be mixed with an oscillator set to this offset
|
||||
* frequency to produce an output where the desired signal is centered in the passband.
|
||||
*
|
||||
* @param frequency in hertz
|
||||
*/
|
||||
void setFrequencyOffset(long frequency);
|
||||
|
||||
/**
|
||||
* Indicates the number of input channels processed by this output processor
|
||||
* @return
|
||||
*/
|
||||
int getInputChannelCount();
|
||||
|
||||
/**
|
||||
* Updates the input polyphase channel index(es) used by this output processor
|
||||
* @param indexes
|
||||
*/
|
||||
void setPolyphaseChannelIndices(List<Integer> indexes);
|
||||
|
||||
/**
|
||||
* Sets the listener to receive buffer overflow notifications
|
||||
* @param source to receive overflow notifications
|
||||
*/
|
||||
void setSourceOverflowListener(Source source);
|
||||
}
|
||||
@@ -0,0 +1,96 @@
|
||||
/*******************************************************************************
|
||||
* sdrtrunk
|
||||
* Copyright (C) 2014-2017 Dennis Sheirer
|
||||
*
|
||||
* This program is free software: you can redistribute it and/or modify
|
||||
* it under the terms of the GNU General Public License as published by
|
||||
* the Free Software Foundation, either version 3 of the License, or
|
||||
* (at your option) any later version.
|
||||
*
|
||||
* This program is distributed in the hope that it will be useful,
|
||||
* but WITHOUT ANY WARRANTY; without even the implied warranty of
|
||||
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
|
||||
* GNU General Public License for more details.
|
||||
*
|
||||
* You should have received a copy of the GNU General Public License
|
||||
* along with this program. If not, see <http://www.gnu.org/licenses/>
|
||||
*
|
||||
******************************************************************************/
|
||||
package io.github.dsheirer.dsp.filter.channelizer.output;
|
||||
|
||||
import io.github.dsheirer.sample.complex.ComplexSampleListener;
|
||||
import org.slf4j.Logger;
|
||||
import org.slf4j.LoggerFactory;
|
||||
|
||||
import java.util.List;
|
||||
|
||||
public class OneChannelOutputProcessor extends ChannelOutputProcessor
|
||||
{
|
||||
private final static Logger mLog = LoggerFactory.getLogger(OneChannelOutputProcessor.class);
|
||||
|
||||
private int mChannelOffset;
|
||||
|
||||
/**
|
||||
* Processor to extract a single channel from a polyphase channelizer and produce an output I/Q complex sample
|
||||
* from each polyphase channelizer output results array.
|
||||
*
|
||||
* @param sampleRate of the output sample stream.
|
||||
* @param channelIndexes containing a single channel index.
|
||||
*/
|
||||
public OneChannelOutputProcessor(double sampleRate, List<Integer> channelIndexes)
|
||||
{
|
||||
super(1, sampleRate);
|
||||
setPolyphaseChannelIndices(channelIndexes);
|
||||
mLog.debug("Sample Rate:" + sampleRate + " PolyPhase Channel Indexes:" + channelIndexes);
|
||||
}
|
||||
|
||||
/**
|
||||
* Updates this processor to extract a single, specified channel index.
|
||||
* @param indexes containing a single channel index value.
|
||||
* @throws IllegalArgumentException if the list of indexes does not contain a single channel index.
|
||||
*/
|
||||
public void setPolyphaseChannelIndices(List<Integer> indexes)
|
||||
{
|
||||
if(indexes.size() != 1)
|
||||
{
|
||||
throw new IllegalArgumentException("Single channel output processor requires a single index to " +
|
||||
"process - provided indexes " + indexes.toString());
|
||||
}
|
||||
|
||||
//Set the channelized output results offset to twice the channel index to account for each channel having
|
||||
//an I/Q pair
|
||||
mChannelOffset = indexes.get(0) * 2;
|
||||
}
|
||||
|
||||
/**
|
||||
* Extract the channel from the channel results array, apply frequency translation, and deliver the
|
||||
* extracted frequency-corrected channel I/Q sample set to the complex sample listener.
|
||||
*
|
||||
* @param channelResults to process containing a list of channel array of I/Q sample pairs (I0,Q0,I1,Q1...In,Qn)
|
||||
* @param listener to receive the extracted, frequency-translated channel results
|
||||
*/
|
||||
@Override
|
||||
public void process(List<float[]> channelResults, ComplexSampleListener listener)
|
||||
{
|
||||
for(float[] channels: channelResults)
|
||||
{
|
||||
if(channels.length < mChannelOffset + 1)
|
||||
{
|
||||
throw new IllegalArgumentException("Polyphase channelizer output channels array is not large enough to " +
|
||||
"cover this single channel output processor with channel offset [" + mChannelOffset + "]");
|
||||
}
|
||||
|
||||
float i = channels[mChannelOffset];
|
||||
float q = channels[mChannelOffset + 1];
|
||||
|
||||
if(hasFrequencyCorrection())
|
||||
{
|
||||
listener.receive(getFrequencyCorrectedInphase(i, q), getFrequencyCorrectedQuadrature(i, q));
|
||||
}
|
||||
else
|
||||
{
|
||||
listener.receive(i, q);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,91 @@
|
||||
/*******************************************************************************
|
||||
* sdrtrunk
|
||||
* Copyright (C) 2014-2017 Dennis Sheirer
|
||||
*
|
||||
* This program is free software: you can redistribute it and/or modify
|
||||
* it under the terms of the GNU General Public License as published by
|
||||
* the Free Software Foundation, either version 3 of the License, or
|
||||
* (at your option) any later version.
|
||||
*
|
||||
* This program is distributed in the hope that it will be useful,
|
||||
* but WITHOUT ANY WARRANTY; without even the implied warranty of
|
||||
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
|
||||
* GNU General Public License for more details.
|
||||
*
|
||||
* You should have received a copy of the GNU General Public License
|
||||
* along with this program. If not, see <http://www.gnu.org/licenses/>
|
||||
*
|
||||
******************************************************************************/
|
||||
package io.github.dsheirer.dsp.filter.channelizer.output;
|
||||
|
||||
import io.github.dsheirer.sample.complex.ComplexSampleListener;
|
||||
import org.slf4j.Logger;
|
||||
import org.slf4j.LoggerFactory;
|
||||
|
||||
import java.util.List;
|
||||
|
||||
public class TwoChannelOutputProcessor extends ChannelOutputProcessor
|
||||
{
|
||||
private final static Logger mLog = LoggerFactory.getLogger(TwoChannelOutputProcessor.class);
|
||||
|
||||
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(int sampleRate, List<Integer> channelIndexes)
|
||||
{
|
||||
//Set the frequency correction oscillator to 2 x output sample rate since we'll be correcting the frequency
|
||||
//after synthesizing both input channels
|
||||
super(2, sampleRate * 2);
|
||||
setPolyphaseChannelIndices(channelIndexes);
|
||||
}
|
||||
|
||||
/**
|
||||
* Updates this processor to extract the two specified channel indexes.
|
||||
*
|
||||
* @param indexes containing a single channel index value.
|
||||
* @throws IllegalArgumentException if the list of indexes does not contain a single channel index.
|
||||
*/
|
||||
public void setPolyphaseChannelIndices(List<Integer> indexes)
|
||||
{
|
||||
if(indexes.size() != 2)
|
||||
{
|
||||
throw new IllegalArgumentException("Double channel output processor requires two indexes to " +
|
||||
"process - provided indexes " + indexes.toString());
|
||||
}
|
||||
|
||||
//Set the channelized output results offsets to twice the channel index to account for each channel having
|
||||
//an I/Q pair
|
||||
mChannelOffset1 = indexes.get(0) * 2;
|
||||
mChannelOffset2 = indexes.get(1) * 2;
|
||||
}
|
||||
|
||||
/**
|
||||
* Extract the channel from the channel results array, apply frequency translation, and deliver the
|
||||
* extracted frequency-corrected channel I/Q sample set to the complex sample listener.
|
||||
*
|
||||
* @param channelResults to process containing an array of channel I/Q sample pairs (I0,Q0,I1,Q1...In,Qn)
|
||||
* @param listener to receive the extracted, frequency-translated channel results
|
||||
*/
|
||||
@Override
|
||||
public void process(List<float[]> channelResults, ComplexSampleListener listener)
|
||||
{
|
||||
mLog.debug("I got channel results - doing nothing !!");
|
||||
// if(channels.length < mChannelOffset + 1)
|
||||
// {
|
||||
// throw new IllegalArgumentException("Polyphase channelizer output channels array is not large enough to " +
|
||||
// "cover this single channel output processor with channel offset [" + mChannelOffset + "]");
|
||||
// }
|
||||
//
|
||||
// float i = channels[mChannelOffset];
|
||||
// float q = channels[mChannelOffset + 1];
|
||||
|
||||
// listener.receive(getFrequencyCorrectedInphase(i, q), getFrequencyCorrectedQuadrature(i, q));
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,51 @@
|
||||
/*******************************************************************************
|
||||
* sdrtrunk
|
||||
* Copyright (C) 2014-2017 Dennis Sheirer
|
||||
*
|
||||
* This program is free software: you can redistribute it and/or modify
|
||||
* it under the terms of the GNU General Public License as published by
|
||||
* the Free Software Foundation, either version 3 of the License, or
|
||||
* (at your option) any later version.
|
||||
*
|
||||
* This program is distributed in the hope that it will be useful,
|
||||
* but WITHOUT ANY WARRANTY; without even the implied warranty of
|
||||
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
|
||||
* GNU General Public License for more details.
|
||||
*
|
||||
* You should have received a copy of the GNU General Public License
|
||||
* along with this program. If not, see <http://www.gnu.org/licenses/>
|
||||
*
|
||||
******************************************************************************/
|
||||
package io.github.dsheirer.dsp.filter.correction;
|
||||
|
||||
import io.github.dsheirer.sample.complex.ComplexBuffer;
|
||||
|
||||
public class IQCorrectionFilter
|
||||
{
|
||||
private float mRatio = 0.00001f;
|
||||
private float mAverageInphase = 0.0f;
|
||||
private float mAverageQuadrature = 0.0f;
|
||||
|
||||
public IQCorrectionFilter(float ratio)
|
||||
{
|
||||
mRatio = ratio;
|
||||
}
|
||||
|
||||
public IQCorrectionFilter()
|
||||
{
|
||||
}
|
||||
|
||||
public void filter(ComplexBuffer buffer)
|
||||
{
|
||||
float[] samples = buffer.getSamples();
|
||||
|
||||
for(int x = 0; x < samples.length; x += 2)
|
||||
{
|
||||
mAverageInphase = mAverageInphase + (mRatio * (samples[x] - mAverageInphase));
|
||||
samples[x] -= mAverageInphase;
|
||||
|
||||
mAverageQuadrature = mAverageQuadrature + (mRatio * (samples[x + 1] - mAverageQuadrature));
|
||||
samples[x + 1] -= mAverageQuadrature;
|
||||
}
|
||||
}
|
||||
}
|
||||
File diff suppressed because it is too large
Load Diff
@@ -0,0 +1,37 @@
|
||||
/*******************************************************************************
|
||||
* sdrtrunk
|
||||
* Copyright (C) 2014-2017 Dennis Sheirer
|
||||
*
|
||||
* This program is free software: you can redistribute it and/or modify
|
||||
* it under the terms of the GNU General Public License as published by
|
||||
* the Free Software Foundation, either version 3 of the License, or
|
||||
* (at your option) any later version.
|
||||
*
|
||||
* This program is distributed in the hope that it will be useful,
|
||||
* but WITHOUT ANY WARRANTY; without even the implied warranty of
|
||||
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
|
||||
* GNU General Public License for more details.
|
||||
*
|
||||
* You should have received a copy of the GNU General Public License
|
||||
* along with this program. If not, see <http://www.gnu.org/licenses/>
|
||||
*
|
||||
******************************************************************************/
|
||||
package io.github.dsheirer.dsp.filter.fir.remez;
|
||||
|
||||
import io.github.dsheirer.dsp.filter.fir.FIRFilterSpecification;
|
||||
|
||||
public class PolyphaseChannelizerDesigner extends FIRFilterSpecification
|
||||
{
|
||||
/**
|
||||
* Constructs a Polyphase TunerChannelizer Remez filter specification.
|
||||
*
|
||||
* Constructor is package private -- use the PolyphaseChannelizerFilterFactory to create a filter
|
||||
*
|
||||
* @param order of the filter
|
||||
* @param gridDensity to use when calculating the coefficients
|
||||
*/
|
||||
PolyphaseChannelizerDesigner(int order, int gridDensity)
|
||||
{
|
||||
super(FIRLinearPhaseFilterType.TYPE_1_ODD_LENGTH_EVEN_ORDER_SYMMETRICAL, order, gridDensity);
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,97 @@
|
||||
/*******************************************************************************
|
||||
* sdrtrunk
|
||||
* Copyright (C) 2014-2017 Dennis Sheirer
|
||||
*
|
||||
* This program is free software: you can redistribute it and/or modify
|
||||
* it under the terms of the GNU General Public License as published by
|
||||
* the Free Software Foundation, either version 3 of the License, or
|
||||
* (at your option) any later version.
|
||||
*
|
||||
* This program is distributed in the hope that it will be useful,
|
||||
* but WITHOUT ANY WARRANTY; without even the implied warranty of
|
||||
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
|
||||
* GNU General Public License for more details.
|
||||
*
|
||||
* You should have received a copy of the GNU General Public License
|
||||
* along with this program. If not, see <http://www.gnu.org/licenses/>
|
||||
*
|
||||
******************************************************************************/
|
||||
package io.github.dsheirer.dsp.filter.fir.remez;
|
||||
|
||||
import io.github.dsheirer.dsp.filter.fir.FIRFilterSpecification;
|
||||
import org.slf4j.Logger;
|
||||
import org.slf4j.LoggerFactory;
|
||||
|
||||
public class PolyphaseChannelizerFilterFactory
|
||||
{
|
||||
private final static Logger mLog = LoggerFactory.getLogger(PolyphaseChannelizerFilterFactory.class);
|
||||
|
||||
private static final double OBJECTIVE_BAND_EDGE_COEFFICIENT_AMPLITUDE = Math.sqrt(2.0) / 2.0; //.707xxx
|
||||
|
||||
public static float[] getFilter(int sampleRate, int channelBandwidth, double alpha)
|
||||
{
|
||||
FIRLinearPhaseFilterType type = FIRLinearPhaseFilterType.TYPE_2_EVEN_LENGTH_ODD_ORDER_SYMMETRICAL;
|
||||
|
||||
int passBandStart = 0;
|
||||
int gridDensity = 60;
|
||||
|
||||
int rolloffFrequency = (int)(alpha * (double)channelBandwidth);
|
||||
|
||||
int passBandStop = channelBandwidth - rolloffFrequency;
|
||||
|
||||
//Set the stop band to start at 40% into the adjacent channel, since we're oversampling by 2.
|
||||
int stopBandStart = channelBandwidth + rolloffFrequency;
|
||||
|
||||
double passRipple = 0.01d;
|
||||
double transitionRipple = 0.02d;
|
||||
double stopRipple = 0.001d; //Approximately 90 dB of attenuation
|
||||
|
||||
int order = FIRFilterSpecification.estimateFilterOrder( sampleRate, passBandStop, stopBandStart,
|
||||
passRipple, stopRipple );
|
||||
|
||||
// order = 1001;
|
||||
|
||||
// Ensure odd order since we're designing a Type 2 filter
|
||||
if(order % 2 == 0)
|
||||
{
|
||||
order++;
|
||||
}
|
||||
|
||||
mLog.info("Filter Order: " + order);
|
||||
|
||||
FIRFilterSpecification specification = new PolyphaseChannelizerDesigner(order, gridDensity);
|
||||
|
||||
FIRFilterSpecification.FrequencyBand passBand = new FIRFilterSpecification.FrequencyBand(sampleRate, passBandStart,
|
||||
passBandStop, 1.0, passRipple, 1.0);
|
||||
|
||||
//Use the filter order as the weighting for the pass band edge frequency
|
||||
// double weight = order;
|
||||
|
||||
FIRFilterSpecification.FrequencyBand transitionBand = new FIRFilterSpecification.FrequencyBand(sampleRate,
|
||||
channelBandwidth, channelBandwidth, OBJECTIVE_BAND_EDGE_COEFFICIENT_AMPLITUDE, transitionRipple, 5.0);
|
||||
|
||||
FIRFilterSpecification.FrequencyBand stopBand = new FIRFilterSpecification.FrequencyBand(sampleRate,
|
||||
stopBandStart, (int)(sampleRate / 2), 0.0, stopRipple);
|
||||
|
||||
specification.addFrequencyBand(passBand);
|
||||
specification.addFrequencyBand(transitionBand);
|
||||
specification.addFrequencyBand(stopBand);
|
||||
|
||||
double bandEdgeFrequency = Math.cos(Math.PI * (double)channelBandwidth / (double)(sampleRate / 2));
|
||||
float[] filter = null;
|
||||
|
||||
try
|
||||
{
|
||||
RemezFIRFilterDesignerWithLagrange designer = new RemezFIRFilterDesignerWithLagrange(specification);
|
||||
filter = designer.getImpulseResponse();
|
||||
double bandEdgeAmplitude = designer.getFrequencyResponse(bandEdgeFrequency);
|
||||
mLog.debug("Coefficient Amplitude at Band Edge is: " + bandEdgeAmplitude + " for frequency: " + passBandStop);
|
||||
}
|
||||
catch(Exception e)
|
||||
{
|
||||
mLog.error("Error designing filter", e);
|
||||
}
|
||||
|
||||
return filter;
|
||||
}
|
||||
}
|
||||
@@ -211,15 +211,12 @@ public class RemezFIRFilterDesigner
|
||||
|
||||
for( int k = 0; k < mExtremalIndices.size() - 1; k++ )
|
||||
{
|
||||
double cosineDelta = cosineOfFrequency -
|
||||
mGrid.getCosineFrequencyGrid()[ mExtremalIndices.get( k ) ];
|
||||
double cosineDelta = cosineOfFrequency - mGrid.getCosineFrequencyGrid()[ mExtremalIndices.get( k ) ];
|
||||
|
||||
//If this frequency is or is close to one of the polynomial points, use c[k] for that point
|
||||
//If this frequency is close to one of the polynomial points, use the polynomial point for the response
|
||||
if( Math.abs( cosineDelta ) < 1.0e-7 )
|
||||
{
|
||||
numerator = mIdealFrequencyResponse[ k ];
|
||||
denominator = 1.0;
|
||||
break;
|
||||
return mIdealFrequencyResponse[ k ];
|
||||
}
|
||||
else
|
||||
{
|
||||
|
||||
@@ -0,0 +1,710 @@
|
||||
/*******************************************************************************
|
||||
* sdrtrunk
|
||||
* Copyright (C) 2014-2017 Dennis Sheirer
|
||||
*
|
||||
* This program is free software: you can redistribute it and/or modify
|
||||
* it under the terms of the GNU General Public License as published by
|
||||
* the Free Software Foundation, either version 3 of the License, or
|
||||
* (at your option) any later version.
|
||||
*
|
||||
* This program is distributed in the hope that it will be useful,
|
||||
* but WITHOUT ANY WARRANTY; without even the implied warranty of
|
||||
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
|
||||
* GNU General Public License for more details.
|
||||
*
|
||||
* You should have received a copy of the GNU General Public License
|
||||
* along with this program. If not, see <http://www.gnu.org/licenses/>
|
||||
*
|
||||
******************************************************************************/
|
||||
package io.github.dsheirer.dsp.filter.fir.remez;
|
||||
|
||||
import io.github.dsheirer.dsp.filter.design.FilterDesignException;
|
||||
import io.github.dsheirer.dsp.filter.fir.FIRFilterSpecification;
|
||||
import org.slf4j.Logger;
|
||||
import org.slf4j.LoggerFactory;
|
||||
|
||||
import java.math.BigDecimal;
|
||||
import java.math.RoundingMode;
|
||||
import java.util.ArrayList;
|
||||
import java.util.Iterator;
|
||||
import java.util.List;
|
||||
|
||||
public class RemezFIRFilterDesigner2
|
||||
{
|
||||
private final static Logger mLog = LoggerFactory.getLogger(RemezFIRFilterDesigner2.class);
|
||||
|
||||
private static final double CONVERGENCE_THRESHOLD = 0.0001;
|
||||
public static final int MAXIMUM_ITERATION_COUNT = 40;
|
||||
public static final double TWO_PI = 2.0 * Math.PI;
|
||||
public static final double FREQUENCY_PROXIMITY_THRESHOLD = 1.0e-7;
|
||||
|
||||
private FIRFilterSpecification mSpecification;
|
||||
private Grid mGrid;
|
||||
private List<Integer> mExtremalIndices;
|
||||
|
||||
private BigDecimal[] mD;
|
||||
private double[] mGridErrors;
|
||||
private double[] mGridFrequencyResponse;
|
||||
private double[] mIdealFrequencyResponse;
|
||||
|
||||
private double mDelta;
|
||||
|
||||
private boolean mConverged;
|
||||
|
||||
/**
|
||||
* Linear FIR filter designer using the Parks-McClelland Remez exchange algorithm.
|
||||
*
|
||||
* Construct a filter specification using the builder methods in the FIRFilterSpecification
|
||||
* class and construct an instance of this class with the specification. Check the isValid()
|
||||
* method after construction to see if the filter design was successful. If successful, use the
|
||||
* getFrequencyResponse() and getImpulseResponse() methods to access the filter taps as the
|
||||
* specified length.
|
||||
*
|
||||
* If the filter cannot be designed from the specification the isValid() method will return
|
||||
* false and any attempts to access the getFrequencyResponse() or getImpulseResponse() methods
|
||||
* will throw a FilterDesignException.
|
||||
*
|
||||
* @param specification that defines a Type 1-4 linear phase FIR Filter
|
||||
*/
|
||||
public RemezFIRFilterDesigner2(FIRFilterSpecification specification)
|
||||
{
|
||||
mSpecification = specification;
|
||||
|
||||
design();
|
||||
}
|
||||
|
||||
/**
|
||||
* Designs the filter according to the specification
|
||||
*/
|
||||
private void design()
|
||||
{
|
||||
mLog.debug("Designing filter ...");
|
||||
mGrid = new Grid(mSpecification);
|
||||
mExtremalIndices = getInitialExtremalIndices();
|
||||
|
||||
int iterationCount = 0;
|
||||
|
||||
try
|
||||
{
|
||||
do
|
||||
{
|
||||
mLog.debug("Calculating grid frequency response");
|
||||
calculateGridFrequencyResponse();
|
||||
mLog.debug("Calculating grid error");
|
||||
calculateGridError();
|
||||
mLog.debug("Finding extremal indices");
|
||||
findExtremalIndices();
|
||||
mLog.debug("Checking Convergence");
|
||||
checkConvergence();
|
||||
|
||||
iterationCount++;
|
||||
|
||||
mLog.debug("Iteration: " + iterationCount);
|
||||
}
|
||||
while(!mConverged && iterationCount < MAXIMUM_ITERATION_COUNT);
|
||||
}
|
||||
catch(FilterDesignException fde)
|
||||
{
|
||||
mLog.error("Filter design error - couldn't find extremal indices at count [" +
|
||||
iterationCount + "] try changing filter order up/down from [" +
|
||||
mSpecification.getOrder() + "]");
|
||||
|
||||
mConverged = false;
|
||||
}
|
||||
|
||||
if(mConverged)
|
||||
{
|
||||
//Update frequency response using the final set of extremal indices
|
||||
calculateGridFrequencyResponse();
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Indicates if the filter was successfully created from the filter specification. Check this
|
||||
* method before accessing frequency response or filter response, otherwise a FilterDesignException
|
||||
* is thrown.
|
||||
*
|
||||
* @return true if the filter was successfully designed.
|
||||
*/
|
||||
public boolean isValid()
|
||||
{
|
||||
return mConverged;
|
||||
}
|
||||
|
||||
/**
|
||||
* Frequency response of the designed filter with double precision
|
||||
*
|
||||
* @return half ( length / 2 ) of filter frequency response sampled at 0.0 - PI radians
|
||||
* @throws FilterDesignException if the specified filter cannot be designed
|
||||
*/
|
||||
public double[] getFrequencyResponse() throws FilterDesignException
|
||||
{
|
||||
if(!mConverged)
|
||||
{
|
||||
throw new FilterDesignException("Can't create filter from specification - failed "
|
||||
+ "to converge");
|
||||
}
|
||||
|
||||
//Resample the final polynomial at the desired filter length
|
||||
double[] resampled = resample();
|
||||
|
||||
//Apply Parks/McClelland frequency response corrections according to the filter type
|
||||
return correctFrequencyResponse(resampled);
|
||||
}
|
||||
|
||||
/**
|
||||
* Impulse response of the designed filter with floating point precision.
|
||||
*
|
||||
* @return filter impulse response
|
||||
* @throws FilterDesignException if the specified filter cannot be designed
|
||||
*/
|
||||
public float[] getImpulseResponse() throws FilterDesignException
|
||||
{
|
||||
return convertToFloatArray(getImpulseResponseDoubles());
|
||||
}
|
||||
|
||||
/**
|
||||
* Impulse response of the designed filter with double precision
|
||||
*/
|
||||
public double[] getImpulseResponseDoubles() throws FilterDesignException
|
||||
{
|
||||
double[] frequencyResponse = getFrequencyResponse();
|
||||
|
||||
int length = mSpecification.getFilterLength();
|
||||
|
||||
double[] impulseResponse = new double[length];
|
||||
|
||||
double M;
|
||||
|
||||
switch(mSpecification.getFilterType())
|
||||
{
|
||||
case TYPE_1_ODD_LENGTH_EVEN_ORDER_SYMMETRICAL:
|
||||
M = ((double)length - 1.0) / 2.0;
|
||||
|
||||
for(int n = 0; n < length; n++)
|
||||
{
|
||||
double accumulator = frequencyResponse[0];
|
||||
|
||||
double frequency = TWO_PI * (n - M) / length;
|
||||
|
||||
for(int k = 1; k <= M; k++)
|
||||
{
|
||||
accumulator += 2.0 * frequencyResponse[k] * Math.cos(frequency * (double)k);
|
||||
}
|
||||
|
||||
impulseResponse[n] = accumulator / (double)length;
|
||||
}
|
||||
break;
|
||||
case TYPE_2_EVEN_LENGTH_ODD_ORDER_SYMMETRICAL:
|
||||
double offset = (double)(length - 1) / 2.0;
|
||||
|
||||
for(int n = 0; n < length; n++)
|
||||
{
|
||||
double accumulator = frequencyResponse[0];
|
||||
|
||||
double frequency = TWO_PI * ((double)n - offset) / (double)length;
|
||||
|
||||
for(int k = 1; k < frequencyResponse.length; k++)
|
||||
{
|
||||
accumulator += 2.0 * frequencyResponse[k] * Math.cos(frequency * (double)k);
|
||||
}
|
||||
|
||||
impulseResponse[n] = accumulator / (double)length;
|
||||
}
|
||||
break;
|
||||
}
|
||||
|
||||
return impulseResponse;
|
||||
}
|
||||
|
||||
/**
|
||||
* Calculates the frequency response of this filter at the specified cosine of the frequency
|
||||
*
|
||||
* Implements Oppenheim/Schafer Discrete Time Signal Processing, 3e, 2016, equation 116a to
|
||||
* determine the actual response of the specified frequency cosine using Lagrange
|
||||
* interpolation of the polynomial formed at the extremal index set.
|
||||
*
|
||||
* @param cosineOfFrequency to calculate the frequency response
|
||||
* @return actual frequency response for the specified frequency with the current set of
|
||||
* extreaml indices
|
||||
*/
|
||||
public double getFrequencyResponse(double cosineOfFrequency)
|
||||
{
|
||||
BigDecimal numerator = BigDecimal.ZERO;
|
||||
BigDecimal denominator = BigDecimal.ZERO;
|
||||
|
||||
for(int k = 0; k < mExtremalIndices.size() - 1; k++)
|
||||
{
|
||||
double cosineDelta = cosineOfFrequency - mGrid.getCosineFrequencyGrid()[mExtremalIndices.get(k)];
|
||||
|
||||
//If this frequency is close to one of the polynomial points, use the point as the response value
|
||||
if(Math.abs(cosineDelta) < FREQUENCY_PROXIMITY_THRESHOLD)
|
||||
{
|
||||
return mIdealFrequencyResponse[k];
|
||||
}
|
||||
else
|
||||
{
|
||||
BigDecimal dkOverCosineDelta = mD[k].divide(BigDecimal.valueOf(cosineDelta), 25, RoundingMode.HALF_UP);
|
||||
|
||||
numerator = numerator.add(dkOverCosineDelta.multiply(BigDecimal.valueOf(mIdealFrequencyResponse[k])));
|
||||
denominator = denominator.add(dkOverCosineDelta);
|
||||
}
|
||||
}
|
||||
|
||||
if(denominator.doubleValue() == 0.0)
|
||||
{
|
||||
int a = 0;
|
||||
}
|
||||
|
||||
return numerator.divide(denominator, 25, RoundingMode.HALF_UP).doubleValue();
|
||||
}
|
||||
|
||||
/**
|
||||
* Calculated Delta value
|
||||
*
|
||||
* @return delta value
|
||||
*/
|
||||
public double getDelta()
|
||||
{
|
||||
return mDelta;
|
||||
}
|
||||
|
||||
/**
|
||||
* Creates initial array of extremal frequency indices spaced at intervals of the grid frequency
|
||||
* set for all frequencies where a frequency band weighting is specified.
|
||||
*/
|
||||
private List<Integer> getInitialExtremalIndices()
|
||||
{
|
||||
int count = mSpecification.getExtremaCount();
|
||||
|
||||
int density = mSpecification.getGridDensity();
|
||||
|
||||
List<Integer> extremalIndices = new ArrayList<>();
|
||||
|
||||
for(int i = 0; i < count; i++)
|
||||
{
|
||||
extremalIndices.add(i * density);
|
||||
}
|
||||
|
||||
return extremalIndices;
|
||||
}
|
||||
|
||||
/**
|
||||
* Calculates the actual frequency response of a dense frequency grid using the polynomial
|
||||
* represented by the extremal indices.
|
||||
*/
|
||||
private void calculateGridFrequencyResponse()
|
||||
{
|
||||
mLog.debug("\tCalculating B");
|
||||
BigDecimal[] b = calculateB();
|
||||
|
||||
mLog.debug("\tCalculating Delta");
|
||||
calculateDelta(b);
|
||||
|
||||
mLog.debug("\tCalculating C");
|
||||
calculateC();
|
||||
|
||||
mLog.debug("\tCalculating D");
|
||||
calculateD(b);
|
||||
|
||||
mLog.debug("\tUpdating Grid Frequency Response");
|
||||
updateGridFrequencyResponse();
|
||||
}
|
||||
|
||||
/**
|
||||
* Updates the frequency response of frequencies specified in the dense frequency grid
|
||||
*/
|
||||
private void updateGridFrequencyResponse()
|
||||
{
|
||||
mGridFrequencyResponse = new double[mGrid.getSize()];
|
||||
|
||||
double[] gridFrequencyCosines = mGrid.getCosineFrequencyGrid();
|
||||
|
||||
for(int i = 0; i < mGridFrequencyResponse.length; i++)
|
||||
{
|
||||
//TODO: parallelize this part!!!!
|
||||
mGridFrequencyResponse[i] = getFrequencyResponse(gridFrequencyCosines[i]);
|
||||
|
||||
if(i % 1000 == 0)
|
||||
{
|
||||
mLog.debug("\t\tUpdating Frequency Response - index:" + i + " of " +
|
||||
mGridFrequencyResponse.length + " Value:" + mGridFrequencyResponse[i]);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Calculates the value of b across the set of extremal indices.
|
||||
*
|
||||
* Implements Oppenheim/Schafer Discrete Time Signal Processing, 3e, 2016, equation 115
|
||||
*
|
||||
* @return the array of b values of length L+2
|
||||
*/
|
||||
private BigDecimal[] calculateB()
|
||||
{
|
||||
int length = mExtremalIndices.size();
|
||||
|
||||
BigDecimal[] b = new BigDecimal[length];
|
||||
|
||||
for(int k = 0; k < length; k++)
|
||||
{
|
||||
b[k] = BigDecimal.ONE;
|
||||
|
||||
double xk = mGrid.getCosineFrequencyGrid()[mExtremalIndices.get(k)];
|
||||
|
||||
for(int i = 0; i < length; i++)
|
||||
{
|
||||
if(i != k)
|
||||
{
|
||||
double xi = mGrid.getCosineFrequencyGrid()[mExtremalIndices.get(i)];
|
||||
|
||||
double denominator = xk - xi;
|
||||
|
||||
if(Math.abs(denominator) < 0.00001)
|
||||
{
|
||||
denominator = 0.00001;
|
||||
}
|
||||
|
||||
b[k] = b[k].multiply(BigDecimal.valueOf(1.0 / denominator));
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
return b;
|
||||
}
|
||||
|
||||
/**
|
||||
* Calculates the value of delta that represents the maximum ripple for the current set of
|
||||
* extremal indices (L+2)
|
||||
*
|
||||
* Implements Oppenheim/Schafer Discrete Time Signal Processing, 3e, 2016, equation 114
|
||||
*
|
||||
* @param b calculated for the grid from equation 115
|
||||
*/
|
||||
private void calculateDelta(BigDecimal[] b)
|
||||
{
|
||||
BigDecimal numerator = BigDecimal.ZERO;
|
||||
BigDecimal denominator = BigDecimal.ZERO;
|
||||
|
||||
double sign = 1.0;
|
||||
|
||||
for(int k = 0; k < b.length; k++)
|
||||
{
|
||||
if(k < mExtremalIndices.size())
|
||||
{
|
||||
int extremalIndex = mExtremalIndices.get(k);
|
||||
|
||||
numerator = numerator.add(b[k].multiply(BigDecimal.valueOf(mGrid.getDesiredResponse()[extremalIndex])));
|
||||
denominator = denominator.add(b[k].multiply(BigDecimal.valueOf(sign / mGrid.getWeight()[extremalIndex])));
|
||||
sign = -sign;
|
||||
}
|
||||
else
|
||||
{
|
||||
mLog.error("Something went wrong -- the length of b exceeds the set of extremal indices");
|
||||
}
|
||||
}
|
||||
|
||||
mDelta = numerator.divide(denominator, 1023, BigDecimal.ROUND_HALF_DOWN).doubleValue();
|
||||
}
|
||||
|
||||
/**
|
||||
* Calculates the ideal frequency response (C) of the current extremal index set. This reference
|
||||
* is used to determine the level of error in the set of actual frequency responses calculated
|
||||
* from this set of extremal indices to determine convergence.
|
||||
*
|
||||
* Implements Oppenheim/Schafer Discrete Time Signal Processing, 3e, 2016, equation 116b
|
||||
*/
|
||||
private void calculateC()
|
||||
{
|
||||
int length = mSpecification.getExtremaCount() - 1;
|
||||
|
||||
mIdealFrequencyResponse = new double[length];
|
||||
|
||||
double sign = 1.0;
|
||||
|
||||
for(int k = 0; k < length; k++)
|
||||
{
|
||||
if(k < mExtremalIndices.size())
|
||||
{
|
||||
int index = mExtremalIndices.get(k);
|
||||
|
||||
mIdealFrequencyResponse[k] = mGrid.getDesiredResponse()[index] -
|
||||
(sign * mDelta / mGrid.getWeight()[index]);
|
||||
|
||||
sign = -sign;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Calculates the set of D values for the current extremal index set.
|
||||
*
|
||||
* Implements Oppenheim/Schafer Discrete Time Signal Processing, 3e, 2016, equation 116c
|
||||
*/
|
||||
private void calculateD(BigDecimal[] b)
|
||||
{
|
||||
int length = mExtremalIndices.size() - 1;
|
||||
|
||||
mD = new BigDecimal[length];
|
||||
|
||||
for(int k = 0; k < length; k++)
|
||||
{
|
||||
//Note: extremalCosines array is one index longer than d array, so we use length as
|
||||
//a pointer to the final (L+2) extremalCosine index
|
||||
mD[k] = b[k].multiply(BigDecimal.valueOf((mGrid.getCosineFrequencyGrid()[mExtremalIndices.get(k)] -
|
||||
mGrid.getCosineFrequencyGrid()[mExtremalIndices.get(length)])));
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Calculates the weighted error between the specification desired frequency response and the
|
||||
* actual frequency response of the current extremal index set across the frequencies in the
|
||||
* dense frequency grid.
|
||||
*
|
||||
* Implements Oppenheim/Schafer Discrete Time Signal Processing, 3e, 2016, equation 112
|
||||
*/
|
||||
public void calculateGridError()
|
||||
{
|
||||
int length = mGridFrequencyResponse.length;
|
||||
|
||||
mGridErrors = new double[length];
|
||||
|
||||
for(int i = 0; i < length; i++)
|
||||
{
|
||||
mGridErrors[i] = mGrid.getWeight()[i] *
|
||||
(mGrid.getDesiredResponse()[i] - mGridFrequencyResponse[i]);
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Identifies the indexes of the extremal error values using the Alternation theorem
|
||||
*
|
||||
* @throws FilterDesignException if the full set of extremal indices can't be identified
|
||||
*/
|
||||
private void findExtremalIndices() throws FilterDesignException
|
||||
{
|
||||
mExtremalIndices.clear();
|
||||
|
||||
//Check for extremum at error index 0
|
||||
if(((mGridErrors[0] > 0.0 && mGridErrors[0] > mGridErrors[1]) ||
|
||||
(mGridErrors[0] < 0.0 && mGridErrors[0] < mGridErrors[1])) &&
|
||||
isGTEDelta(mGridErrors[0]))
|
||||
{
|
||||
mExtremalIndices.add(0);
|
||||
}
|
||||
|
||||
//Check for extrema in middle error indices )
|
||||
for(int x = 1; x < mGridErrors.length - 1; x++)
|
||||
{
|
||||
if(((mGridErrors[x] > 0.0 && (mGridErrors[x - 1] <= mGridErrors[x] && mGridErrors[x] > mGridErrors[x + 1])) ||
|
||||
(mGridErrors[x] < 0.0 && (mGridErrors[x - 1] >= mGridErrors[x] && mGridErrors[x] < mGridErrors[x + 1]))) &&
|
||||
isGTEDelta(mGridErrors[x]))
|
||||
{
|
||||
mExtremalIndices.add(x);
|
||||
}
|
||||
}
|
||||
|
||||
//Check for extremum at final error index
|
||||
int last = mGridErrors.length - 1;
|
||||
|
||||
if(((mGridErrors[last] > 0.0 && (mGridErrors[last] > mGridErrors[last - 1])) ||
|
||||
(mGridErrors[last] < 0.0 && (mGridErrors[last] < mGridErrors[last - 1]))) &&
|
||||
isGTEDelta(mGridErrors[last]))
|
||||
{
|
||||
mExtremalIndices.add(last);
|
||||
}
|
||||
|
||||
//Ensure we have the minimum of extremals before we continue
|
||||
if(mExtremalIndices.size() < mSpecification.getExtremaCount())
|
||||
{
|
||||
throw new FilterDesignException("Couldn't find the minimum extremal frequencies in "
|
||||
+ "error set");
|
||||
}
|
||||
|
||||
//Enforce alternation theory -- only one extremal for each transition about the zero axis
|
||||
List<Integer> indicesToRemove = new ArrayList<>();
|
||||
|
||||
Iterator<Integer> it = mExtremalIndices.iterator();
|
||||
Integer current = it.next();
|
||||
Integer next;
|
||||
|
||||
boolean positiveAxis = mGridErrors[current] > 0.0;
|
||||
|
||||
while(it.hasNext())
|
||||
{
|
||||
next = it.next();
|
||||
|
||||
//Check for consecutive (redundant) indices on same side of zero axis - retain largest
|
||||
if(!(positiveAxis ^ (mGridErrors[next] > 0.0)))
|
||||
{
|
||||
if(Math.abs(mGridErrors[next]) <= Math.abs(mGridErrors[current]))
|
||||
{
|
||||
//Remove next error index that is less than current index and on
|
||||
it.remove();
|
||||
next = current;
|
||||
}
|
||||
else
|
||||
{
|
||||
//Since we can't remove the current index with the iterator, queue for later removal
|
||||
indicesToRemove.add(current);
|
||||
}
|
||||
}
|
||||
else
|
||||
{
|
||||
positiveAxis = !positiveAxis;
|
||||
}
|
||||
|
||||
current = next;
|
||||
}
|
||||
|
||||
//Remove redundant extremal indices that couldn't be removed via the iterator
|
||||
mExtremalIndices.removeAll(indicesToRemove);
|
||||
|
||||
//Truncate the list to one larger than needed by removing excess tailing indices
|
||||
while(mExtremalIndices.size() > mSpecification.getExtremaCount())
|
||||
{
|
||||
mExtremalIndices.remove(mExtremalIndices.size() - 1);
|
||||
}
|
||||
|
||||
//If we have one too many indices, delete the smaller of the first or last
|
||||
if(mExtremalIndices.size() > mSpecification.getExtremaCount())
|
||||
{
|
||||
int lastIndex = mExtremalIndices.size() - 1;
|
||||
|
||||
if(Math.abs(mGridErrors[mExtremalIndices.get(0)]) >
|
||||
Math.abs(mGridErrors[mExtremalIndices.get(lastIndex)]))
|
||||
{
|
||||
mExtremalIndices.remove(lastIndex);
|
||||
}
|
||||
else
|
||||
{
|
||||
mExtremalIndices.remove(0);
|
||||
}
|
||||
}
|
||||
|
||||
//Detect if we have too few indices
|
||||
if(mExtremalIndices.size() < mSpecification.getExtremaCount())
|
||||
{
|
||||
throw new FilterDesignException("Couldn't find the minimum extremal frequencies in "
|
||||
+ "error set");
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Indicates if the absolute value of the argument is greater than or equal to the delta
|
||||
* value with accuracy to 14 digits of precision. This avoids rounding errors at 15 digits or
|
||||
* precision or greater.
|
||||
*
|
||||
* @param value to evaluate
|
||||
* @return true if the absolute value is greater than or equal to the absolute delta value
|
||||
*/
|
||||
private boolean isGTEDelta(double value)
|
||||
{
|
||||
return Math.abs(value) - Math.abs(mDelta) > -1.0e-5;
|
||||
}
|
||||
|
||||
/**
|
||||
* Checks for convergence of the frequency response of the current set of extremal indices to
|
||||
* the filter specification by comparing the maximum absolute error value against the delta
|
||||
* value to determine if these two values are within the convergence threshold.
|
||||
*/
|
||||
public void checkConvergence()
|
||||
{
|
||||
double maximum = Math.abs(mGridErrors[mExtremalIndices.get(0)]);
|
||||
|
||||
for(int i = 1; i < mExtremalIndices.size(); i++)
|
||||
{
|
||||
double current = Math.abs(mGridErrors[mExtremalIndices.get(i)]);
|
||||
|
||||
if(current > maximum)
|
||||
{
|
||||
maximum = current;
|
||||
}
|
||||
}
|
||||
|
||||
double convergence = maximum - Math.abs(mDelta);
|
||||
|
||||
mConverged = convergence < CONVERGENCE_THRESHOLD;
|
||||
}
|
||||
|
||||
/**
|
||||
* Coverts/casts the double array to a float array
|
||||
*/
|
||||
private static float[] convertToFloatArray(double[] samples)
|
||||
{
|
||||
float[] converted = new float[samples.length];
|
||||
|
||||
for(int x = 0; x < samples.length; x++)
|
||||
{
|
||||
converted[x] = (float)samples[x];
|
||||
}
|
||||
|
||||
return converted;
|
||||
}
|
||||
|
||||
/**
|
||||
* Resamples the designed filter at half of the filter length at evenly spaced intervals of the
|
||||
* frequency spectrum from 0 to PI radians.
|
||||
*
|
||||
* @return frequency response of the filter sampled the desired filter length and order
|
||||
*/
|
||||
private double[] resample()
|
||||
{
|
||||
int length = mSpecification.getFilterLength();
|
||||
|
||||
if(length % 2 == 0)
|
||||
{
|
||||
length--;
|
||||
}
|
||||
|
||||
double half = (double)length / 2.0;
|
||||
|
||||
double[] resampled = new double[(int)Math.ceil(half)];
|
||||
|
||||
for(int x = 0; x < resampled.length; x++)
|
||||
{
|
||||
resampled[x] = getFrequencyResponse(Math.cos(Math.PI * (double)x / half));
|
||||
}
|
||||
|
||||
return resampled;
|
||||
}
|
||||
|
||||
/**
|
||||
* Applies Parks/McClelland filter type correction to the final frequency response array
|
||||
*
|
||||
* @param frequencyResponse of the resampled filter design from 0 <> ~ PI radians
|
||||
* @return corrected frequency response
|
||||
*/
|
||||
private double[] correctFrequencyResponse(double[] frequencyResponse)
|
||||
{
|
||||
double filterLength = mSpecification.getFilterLength();
|
||||
|
||||
switch(mSpecification.getFilterType())
|
||||
{
|
||||
case TYPE_1_ODD_LENGTH_EVEN_ORDER_SYMMETRICAL:
|
||||
//No correction needed
|
||||
break;
|
||||
case TYPE_2_EVEN_LENGTH_ODD_ORDER_SYMMETRICAL:
|
||||
//No correction needed
|
||||
break;
|
||||
case TYPE_3_ODD_LENGTH_EVEN_ORDER_ANTI_SYMMETRICAL:
|
||||
for(int x = 0; x < frequencyResponse.length; x++)
|
||||
{
|
||||
double frequencyRadians = Math.PI * (((double)x) / filterLength);
|
||||
frequencyResponse[x] *= Math.sin(TWO_PI * frequencyRadians);
|
||||
}
|
||||
break;
|
||||
case TYPE_4_EVEN_LENGTH_ODD_ORDER_ANTI_SYMMETRICAL:
|
||||
for(int x = 0; x < frequencyResponse.length; x++)
|
||||
{
|
||||
double frequencyRadians = Math.PI * (((double)x) / filterLength);
|
||||
frequencyResponse[x] *= Math.sin(Math.PI * frequencyRadians);
|
||||
}
|
||||
break;
|
||||
default:
|
||||
break;
|
||||
}
|
||||
|
||||
return frequencyResponse;
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,686 @@
|
||||
/*******************************************************************************
|
||||
* sdrtrunk
|
||||
* Copyright (C) 2014-2017 Dennis Sheirer
|
||||
*
|
||||
* This program is free software: you can redistribute it and/or modify
|
||||
* it under the terms of the GNU General Public License as published by
|
||||
* the Free Software Foundation, either version 3 of the License, or
|
||||
* (at your option) any later version.
|
||||
*
|
||||
* This program is distributed in the hope that it will be useful,
|
||||
* but WITHOUT ANY WARRANTY; without even the implied warranty of
|
||||
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
|
||||
* GNU General Public License for more details.
|
||||
*
|
||||
* You should have received a copy of the GNU General Public License
|
||||
* along with this program. If not, see <http://www.gnu.org/licenses/>
|
||||
*
|
||||
******************************************************************************/
|
||||
package io.github.dsheirer.dsp.filter.fir.remez;
|
||||
|
||||
import io.github.dsheirer.dsp.filter.design.FilterDesignException;
|
||||
import io.github.dsheirer.dsp.filter.fir.FIRFilterSpecification;
|
||||
import org.apache.commons.math3.analysis.polynomials.PolynomialFunctionLagrangeForm;
|
||||
import org.slf4j.Logger;
|
||||
import org.slf4j.LoggerFactory;
|
||||
|
||||
import java.math.BigDecimal;
|
||||
import java.text.DecimalFormat;
|
||||
import java.util.ArrayList;
|
||||
import java.util.Iterator;
|
||||
import java.util.List;
|
||||
|
||||
public class RemezFIRFilterDesignerWithLagrange
|
||||
{
|
||||
private final static Logger mLog = LoggerFactory.getLogger(RemezFIRFilterDesignerWithLagrange.class);
|
||||
|
||||
private static final double CONVERGENCE_THRESHOLD = 0.0001;
|
||||
public static final int MAXIMUM_ITERATION_COUNT = 40;
|
||||
public static final double TWO_PI = 2.0 * Math.PI;
|
||||
|
||||
private static final DecimalFormat DECIMAL_FORMATTER = new DecimalFormat("0.00000000");
|
||||
|
||||
private FIRFilterSpecification mSpecification;
|
||||
private Grid mGrid;
|
||||
private List<Integer> mExtremalIndices;
|
||||
|
||||
private double[] mD;
|
||||
private double[] mGridErrors;
|
||||
private double[] mGridFrequencyResponse;
|
||||
private double[] mIdealFrequencyResponse;
|
||||
private PolynomialFunctionLagrangeForm mLagrangeInterpolator;
|
||||
|
||||
private double mDelta;
|
||||
|
||||
private boolean mConverged;
|
||||
|
||||
/**
|
||||
* Linear FIR filter designer using the Parks-McClelland Remez exchange algorithm.
|
||||
*
|
||||
* Construct a filter specification using the builder methods in the FIRFilterSpecification
|
||||
* class and construct an instance of this class with the specification. Check the isValid()
|
||||
* method after construction to see if the filter design was successful. If successful, use the
|
||||
* getFrequencyResponse() and getImpulseResponse() methods to access the filter taps as the
|
||||
* specified length.
|
||||
*
|
||||
* If the filter cannot be designed from the specification the isValid() method will return
|
||||
* false and any attempts to access the getFrequencyResponse() or getImpulseResponse() methods
|
||||
* will throw a FilterDesignException.
|
||||
*
|
||||
* @param specification that defines a Type 1-4 linear phase FIR Filter
|
||||
*/
|
||||
public RemezFIRFilterDesignerWithLagrange(FIRFilterSpecification specification) throws FilterDesignException
|
||||
{
|
||||
mSpecification = specification;
|
||||
|
||||
design();
|
||||
}
|
||||
|
||||
/**
|
||||
* Designs the filter according to the specification
|
||||
*/
|
||||
private void design() throws FilterDesignException
|
||||
{
|
||||
mGrid = new Grid(mSpecification);
|
||||
mExtremalIndices = getInitialExtremalIndices();
|
||||
|
||||
int iterationCount = 0;
|
||||
|
||||
do
|
||||
{
|
||||
calculateGridFrequencyResponse();
|
||||
calculateGridError();
|
||||
findExtremalIndices();
|
||||
checkConvergence();
|
||||
|
||||
iterationCount++;
|
||||
}
|
||||
while(!mConverged && iterationCount < MAXIMUM_ITERATION_COUNT);
|
||||
|
||||
if(mConverged)
|
||||
{
|
||||
//Update frequency response using the final set of extremal indices
|
||||
calculateGridFrequencyResponse();
|
||||
}
|
||||
else
|
||||
{
|
||||
throw new FilterDesignException("Unable to design filter after [" + iterationCount + "] iterations");
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Indicates if the filter was successfully created from the filter specification. Check this
|
||||
* method before accessing frequency response or filter response, otherwise a FilterDesignException
|
||||
* is thrown.
|
||||
*
|
||||
* @return true if the filter was successfully designed.
|
||||
*/
|
||||
public boolean isValid()
|
||||
{
|
||||
return mConverged;
|
||||
}
|
||||
|
||||
/**
|
||||
* Frequency response of the designed filter with double precision
|
||||
*
|
||||
* @return half ( length / 2 ) of filter frequency response sampled at 0.0 - PI radians
|
||||
* @throws FilterDesignException if the specified filter cannot be designed
|
||||
*/
|
||||
public double[] getFrequencyResponse() throws FilterDesignException
|
||||
{
|
||||
if(!mConverged)
|
||||
{
|
||||
throw new FilterDesignException("Can't create filter from specification - failed "
|
||||
+ "to converge ");
|
||||
}
|
||||
|
||||
//Resample the final polynomial at the desired filter length
|
||||
double[] resampled = resample();
|
||||
|
||||
//Apply Parks/McClelland frequency response corrections according to the filter type
|
||||
return correctFrequencyResponse(resampled);
|
||||
}
|
||||
|
||||
/**
|
||||
* Impulse response of the designed filter with floating point precision.
|
||||
*
|
||||
* @return filter impulse response
|
||||
* @throws FilterDesignException if the specified filter cannot be designed
|
||||
*/
|
||||
public float[] getImpulseResponse() throws FilterDesignException
|
||||
{
|
||||
return convertToFloatArray(getImpulseResponseDoubles());
|
||||
}
|
||||
|
||||
/**
|
||||
* Impulse response of the designed filter with double precision
|
||||
*/
|
||||
public double[] getImpulseResponseDoubles() throws FilterDesignException
|
||||
{
|
||||
double[] frequencyResponse = getFrequencyResponse();
|
||||
|
||||
int length = mSpecification.getFilterLength();
|
||||
|
||||
double[] impulseResponse = new double[length];
|
||||
|
||||
double M;
|
||||
|
||||
switch(mSpecification.getFilterType())
|
||||
{
|
||||
case TYPE_1_ODD_LENGTH_EVEN_ORDER_SYMMETRICAL:
|
||||
M = ((double)length - 1.0) / 2.0;
|
||||
|
||||
for(int n = 0; n < length; n++)
|
||||
{
|
||||
double accumulator = frequencyResponse[0];
|
||||
|
||||
double frequency = TWO_PI * (n - M) / length;
|
||||
|
||||
for(int k = 1; k <= M; k++)
|
||||
{
|
||||
accumulator += 2.0 * frequencyResponse[k] * Math.cos(frequency * (double)k);
|
||||
}
|
||||
|
||||
impulseResponse[n] = accumulator / (double)length;
|
||||
}
|
||||
break;
|
||||
case TYPE_2_EVEN_LENGTH_ODD_ORDER_SYMMETRICAL:
|
||||
double offset = (double)(length - 1) / 2.0;
|
||||
|
||||
for(int n = 0; n < length; n++)
|
||||
{
|
||||
double accumulator = frequencyResponse[0];
|
||||
|
||||
double frequency = TWO_PI * ((double)n - offset) / (double)length;
|
||||
|
||||
for(int k = 1; k < frequencyResponse.length; k++)
|
||||
{
|
||||
accumulator += 2.0 * frequencyResponse[k] * Math.cos(frequency * (double)k);
|
||||
}
|
||||
|
||||
impulseResponse[n] = accumulator / (double)length;
|
||||
}
|
||||
break;
|
||||
}
|
||||
|
||||
return impulseResponse;
|
||||
}
|
||||
|
||||
/**
|
||||
* Calculates the frequency response of this filter at the specified cosine of the frequency
|
||||
*
|
||||
* Implements Oppenheim/Schafer Discrete Time Signal Processing, 3e, 2016, equation 116a to
|
||||
* determine the actual response of the specified frequency cosine using Lagrange
|
||||
* interpolation of the polynomial formed at the extremal index set.
|
||||
*
|
||||
* @param cosineOfFrequency to calculate the frequency response
|
||||
* @return actual frequency response for the specified frequency with the current set of
|
||||
* extreaml indices
|
||||
*/
|
||||
public double getFrequencyResponse(double cosineOfFrequency)
|
||||
{
|
||||
if(mLagrangeInterpolator == null)
|
||||
{
|
||||
throw new IllegalStateException("Lagrange interpolator is not setup - can't calculate frequency response");
|
||||
}
|
||||
|
||||
return mLagrangeInterpolator.value(cosineOfFrequency);
|
||||
|
||||
// double numerator = 0.0;
|
||||
// double denominator = 0.0;
|
||||
//
|
||||
// for( int k = 0; k < mExtremalIndices.size() - 1; k++ )
|
||||
// {
|
||||
// double cosineDelta = cosineOfFrequency - mGrid.getCosineFrequencyGrid()[ mExtremalIndices.get( k ) ];
|
||||
//
|
||||
// //If this frequency is close to one of the polynomial points, use the polynomial point for the response
|
||||
// if( Math.abs( cosineDelta ) < 1.0e-7 )
|
||||
// {
|
||||
// return mIdealFrequencyResponse[ k ];
|
||||
// }
|
||||
// else
|
||||
// {
|
||||
// double dkOverCosineDelta = mD[ k ] / cosineDelta;
|
||||
//
|
||||
// numerator += dkOverCosineDelta * mIdealFrequencyResponse[ k ];
|
||||
// denominator += dkOverCosineDelta;
|
||||
// }
|
||||
// }
|
||||
//
|
||||
// return numerator / denominator;
|
||||
}
|
||||
|
||||
/**
|
||||
* Calculated Delta value
|
||||
*
|
||||
* @return delta value
|
||||
*/
|
||||
public double getDelta()
|
||||
{
|
||||
return mDelta;
|
||||
}
|
||||
|
||||
/**
|
||||
* Creates initial array of extremal frequency indices spaced at intervals of the grid frequency
|
||||
* set for all frequencies where a frequency band weighting is specified.
|
||||
*/
|
||||
private List<Integer> getInitialExtremalIndices()
|
||||
{
|
||||
int count = mSpecification.getExtremaCount();
|
||||
|
||||
int density = mSpecification.getGridDensity();
|
||||
|
||||
List<Integer> extremalIndices = new ArrayList<>();
|
||||
|
||||
for(int i = 0; i < count; i++)
|
||||
{
|
||||
extremalIndices.add(i * density);
|
||||
}
|
||||
|
||||
return extremalIndices;
|
||||
}
|
||||
|
||||
/**
|
||||
* Calculates the actual frequency response of a dense frequency grid using the polynomial
|
||||
* represented by the extremal indices.
|
||||
*/
|
||||
private void calculateGridFrequencyResponse()
|
||||
{
|
||||
BigDecimal[] b = calculateB();
|
||||
|
||||
calculateDelta(b);
|
||||
|
||||
calculateC();
|
||||
|
||||
//Setup the lagrange polynomial so that we can determine the frequency response at the grid frequencies
|
||||
//where mExtremalIndices are the polynomial points, the subset of cosine frequency grid identified by the
|
||||
//extremal indices is the x value set and mIdealFrequencyResponse is the y axis value set
|
||||
double[] xAxis = new double[mIdealFrequencyResponse.length];
|
||||
|
||||
for(int x = 0; x < mExtremalIndices.size() - 1; x++)
|
||||
{
|
||||
int index = mExtremalIndices.get(x);
|
||||
|
||||
xAxis[x] = mGrid.getCosineFrequencyGrid()[index];
|
||||
}
|
||||
|
||||
mLagrangeInterpolator = new PolynomialFunctionLagrangeForm(xAxis, mIdealFrequencyResponse);
|
||||
|
||||
updateGridFrequencyResponse();
|
||||
}
|
||||
|
||||
/**
|
||||
* Updates the frequency response of frequencies specified in the dense frequency grid
|
||||
*/
|
||||
private void updateGridFrequencyResponse()
|
||||
{
|
||||
mGridFrequencyResponse = new double[mGrid.getSize()];
|
||||
|
||||
double[] gridFrequencyCosines = mGrid.getCosineFrequencyGrid();
|
||||
|
||||
for(int i = 0; i < mGridFrequencyResponse.length; i++)
|
||||
{
|
||||
mGridFrequencyResponse[i] = getFrequencyResponse(gridFrequencyCosines[i]);
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Calculates the value of b across the set of extremal indices.
|
||||
*
|
||||
* Implements Oppenheim/Schafer Discrete Time Signal Processing, 3e, 2016, equation 115
|
||||
*
|
||||
* @return the array of b values of length L+2
|
||||
*/
|
||||
private BigDecimal[] calculateB()
|
||||
{
|
||||
int length = mExtremalIndices.size();
|
||||
|
||||
BigDecimal[] b = new BigDecimal[length];
|
||||
|
||||
for(int k = 0; k < length; k++)
|
||||
{
|
||||
b[k] = BigDecimal.ONE;
|
||||
|
||||
double xk = mGrid.getCosineFrequencyGrid()[mExtremalIndices.get(k)];
|
||||
|
||||
for(int i = 0; i < length; i++)
|
||||
{
|
||||
if(i != k)
|
||||
{
|
||||
double xi = mGrid.getCosineFrequencyGrid()[mExtremalIndices.get(i)];
|
||||
|
||||
double denominator = xk - xi;
|
||||
|
||||
if(Math.abs(denominator) < 0.00001)
|
||||
{
|
||||
denominator = 0.00001;
|
||||
}
|
||||
|
||||
b[k] = b[k].multiply(BigDecimal.valueOf(1.0 / denominator));
|
||||
// b[ k ] *= 1.0 / denominator;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
return b;
|
||||
}
|
||||
|
||||
/**
|
||||
* Calculates the value of delta that represents the maximum ripple for the current set of
|
||||
* extremal indices (L+2)
|
||||
*
|
||||
* Implements Oppenheim/Schafer Discrete Time Signal Processing, 3e, 2016, equation 114
|
||||
*
|
||||
* @param b calculated for the grid from equation 115
|
||||
*/
|
||||
private void calculateDelta(BigDecimal[] b)
|
||||
{
|
||||
BigDecimal numerator = BigDecimal.ZERO;
|
||||
BigDecimal denominator = BigDecimal.ZERO;
|
||||
|
||||
double sign = 1.0;
|
||||
|
||||
for(int k = 0; k < b.length; k++)
|
||||
{
|
||||
if(k < mExtremalIndices.size())
|
||||
{
|
||||
int extremalIndex = mExtremalIndices.get(k);
|
||||
|
||||
numerator = numerator.add(b[k].multiply(BigDecimal.valueOf(mGrid.getDesiredResponse()[extremalIndex])));
|
||||
denominator = denominator.add(b[k].multiply(BigDecimal.valueOf(sign / mGrid.getWeight()[extremalIndex])));
|
||||
|
||||
sign = -sign;
|
||||
}
|
||||
else
|
||||
{
|
||||
mLog.error("Something went wrong -- the length of b exceeds the set of extremal indices");
|
||||
}
|
||||
}
|
||||
|
||||
//Set the scale to the maximum scale we could see for a primitive decimal
|
||||
mDelta = numerator.divide(denominator, 1023, BigDecimal.ROUND_HALF_DOWN).doubleValue();
|
||||
}
|
||||
|
||||
/**
|
||||
* Calculates the ideal frequency response (C) of the current extremal index set. This reference
|
||||
* is used to determine the level of error in the set of actual frequency responses calculated
|
||||
* from this set of extremal indices to determine convergence.
|
||||
*
|
||||
* Implements Oppenheim/Schafer Discrete Time Signal Processing, 3e, 2016, equation 116b
|
||||
*/
|
||||
private void calculateC()
|
||||
{
|
||||
int length = mSpecification.getExtremaCount() - 1;
|
||||
|
||||
mIdealFrequencyResponse = new double[length];
|
||||
|
||||
double sign = 1.0;
|
||||
|
||||
for(int k = 0; k < length; k++)
|
||||
{
|
||||
if(k < mExtremalIndices.size())
|
||||
{
|
||||
int index = mExtremalIndices.get(k);
|
||||
|
||||
mIdealFrequencyResponse[k] = mGrid.getDesiredResponse()[index] -
|
||||
(sign * mDelta / mGrid.getWeight()[index]);
|
||||
|
||||
sign = -sign;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Calculates the weighted error between the specification desired frequency response and the
|
||||
* actual frequency response of the current extremal index set across the frequencies in the
|
||||
* dense frequency grid.
|
||||
*
|
||||
* Implements Oppenheim/Schafer Discrete Time Signal Processing, 3e, 2016, equation 112
|
||||
*/
|
||||
public void calculateGridError()
|
||||
{
|
||||
int length = mGridFrequencyResponse.length;
|
||||
|
||||
mGridErrors = new double[length];
|
||||
|
||||
for(int i = 0; i < length; i++)
|
||||
{
|
||||
mGridErrors[i] = mGrid.getWeight()[i] * (mGrid.getDesiredResponse()[i] - mGridFrequencyResponse[i]);
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Identifies the indexes of the extremal error values using the Alternation theorem
|
||||
*
|
||||
* @throws FilterDesignException if the full set of extremal indices can't be identified
|
||||
*/
|
||||
private void findExtremalIndices() throws FilterDesignException
|
||||
{
|
||||
mExtremalIndices.clear();
|
||||
|
||||
//Check for extremum at error index 0
|
||||
if(((mGridErrors[0] > 0.0 && mGridErrors[0] > mGridErrors[1]) ||
|
||||
(mGridErrors[0] < 0.0 && mGridErrors[0] < mGridErrors[1])) &&
|
||||
isGTEDelta(mGridErrors[0]))
|
||||
{
|
||||
mExtremalIndices.add(0);
|
||||
}
|
||||
|
||||
//Check for extrema in middle error indices )
|
||||
for(int x = 1; x < mGridErrors.length - 1; x++)
|
||||
{
|
||||
if(((mGridErrors[x] > 0.0 && (mGridErrors[x - 1] <= mGridErrors[x] && mGridErrors[x] > mGridErrors[x + 1])) ||
|
||||
(mGridErrors[x] < 0.0 && (mGridErrors[x - 1] >= mGridErrors[x] && mGridErrors[x] < mGridErrors[x + 1]))) &&
|
||||
isGTEDelta(mGridErrors[x]))
|
||||
{
|
||||
mExtremalIndices.add(x);
|
||||
}
|
||||
}
|
||||
|
||||
//Check for extremum at final error index
|
||||
int last = mGridErrors.length - 1;
|
||||
|
||||
if(((mGridErrors[last] > 0.0 && (mGridErrors[last] > mGridErrors[last - 1])) ||
|
||||
(mGridErrors[last] < 0.0 && (mGridErrors[last] < mGridErrors[last - 1]))) &&
|
||||
isGTEDelta(mGridErrors[last]))
|
||||
{
|
||||
mExtremalIndices.add(last);
|
||||
}
|
||||
|
||||
//Ensure we have the minimum of extremals before we continue
|
||||
if(mExtremalIndices.size() < mSpecification.getExtremaCount())
|
||||
{
|
||||
throw new FilterDesignException("Couldn't find the minimum extremal frequencies in "
|
||||
+ "error set before checking for alternation theory - found:" + mExtremalIndices.size() +
|
||||
" required:" + mSpecification.getExtremaCount());
|
||||
}
|
||||
|
||||
//Enforce alternation theory -- only one extremal for each transition about the zero axis
|
||||
List<Integer> indicesToRemove = new ArrayList<>();
|
||||
|
||||
Iterator<Integer> it = mExtremalIndices.iterator();
|
||||
Integer current = it.next();
|
||||
Integer next;
|
||||
|
||||
boolean positiveAxis = mGridErrors[current] > 0.0;
|
||||
|
||||
while(it.hasNext())
|
||||
{
|
||||
next = it.next();
|
||||
|
||||
//Check for consecutive (redundant) indices on same side of zero axis - retain largest
|
||||
if(!(positiveAxis ^ (mGridErrors[next] > 0.0)))
|
||||
{
|
||||
if(Math.abs(mGridErrors[next]) <= Math.abs(mGridErrors[current]))
|
||||
{
|
||||
//Remove next error index that is less than current index and on
|
||||
it.remove();
|
||||
next = current;
|
||||
}
|
||||
else
|
||||
{
|
||||
//Since we can't remove the current index with the iterator, queue for later removal
|
||||
indicesToRemove.add(current);
|
||||
}
|
||||
}
|
||||
else
|
||||
{
|
||||
positiveAxis = !positiveAxis;
|
||||
}
|
||||
|
||||
current = next;
|
||||
}
|
||||
|
||||
//Remove redundant extremal indices that couldn't be removed via the iterator
|
||||
mExtremalIndices.removeAll(indicesToRemove);
|
||||
|
||||
//Truncate the list to one larger than needed by removing excess tailing indices
|
||||
while(mExtremalIndices.size() > mSpecification.getExtremaCount())
|
||||
{
|
||||
mExtremalIndices.remove(mExtremalIndices.size() - 1);
|
||||
}
|
||||
|
||||
//If we have one too many indices, delete the smaller of the first or last
|
||||
if(mExtremalIndices.size() > mSpecification.getExtremaCount())
|
||||
{
|
||||
int lastIndex = mExtremalIndices.size() - 1;
|
||||
|
||||
if(Math.abs(mGridErrors[mExtremalIndices.get(0)]) >
|
||||
Math.abs(mGridErrors[mExtremalIndices.get(lastIndex)]))
|
||||
{
|
||||
mExtremalIndices.remove(lastIndex);
|
||||
}
|
||||
else
|
||||
{
|
||||
mExtremalIndices.remove(0);
|
||||
}
|
||||
}
|
||||
|
||||
//Detect if we have too few indices
|
||||
if(mExtremalIndices.size() < mSpecification.getExtremaCount())
|
||||
{
|
||||
throw new FilterDesignException("Couldn't find the minimum extremal frequencies in "
|
||||
+ "error set - found:" + mExtremalIndices.size() + " required:" + mSpecification.getExtremaCount());
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Indicates if the absolute value of the argument is greater than or equal to the delta
|
||||
* value with accuracy to 14 digits of precision. This avoids rounding errors at 15 digits or
|
||||
* precision or greater.
|
||||
*
|
||||
* @param value to evaluate
|
||||
* @return true if the absolute value is greater than or equal to the absolute delta value
|
||||
*/
|
||||
private boolean isGTEDelta(double value)
|
||||
{
|
||||
return Math.abs(value) - Math.abs(mDelta) > -1.0e-5;
|
||||
}
|
||||
|
||||
/**
|
||||
* Checks for convergence of the frequency response of the current set of extremal indices to
|
||||
* the filter specification by comparing the maximum absolute error value against the delta
|
||||
* value to determine if these two values are within the convergence threshold.
|
||||
*/
|
||||
public void checkConvergence()
|
||||
{
|
||||
double maximum = Math.abs(mGridErrors[mExtremalIndices.get(0)]);
|
||||
|
||||
for(int i = 1; i < mExtremalIndices.size(); i++)
|
||||
{
|
||||
double current = Math.abs(mGridErrors[mExtremalIndices.get(i)]);
|
||||
|
||||
if(current > maximum)
|
||||
{
|
||||
maximum = current;
|
||||
}
|
||||
}
|
||||
|
||||
double convergence = maximum - Math.abs(mDelta);
|
||||
|
||||
mLog.debug("Convergence: " + convergence);
|
||||
|
||||
mConverged = convergence < CONVERGENCE_THRESHOLD;
|
||||
}
|
||||
|
||||
/**
|
||||
* Coverts/casts the double array to a float array
|
||||
*/
|
||||
private static float[] convertToFloatArray(double[] samples)
|
||||
{
|
||||
float[] converted = new float[samples.length];
|
||||
|
||||
for(int x = 0; x < samples.length; x++)
|
||||
{
|
||||
converted[x] = (float)samples[x];
|
||||
}
|
||||
|
||||
return converted;
|
||||
}
|
||||
|
||||
/**
|
||||
* Resamples the designed filter at half of the filter length at evenly spaced intervals of the
|
||||
* frequency spectrum from 0 to PI radians.
|
||||
*
|
||||
* @return frequency response of the filter sampled the desired filter length and order
|
||||
*/
|
||||
private double[] resample()
|
||||
{
|
||||
int length = mSpecification.getFilterLength();
|
||||
|
||||
if(length % 2 == 0)
|
||||
{
|
||||
length--;
|
||||
}
|
||||
|
||||
double half = (double)length / 2.0;
|
||||
|
||||
double[] resampled = new double[(int)Math.ceil(half)];
|
||||
|
||||
for(int x = 0; x < resampled.length; x++)
|
||||
{
|
||||
resampled[x] = getFrequencyResponse(Math.cos(Math.PI * (double)x / half));
|
||||
}
|
||||
|
||||
return resampled;
|
||||
}
|
||||
|
||||
/**
|
||||
* Applies Parks/McClelland filter type correction to the final frequency response array
|
||||
*
|
||||
* @param frequencyResponse of the resampled filter design from 0 <> ~ PI radians
|
||||
* @return corrected frequency response
|
||||
*/
|
||||
private double[] correctFrequencyResponse(double[] frequencyResponse)
|
||||
{
|
||||
double filterLength = mSpecification.getFilterLength();
|
||||
|
||||
switch(mSpecification.getFilterType())
|
||||
{
|
||||
case TYPE_1_ODD_LENGTH_EVEN_ORDER_SYMMETRICAL:
|
||||
//No correction needed
|
||||
break;
|
||||
case TYPE_2_EVEN_LENGTH_ODD_ORDER_SYMMETRICAL:
|
||||
//No correction needed
|
||||
break;
|
||||
case TYPE_3_ODD_LENGTH_EVEN_ORDER_ANTI_SYMMETRICAL:
|
||||
for(int x = 0; x < frequencyResponse.length; x++)
|
||||
{
|
||||
double frequencyRadians = Math.PI * (((double)x) / filterLength);
|
||||
frequencyResponse[x] *= Math.sin(TWO_PI * frequencyRadians);
|
||||
}
|
||||
break;
|
||||
case TYPE_4_EVEN_LENGTH_ODD_ORDER_ANTI_SYMMETRICAL:
|
||||
for(int x = 0; x < frequencyResponse.length; x++)
|
||||
{
|
||||
double frequencyRadians = Math.PI * (((double)x) / filterLength);
|
||||
frequencyResponse[x] *= Math.sin(Math.PI * frequencyRadians);
|
||||
}
|
||||
break;
|
||||
default:
|
||||
break;
|
||||
}
|
||||
|
||||
return frequencyResponse;
|
||||
}
|
||||
}
|
||||
@@ -142,7 +142,7 @@ public class AutomaticGainControl
|
||||
|
||||
public float process( float currentSample )
|
||||
{
|
||||
float delayedSample = mDelayBuffer.get( currentSample );
|
||||
float delayedSample = mDelayBuffer.putAndGet( currentSample );
|
||||
|
||||
double gain = MANUAL_AGC_GAIN;
|
||||
|
||||
|
||||
@@ -74,7 +74,7 @@ public class ComplexFeedForwardGainControl implements ComplexSampleListener
|
||||
}
|
||||
|
||||
/* Replace oldest envelope value with current envelope value */
|
||||
float oldestEnvelope = mEnvelopeHistory.get( envelope );
|
||||
float oldestEnvelope = mEnvelopeHistory.putAndGet( envelope );
|
||||
|
||||
/* If the oldest envelope value was the max envelope value, then we
|
||||
* have to rediscover the max value from the envelope history */
|
||||
|
||||
@@ -0,0 +1,188 @@
|
||||
/*******************************************************************************
|
||||
* sdrtrunk
|
||||
* Copyright (C) 2014-2017 Dennis Sheirer
|
||||
*
|
||||
* This program is free software: you can redistribute it and/or modify
|
||||
* it under the terms of the GNU General Public License as published by
|
||||
* the Free Software Foundation, either version 3 of the License, or
|
||||
* (at your option) any later version.
|
||||
*
|
||||
* This program is distributed in the hope that it will be useful,
|
||||
* but WITHOUT ANY WARRANTY; without even the implied warranty of
|
||||
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
|
||||
* GNU General Public License for more details.
|
||||
*
|
||||
* You should have received a copy of the GNU General Public License
|
||||
* along with this program. If not, see <http://www.gnu.org/licenses/>
|
||||
*
|
||||
******************************************************************************/
|
||||
package io.github.dsheirer.dsp.mixer;
|
||||
|
||||
import io.github.dsheirer.sample.complex.ComplexBuffer;
|
||||
import org.slf4j.Logger;
|
||||
import org.slf4j.LoggerFactory;
|
||||
|
||||
public class LowPhaseNoiseOscillator
|
||||
{
|
||||
private final static Logger mLog = LoggerFactory.getLogger(LowPhaseNoiseOscillator.class);
|
||||
|
||||
private static final double TWO_PI = 2.0 * Math.PI;
|
||||
private static final float THREE_HALVES = 3.0f / 2.0f;
|
||||
private int mSampleRate;
|
||||
private long mFrequency;
|
||||
private float mInphase = 1.0f;
|
||||
private float mQuadrature = 0.0f;
|
||||
private float mPreviousInphase = 1.0f;
|
||||
private float mPreviousQuadrature = 0.0f;
|
||||
private float mCosineAngle;
|
||||
private float mSineAngle;
|
||||
private float mGain = 1.0f;
|
||||
|
||||
/**
|
||||
* Ultra-low phase noise complex oscillator as described in Digital Signal Processing 3e, Lyons, p.786
|
||||
*
|
||||
* @param sampleRate for the oscillator
|
||||
* @param frequency to generate
|
||||
*/
|
||||
public LowPhaseNoiseOscillator(int sampleRate, long frequency)
|
||||
{
|
||||
mSampleRate = sampleRate;
|
||||
mFrequency = frequency;
|
||||
|
||||
update();
|
||||
}
|
||||
|
||||
/**
|
||||
* Sets the sample rate for this oscillator
|
||||
*/
|
||||
public void setSampleRate(int sampleRate)
|
||||
{
|
||||
mSampleRate = sampleRate;
|
||||
update();
|
||||
}
|
||||
|
||||
/**
|
||||
* Sample rate for this oscillator
|
||||
*/
|
||||
public int getSampleRate()
|
||||
{
|
||||
return mSampleRate;
|
||||
}
|
||||
|
||||
/**
|
||||
* Sets the tone frequency generated by this oscillator
|
||||
*/
|
||||
public void setFrequency(long frequency)
|
||||
{
|
||||
mFrequency = frequency;
|
||||
update();
|
||||
}
|
||||
|
||||
/**
|
||||
* Current tone frequency for this oscillator
|
||||
*/
|
||||
public long getFrequency()
|
||||
{
|
||||
return mFrequency;
|
||||
}
|
||||
|
||||
/**
|
||||
* Updates the internal values after a frequency or sample rate change
|
||||
*/
|
||||
private void update()
|
||||
{
|
||||
double anglePerSample = TWO_PI * mFrequency / mSampleRate;
|
||||
|
||||
mCosineAngle = (float)Math.cos(anglePerSample);
|
||||
mSineAngle = (float)Math.sin(anglePerSample);
|
||||
}
|
||||
|
||||
private void rotate()
|
||||
{
|
||||
mInphase = ((mPreviousInphase * mCosineAngle) - (mPreviousQuadrature * mSineAngle)) * mGain;
|
||||
mQuadrature = ((mPreviousInphase * mSineAngle) + (mPreviousQuadrature * mCosineAngle)) * mGain;
|
||||
|
||||
mPreviousInphase = mInphase;
|
||||
mPreviousQuadrature = mQuadrature;
|
||||
|
||||
mGain = THREE_HALVES - ((mPreviousInphase * mPreviousInphase) + (mPreviousQuadrature * mPreviousQuadrature));
|
||||
}
|
||||
|
||||
/**
|
||||
* Generates an array of samples with length of 2 x sample count containing interleaved inphase and quadrature
|
||||
* samples.
|
||||
*
|
||||
* @param sampleCount number of samples to generate
|
||||
* @return array that is sampleCount * 2 in length
|
||||
*/
|
||||
public float[] generate(int sampleCount)
|
||||
{
|
||||
float[] samples = new float[sampleCount * 2];
|
||||
|
||||
for(int x = 0; x < sampleCount; x++)
|
||||
{
|
||||
int index = x * 2;
|
||||
samples[index] = mInphase;
|
||||
samples[index + 1] = mQuadrature;
|
||||
rotate();
|
||||
}
|
||||
|
||||
return samples;
|
||||
}
|
||||
|
||||
public static void main(String[] args)
|
||||
{
|
||||
int sampleRate = 10000000;
|
||||
int frequency = 24000;
|
||||
int sampleSize = 65536;
|
||||
int warmUpIterations = 100000;
|
||||
int testIterations = 500000;
|
||||
|
||||
LowPhaseNoiseOscillator o = new LowPhaseNoiseOscillator(sampleRate, frequency);
|
||||
|
||||
mLog.debug("Warming up ...");
|
||||
|
||||
for(int x = 0; x < warmUpIterations; x++)
|
||||
{
|
||||
float[] samples = o.generate(sampleSize);
|
||||
}
|
||||
|
||||
mLog.debug("Testing ...");
|
||||
|
||||
long start = System.nanoTime();
|
||||
|
||||
for(int x = 0; x < testIterations; x++)
|
||||
{
|
||||
float[] samples = o.generate(sampleSize);
|
||||
}
|
||||
|
||||
long duration = System.nanoTime() - start;
|
||||
|
||||
double durationSeconds = (double)duration / 1000000000.0;
|
||||
mLog.debug("New Took:" + durationSeconds + " seconds to generate " + (testIterations * sampleSize) + " samples");
|
||||
|
||||
Oscillator o2 = new Oscillator(frequency, sampleRate);
|
||||
|
||||
mLog.debug("Warming up ...");
|
||||
|
||||
for(int x = 0; x < warmUpIterations; x++)
|
||||
{
|
||||
ComplexBuffer samples = o2.generateComplexBuffer(sampleSize);
|
||||
}
|
||||
|
||||
mLog.debug("Testing ...");
|
||||
|
||||
long start2 = System.nanoTime();
|
||||
|
||||
for(int x = 0; x < testIterations; x++)
|
||||
{
|
||||
float[] samples = o.generate(sampleSize);
|
||||
}
|
||||
|
||||
long duration2 = System.nanoTime() - start2;
|
||||
|
||||
double durationSeconds2 = (double)duration2 / 1000000000.0;
|
||||
mLog.debug("New Took:" + durationSeconds2 + " seconds to generate " + (testIterations * sampleSize) + " samples");
|
||||
|
||||
}
|
||||
}
|
||||
@@ -1,112 +1,152 @@
|
||||
/*******************************************************************************
|
||||
* SDR Trunk
|
||||
* Copyright (C) 2014 Dennis Sheirer
|
||||
*
|
||||
* This program is free software: you can redistribute it and/or modify
|
||||
* it under the terms of the GNU General Public License as published by
|
||||
* the Free Software Foundation, either version 3 of the License, or
|
||||
* (at your option) any later version.
|
||||
*
|
||||
* This program is distributed in the hope that it will be useful,
|
||||
* but WITHOUT ANY WARRANTY; without even the implied warranty of
|
||||
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
|
||||
* GNU General Public License for more details.
|
||||
*
|
||||
* You should have received a copy of the GNU General Public License
|
||||
* along with this program. If not, see <http://www.gnu.org/licenses/>
|
||||
* sdrtrunk
|
||||
* Copyright (C) 2014-2017 Dennis Sheirer
|
||||
*
|
||||
* This program is free software: you can redistribute it and/or modify
|
||||
* it under the terms of the GNU General Public License as published by
|
||||
* the Free Software Foundation, either version 3 of the License, or
|
||||
* (at your option) any later version.
|
||||
*
|
||||
* This program is distributed in the hope that it will be useful,
|
||||
* but WITHOUT ANY WARRANTY; without even the implied warranty of
|
||||
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
|
||||
* GNU General Public License for more details.
|
||||
*
|
||||
* You should have received a copy of the GNU General Public License
|
||||
* along with this program. If not, see <http://www.gnu.org/licenses/>
|
||||
*
|
||||
******************************************************************************/
|
||||
package io.github.dsheirer.dsp.mixer;
|
||||
|
||||
import io.github.dsheirer.sample.complex.Complex;
|
||||
import io.github.dsheirer.sample.complex.ComplexBuffer;
|
||||
import org.slf4j.Logger;
|
||||
import org.slf4j.LoggerFactory;
|
||||
|
||||
public class Oscillator
|
||||
{
|
||||
private final static Logger mLog = LoggerFactory.getLogger( Oscillator.class );
|
||||
private final static Logger mLog = LoggerFactory.getLogger(Oscillator.class);
|
||||
|
||||
private double mFrequency;
|
||||
private double mSampleRate;
|
||||
private boolean mEnabled = true;
|
||||
private double mFrequency;
|
||||
private double mSampleRate;
|
||||
|
||||
private Complex mAnglePerSample;
|
||||
private Complex mCurrentAngle = new Complex( 0.0f, -1.0f );
|
||||
private Complex mAnglePerSample;
|
||||
private Complex mCurrentAngle = new Complex(0.0f, -1.0f);
|
||||
|
||||
/**
|
||||
* Oscillator produces complex or float samples corresponding to a sine wave
|
||||
* oscillating at the specified frequency and sample rate
|
||||
*
|
||||
* @param frequency - positive or negative frequency in hertz
|
||||
* @param sampleRate - in hertz
|
||||
*/
|
||||
public Oscillator( long frequency, int sampleRate )
|
||||
{
|
||||
mSampleRate = (double)sampleRate;
|
||||
mFrequency = (double)frequency;
|
||||
|
||||
update();
|
||||
}
|
||||
|
||||
/**
|
||||
* Updates the internal values after a frequency or sample rate change
|
||||
*/
|
||||
private void update()
|
||||
{
|
||||
float anglePerSample =
|
||||
(float)( 2.0d * Math.PI * mFrequency / mSampleRate );
|
||||
/**
|
||||
* Oscillator produces complex or float samples corresponding to a sine wave
|
||||
* oscillating at the specified frequency and sample rate
|
||||
*
|
||||
* @param frequency - positive or negative frequency in hertz
|
||||
* @param sampleRate - in hertz
|
||||
*/
|
||||
public Oscillator(long frequency, double sampleRate)
|
||||
{
|
||||
mEnabled = frequency != 0;
|
||||
|
||||
mAnglePerSample = Complex.fromAngle( anglePerSample );
|
||||
}
|
||||
mSampleRate = sampleRate;
|
||||
mFrequency = (double)frequency;
|
||||
|
||||
/**
|
||||
* Sets or changes the frequency of this oscillator
|
||||
*/
|
||||
public void setFrequency( long frequency )
|
||||
{
|
||||
mFrequency = (double)frequency;
|
||||
update();
|
||||
}
|
||||
update();
|
||||
}
|
||||
|
||||
/**
|
||||
* Sets or changes the sample rate of this oscillator
|
||||
*/
|
||||
public void setSampleRate( int sampleRate )
|
||||
{
|
||||
mSampleRate = (double)sampleRate;
|
||||
update();
|
||||
}
|
||||
/**
|
||||
* Indicates if this oscillator has a non-zero frequency setting. When used as a frequency translating
|
||||
* mixer, setting a frequency of zero indicates that no frequency correction is needed, so this flag can
|
||||
* indicate when to apply the oscillator to frequency correct samples.
|
||||
*
|
||||
* @return true if the current frequency value is non-zero.
|
||||
*/
|
||||
public boolean enabled()
|
||||
{
|
||||
return mEnabled;
|
||||
}
|
||||
|
||||
/**
|
||||
* Steps the current angle by the angle per sample amount
|
||||
*/
|
||||
public void rotate()
|
||||
{
|
||||
mCurrentAngle.multiply( mAnglePerSample );
|
||||
mCurrentAngle.fastNormalize();
|
||||
}
|
||||
/**
|
||||
* Updates the internal values after a frequency or sample rate change
|
||||
*/
|
||||
private void update()
|
||||
{
|
||||
float anglePerSample =
|
||||
(float)(2.0d * Math.PI * mFrequency / mSampleRate);
|
||||
|
||||
public float inphase()
|
||||
{
|
||||
return mCurrentAngle.inphase();
|
||||
}
|
||||
|
||||
public float quadrature()
|
||||
{
|
||||
return mCurrentAngle.quadrature();
|
||||
}
|
||||
|
||||
/**
|
||||
* Get next complex sample
|
||||
*/
|
||||
public Complex getComplex()
|
||||
{
|
||||
return mCurrentAngle.copy();
|
||||
}
|
||||
mAnglePerSample = Complex.fromAngle(anglePerSample);
|
||||
}
|
||||
|
||||
/**
|
||||
* Get the next float sample
|
||||
*/
|
||||
public float getFloat()
|
||||
{
|
||||
return mCurrentAngle.real();
|
||||
}
|
||||
/**
|
||||
* Sets or changes the frequency of this oscillator
|
||||
*/
|
||||
public void setFrequency(long frequency)
|
||||
{
|
||||
mEnabled = frequency != 0;
|
||||
mFrequency = (double)frequency;
|
||||
update();
|
||||
}
|
||||
|
||||
/**
|
||||
* Sets or changes the sample rate of this oscillator
|
||||
*/
|
||||
public void setSampleRate(double sampleRate)
|
||||
{
|
||||
mSampleRate = sampleRate;
|
||||
update();
|
||||
}
|
||||
|
||||
/**
|
||||
* Steps the current angle by the angle per sample amount
|
||||
*/
|
||||
public void rotate()
|
||||
{
|
||||
mCurrentAngle.multiply(mAnglePerSample);
|
||||
mCurrentAngle.fastNormalize();
|
||||
}
|
||||
|
||||
public float inphase()
|
||||
{
|
||||
return mCurrentAngle.inphase();
|
||||
}
|
||||
|
||||
public float quadrature()
|
||||
{
|
||||
return mCurrentAngle.quadrature();
|
||||
}
|
||||
|
||||
/**
|
||||
* Get next complex sample
|
||||
*/
|
||||
public Complex getComplex()
|
||||
{
|
||||
return mCurrentAngle.copy();
|
||||
}
|
||||
|
||||
/**
|
||||
* Get the next float sample
|
||||
*/
|
||||
public float getFloat()
|
||||
{
|
||||
return mCurrentAngle.real();
|
||||
}
|
||||
|
||||
/**
|
||||
* Generates the requested number of complex samples and returns them in a complex buffer
|
||||
*
|
||||
* @param sampleCount to generate
|
||||
* @return complex buffer containing the requested complex samples
|
||||
*/
|
||||
public ComplexBuffer generateComplexBuffer(int sampleCount)
|
||||
{
|
||||
float[] buffer = new float[sampleCount * 2];
|
||||
|
||||
for(int x = 0; x < sampleCount; x++)
|
||||
{
|
||||
int index = 2 * x;
|
||||
buffer[index] = inphase();
|
||||
buffer[index + 1] = quadrature();
|
||||
|
||||
rotate();
|
||||
}
|
||||
|
||||
return new ComplexBuffer(buffer);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -41,7 +41,7 @@ import io.github.dsheirer.instrument.tap.stream.EyeDiagramDataTap;
|
||||
import io.github.dsheirer.sample.Listener;
|
||||
import io.github.dsheirer.sample.complex.Complex;
|
||||
import io.github.dsheirer.sample.complex.ComplexSampleListener;
|
||||
import io.github.dsheirer.source.tuner.frequency.IFrequencyChangeListener;
|
||||
import io.github.dsheirer.source.ISourceEventListener;
|
||||
|
||||
import java.util.ArrayList;
|
||||
import java.util.Arrays;
|
||||
@@ -72,7 +72,7 @@ public class LSMDemodulator implements Instrumentable, ComplexSampleListener
|
||||
|
||||
private CostasLoop mCostasLoop = new CostasLoop();
|
||||
|
||||
private IFrequencyChangeListener mFrequencyChangeListener;
|
||||
private ISourceEventListener mFrequencyChangeListener;
|
||||
|
||||
private EyeDiagramDataTap mEyeDiagramDataTap;
|
||||
|
||||
@@ -146,12 +146,12 @@ public class LSMDemodulator implements Instrumentable, ComplexSampleListener
|
||||
}
|
||||
}
|
||||
|
||||
public void addListener( IFrequencyChangeListener listener )
|
||||
public void addListener( ISourceEventListener listener )
|
||||
{
|
||||
mFrequencyChangeListener = listener;
|
||||
}
|
||||
|
||||
public void removeListener( IFrequencyChangeListener listener )
|
||||
public void removeListener( ISourceEventListener listener )
|
||||
{
|
||||
mFrequencyChangeListener = null;
|
||||
}
|
||||
@@ -201,8 +201,8 @@ public class LSMDemodulator implements Instrumentable, ComplexSampleListener
|
||||
// if( correctionNeeded && mFrequencyChangeListener != null )
|
||||
// {
|
||||
// mLog.debug( "Issuing Frequency Correction: " + mFrequencyError );
|
||||
// mFrequencyChangeListener.frequencyChanged(
|
||||
// new FrequencyChangeEvent( Attribute.FREQUENCY_ERROR,
|
||||
// mFrequencyChangeListener.process(
|
||||
// new SourceEvent( Attribute.FREQUENCY_ERROR,
|
||||
// mFrequencyError ) );
|
||||
// }
|
||||
//
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
/*******************************************************************************
|
||||
* SDR Trunk
|
||||
* Copyright (C) 2014 Dennis Sheirer
|
||||
* 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
|
||||
@@ -17,17 +17,25 @@
|
||||
******************************************************************************/
|
||||
package io.github.dsheirer.gui.control;
|
||||
|
||||
import io.github.dsheirer.source.ISourceEventProcessor;
|
||||
import io.github.dsheirer.source.InvalidFrequencyException;
|
||||
import io.github.dsheirer.source.SourceEvent;
|
||||
import io.github.dsheirer.source.SourceEvent.Event;
|
||||
import io.github.dsheirer.source.SourceException;
|
||||
import io.github.dsheirer.source.tuner.frequency.FrequencyChangeEvent;
|
||||
import io.github.dsheirer.source.tuner.frequency.FrequencyChangeEvent.Event;
|
||||
import io.github.dsheirer.source.tuner.frequency.IFrequencyChangeProcessor;
|
||||
import net.miginfocom.swing.MigLayout;
|
||||
import org.slf4j.Logger;
|
||||
import org.slf4j.LoggerFactory;
|
||||
|
||||
import javax.swing.*;
|
||||
import java.awt.*;
|
||||
import javax.swing.JLabel;
|
||||
import javax.swing.JOptionPane;
|
||||
import javax.swing.JPanel;
|
||||
import javax.swing.JTextField;
|
||||
import java.awt.Color;
|
||||
import java.awt.Cursor;
|
||||
import java.awt.Font;
|
||||
import java.awt.Image;
|
||||
import java.awt.Point;
|
||||
import java.awt.Toolkit;
|
||||
import java.awt.event.KeyEvent;
|
||||
import java.awt.event.KeyListener;
|
||||
import java.awt.event.MouseEvent;
|
||||
@@ -40,13 +48,12 @@ import java.util.HashMap;
|
||||
import java.util.Iterator;
|
||||
import java.util.List;
|
||||
|
||||
public class JFrequencyControl extends JPanel implements IFrequencyChangeProcessor
|
||||
public class JFrequencyControl extends JPanel implements ISourceEventProcessor
|
||||
{
|
||||
private static final long serialVersionUID = 1L;
|
||||
private final static Logger mLog =
|
||||
LoggerFactory.getLogger(JFrequencyControl.class);
|
||||
private final static Logger mLog = LoggerFactory.getLogger(JFrequencyControl.class);
|
||||
|
||||
private List<IFrequencyChangeProcessor> mProcessors = new ArrayList<>();
|
||||
private List<ISourceEventProcessor> mProcessors = new ArrayList<>();
|
||||
|
||||
private Color mHighlightColor = Color.YELLOW;
|
||||
|
||||
@@ -54,7 +61,7 @@ public class JFrequencyControl extends JPanel implements IFrequencyChangeProcess
|
||||
|
||||
private Cursor mBlankCursor;
|
||||
|
||||
private HashMap<Integer, Digit> mDigits = new HashMap<Integer, Digit>();
|
||||
private HashMap<Integer,Digit> mDigits = new HashMap<Integer,Digit>();
|
||||
|
||||
public JFrequencyControl()
|
||||
{
|
||||
@@ -73,7 +80,7 @@ public class JFrequencyControl extends JPanel implements IFrequencyChangeProcess
|
||||
|
||||
Font font = new Font(Font.MONOSPACED, Font.BOLD, 30);
|
||||
|
||||
for (int x = 9; x >= 0; x--)
|
||||
for(int x = 9; x >= 0; x--)
|
||||
{
|
||||
Digit digit = null;
|
||||
|
||||
@@ -81,13 +88,13 @@ public class JFrequencyControl extends JPanel implements IFrequencyChangeProcess
|
||||
{
|
||||
digit = new Digit(x);
|
||||
}
|
||||
catch (ParseException e)
|
||||
catch(ParseException e)
|
||||
{
|
||||
mLog.error("JFrequencyControl - parse exception "
|
||||
+ "constructing a digit - " + e);
|
||||
+ "constructing a digit - " + e);
|
||||
}
|
||||
|
||||
if (digit != null)
|
||||
if(digit != null)
|
||||
{
|
||||
mDigits.put(x, digit);
|
||||
|
||||
@@ -95,7 +102,7 @@ public class JFrequencyControl extends JPanel implements IFrequencyChangeProcess
|
||||
|
||||
digit.setFont(font);
|
||||
|
||||
if (x == 6)
|
||||
if(x == 6)
|
||||
{
|
||||
JLabel period = new JLabel(".");
|
||||
|
||||
@@ -116,7 +123,7 @@ public class JFrequencyControl extends JPanel implements IFrequencyChangeProcess
|
||||
Image cursorImage = Toolkit.getDefaultToolkit().createImage(imageByte);
|
||||
|
||||
mBlankCursor = Toolkit.getDefaultToolkit()
|
||||
.createCustomCursor(cursorImage, new Point(0, 0), "cursor");
|
||||
.createCustomCursor(cursorImage, new Point(0, 0), "cursor");
|
||||
|
||||
revalidate();
|
||||
}
|
||||
@@ -126,7 +133,7 @@ public class JFrequencyControl extends JPanel implements IFrequencyChangeProcess
|
||||
{
|
||||
super.setEnabled(enabled);
|
||||
|
||||
for (Digit digit : mDigits.values())
|
||||
for(Digit digit : mDigits.values())
|
||||
{
|
||||
digit.setEnabled(enabled);
|
||||
}
|
||||
@@ -137,9 +144,9 @@ public class JFrequencyControl extends JPanel implements IFrequencyChangeProcess
|
||||
* rebroadcast this event, just set the control to indicate the new frequency.
|
||||
*/
|
||||
@Override
|
||||
public void frequencyChanged(FrequencyChangeEvent event)
|
||||
public void process(SourceEvent event)
|
||||
{
|
||||
if (event.getEvent() == Event.NOTIFICATION_FREQUENCY_CHANGE)
|
||||
if(event.getEvent() == Event.NOTIFICATION_FREQUENCY_CHANGE)
|
||||
{
|
||||
setFrequency(event.getValue().longValue(), false);
|
||||
}
|
||||
@@ -153,7 +160,7 @@ public class JFrequencyControl extends JPanel implements IFrequencyChangeProcess
|
||||
{
|
||||
mFrequency = frequency;
|
||||
|
||||
for (Digit digit : mDigits.values())
|
||||
for(Digit digit : mDigits.values())
|
||||
{
|
||||
digit.setFrequency(frequency, fireChangeEvent);
|
||||
}
|
||||
@@ -168,7 +175,7 @@ public class JFrequencyControl extends JPanel implements IFrequencyChangeProcess
|
||||
{
|
||||
long frequency = 0;
|
||||
|
||||
for (Digit digit : mDigits.values())
|
||||
for(Digit digit : mDigits.values())
|
||||
{
|
||||
frequency += digit.getFrequency();
|
||||
}
|
||||
@@ -176,27 +183,26 @@ public class JFrequencyControl extends JPanel implements IFrequencyChangeProcess
|
||||
mFrequency = frequency;
|
||||
}
|
||||
|
||||
private void fireFrequencyChanged() throws SourceException
|
||||
private void fireSourceChanged() throws SourceException
|
||||
{
|
||||
updateFrequency();
|
||||
|
||||
Iterator<IFrequencyChangeProcessor> it = mProcessors.iterator();
|
||||
Iterator<ISourceEventProcessor> it = mProcessors.iterator();
|
||||
|
||||
FrequencyChangeEvent event = new FrequencyChangeEvent(
|
||||
Event.REQUEST_FREQUENCY_CHANGE, mFrequency);
|
||||
SourceEvent event = SourceEvent.frequencyRequest(mFrequency);
|
||||
|
||||
while (it.hasNext())
|
||||
while(it.hasNext())
|
||||
{
|
||||
it.next().frequencyChanged(event);
|
||||
it.next().process(event);
|
||||
}
|
||||
}
|
||||
|
||||
public void addListener(IFrequencyChangeProcessor processor)
|
||||
public void addListener(ISourceEventProcessor processor)
|
||||
{
|
||||
mProcessors.add(processor);
|
||||
}
|
||||
|
||||
public void removeListener(IFrequencyChangeProcessor processor)
|
||||
public void removeListener(ISourceEventProcessor processor)
|
||||
{
|
||||
mProcessors.remove(processor);
|
||||
}
|
||||
@@ -228,17 +234,17 @@ public class JFrequencyControl extends JPanel implements IFrequencyChangeProcess
|
||||
public void setFrequency(long frequency, boolean fireChangeEvent)
|
||||
{
|
||||
//Strip the digits higher than this one
|
||||
long lower = frequency % (long) (Math.pow(10, mPower + 1));
|
||||
long lower = frequency % (long)(Math.pow(10, mPower + 1));
|
||||
|
||||
//Set the value to int value of dividing by 10 to this power
|
||||
long value = (long) (lower / (long) (Math.pow(10, mPower)));
|
||||
long value = (long)(lower / (long)(Math.pow(10, mPower)));
|
||||
|
||||
set(value, fireChangeEvent);
|
||||
}
|
||||
|
||||
public long getFrequency()
|
||||
{
|
||||
return mValue * (long) Math.pow(10, mPower);
|
||||
return mValue * (long)Math.pow(10, mPower);
|
||||
}
|
||||
|
||||
public void increment()
|
||||
@@ -248,7 +254,7 @@ public class JFrequencyControl extends JPanel implements IFrequencyChangeProcess
|
||||
|
||||
public void increment(boolean fireChangeEvent)
|
||||
{
|
||||
if (isEnabled())
|
||||
if(isEnabled())
|
||||
{
|
||||
set(mValue + 1, fireChangeEvent);
|
||||
}
|
||||
@@ -261,7 +267,7 @@ public class JFrequencyControl extends JPanel implements IFrequencyChangeProcess
|
||||
|
||||
public void decrement(boolean fireChangeEvent)
|
||||
{
|
||||
if (isEnabled())
|
||||
if(isEnabled())
|
||||
{
|
||||
set(mValue - 1, fireChangeEvent);
|
||||
}
|
||||
@@ -287,13 +293,13 @@ public class JFrequencyControl extends JPanel implements IFrequencyChangeProcess
|
||||
mValue = amount;
|
||||
|
||||
|
||||
while (mValue < 0)
|
||||
while(mValue < 0)
|
||||
{
|
||||
mValue += 10;
|
||||
|
||||
Digit nextHigherDigit = mDigits.get(mPower + 1);
|
||||
|
||||
if (nextHigherDigit != null)
|
||||
if(nextHigherDigit != null)
|
||||
{
|
||||
nextHigherDigit.decrement(false);
|
||||
higherDigitDecremented = true;
|
||||
@@ -301,55 +307,55 @@ public class JFrequencyControl extends JPanel implements IFrequencyChangeProcess
|
||||
|
||||
}
|
||||
|
||||
while (mValue > 9)
|
||||
while(mValue > 9)
|
||||
{
|
||||
mValue -= 10;
|
||||
|
||||
Digit nextHigherDigit = mDigits.get(mPower + 1);
|
||||
|
||||
if (nextHigherDigit != null)
|
||||
if(nextHigherDigit != null)
|
||||
{
|
||||
nextHigherDigit.increment(false);
|
||||
higherDigitIncremented = true;
|
||||
}
|
||||
}
|
||||
|
||||
if (fireChangeEvent)
|
||||
if(fireChangeEvent)
|
||||
{
|
||||
try
|
||||
{
|
||||
fireFrequencyChanged();
|
||||
fireSourceChanged();
|
||||
}
|
||||
catch (SourceException se)
|
||||
catch(SourceException se)
|
||||
{
|
||||
mValue = previous;
|
||||
|
||||
if (higherDigitIncremented)
|
||||
if(higherDigitIncremented)
|
||||
{
|
||||
Digit nextHigherDigit = mDigits.get(mPower + 1);
|
||||
|
||||
if (nextHigherDigit != null)
|
||||
if(nextHigherDigit != null)
|
||||
{
|
||||
nextHigherDigit.decrement(false);
|
||||
}
|
||||
}
|
||||
|
||||
if (higherDigitDecremented)
|
||||
if(higherDigitDecremented)
|
||||
{
|
||||
Digit nextHigherDigit = mDigits.get(mPower + 1);
|
||||
|
||||
if (nextHigherDigit != null)
|
||||
if(nextHigherDigit != null)
|
||||
{
|
||||
nextHigherDigit.increment(false);
|
||||
}
|
||||
}
|
||||
|
||||
if (se instanceof InvalidFrequencyException)
|
||||
if(se instanceof InvalidFrequencyException)
|
||||
{
|
||||
InvalidFrequencyException ife = (InvalidFrequencyException) se;
|
||||
InvalidFrequencyException ife = (InvalidFrequencyException)se;
|
||||
|
||||
JOptionPane.showMessageDialog(this, "Frequency [" + ife.getInvalidFrequency() +
|
||||
"] exceeds the frequency limit [" + ife.getValidFrequency() + "] for this tuner.");
|
||||
"] exceeds the frequency limit [" + ife.getValidFrequency() + "] for this tuner.");
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -361,15 +367,15 @@ public class JFrequencyControl extends JPanel implements IFrequencyChangeProcess
|
||||
}
|
||||
|
||||
private class Listener implements KeyListener,
|
||||
MouseListener,
|
||||
MouseWheelListener
|
||||
MouseListener,
|
||||
MouseWheelListener
|
||||
{
|
||||
@Override
|
||||
public void keyReleased(KeyEvent e)
|
||||
{
|
||||
int key = e.getKeyCode();
|
||||
|
||||
switch (key)
|
||||
switch(key)
|
||||
{
|
||||
case KeyEvent.VK_0:
|
||||
case KeyEvent.VK_NUMPAD0:
|
||||
@@ -456,7 +462,7 @@ public class JFrequencyControl extends JPanel implements IFrequencyChangeProcess
|
||||
@Override
|
||||
public void mouseClicked(MouseEvent e)
|
||||
{
|
||||
switch (e.getButton())
|
||||
switch(e.getButton())
|
||||
{
|
||||
case MouseEvent.BUTTON1:
|
||||
increment();
|
||||
|
||||
@@ -21,7 +21,10 @@ import io.github.dsheirer.instrument.gui.SampleModel;
|
||||
import io.github.dsheirer.instrument.tap.Tap;
|
||||
import io.github.dsheirer.instrument.tap.TapViewPanel;
|
||||
|
||||
import java.awt.*;
|
||||
import java.awt.Color;
|
||||
import java.awt.FontMetrics;
|
||||
import java.awt.Graphics;
|
||||
import java.awt.Graphics2D;
|
||||
import java.awt.geom.Path2D;
|
||||
import java.awt.geom.Rectangle2D;
|
||||
import java.util.List;
|
||||
@@ -90,7 +93,7 @@ public class BinaryTapViewPanel extends TapViewPanel
|
||||
* Paints a polyline representing a boolean value with a 0 or 1 label in
|
||||
* the middle of it
|
||||
*
|
||||
* @param graphics
|
||||
* @param graphics object
|
||||
* @param index - index of the sample int the sample buffer
|
||||
* @param value - bit value, 0 or 1
|
||||
*/
|
||||
|
||||
@@ -18,8 +18,6 @@
|
||||
|
||||
package io.github.dsheirer.module;
|
||||
|
||||
import java.util.concurrent.ScheduledExecutorService;
|
||||
|
||||
public abstract class Module
|
||||
{
|
||||
/**
|
||||
@@ -59,9 +57,9 @@ public abstract class Module
|
||||
public abstract void reset();
|
||||
|
||||
/**
|
||||
* Start processing. Provides an executor to use for scheduling tasks
|
||||
* Start processing.
|
||||
*/
|
||||
public abstract void start( ScheduledExecutorService executor );
|
||||
public abstract void start();
|
||||
|
||||
/**
|
||||
* Stop processing
|
||||
|
||||
@@ -24,9 +24,6 @@ import io.github.dsheirer.audio.IAudioPacketProvider;
|
||||
import io.github.dsheirer.audio.squelch.ISquelchStateListener;
|
||||
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.heartbeat.IHeartbeatProvider;
|
||||
import io.github.dsheirer.channel.metadata.AttributeChangeRequest;
|
||||
import io.github.dsheirer.channel.metadata.IAttributeChangeRequestListener;
|
||||
import io.github.dsheirer.channel.metadata.IAttributeChangeRequestProvider;
|
||||
@@ -60,12 +57,14 @@ import io.github.dsheirer.sample.real.IUnFilteredRealBufferListener;
|
||||
import io.github.dsheirer.sample.real.IUnFilteredRealBufferProvider;
|
||||
import io.github.dsheirer.sample.real.RealBuffer;
|
||||
import io.github.dsheirer.source.ComplexSource;
|
||||
import io.github.dsheirer.source.ISourceEventListener;
|
||||
import io.github.dsheirer.source.ISourceEventProvider;
|
||||
import io.github.dsheirer.source.RealSource;
|
||||
import io.github.dsheirer.source.Source;
|
||||
import io.github.dsheirer.source.tuner.frequency.FrequencyChangeEvent;
|
||||
import io.github.dsheirer.source.tuner.frequency.IFrequencyChangeListener;
|
||||
import io.github.dsheirer.source.tuner.frequency.IFrequencyChangeProvider;
|
||||
import io.github.dsheirer.util.ThreadPool;
|
||||
import io.github.dsheirer.source.SourceEvent;
|
||||
import io.github.dsheirer.source.heartbeat.Heartbeat;
|
||||
import io.github.dsheirer.source.heartbeat.IHeartbeatListener;
|
||||
import io.github.dsheirer.source.heartbeat.IHeartbeatProvider;
|
||||
import org.slf4j.Logger;
|
||||
import org.slf4j.LoggerFactory;
|
||||
|
||||
@@ -101,8 +100,8 @@ public class ProcessingChain implements IChannelEventListener
|
||||
private Broadcaster<ChannelEvent> mChannelEventBroadcaster = new Broadcaster<>();
|
||||
private Broadcaster<ComplexBuffer> mComplexBufferBroadcaster = new Broadcaster<>();
|
||||
private Broadcaster<DecoderStateEvent> mDecoderStateEventBroadcaster = new Broadcaster<>();
|
||||
private Broadcaster<FrequencyChangeEvent> mFrequencyChangeEventBroadcaster = new Broadcaster<>();
|
||||
private Broadcaster<Heartbeat> mHeartbeatBroadcaster = new Broadcaster<>();
|
||||
private Broadcaster<SourceEvent> mSourceEventBroadcaster = new Broadcaster<>();
|
||||
private Broadcaster<Message> mMessageBroadcaster = new Broadcaster<>();
|
||||
private Broadcaster<RealBuffer> mFilteredRealBufferBroadcaster = new Broadcaster<>();
|
||||
private Broadcaster<RealBuffer> mUnFilteredRealBufferBroadcaster = new Broadcaster<>();
|
||||
@@ -190,11 +189,13 @@ public class ProcessingChain implements IChannelEventListener
|
||||
}
|
||||
|
||||
/**
|
||||
* Applies a source to this processing chain. Processing won't start until the start() method is invoked.
|
||||
* Applies a sample source to this processing chain. Processing won't
|
||||
* start until the start() method is invoked.
|
||||
*
|
||||
* @param source - real or complex source
|
||||
* @throws IllegalStateException if the processing chain is currently processing with another source. Invoke stop()
|
||||
* before applying a new source.
|
||||
* @param source - real or complex sample source
|
||||
* @throws IllegalStateException if the processing chain is currently
|
||||
* processing with another source. Invoke stop() before applying a new
|
||||
* source.
|
||||
*/
|
||||
public void setSource(Source source) throws IllegalStateException
|
||||
{
|
||||
@@ -316,14 +317,13 @@ public class ProcessingChain implements IChannelEventListener
|
||||
mDecoderStateEventBroadcaster.addListener(((IDecoderStateEventListener) module).getDecoderStateListener());
|
||||
}
|
||||
|
||||
if(module instanceof IFrequencyChangeListener)
|
||||
if(module instanceof ISourceEventListener)
|
||||
{
|
||||
Listener<FrequencyChangeEvent> frequencyChangeEventListener =
|
||||
((IFrequencyChangeListener)module).getFrequencyChangeListener();
|
||||
Listener<SourceEvent> listener = ((ISourceEventListener)module).getSourceEventListener();
|
||||
|
||||
if(frequencyChangeEventListener != null)
|
||||
if(listener != null)
|
||||
{
|
||||
mFrequencyChangeEventBroadcaster.addListener(frequencyChangeEventListener);
|
||||
mSourceEventBroadcaster.addListener(listener);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -389,9 +389,9 @@ public class ProcessingChain implements IChannelEventListener
|
||||
mDecoderStateEventBroadcaster.removeListener(((IDecoderStateEventListener) module).getDecoderStateListener());
|
||||
}
|
||||
|
||||
if(module instanceof IFrequencyChangeListener)
|
||||
if(module instanceof ISourceEventListener)
|
||||
{
|
||||
mFrequencyChangeEventBroadcaster.removeListener(((IFrequencyChangeListener) module).getFrequencyChangeListener());
|
||||
mSourceEventBroadcaster.removeListener(((ISourceEventListener) module).getSourceEventListener());
|
||||
}
|
||||
|
||||
if(module instanceof IHeartbeatListener)
|
||||
@@ -451,14 +451,9 @@ public class ProcessingChain implements IChannelEventListener
|
||||
((IDecoderStateEventProvider) module).setDecoderStateListener(mDecoderStateEventBroadcaster);
|
||||
}
|
||||
|
||||
if(module instanceof IFrequencyChangeProvider)
|
||||
{
|
||||
((IFrequencyChangeProvider) module).setFrequencyChangeListener(mFrequencyChangeEventBroadcaster);
|
||||
}
|
||||
|
||||
if(module instanceof IHeartbeatProvider)
|
||||
{
|
||||
((IHeartbeatProvider)module).setHeartbeatListener(mHeartbeatBroadcaster);
|
||||
((IHeartbeatProvider)module).getHeartbeatManager().addHeartbeatListener(mHeartbeatBroadcaster);
|
||||
}
|
||||
|
||||
if(module instanceof IMessageProvider)
|
||||
@@ -471,6 +466,16 @@ public class ProcessingChain implements IChannelEventListener
|
||||
((IFilteredRealBufferProvider) module).setFilteredRealBufferListener(mFilteredRealBufferBroadcaster);
|
||||
}
|
||||
|
||||
if(module instanceof IMessageProvider)
|
||||
{
|
||||
((IMessageProvider) module).setMessageListener(mMessageBroadcaster);
|
||||
}
|
||||
|
||||
if(module instanceof ISourceEventProvider)
|
||||
{
|
||||
((ISourceEventProvider) module).setSourceEventListener(mSourceEventBroadcaster);
|
||||
}
|
||||
|
||||
if(module instanceof ISquelchStateProvider)
|
||||
{
|
||||
((ISquelchStateProvider) module).setSquelchStateListener(mSquelchStateBroadcaster);
|
||||
@@ -513,14 +518,9 @@ public class ProcessingChain implements IChannelEventListener
|
||||
((IDecoderStateEventProvider) module).setDecoderStateListener(null);
|
||||
}
|
||||
|
||||
if(module instanceof IFrequencyChangeProvider)
|
||||
{
|
||||
((IFrequencyChangeProvider) module).setFrequencyChangeListener(null);
|
||||
}
|
||||
|
||||
if(module instanceof IHeartbeatProvider)
|
||||
{
|
||||
((IHeartbeatProvider)module).removeHeartbeatListener();
|
||||
((IHeartbeatProvider)module).getHeartbeatManager().removeHeartbeatListener(mHeartbeatBroadcaster);
|
||||
}
|
||||
|
||||
if(module instanceof IMessageProvider)
|
||||
@@ -533,6 +533,16 @@ public class ProcessingChain implements IChannelEventListener
|
||||
((IFilteredRealBufferProvider) module).setFilteredRealBufferListener(null);
|
||||
}
|
||||
|
||||
if(module instanceof IMessageProvider)
|
||||
{
|
||||
((IMessageProvider) module).setMessageListener(null);
|
||||
}
|
||||
|
||||
if(module instanceof ISourceEventProvider)
|
||||
{
|
||||
((ISourceEventProvider) module).setSourceEventListener(null);
|
||||
}
|
||||
|
||||
if(module instanceof ISquelchStateProvider)
|
||||
{
|
||||
((ISquelchStateProvider) module).setSquelchStateListener(null);
|
||||
@@ -584,7 +594,7 @@ public class ProcessingChain implements IChannelEventListener
|
||||
{
|
||||
try
|
||||
{
|
||||
module.start(ThreadPool.SCHEDULED);
|
||||
module.start();
|
||||
}
|
||||
catch(Exception e)
|
||||
{
|
||||
@@ -600,8 +610,7 @@ public class ProcessingChain implements IChannelEventListener
|
||||
}
|
||||
|
||||
/**
|
||||
* Stops processing if the chain is currently processing. Invocations on
|
||||
* an already stopped chain have no effect.
|
||||
* Stops processing if the chain is currently processing. Invocations on an already stopped chain have no effect.
|
||||
*/
|
||||
public void stop()
|
||||
{
|
||||
@@ -628,15 +637,14 @@ public class ProcessingChain implements IChannelEventListener
|
||||
((RealSource) mSource).removeListener(mFilteredRealBufferBroadcaster);
|
||||
break;
|
||||
default:
|
||||
throw new IllegalArgumentException("Unrecognized source "
|
||||
+ "sample type - cannot start processing chain");
|
||||
throw new IllegalArgumentException("Unrecognized source sample type - cannot start processing " +
|
||||
"chain");
|
||||
}
|
||||
|
||||
/* Release the source */
|
||||
mSource.dispose();
|
||||
mSource = null;
|
||||
}
|
||||
|
||||
}
|
||||
}
|
||||
|
||||
@@ -767,18 +775,18 @@ public class ProcessingChain implements IChannelEventListener
|
||||
* Adds the listener to receive frequency change events from the processing chain
|
||||
* @param listener to receive events
|
||||
*/
|
||||
public void addFrequencyChangeListener(Listener<FrequencyChangeEvent> listener)
|
||||
public void addFrequencyChangeListener(Listener<SourceEvent> listener)
|
||||
{
|
||||
mFrequencyChangeEventBroadcaster.addListener(listener);
|
||||
mSourceEventBroadcaster.addListener(listener);
|
||||
}
|
||||
|
||||
/**
|
||||
* Removes the listener from receiving frequency change events
|
||||
* @param listener to remove
|
||||
*/
|
||||
public void removeFrequencyChangeListener(Listener<FrequencyChangeEvent> listener)
|
||||
public void removeFrequencyChangeListener(Listener<SourceEvent> listener)
|
||||
{
|
||||
mFrequencyChangeEventBroadcaster.removeListener(listener);
|
||||
mSourceEventBroadcaster.removeListener(listener);
|
||||
}
|
||||
|
||||
/**
|
||||
|
||||
@@ -22,8 +22,7 @@ import io.github.dsheirer.message.Message;
|
||||
import io.github.dsheirer.module.Module;
|
||||
import io.github.dsheirer.sample.Listener;
|
||||
|
||||
public abstract class Decoder extends Module
|
||||
implements IMessageProvider, Listener<Message>
|
||||
public abstract class Decoder extends Module implements IMessageProvider, Listener<Message>
|
||||
{
|
||||
/* This has to be a broadcaster in order for references to persist */
|
||||
protected Listener<Message> mMessageListener;
|
||||
@@ -82,4 +81,19 @@ public abstract class Decoder extends Module
|
||||
{
|
||||
broadcast( message );
|
||||
}
|
||||
|
||||
@Override
|
||||
public void reset()
|
||||
{
|
||||
}
|
||||
|
||||
@Override
|
||||
public void start()
|
||||
{
|
||||
}
|
||||
|
||||
@Override
|
||||
public void stop()
|
||||
{
|
||||
}
|
||||
}
|
||||
|
||||
@@ -66,6 +66,7 @@ import io.github.dsheirer.module.decode.mdc1200.MDCDecoderState;
|
||||
import io.github.dsheirer.module.decode.mdc1200.MDCMessageFilter;
|
||||
import io.github.dsheirer.module.decode.mpt1327.DecodeConfigMPT1327;
|
||||
import io.github.dsheirer.module.decode.mpt1327.MPT1327Decoder;
|
||||
import io.github.dsheirer.module.decode.mpt1327.MPT1327Decoder.Sync;
|
||||
import io.github.dsheirer.module.decode.mpt1327.MPT1327DecoderEditor;
|
||||
import io.github.dsheirer.module.decode.mpt1327.MPT1327DecoderState;
|
||||
import io.github.dsheirer.module.decode.mpt1327.MPT1327MessageFilter;
|
||||
@@ -73,7 +74,7 @@ import io.github.dsheirer.module.decode.nbfm.DecodeConfigNBFM;
|
||||
import io.github.dsheirer.module.decode.nbfm.NBFMDecoder;
|
||||
import io.github.dsheirer.module.decode.nbfm.NBFMDecoderEditor;
|
||||
import io.github.dsheirer.module.decode.p25.DecodeConfigP25Phase1;
|
||||
import io.github.dsheirer.module.decode.p25.P25Decoder;
|
||||
import io.github.dsheirer.module.decode.p25.P25Decoder.Modulation;
|
||||
import io.github.dsheirer.module.decode.p25.P25DecoderEditor;
|
||||
import io.github.dsheirer.module.decode.p25.P25DecoderState;
|
||||
import io.github.dsheirer.module.decode.p25.P25_C4FMDecoder;
|
||||
@@ -145,7 +146,7 @@ public class DecoderFactory
|
||||
Metadata metadata)
|
||||
{
|
||||
|
||||
/* Get the optional alias list for the decode modules to use */
|
||||
/* Get the optional alias list for the decode modules to use */
|
||||
AliasList aliasList = aliasModel.getAliasList(channel.getAliasListName());
|
||||
|
||||
List<Module> modules = getPrimaryModules(channelModel, channelMapModel, channelProcessingManager, aliasList,
|
||||
@@ -170,7 +171,7 @@ public class DecoderFactory
|
||||
|
||||
ChannelType channelType = channel.getChannelType();
|
||||
|
||||
/* Baseband low-pass filter pass and stop frequencies */
|
||||
/* Baseband low-pass filter pass and stop frequencies */
|
||||
DecodeConfiguration decodeConfig = channel.getDecodeConfiguration();
|
||||
|
||||
int iqPass = decodeConfig.getDecoderType().getChannelBandwidth() / 2;
|
||||
@@ -212,7 +213,7 @@ public class DecoderFactory
|
||||
|
||||
ChannelMap channelMap = channelMapModel.getChannelMap(mptConfig.getChannelMapName());
|
||||
|
||||
MPT1327Decoder.Sync sync = mptConfig.getSync();
|
||||
Sync sync = mptConfig.getSync();
|
||||
|
||||
modules.add(new MPT1327Decoder(aliasList, sync));
|
||||
|
||||
@@ -221,7 +222,7 @@ public class DecoderFactory
|
||||
|
||||
if(channelType == ChannelType.STANDARD)
|
||||
{
|
||||
modules.add(new TrafficChannelManager(channelModel, decodeConfig, channel.getEventLogConfiguration(),
|
||||
modules.add(new TrafficChannelManager(channelModel, decodeConfig,
|
||||
channel.getRecordConfiguration(), channel.getSystem(), channel.getSite(),
|
||||
(aliasList != null ? aliasList.getName() : null), mptConfig.getTrafficChannelPoolSize()));
|
||||
}
|
||||
@@ -240,7 +241,7 @@ public class DecoderFactory
|
||||
case P25_PHASE1:
|
||||
DecodeConfigP25Phase1 p25Config = (DecodeConfigP25Phase1) decodeConfig;
|
||||
|
||||
P25Decoder.Modulation modulation = p25Config.getModulation();
|
||||
Modulation modulation = p25Config.getModulation();
|
||||
|
||||
|
||||
switch(modulation)
|
||||
@@ -249,12 +250,12 @@ public class DecoderFactory
|
||||
modules.add(new FMDemodulatorModule(P25_C4FM_IQ_FILTER));
|
||||
modules.add(new DemodulatedAudioFilterModule(P25_C4FM_DEMOD_FILTER, 1.0f));
|
||||
modules.add(new P25_C4FMDecoder(aliasList, decodeConfig.getAFCMaximumCorrection()));
|
||||
modules.add(new P25DecoderState(aliasList, channelType, P25Decoder.Modulation.C4FM,
|
||||
modules.add(new P25DecoderState(aliasList, channelType, Modulation.C4FM,
|
||||
p25Config.getIgnoreDataCalls()));
|
||||
break;
|
||||
case CQPSK:
|
||||
modules.add(new P25_LSMDecoder(aliasList));
|
||||
modules.add(new P25DecoderState(aliasList, channelType, P25Decoder.Modulation.CQPSK, p25Config.getIgnoreDataCalls()));
|
||||
modules.add(new P25DecoderState(aliasList, channelType, Modulation.CQPSK, p25Config.getIgnoreDataCalls()));
|
||||
break;
|
||||
default:
|
||||
throw new IllegalArgumentException("Unrecognized P25 Phase 1 Modulation [" + modulation + "]");
|
||||
@@ -262,7 +263,7 @@ public class DecoderFactory
|
||||
|
||||
if(channelType == ChannelType.STANDARD)
|
||||
{
|
||||
modules.add(new TrafficChannelManager(channelModel, decodeConfig, channel.getEventLogConfiguration(),
|
||||
modules.add(new TrafficChannelManager(channelModel, decodeConfig,
|
||||
channel.getRecordConfiguration(), channel.getSystem(), channel.getSite(),
|
||||
(aliasList != null ? aliasList.getName() : null), p25Config.getTrafficChannelPoolSize()));
|
||||
}
|
||||
@@ -337,7 +338,7 @@ public class DecoderFactory
|
||||
}
|
||||
}
|
||||
|
||||
/* If we don't have any filters, add an ALL-PASS filter */
|
||||
/* If we don't have any filters, add an ALL-PASS filter */
|
||||
if(filterSet.getFilters().isEmpty())
|
||||
{
|
||||
filterSet.addFilter(new AllPassFilter<Message>());
|
||||
@@ -510,4 +511,4 @@ public class DecoderFactory
|
||||
|
||||
return null;
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -22,8 +22,6 @@ import io.github.dsheirer.module.decode.DecoderType;
|
||||
import io.github.dsheirer.module.decode.PrimaryDecoder;
|
||||
import io.github.dsheirer.module.decode.config.DecodeConfiguration;
|
||||
|
||||
import java.util.concurrent.ScheduledExecutorService;
|
||||
|
||||
public class AMDecoder extends PrimaryDecoder
|
||||
{
|
||||
public AMDecoder( DecodeConfiguration config )
|
||||
@@ -42,10 +40,10 @@ public class AMDecoder extends PrimaryDecoder
|
||||
{
|
||||
}
|
||||
|
||||
@Override
|
||||
public void start( ScheduledExecutorService executor )
|
||||
{
|
||||
}
|
||||
@Override
|
||||
public void start()
|
||||
{
|
||||
}
|
||||
|
||||
@Override
|
||||
public void stop()
|
||||
|
||||
@@ -38,7 +38,6 @@ import io.github.dsheirer.sample.real.RealBuffer;
|
||||
|
||||
import java.util.ArrayList;
|
||||
import java.util.List;
|
||||
import java.util.concurrent.ScheduledExecutorService;
|
||||
|
||||
/**
|
||||
* Fleetsync II Decoder - 1200 baud 2FSK decoder that can process 48k sample rate
|
||||
@@ -173,19 +172,4 @@ public class Fleetsync2Decoder extends Decoder implements IFilteredRealBufferLis
|
||||
{
|
||||
return mDecimationFilter;
|
||||
}
|
||||
|
||||
@Override
|
||||
public void reset()
|
||||
{
|
||||
}
|
||||
|
||||
@Override
|
||||
public void start( ScheduledExecutorService executor )
|
||||
{
|
||||
}
|
||||
|
||||
@Override
|
||||
public void stop()
|
||||
{
|
||||
}
|
||||
}
|
||||
|
||||
@@ -34,7 +34,6 @@ import io.github.dsheirer.util.StringUtils;
|
||||
|
||||
import java.util.Iterator;
|
||||
import java.util.TreeSet;
|
||||
import java.util.concurrent.ScheduledExecutorService;
|
||||
|
||||
public class Fleetsync2DecoderState extends DecoderState
|
||||
{
|
||||
@@ -63,27 +62,12 @@ public class Fleetsync2DecoderState extends DecoderState
|
||||
return DecoderType.FLEETSYNC2;
|
||||
}
|
||||
|
||||
@Override
|
||||
public void start(ScheduledExecutorService executor)
|
||||
{
|
||||
}
|
||||
|
||||
@Override
|
||||
public void stop()
|
||||
{
|
||||
}
|
||||
|
||||
@Override
|
||||
public void dispose()
|
||||
{
|
||||
super.dispose();
|
||||
}
|
||||
|
||||
@Override
|
||||
public void init()
|
||||
{
|
||||
}
|
||||
|
||||
/**
|
||||
* Resets the overall decoder state and clears any accumulated event details
|
||||
*/
|
||||
@@ -96,6 +80,24 @@ public class Fleetsync2DecoderState extends DecoderState
|
||||
resetState();
|
||||
}
|
||||
|
||||
@Override
|
||||
public void start()
|
||||
{
|
||||
|
||||
}
|
||||
|
||||
@Override
|
||||
public void stop()
|
||||
{
|
||||
|
||||
}
|
||||
|
||||
@Override
|
||||
public void init()
|
||||
{
|
||||
|
||||
}
|
||||
|
||||
/**
|
||||
* Resets this decoder state
|
||||
*/
|
||||
|
||||
@@ -41,7 +41,6 @@ import io.github.dsheirer.sample.real.RealSampleListener;
|
||||
|
||||
import java.util.ArrayList;
|
||||
import java.util.List;
|
||||
import java.util.concurrent.ScheduledExecutorService;
|
||||
|
||||
/**
|
||||
* LJ1200 - 1200 baud 2FSK decoder
|
||||
@@ -191,19 +190,4 @@ public class LJ1200Decoder extends Decoder implements IFilteredRealBufferListene
|
||||
{
|
||||
return mDecimationFilter;
|
||||
}
|
||||
|
||||
@Override
|
||||
public void reset()
|
||||
{
|
||||
}
|
||||
|
||||
@Override
|
||||
public void start( ScheduledExecutorService executor )
|
||||
{
|
||||
}
|
||||
|
||||
@Override
|
||||
public void stop()
|
||||
{
|
||||
}
|
||||
}
|
||||
|
||||
@@ -36,7 +36,6 @@ import java.util.Collections;
|
||||
import java.util.List;
|
||||
import java.util.Set;
|
||||
import java.util.TreeSet;
|
||||
import java.util.concurrent.ScheduledExecutorService;
|
||||
|
||||
public class LJ1200DecoderState extends DecoderState
|
||||
{
|
||||
@@ -60,16 +59,6 @@ public class LJ1200DecoderState extends DecoderState
|
||||
return DecoderType.LJ_1200;
|
||||
}
|
||||
|
||||
@Override
|
||||
public void start(ScheduledExecutorService executor)
|
||||
{
|
||||
}
|
||||
|
||||
@Override
|
||||
public void stop()
|
||||
{
|
||||
}
|
||||
|
||||
@Override
|
||||
public void receive(Message message)
|
||||
{
|
||||
@@ -122,12 +111,6 @@ public class LJ1200DecoderState extends DecoderState
|
||||
return sb.toString();
|
||||
}
|
||||
|
||||
@Override
|
||||
public void init()
|
||||
{
|
||||
/* No initialization steps */
|
||||
}
|
||||
|
||||
@Override
|
||||
public void receiveDecoderStateEvent(DecoderStateEvent event)
|
||||
{
|
||||
@@ -148,4 +131,22 @@ public class LJ1200DecoderState extends DecoderState
|
||||
mAddresses.clear();
|
||||
resetState();
|
||||
}
|
||||
|
||||
@Override
|
||||
public void start()
|
||||
{
|
||||
|
||||
}
|
||||
|
||||
@Override
|
||||
public void stop()
|
||||
{
|
||||
|
||||
}
|
||||
|
||||
@Override
|
||||
public void init()
|
||||
{
|
||||
|
||||
}
|
||||
}
|
||||
|
||||
@@ -33,7 +33,6 @@ import io.github.dsheirer.sample.real.RealBuffer;
|
||||
|
||||
import java.util.ArrayList;
|
||||
import java.util.List;
|
||||
import java.util.concurrent.ScheduledExecutorService;
|
||||
|
||||
public class LTRNetDecoder extends Decoder
|
||||
implements IUnFilteredRealBufferListener, Instrumentable
|
||||
@@ -119,14 +118,4 @@ public class LTRNetDecoder extends Decoder
|
||||
{
|
||||
mLTRMessageFramer.reset();
|
||||
}
|
||||
|
||||
@Override
|
||||
public void start( ScheduledExecutorService executor )
|
||||
{
|
||||
}
|
||||
|
||||
@Override
|
||||
public void stop()
|
||||
{
|
||||
}
|
||||
}
|
||||
|
||||
@@ -39,7 +39,6 @@ import java.text.DecimalFormat;
|
||||
import java.util.HashMap;
|
||||
import java.util.Iterator;
|
||||
import java.util.TreeSet;
|
||||
import java.util.concurrent.ScheduledExecutorService;
|
||||
|
||||
public class LTRNetDecoderState extends DecoderState
|
||||
{
|
||||
@@ -695,6 +694,24 @@ public class LTRNetDecoderState extends DecoderState
|
||||
resetState();
|
||||
}
|
||||
|
||||
@Override
|
||||
public void start()
|
||||
{
|
||||
|
||||
}
|
||||
|
||||
@Override
|
||||
public void stop()
|
||||
{
|
||||
|
||||
}
|
||||
|
||||
@Override
|
||||
public void init()
|
||||
{
|
||||
|
||||
}
|
||||
|
||||
/**
|
||||
* Resets the decoder state after a call or other decode event
|
||||
*/
|
||||
@@ -764,12 +781,6 @@ public class LTRNetDecoderState extends DecoderState
|
||||
}
|
||||
}
|
||||
|
||||
@Override
|
||||
public void init()
|
||||
{
|
||||
/* No initialization steps required */
|
||||
}
|
||||
|
||||
@Override
|
||||
public void receiveDecoderStateEvent(DecoderStateEvent event)
|
||||
{
|
||||
@@ -785,14 +796,4 @@ public class LTRNetDecoderState extends DecoderState
|
||||
break;
|
||||
}
|
||||
}
|
||||
|
||||
@Override
|
||||
public void start(ScheduledExecutorService executor)
|
||||
{
|
||||
}
|
||||
|
||||
@Override
|
||||
public void stop()
|
||||
{
|
||||
}
|
||||
}
|
||||
|
||||
@@ -28,8 +28,6 @@ import io.github.dsheirer.sample.Listener;
|
||||
import io.github.dsheirer.sample.real.IUnFilteredRealBufferListener;
|
||||
import io.github.dsheirer.sample.real.RealBuffer;
|
||||
|
||||
import java.util.concurrent.ScheduledExecutorService;
|
||||
|
||||
public class LTRStandardDecoder extends Decoder implements IUnFilteredRealBufferListener
|
||||
{
|
||||
public static final int LTR_STANDARD_MESSAGE_LENGTH = 40;
|
||||
@@ -84,14 +82,4 @@ public class LTRStandardDecoder extends Decoder implements IUnFilteredRealBuffer
|
||||
{
|
||||
mLTRMessageFramer.reset();
|
||||
}
|
||||
|
||||
@Override
|
||||
public void start( ScheduledExecutorService executor )
|
||||
{
|
||||
}
|
||||
|
||||
@Override
|
||||
public void stop()
|
||||
{
|
||||
}
|
||||
}
|
||||
|
||||
@@ -46,7 +46,6 @@ import java.util.List;
|
||||
import java.util.Map;
|
||||
import java.util.Set;
|
||||
import java.util.TreeSet;
|
||||
import java.util.concurrent.ScheduledExecutorService;
|
||||
|
||||
public class LTRStandardDecoderState extends DecoderState
|
||||
{
|
||||
@@ -75,18 +74,6 @@ public class LTRStandardDecoderState extends DecoderState
|
||||
return DecoderType.LTR_STANDARD;
|
||||
}
|
||||
|
||||
@Override
|
||||
public void start(ScheduledExecutorService executor)
|
||||
{
|
||||
// TODO Auto-generated method stub
|
||||
|
||||
}
|
||||
|
||||
@Override
|
||||
public void stop()
|
||||
{
|
||||
}
|
||||
|
||||
@Override
|
||||
public void receive(Message message)
|
||||
{
|
||||
@@ -211,6 +198,24 @@ public class LTRStandardDecoderState extends DecoderState
|
||||
resetState();
|
||||
}
|
||||
|
||||
@Override
|
||||
public void start()
|
||||
{
|
||||
|
||||
}
|
||||
|
||||
@Override
|
||||
public void stop()
|
||||
{
|
||||
|
||||
}
|
||||
|
||||
@Override
|
||||
public void init()
|
||||
{
|
||||
|
||||
}
|
||||
|
||||
/**
|
||||
* Performs a temporal reset following a call or other decode event
|
||||
*/
|
||||
@@ -256,11 +261,6 @@ public class LTRStandardDecoderState extends DecoderState
|
||||
}
|
||||
}
|
||||
|
||||
@Override
|
||||
public void init()
|
||||
{
|
||||
}
|
||||
|
||||
@Override
|
||||
public void receiveDecoderStateEvent(DecoderStateEvent event)
|
||||
{
|
||||
|
||||
@@ -40,7 +40,6 @@ import io.github.dsheirer.sample.real.RealBuffer;
|
||||
|
||||
import java.util.ArrayList;
|
||||
import java.util.List;
|
||||
import java.util.concurrent.ScheduledExecutorService;
|
||||
|
||||
/**
|
||||
* MDC1200 Decoder - 1200 baud 2FSK decoder that can process 48k sample rate
|
||||
@@ -201,25 +200,4 @@ public class MDCDecoder extends Decoder implements IFilteredRealBufferListener,
|
||||
{
|
||||
return mDecimationFilter;
|
||||
}
|
||||
|
||||
@Override
|
||||
public void reset()
|
||||
{
|
||||
// TODO Auto-generated method stub
|
||||
|
||||
}
|
||||
|
||||
@Override
|
||||
public void start( ScheduledExecutorService executor )
|
||||
{
|
||||
// TODO Auto-generated method stub
|
||||
|
||||
}
|
||||
|
||||
@Override
|
||||
public void stop()
|
||||
{
|
||||
// TODO Auto-generated method stub
|
||||
|
||||
}
|
||||
}
|
||||
|
||||
@@ -34,7 +34,6 @@ import io.github.dsheirer.util.StringUtils;
|
||||
|
||||
import java.util.Iterator;
|
||||
import java.util.TreeSet;
|
||||
import java.util.concurrent.ScheduledExecutorService;
|
||||
|
||||
public class MDCDecoderState extends DecoderState
|
||||
{
|
||||
@@ -59,16 +58,6 @@ public class MDCDecoderState extends DecoderState
|
||||
return DecoderType.MDC1200;
|
||||
}
|
||||
|
||||
@Override
|
||||
public void start(ScheduledExecutorService executor)
|
||||
{
|
||||
}
|
||||
|
||||
@Override
|
||||
public void stop()
|
||||
{
|
||||
}
|
||||
|
||||
@Override
|
||||
public void reset()
|
||||
{
|
||||
@@ -78,6 +67,24 @@ public class MDCDecoderState extends DecoderState
|
||||
resetState();
|
||||
}
|
||||
|
||||
@Override
|
||||
public void start()
|
||||
{
|
||||
|
||||
}
|
||||
|
||||
@Override
|
||||
public void stop()
|
||||
{
|
||||
|
||||
}
|
||||
|
||||
@Override
|
||||
public void init()
|
||||
{
|
||||
|
||||
}
|
||||
|
||||
private void resetState()
|
||||
{
|
||||
mFromAttribute.reset();
|
||||
@@ -245,12 +252,6 @@ public class MDCDecoderState extends DecoderState
|
||||
return sb.toString();
|
||||
}
|
||||
|
||||
@Override
|
||||
public void init()
|
||||
{
|
||||
/* No initialization required */
|
||||
}
|
||||
|
||||
/**
|
||||
* Responds to reset events issued by the channel state
|
||||
*/
|
||||
|
||||
@@ -44,7 +44,6 @@ import org.slf4j.LoggerFactory;
|
||||
|
||||
import java.util.ArrayList;
|
||||
import java.util.List;
|
||||
import java.util.concurrent.ScheduledExecutorService;
|
||||
|
||||
/**
|
||||
* MPT1327 Decoder - 1200 baud 2FSK decoder that can process 48k sample rate
|
||||
@@ -296,16 +295,4 @@ public class MPT1327Decoder extends Decoder implements IFilteredRealBufferListen
|
||||
mControlMessageFramer.reset();
|
||||
mTrafficMessageFramer.reset();
|
||||
}
|
||||
|
||||
@Override
|
||||
public void start(ScheduledExecutorService executor)
|
||||
{
|
||||
// TODO Auto-generated method stub
|
||||
}
|
||||
|
||||
@Override
|
||||
public void stop()
|
||||
{
|
||||
// TODO Auto-generated method stub
|
||||
}
|
||||
}
|
||||
|
||||
@@ -46,7 +46,6 @@ import java.util.HashMap;
|
||||
import java.util.Iterator;
|
||||
import java.util.List;
|
||||
import java.util.TreeSet;
|
||||
import java.util.concurrent.ScheduledExecutorService;
|
||||
|
||||
public class MPT1327DecoderState extends DecoderState
|
||||
{
|
||||
@@ -85,16 +84,6 @@ public class MPT1327DecoderState extends DecoderState
|
||||
return DecoderType.MPT1327;
|
||||
}
|
||||
|
||||
@Override
|
||||
public void start(ScheduledExecutorService executor)
|
||||
{
|
||||
}
|
||||
|
||||
@Override
|
||||
public void stop()
|
||||
{
|
||||
}
|
||||
|
||||
public ChannelType getChannelType()
|
||||
{
|
||||
return mChannelType;
|
||||
@@ -300,6 +289,24 @@ public class MPT1327DecoderState extends DecoderState
|
||||
resetState();
|
||||
}
|
||||
|
||||
@Override
|
||||
public void start()
|
||||
{
|
||||
|
||||
}
|
||||
|
||||
@Override
|
||||
public void stop()
|
||||
{
|
||||
|
||||
}
|
||||
|
||||
@Override
|
||||
public void init()
|
||||
{
|
||||
|
||||
}
|
||||
|
||||
private void resetState()
|
||||
{
|
||||
mFromAttribute.reset();
|
||||
@@ -499,12 +506,6 @@ public class MPT1327DecoderState extends DecoderState
|
||||
return sb.toString();
|
||||
}
|
||||
|
||||
@Override
|
||||
public void init()
|
||||
{
|
||||
/* No initialization required */
|
||||
}
|
||||
|
||||
@Override
|
||||
public void receiveDecoderStateEvent(DecoderStateEvent event)
|
||||
{
|
||||
|
||||
@@ -22,8 +22,6 @@ import io.github.dsheirer.module.decode.DecoderType;
|
||||
import io.github.dsheirer.module.decode.PrimaryDecoder;
|
||||
import io.github.dsheirer.module.decode.config.DecodeConfiguration;
|
||||
|
||||
import java.util.concurrent.ScheduledExecutorService;
|
||||
|
||||
public class NBFMDecoder extends PrimaryDecoder
|
||||
{
|
||||
public NBFMDecoder( DecodeConfiguration config )
|
||||
@@ -43,7 +41,7 @@ public class NBFMDecoder extends PrimaryDecoder
|
||||
}
|
||||
|
||||
@Override
|
||||
public void start( ScheduledExecutorService executor )
|
||||
public void start()
|
||||
{
|
||||
}
|
||||
|
||||
|
||||
@@ -10,499 +10,499 @@ 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.RealSampleProvider;
|
||||
import io.github.dsheirer.source.tuner.frequency.FrequencyChangeEvent;
|
||||
import io.github.dsheirer.source.tuner.frequency.FrequencyChangeEvent.Event;
|
||||
import io.github.dsheirer.source.tuner.frequency.IFrequencyChangeListener;
|
||||
import io.github.dsheirer.source.ISourceEventListener;
|
||||
import io.github.dsheirer.source.SourceEvent;
|
||||
|
||||
import java.util.ArrayList;
|
||||
import java.util.List;
|
||||
|
||||
public class C4FMSymbolFilter implements Listener<RealBuffer>, IFrequencyChangeListener, RealSampleProvider, Instrumentable
|
||||
public class C4FMSymbolFilter implements Listener<RealBuffer>, ISourceEventListener, RealSampleProvider, Instrumentable
|
||||
{
|
||||
private static final float TAPS[][] =
|
||||
{
|
||||
{ 0.00000e+00f, 0.00000e+00f, 0.00000e+00f, 0.00000e+00f, 1.00000e+00f, 0.00000e+00f, 0.00000e+00f, 0.00000e+00f }, // 0/128
|
||||
{ -1.54700e-04f, 8.53777e-04f, -2.76968e-03f, 7.89295e-03f, 9.98534e-01f, -5.41054e-03f, 1.24642e-03f, -1.98993e-04f }, // 1/128
|
||||
{ -3.09412e-04f, 1.70888e-03f, -5.55134e-03f, 1.58840e-02f, 9.96891e-01f, -1.07209e-02f, 2.47942e-03f, -3.96391e-04f }, // 2/128
|
||||
{ -4.64053e-04f, 2.56486e-03f, -8.34364e-03f, 2.39714e-02f, 9.95074e-01f, -1.59305e-02f, 3.69852e-03f, -5.92100e-04f }, // 3/128
|
||||
{ -6.18544e-04f, 3.42130e-03f, -1.11453e-02f, 3.21531e-02f, 9.93082e-01f, -2.10389e-02f, 4.90322e-03f, -7.86031e-04f }, // 4/128
|
||||
{ -7.72802e-04f, 4.27773e-03f, -1.39548e-02f, 4.04274e-02f, 9.90917e-01f, -2.60456e-02f, 6.09305e-03f, -9.78093e-04f }, // 5/128
|
||||
{ -9.26747e-04f, 5.13372e-03f, -1.67710e-02f, 4.87921e-02f, 9.88580e-01f, -3.09503e-02f, 7.26755e-03f, -1.16820e-03f }, // 6/128
|
||||
{ -1.08030e-03f, 5.98883e-03f, -1.95925e-02f, 5.72454e-02f, 9.86071e-01f, -3.57525e-02f, 8.42626e-03f, -1.35627e-03f }, // 7/128
|
||||
{ -1.23337e-03f, 6.84261e-03f, -2.24178e-02f, 6.57852e-02f, 9.83392e-01f, -4.04519e-02f, 9.56876e-03f, -1.54221e-03f }, // 8/128
|
||||
{ -1.38589e-03f, 7.69462e-03f, -2.52457e-02f, 7.44095e-02f, 9.80543e-01f, -4.50483e-02f, 1.06946e-02f, -1.72594e-03f }, // 9/128
|
||||
{ -1.53777e-03f, 8.54441e-03f, -2.80746e-02f, 8.31162e-02f, 9.77526e-01f, -4.95412e-02f, 1.18034e-02f, -1.90738e-03f }, // 10/128
|
||||
{ -1.68894e-03f, 9.39154e-03f, -3.09033e-02f, 9.19033e-02f, 9.74342e-01f, -5.39305e-02f, 1.28947e-02f, -2.08645e-03f }, // 11/128
|
||||
{ -1.83931e-03f, 1.02356e-02f, -3.37303e-02f, 1.00769e-01f, 9.70992e-01f, -5.82159e-02f, 1.39681e-02f, -2.26307e-03f }, // 12/128
|
||||
{ -1.98880e-03f, 1.10760e-02f, -3.65541e-02f, 1.09710e-01f, 9.67477e-01f, -6.23972e-02f, 1.50233e-02f, -2.43718e-03f }, // 13/128
|
||||
{ -2.13733e-03f, 1.19125e-02f, -3.93735e-02f, 1.18725e-01f, 9.63798e-01f, -6.64743e-02f, 1.60599e-02f, -2.60868e-03f }, // 14/128
|
||||
{ -2.28483e-03f, 1.27445e-02f, -4.21869e-02f, 1.27812e-01f, 9.59958e-01f, -7.04471e-02f, 1.70776e-02f, -2.77751e-03f }, // 15/128
|
||||
{ -2.43121e-03f, 1.35716e-02f, -4.49929e-02f, 1.36968e-01f, 9.55956e-01f, -7.43154e-02f, 1.80759e-02f, -2.94361e-03f }, // 16/128
|
||||
{ -2.57640e-03f, 1.43934e-02f, -4.77900e-02f, 1.46192e-01f, 9.51795e-01f, -7.80792e-02f, 1.90545e-02f, -3.10689e-03f }, // 17/128
|
||||
{ -2.72032e-03f, 1.52095e-02f, -5.05770e-02f, 1.55480e-01f, 9.47477e-01f, -8.17385e-02f, 2.00132e-02f, -3.26730e-03f }, // 18/128
|
||||
{ -2.86289e-03f, 1.60193e-02f, -5.33522e-02f, 1.64831e-01f, 9.43001e-01f, -8.52933e-02f, 2.09516e-02f, -3.42477e-03f }, // 19/128
|
||||
{ -3.00403e-03f, 1.68225e-02f, -5.61142e-02f, 1.74242e-01f, 9.38371e-01f, -8.87435e-02f, 2.18695e-02f, -3.57923e-03f }, // 20/128
|
||||
{ -3.14367e-03f, 1.76185e-02f, -5.88617e-02f, 1.83711e-01f, 9.33586e-01f, -9.20893e-02f, 2.27664e-02f, -3.73062e-03f }, // 21/128
|
||||
{ -3.28174e-03f, 1.84071e-02f, -6.15931e-02f, 1.93236e-01f, 9.28650e-01f, -9.53307e-02f, 2.36423e-02f, -3.87888e-03f }, // 22/128
|
||||
{ -3.41815e-03f, 1.91877e-02f, -6.43069e-02f, 2.02814e-01f, 9.23564e-01f, -9.84679e-02f, 2.44967e-02f, -4.02397e-03f }, // 23/128
|
||||
{ -3.55283e-03f, 1.99599e-02f, -6.70018e-02f, 2.12443e-01f, 9.18329e-01f, -1.01501e-01f, 2.53295e-02f, -4.16581e-03f }, // 24/128
|
||||
{ -3.68570e-03f, 2.07233e-02f, -6.96762e-02f, 2.22120e-01f, 9.12947e-01f, -1.04430e-01f, 2.61404e-02f, -4.30435e-03f }, // 25/128
|
||||
{ -3.81671e-03f, 2.14774e-02f, -7.23286e-02f, 2.31843e-01f, 9.07420e-01f, -1.07256e-01f, 2.69293e-02f, -4.43955e-03f }, // 26/128
|
||||
{ -3.94576e-03f, 2.22218e-02f, -7.49577e-02f, 2.41609e-01f, 9.01749e-01f, -1.09978e-01f, 2.76957e-02f, -4.57135e-03f }, // 27/128
|
||||
{ -4.07279e-03f, 2.29562e-02f, -7.75620e-02f, 2.51417e-01f, 8.95936e-01f, -1.12597e-01f, 2.84397e-02f, -4.69970e-03f }, // 28/128
|
||||
{ -4.19774e-03f, 2.36801e-02f, -8.01399e-02f, 2.61263e-01f, 8.89984e-01f, -1.15113e-01f, 2.91609e-02f, -4.82456e-03f }, // 29/128
|
||||
{ -4.32052e-03f, 2.43930e-02f, -8.26900e-02f, 2.71144e-01f, 8.83893e-01f, -1.17526e-01f, 2.98593e-02f, -4.94589e-03f }, // 30/128
|
||||
{ -4.44107e-03f, 2.50946e-02f, -8.52109e-02f, 2.81060e-01f, 8.77666e-01f, -1.19837e-01f, 3.05345e-02f, -5.06363e-03f }, // 31/128
|
||||
{ -4.55932e-03f, 2.57844e-02f, -8.77011e-02f, 2.91006e-01f, 8.71305e-01f, -1.22047e-01f, 3.11866e-02f, -5.17776e-03f }, // 32/128
|
||||
{ -4.67520e-03f, 2.64621e-02f, -9.01591e-02f, 3.00980e-01f, 8.64812e-01f, -1.24154e-01f, 3.18153e-02f, -5.28823e-03f }, // 33/128
|
||||
{ -4.78866e-03f, 2.71272e-02f, -9.25834e-02f, 3.10980e-01f, 8.58189e-01f, -1.26161e-01f, 3.24205e-02f, -5.39500e-03f }, // 34/128
|
||||
{ -4.89961e-03f, 2.77794e-02f, -9.49727e-02f, 3.21004e-01f, 8.51437e-01f, -1.28068e-01f, 3.30021e-02f, -5.49804e-03f }, // 35/128
|
||||
{ -5.00800e-03f, 2.84182e-02f, -9.73254e-02f, 3.31048e-01f, 8.44559e-01f, -1.29874e-01f, 3.35600e-02f, -5.59731e-03f }, // 36/128
|
||||
{ -5.11376e-03f, 2.90433e-02f, -9.96402e-02f, 3.41109e-01f, 8.37557e-01f, -1.31581e-01f, 3.40940e-02f, -5.69280e-03f }, // 37/128
|
||||
{ -5.21683e-03f, 2.96543e-02f, -1.01915e-01f, 3.51186e-01f, 8.30432e-01f, -1.33189e-01f, 3.46042e-02f, -5.78446e-03f }, // 38/128
|
||||
{ -5.31716e-03f, 3.02507e-02f, -1.04150e-01f, 3.61276e-01f, 8.23188e-01f, -1.34699e-01f, 3.50903e-02f, -5.87227e-03f }, // 39/128
|
||||
{ -5.41467e-03f, 3.08323e-02f, -1.06342e-01f, 3.71376e-01f, 8.15826e-01f, -1.36111e-01f, 3.55525e-02f, -5.95620e-03f }, // 40/128
|
||||
{ -5.50931e-03f, 3.13987e-02f, -1.08490e-01f, 3.81484e-01f, 8.08348e-01f, -1.37426e-01f, 3.59905e-02f, -6.03624e-03f }, // 41/128
|
||||
{ -5.60103e-03f, 3.19495e-02f, -1.10593e-01f, 3.91596e-01f, 8.00757e-01f, -1.38644e-01f, 3.64044e-02f, -6.11236e-03f }, // 42/128
|
||||
{ -5.68976e-03f, 3.24843e-02f, -1.12650e-01f, 4.01710e-01f, 7.93055e-01f, -1.39767e-01f, 3.67941e-02f, -6.18454e-03f }, // 43/128
|
||||
{ -5.77544e-03f, 3.30027e-02f, -1.14659e-01f, 4.11823e-01f, 7.85244e-01f, -1.40794e-01f, 3.71596e-02f, -6.25277e-03f }, // 44/128
|
||||
{ -5.85804e-03f, 3.35046e-02f, -1.16618e-01f, 4.21934e-01f, 7.77327e-01f, -1.41727e-01f, 3.75010e-02f, -6.31703e-03f }, // 45/128
|
||||
{ -5.93749e-03f, 3.39894e-02f, -1.18526e-01f, 4.32038e-01f, 7.69305e-01f, -1.42566e-01f, 3.78182e-02f, -6.37730e-03f }, // 46/128
|
||||
{ -6.01374e-03f, 3.44568e-02f, -1.20382e-01f, 4.42134e-01f, 7.61181e-01f, -1.43313e-01f, 3.81111e-02f, -6.43358e-03f }, // 47/128
|
||||
{ -6.08674e-03f, 3.49066e-02f, -1.22185e-01f, 4.52218e-01f, 7.52958e-01f, -1.43968e-01f, 3.83800e-02f, -6.48585e-03f }, // 48/128
|
||||
{ -6.15644e-03f, 3.53384e-02f, -1.23933e-01f, 4.62289e-01f, 7.44637e-01f, -1.44531e-01f, 3.86247e-02f, -6.53412e-03f }, // 49/128
|
||||
{ -6.22280e-03f, 3.57519e-02f, -1.25624e-01f, 4.72342e-01f, 7.36222e-01f, -1.45004e-01f, 3.88454e-02f, -6.57836e-03f }, // 50/128
|
||||
{ -6.28577e-03f, 3.61468e-02f, -1.27258e-01f, 4.82377e-01f, 7.27714e-01f, -1.45387e-01f, 3.90420e-02f, -6.61859e-03f }, // 51/128
|
||||
{ -6.34530e-03f, 3.65227e-02f, -1.28832e-01f, 4.92389e-01f, 7.19116e-01f, -1.45682e-01f, 3.92147e-02f, -6.65479e-03f }, // 52/128
|
||||
{ -6.40135e-03f, 3.68795e-02f, -1.30347e-01f, 5.02377e-01f, 7.10431e-01f, -1.45889e-01f, 3.93636e-02f, -6.68698e-03f }, // 53/128
|
||||
{ -6.45388e-03f, 3.72167e-02f, -1.31800e-01f, 5.12337e-01f, 7.01661e-01f, -1.46009e-01f, 3.94886e-02f, -6.71514e-03f }, // 54/128
|
||||
{ -6.50285e-03f, 3.75341e-02f, -1.33190e-01f, 5.22267e-01f, 6.92808e-01f, -1.46043e-01f, 3.95900e-02f, -6.73929e-03f }, // 55/128
|
||||
{ -6.54823e-03f, 3.78315e-02f, -1.34515e-01f, 5.32164e-01f, 6.83875e-01f, -1.45993e-01f, 3.96678e-02f, -6.75943e-03f }, // 56/128
|
||||
{ -6.58996e-03f, 3.81085e-02f, -1.35775e-01f, 5.42025e-01f, 6.74865e-01f, -1.45859e-01f, 3.97222e-02f, -6.77557e-03f }, // 57/128
|
||||
{ -6.62802e-03f, 3.83650e-02f, -1.36969e-01f, 5.51849e-01f, 6.65779e-01f, -1.45641e-01f, 3.97532e-02f, -6.78771e-03f }, // 58/128
|
||||
{ -6.66238e-03f, 3.86006e-02f, -1.38094e-01f, 5.61631e-01f, 6.56621e-01f, -1.45343e-01f, 3.97610e-02f, -6.79588e-03f }, // 59/128
|
||||
{ -6.69300e-03f, 3.88151e-02f, -1.39150e-01f, 5.71370e-01f, 6.47394e-01f, -1.44963e-01f, 3.97458e-02f, -6.80007e-03f }, // 60/128
|
||||
{ -6.71985e-03f, 3.90083e-02f, -1.40136e-01f, 5.81063e-01f, 6.38099e-01f, -1.44503e-01f, 3.97077e-02f, -6.80032e-03f }, // 61/128
|
||||
{ -6.74291e-03f, 3.91800e-02f, -1.41050e-01f, 5.90706e-01f, 6.28739e-01f, -1.43965e-01f, 3.96469e-02f, -6.79662e-03f }, // 62/128
|
||||
{ -6.76214e-03f, 3.93299e-02f, -1.41891e-01f, 6.00298e-01f, 6.19318e-01f, -1.43350e-01f, 3.95635e-02f, -6.78902e-03f }, // 63/128
|
||||
{ -6.77751e-03f, 3.94578e-02f, -1.42658e-01f, 6.09836e-01f, 6.09836e-01f, -1.42658e-01f, 3.94578e-02f, -6.77751e-03f }, // 64/128
|
||||
{ -6.78902e-03f, 3.95635e-02f, -1.43350e-01f, 6.19318e-01f, 6.00298e-01f, -1.41891e-01f, 3.93299e-02f, -6.76214e-03f }, // 65/128
|
||||
{ -6.79662e-03f, 3.96469e-02f, -1.43965e-01f, 6.28739e-01f, 5.90706e-01f, -1.41050e-01f, 3.91800e-02f, -6.74291e-03f }, // 66/128
|
||||
{ -6.80032e-03f, 3.97077e-02f, -1.44503e-01f, 6.38099e-01f, 5.81063e-01f, -1.40136e-01f, 3.90083e-02f, -6.71985e-03f }, // 67/128
|
||||
{ -6.80007e-03f, 3.97458e-02f, -1.44963e-01f, 6.47394e-01f, 5.71370e-01f, -1.39150e-01f, 3.88151e-02f, -6.69300e-03f }, // 68/128
|
||||
{ -6.79588e-03f, 3.97610e-02f, -1.45343e-01f, 6.56621e-01f, 5.61631e-01f, -1.38094e-01f, 3.86006e-02f, -6.66238e-03f }, // 69/128
|
||||
{ -6.78771e-03f, 3.97532e-02f, -1.45641e-01f, 6.65779e-01f, 5.51849e-01f, -1.36969e-01f, 3.83650e-02f, -6.62802e-03f }, // 70/128
|
||||
{ -6.77557e-03f, 3.97222e-02f, -1.45859e-01f, 6.74865e-01f, 5.42025e-01f, -1.35775e-01f, 3.81085e-02f, -6.58996e-03f }, // 71/128
|
||||
{ -6.75943e-03f, 3.96678e-02f, -1.45993e-01f, 6.83875e-01f, 5.32164e-01f, -1.34515e-01f, 3.78315e-02f, -6.54823e-03f }, // 72/128
|
||||
{ -6.73929e-03f, 3.95900e-02f, -1.46043e-01f, 6.92808e-01f, 5.22267e-01f, -1.33190e-01f, 3.75341e-02f, -6.50285e-03f }, // 73/128
|
||||
{ -6.71514e-03f, 3.94886e-02f, -1.46009e-01f, 7.01661e-01f, 5.12337e-01f, -1.31800e-01f, 3.72167e-02f, -6.45388e-03f }, // 74/128
|
||||
{ -6.68698e-03f, 3.93636e-02f, -1.45889e-01f, 7.10431e-01f, 5.02377e-01f, -1.30347e-01f, 3.68795e-02f, -6.40135e-03f }, // 75/128
|
||||
{ -6.65479e-03f, 3.92147e-02f, -1.45682e-01f, 7.19116e-01f, 4.92389e-01f, -1.28832e-01f, 3.65227e-02f, -6.34530e-03f }, // 76/128
|
||||
{ -6.61859e-03f, 3.90420e-02f, -1.45387e-01f, 7.27714e-01f, 4.82377e-01f, -1.27258e-01f, 3.61468e-02f, -6.28577e-03f }, // 77/128
|
||||
{ -6.57836e-03f, 3.88454e-02f, -1.45004e-01f, 7.36222e-01f, 4.72342e-01f, -1.25624e-01f, 3.57519e-02f, -6.22280e-03f }, // 78/128
|
||||
{ -6.53412e-03f, 3.86247e-02f, -1.44531e-01f, 7.44637e-01f, 4.62289e-01f, -1.23933e-01f, 3.53384e-02f, -6.15644e-03f }, // 79/128
|
||||
{ -6.48585e-03f, 3.83800e-02f, -1.43968e-01f, 7.52958e-01f, 4.52218e-01f, -1.22185e-01f, 3.49066e-02f, -6.08674e-03f }, // 80/128
|
||||
{ -6.43358e-03f, 3.81111e-02f, -1.43313e-01f, 7.61181e-01f, 4.42134e-01f, -1.20382e-01f, 3.44568e-02f, -6.01374e-03f }, // 81/128
|
||||
{ -6.37730e-03f, 3.78182e-02f, -1.42566e-01f, 7.69305e-01f, 4.32038e-01f, -1.18526e-01f, 3.39894e-02f, -5.93749e-03f }, // 82/128
|
||||
{ -6.31703e-03f, 3.75010e-02f, -1.41727e-01f, 7.77327e-01f, 4.21934e-01f, -1.16618e-01f, 3.35046e-02f, -5.85804e-03f }, // 83/128
|
||||
{ -6.25277e-03f, 3.71596e-02f, -1.40794e-01f, 7.85244e-01f, 4.11823e-01f, -1.14659e-01f, 3.30027e-02f, -5.77544e-03f }, // 84/128
|
||||
{ -6.18454e-03f, 3.67941e-02f, -1.39767e-01f, 7.93055e-01f, 4.01710e-01f, -1.12650e-01f, 3.24843e-02f, -5.68976e-03f }, // 85/128
|
||||
{ -6.11236e-03f, 3.64044e-02f, -1.38644e-01f, 8.00757e-01f, 3.91596e-01f, -1.10593e-01f, 3.19495e-02f, -5.60103e-03f }, // 86/128
|
||||
{ -6.03624e-03f, 3.59905e-02f, -1.37426e-01f, 8.08348e-01f, 3.81484e-01f, -1.08490e-01f, 3.13987e-02f, -5.50931e-03f }, // 87/128
|
||||
{ -5.95620e-03f, 3.55525e-02f, -1.36111e-01f, 8.15826e-01f, 3.71376e-01f, -1.06342e-01f, 3.08323e-02f, -5.41467e-03f }, // 88/128
|
||||
{ -5.87227e-03f, 3.50903e-02f, -1.34699e-01f, 8.23188e-01f, 3.61276e-01f, -1.04150e-01f, 3.02507e-02f, -5.31716e-03f }, // 89/128
|
||||
{ -5.78446e-03f, 3.46042e-02f, -1.33189e-01f, 8.30432e-01f, 3.51186e-01f, -1.01915e-01f, 2.96543e-02f, -5.21683e-03f }, // 90/128
|
||||
{ -5.69280e-03f, 3.40940e-02f, -1.31581e-01f, 8.37557e-01f, 3.41109e-01f, -9.96402e-02f, 2.90433e-02f, -5.11376e-03f }, // 91/128
|
||||
{ -5.59731e-03f, 3.35600e-02f, -1.29874e-01f, 8.44559e-01f, 3.31048e-01f, -9.73254e-02f, 2.84182e-02f, -5.00800e-03f }, // 92/128
|
||||
{ -5.49804e-03f, 3.30021e-02f, -1.28068e-01f, 8.51437e-01f, 3.21004e-01f, -9.49727e-02f, 2.77794e-02f, -4.89961e-03f }, // 93/128
|
||||
{ -5.39500e-03f, 3.24205e-02f, -1.26161e-01f, 8.58189e-01f, 3.10980e-01f, -9.25834e-02f, 2.71272e-02f, -4.78866e-03f }, // 94/128
|
||||
{ -5.28823e-03f, 3.18153e-02f, -1.24154e-01f, 8.64812e-01f, 3.00980e-01f, -9.01591e-02f, 2.64621e-02f, -4.67520e-03f }, // 95/128
|
||||
{ -5.17776e-03f, 3.11866e-02f, -1.22047e-01f, 8.71305e-01f, 2.91006e-01f, -8.77011e-02f, 2.57844e-02f, -4.55932e-03f }, // 96/128
|
||||
{ -5.06363e-03f, 3.05345e-02f, -1.19837e-01f, 8.77666e-01f, 2.81060e-01f, -8.52109e-02f, 2.50946e-02f, -4.44107e-03f }, // 97/128
|
||||
{ -4.94589e-03f, 2.98593e-02f, -1.17526e-01f, 8.83893e-01f, 2.71144e-01f, -8.26900e-02f, 2.43930e-02f, -4.32052e-03f }, // 98/128
|
||||
{ -4.82456e-03f, 2.91609e-02f, -1.15113e-01f, 8.89984e-01f, 2.61263e-01f, -8.01399e-02f, 2.36801e-02f, -4.19774e-03f }, // 99/128
|
||||
{ -4.69970e-03f, 2.84397e-02f, -1.12597e-01f, 8.95936e-01f, 2.51417e-01f, -7.75620e-02f, 2.29562e-02f, -4.07279e-03f }, // 100/128
|
||||
{ -4.57135e-03f, 2.76957e-02f, -1.09978e-01f, 9.01749e-01f, 2.41609e-01f, -7.49577e-02f, 2.22218e-02f, -3.94576e-03f }, // 101/128
|
||||
{ -4.43955e-03f, 2.69293e-02f, -1.07256e-01f, 9.07420e-01f, 2.31843e-01f, -7.23286e-02f, 2.14774e-02f, -3.81671e-03f }, // 102/128
|
||||
{ -4.30435e-03f, 2.61404e-02f, -1.04430e-01f, 9.12947e-01f, 2.22120e-01f, -6.96762e-02f, 2.07233e-02f, -3.68570e-03f }, // 103/128
|
||||
{ -4.16581e-03f, 2.53295e-02f, -1.01501e-01f, 9.18329e-01f, 2.12443e-01f, -6.70018e-02f, 1.99599e-02f, -3.55283e-03f }, // 104/128
|
||||
{ -4.02397e-03f, 2.44967e-02f, -9.84679e-02f, 9.23564e-01f, 2.02814e-01f, -6.43069e-02f, 1.91877e-02f, -3.41815e-03f }, // 105/128
|
||||
{ -3.87888e-03f, 2.36423e-02f, -9.53307e-02f, 9.28650e-01f, 1.93236e-01f, -6.15931e-02f, 1.84071e-02f, -3.28174e-03f }, // 106/128
|
||||
{ -3.73062e-03f, 2.27664e-02f, -9.20893e-02f, 9.33586e-01f, 1.83711e-01f, -5.88617e-02f, 1.76185e-02f, -3.14367e-03f }, // 107/128
|
||||
{ -3.57923e-03f, 2.18695e-02f, -8.87435e-02f, 9.38371e-01f, 1.74242e-01f, -5.61142e-02f, 1.68225e-02f, -3.00403e-03f }, // 108/128
|
||||
{ -3.42477e-03f, 2.09516e-02f, -8.52933e-02f, 9.43001e-01f, 1.64831e-01f, -5.33522e-02f, 1.60193e-02f, -2.86289e-03f }, // 109/128
|
||||
{ -3.26730e-03f, 2.00132e-02f, -8.17385e-02f, 9.47477e-01f, 1.55480e-01f, -5.05770e-02f, 1.52095e-02f, -2.72032e-03f }, // 110/128
|
||||
{ -3.10689e-03f, 1.90545e-02f, -7.80792e-02f, 9.51795e-01f, 1.46192e-01f, -4.77900e-02f, 1.43934e-02f, -2.57640e-03f }, // 111/128
|
||||
{ -2.94361e-03f, 1.80759e-02f, -7.43154e-02f, 9.55956e-01f, 1.36968e-01f, -4.49929e-02f, 1.35716e-02f, -2.43121e-03f }, // 112/128
|
||||
{ -2.77751e-03f, 1.70776e-02f, -7.04471e-02f, 9.59958e-01f, 1.27812e-01f, -4.21869e-02f, 1.27445e-02f, -2.28483e-03f }, // 113/128
|
||||
{ -2.60868e-03f, 1.60599e-02f, -6.64743e-02f, 9.63798e-01f, 1.18725e-01f, -3.93735e-02f, 1.19125e-02f, -2.13733e-03f }, // 114/128
|
||||
{ -2.43718e-03f, 1.50233e-02f, -6.23972e-02f, 9.67477e-01f, 1.09710e-01f, -3.65541e-02f, 1.10760e-02f, -1.98880e-03f }, // 115/128
|
||||
{ -2.26307e-03f, 1.39681e-02f, -5.82159e-02f, 9.70992e-01f, 1.00769e-01f, -3.37303e-02f, 1.02356e-02f, -1.83931e-03f }, // 116/128
|
||||
{ -2.08645e-03f, 1.28947e-02f, -5.39305e-02f, 9.74342e-01f, 9.19033e-02f, -3.09033e-02f, 9.39154e-03f, -1.68894e-03f }, // 117/128
|
||||
{ -1.90738e-03f, 1.18034e-02f, -4.95412e-02f, 9.77526e-01f, 8.31162e-02f, -2.80746e-02f, 8.54441e-03f, -1.53777e-03f }, // 118/128
|
||||
{ -1.72594e-03f, 1.06946e-02f, -4.50483e-02f, 9.80543e-01f, 7.44095e-02f, -2.52457e-02f, 7.69462e-03f, -1.38589e-03f }, // 119/128
|
||||
{ -1.54221e-03f, 9.56876e-03f, -4.04519e-02f, 9.83392e-01f, 6.57852e-02f, -2.24178e-02f, 6.84261e-03f, -1.23337e-03f }, // 120/128
|
||||
{ -1.35627e-03f, 8.42626e-03f, -3.57525e-02f, 9.86071e-01f, 5.72454e-02f, -1.95925e-02f, 5.98883e-03f, -1.08030e-03f }, // 121/128
|
||||
{ -1.16820e-03f, 7.26755e-03f, -3.09503e-02f, 9.88580e-01f, 4.87921e-02f, -1.67710e-02f, 5.13372e-03f, -9.26747e-04f }, // 122/128
|
||||
{ -9.78093e-04f, 6.09305e-03f, -2.60456e-02f, 9.90917e-01f, 4.04274e-02f, -1.39548e-02f, 4.27773e-03f, -7.72802e-04f }, // 123/128
|
||||
{ -7.86031e-04f, 4.90322e-03f, -2.10389e-02f, 9.93082e-01f, 3.21531e-02f, -1.11453e-02f, 3.42130e-03f, -6.18544e-04f }, // 124/128
|
||||
{ -5.92100e-04f, 3.69852e-03f, -1.59305e-02f, 9.95074e-01f, 2.39714e-02f, -8.34364e-03f, 2.56486e-03f, -4.64053e-04f }, // 125/128
|
||||
{ -3.96391e-04f, 2.47942e-03f, -1.07209e-02f, 9.96891e-01f, 1.58840e-02f, -5.55134e-03f, 1.70888e-03f, -3.09412e-04f }, // 126/128
|
||||
{ -1.98993e-04f, 1.24642e-03f, -5.41054e-03f, 9.98534e-01f, 7.89295e-03f, -2.76968e-03f, 8.53777e-04f, -1.54700e-04f }, // 127/128
|
||||
{ 0.00000e+00f, 0.00000e+00f, 0.00000e+00f, 1.00000e+00f, 0.00000e+00f, 0.00000e+00f, 0.00000e+00f, 0.00000e+00f }, // 128/128
|
||||
};
|
||||
|
||||
private static final float TAPS[][] =
|
||||
{
|
||||
{0.00000e+00f, 0.00000e+00f, 0.00000e+00f, 0.00000e+00f, 1.00000e+00f, 0.00000e+00f, 0.00000e+00f, 0.00000e+00f}, // 0/128
|
||||
{-1.54700e-04f, 8.53777e-04f, -2.76968e-03f, 7.89295e-03f, 9.98534e-01f, -5.41054e-03f, 1.24642e-03f, -1.98993e-04f}, // 1/128
|
||||
{-3.09412e-04f, 1.70888e-03f, -5.55134e-03f, 1.58840e-02f, 9.96891e-01f, -1.07209e-02f, 2.47942e-03f, -3.96391e-04f}, // 2/128
|
||||
{-4.64053e-04f, 2.56486e-03f, -8.34364e-03f, 2.39714e-02f, 9.95074e-01f, -1.59305e-02f, 3.69852e-03f, -5.92100e-04f}, // 3/128
|
||||
{-6.18544e-04f, 3.42130e-03f, -1.11453e-02f, 3.21531e-02f, 9.93082e-01f, -2.10389e-02f, 4.90322e-03f, -7.86031e-04f}, // 4/128
|
||||
{-7.72802e-04f, 4.27773e-03f, -1.39548e-02f, 4.04274e-02f, 9.90917e-01f, -2.60456e-02f, 6.09305e-03f, -9.78093e-04f}, // 5/128
|
||||
{-9.26747e-04f, 5.13372e-03f, -1.67710e-02f, 4.87921e-02f, 9.88580e-01f, -3.09503e-02f, 7.26755e-03f, -1.16820e-03f}, // 6/128
|
||||
{-1.08030e-03f, 5.98883e-03f, -1.95925e-02f, 5.72454e-02f, 9.86071e-01f, -3.57525e-02f, 8.42626e-03f, -1.35627e-03f}, // 7/128
|
||||
{-1.23337e-03f, 6.84261e-03f, -2.24178e-02f, 6.57852e-02f, 9.83392e-01f, -4.04519e-02f, 9.56876e-03f, -1.54221e-03f}, // 8/128
|
||||
{-1.38589e-03f, 7.69462e-03f, -2.52457e-02f, 7.44095e-02f, 9.80543e-01f, -4.50483e-02f, 1.06946e-02f, -1.72594e-03f}, // 9/128
|
||||
{-1.53777e-03f, 8.54441e-03f, -2.80746e-02f, 8.31162e-02f, 9.77526e-01f, -4.95412e-02f, 1.18034e-02f, -1.90738e-03f}, // 10/128
|
||||
{-1.68894e-03f, 9.39154e-03f, -3.09033e-02f, 9.19033e-02f, 9.74342e-01f, -5.39305e-02f, 1.28947e-02f, -2.08645e-03f}, // 11/128
|
||||
{-1.83931e-03f, 1.02356e-02f, -3.37303e-02f, 1.00769e-01f, 9.70992e-01f, -5.82159e-02f, 1.39681e-02f, -2.26307e-03f}, // 12/128
|
||||
{-1.98880e-03f, 1.10760e-02f, -3.65541e-02f, 1.09710e-01f, 9.67477e-01f, -6.23972e-02f, 1.50233e-02f, -2.43718e-03f}, // 13/128
|
||||
{-2.13733e-03f, 1.19125e-02f, -3.93735e-02f, 1.18725e-01f, 9.63798e-01f, -6.64743e-02f, 1.60599e-02f, -2.60868e-03f}, // 14/128
|
||||
{-2.28483e-03f, 1.27445e-02f, -4.21869e-02f, 1.27812e-01f, 9.59958e-01f, -7.04471e-02f, 1.70776e-02f, -2.77751e-03f}, // 15/128
|
||||
{-2.43121e-03f, 1.35716e-02f, -4.49929e-02f, 1.36968e-01f, 9.55956e-01f, -7.43154e-02f, 1.80759e-02f, -2.94361e-03f}, // 16/128
|
||||
{-2.57640e-03f, 1.43934e-02f, -4.77900e-02f, 1.46192e-01f, 9.51795e-01f, -7.80792e-02f, 1.90545e-02f, -3.10689e-03f}, // 17/128
|
||||
{-2.72032e-03f, 1.52095e-02f, -5.05770e-02f, 1.55480e-01f, 9.47477e-01f, -8.17385e-02f, 2.00132e-02f, -3.26730e-03f}, // 18/128
|
||||
{-2.86289e-03f, 1.60193e-02f, -5.33522e-02f, 1.64831e-01f, 9.43001e-01f, -8.52933e-02f, 2.09516e-02f, -3.42477e-03f}, // 19/128
|
||||
{-3.00403e-03f, 1.68225e-02f, -5.61142e-02f, 1.74242e-01f, 9.38371e-01f, -8.87435e-02f, 2.18695e-02f, -3.57923e-03f}, // 20/128
|
||||
{-3.14367e-03f, 1.76185e-02f, -5.88617e-02f, 1.83711e-01f, 9.33586e-01f, -9.20893e-02f, 2.27664e-02f, -3.73062e-03f}, // 21/128
|
||||
{-3.28174e-03f, 1.84071e-02f, -6.15931e-02f, 1.93236e-01f, 9.28650e-01f, -9.53307e-02f, 2.36423e-02f, -3.87888e-03f}, // 22/128
|
||||
{-3.41815e-03f, 1.91877e-02f, -6.43069e-02f, 2.02814e-01f, 9.23564e-01f, -9.84679e-02f, 2.44967e-02f, -4.02397e-03f}, // 23/128
|
||||
{-3.55283e-03f, 1.99599e-02f, -6.70018e-02f, 2.12443e-01f, 9.18329e-01f, -1.01501e-01f, 2.53295e-02f, -4.16581e-03f}, // 24/128
|
||||
{-3.68570e-03f, 2.07233e-02f, -6.96762e-02f, 2.22120e-01f, 9.12947e-01f, -1.04430e-01f, 2.61404e-02f, -4.30435e-03f}, // 25/128
|
||||
{-3.81671e-03f, 2.14774e-02f, -7.23286e-02f, 2.31843e-01f, 9.07420e-01f, -1.07256e-01f, 2.69293e-02f, -4.43955e-03f}, // 26/128
|
||||
{-3.94576e-03f, 2.22218e-02f, -7.49577e-02f, 2.41609e-01f, 9.01749e-01f, -1.09978e-01f, 2.76957e-02f, -4.57135e-03f}, // 27/128
|
||||
{-4.07279e-03f, 2.29562e-02f, -7.75620e-02f, 2.51417e-01f, 8.95936e-01f, -1.12597e-01f, 2.84397e-02f, -4.69970e-03f}, // 28/128
|
||||
{-4.19774e-03f, 2.36801e-02f, -8.01399e-02f, 2.61263e-01f, 8.89984e-01f, -1.15113e-01f, 2.91609e-02f, -4.82456e-03f}, // 29/128
|
||||
{-4.32052e-03f, 2.43930e-02f, -8.26900e-02f, 2.71144e-01f, 8.83893e-01f, -1.17526e-01f, 2.98593e-02f, -4.94589e-03f}, // 30/128
|
||||
{-4.44107e-03f, 2.50946e-02f, -8.52109e-02f, 2.81060e-01f, 8.77666e-01f, -1.19837e-01f, 3.05345e-02f, -5.06363e-03f}, // 31/128
|
||||
{-4.55932e-03f, 2.57844e-02f, -8.77011e-02f, 2.91006e-01f, 8.71305e-01f, -1.22047e-01f, 3.11866e-02f, -5.17776e-03f}, // 32/128
|
||||
{-4.67520e-03f, 2.64621e-02f, -9.01591e-02f, 3.00980e-01f, 8.64812e-01f, -1.24154e-01f, 3.18153e-02f, -5.28823e-03f}, // 33/128
|
||||
{-4.78866e-03f, 2.71272e-02f, -9.25834e-02f, 3.10980e-01f, 8.58189e-01f, -1.26161e-01f, 3.24205e-02f, -5.39500e-03f}, // 34/128
|
||||
{-4.89961e-03f, 2.77794e-02f, -9.49727e-02f, 3.21004e-01f, 8.51437e-01f, -1.28068e-01f, 3.30021e-02f, -5.49804e-03f}, // 35/128
|
||||
{-5.00800e-03f, 2.84182e-02f, -9.73254e-02f, 3.31048e-01f, 8.44559e-01f, -1.29874e-01f, 3.35600e-02f, -5.59731e-03f}, // 36/128
|
||||
{-5.11376e-03f, 2.90433e-02f, -9.96402e-02f, 3.41109e-01f, 8.37557e-01f, -1.31581e-01f, 3.40940e-02f, -5.69280e-03f}, // 37/128
|
||||
{-5.21683e-03f, 2.96543e-02f, -1.01915e-01f, 3.51186e-01f, 8.30432e-01f, -1.33189e-01f, 3.46042e-02f, -5.78446e-03f}, // 38/128
|
||||
{-5.31716e-03f, 3.02507e-02f, -1.04150e-01f, 3.61276e-01f, 8.23188e-01f, -1.34699e-01f, 3.50903e-02f, -5.87227e-03f}, // 39/128
|
||||
{-5.41467e-03f, 3.08323e-02f, -1.06342e-01f, 3.71376e-01f, 8.15826e-01f, -1.36111e-01f, 3.55525e-02f, -5.95620e-03f}, // 40/128
|
||||
{-5.50931e-03f, 3.13987e-02f, -1.08490e-01f, 3.81484e-01f, 8.08348e-01f, -1.37426e-01f, 3.59905e-02f, -6.03624e-03f}, // 41/128
|
||||
{-5.60103e-03f, 3.19495e-02f, -1.10593e-01f, 3.91596e-01f, 8.00757e-01f, -1.38644e-01f, 3.64044e-02f, -6.11236e-03f}, // 42/128
|
||||
{-5.68976e-03f, 3.24843e-02f, -1.12650e-01f, 4.01710e-01f, 7.93055e-01f, -1.39767e-01f, 3.67941e-02f, -6.18454e-03f}, // 43/128
|
||||
{-5.77544e-03f, 3.30027e-02f, -1.14659e-01f, 4.11823e-01f, 7.85244e-01f, -1.40794e-01f, 3.71596e-02f, -6.25277e-03f}, // 44/128
|
||||
{-5.85804e-03f, 3.35046e-02f, -1.16618e-01f, 4.21934e-01f, 7.77327e-01f, -1.41727e-01f, 3.75010e-02f, -6.31703e-03f}, // 45/128
|
||||
{-5.93749e-03f, 3.39894e-02f, -1.18526e-01f, 4.32038e-01f, 7.69305e-01f, -1.42566e-01f, 3.78182e-02f, -6.37730e-03f}, // 46/128
|
||||
{-6.01374e-03f, 3.44568e-02f, -1.20382e-01f, 4.42134e-01f, 7.61181e-01f, -1.43313e-01f, 3.81111e-02f, -6.43358e-03f}, // 47/128
|
||||
{-6.08674e-03f, 3.49066e-02f, -1.22185e-01f, 4.52218e-01f, 7.52958e-01f, -1.43968e-01f, 3.83800e-02f, -6.48585e-03f}, // 48/128
|
||||
{-6.15644e-03f, 3.53384e-02f, -1.23933e-01f, 4.62289e-01f, 7.44637e-01f, -1.44531e-01f, 3.86247e-02f, -6.53412e-03f}, // 49/128
|
||||
{-6.22280e-03f, 3.57519e-02f, -1.25624e-01f, 4.72342e-01f, 7.36222e-01f, -1.45004e-01f, 3.88454e-02f, -6.57836e-03f}, // 50/128
|
||||
{-6.28577e-03f, 3.61468e-02f, -1.27258e-01f, 4.82377e-01f, 7.27714e-01f, -1.45387e-01f, 3.90420e-02f, -6.61859e-03f}, // 51/128
|
||||
{-6.34530e-03f, 3.65227e-02f, -1.28832e-01f, 4.92389e-01f, 7.19116e-01f, -1.45682e-01f, 3.92147e-02f, -6.65479e-03f}, // 52/128
|
||||
{-6.40135e-03f, 3.68795e-02f, -1.30347e-01f, 5.02377e-01f, 7.10431e-01f, -1.45889e-01f, 3.93636e-02f, -6.68698e-03f}, // 53/128
|
||||
{-6.45388e-03f, 3.72167e-02f, -1.31800e-01f, 5.12337e-01f, 7.01661e-01f, -1.46009e-01f, 3.94886e-02f, -6.71514e-03f}, // 54/128
|
||||
{-6.50285e-03f, 3.75341e-02f, -1.33190e-01f, 5.22267e-01f, 6.92808e-01f, -1.46043e-01f, 3.95900e-02f, -6.73929e-03f}, // 55/128
|
||||
{-6.54823e-03f, 3.78315e-02f, -1.34515e-01f, 5.32164e-01f, 6.83875e-01f, -1.45993e-01f, 3.96678e-02f, -6.75943e-03f}, // 56/128
|
||||
{-6.58996e-03f, 3.81085e-02f, -1.35775e-01f, 5.42025e-01f, 6.74865e-01f, -1.45859e-01f, 3.97222e-02f, -6.77557e-03f}, // 57/128
|
||||
{-6.62802e-03f, 3.83650e-02f, -1.36969e-01f, 5.51849e-01f, 6.65779e-01f, -1.45641e-01f, 3.97532e-02f, -6.78771e-03f}, // 58/128
|
||||
{-6.66238e-03f, 3.86006e-02f, -1.38094e-01f, 5.61631e-01f, 6.56621e-01f, -1.45343e-01f, 3.97610e-02f, -6.79588e-03f}, // 59/128
|
||||
{-6.69300e-03f, 3.88151e-02f, -1.39150e-01f, 5.71370e-01f, 6.47394e-01f, -1.44963e-01f, 3.97458e-02f, -6.80007e-03f}, // 60/128
|
||||
{-6.71985e-03f, 3.90083e-02f, -1.40136e-01f, 5.81063e-01f, 6.38099e-01f, -1.44503e-01f, 3.97077e-02f, -6.80032e-03f}, // 61/128
|
||||
{-6.74291e-03f, 3.91800e-02f, -1.41050e-01f, 5.90706e-01f, 6.28739e-01f, -1.43965e-01f, 3.96469e-02f, -6.79662e-03f}, // 62/128
|
||||
{-6.76214e-03f, 3.93299e-02f, -1.41891e-01f, 6.00298e-01f, 6.19318e-01f, -1.43350e-01f, 3.95635e-02f, -6.78902e-03f}, // 63/128
|
||||
{-6.77751e-03f, 3.94578e-02f, -1.42658e-01f, 6.09836e-01f, 6.09836e-01f, -1.42658e-01f, 3.94578e-02f, -6.77751e-03f}, // 64/128
|
||||
{-6.78902e-03f, 3.95635e-02f, -1.43350e-01f, 6.19318e-01f, 6.00298e-01f, -1.41891e-01f, 3.93299e-02f, -6.76214e-03f}, // 65/128
|
||||
{-6.79662e-03f, 3.96469e-02f, -1.43965e-01f, 6.28739e-01f, 5.90706e-01f, -1.41050e-01f, 3.91800e-02f, -6.74291e-03f}, // 66/128
|
||||
{-6.80032e-03f, 3.97077e-02f, -1.44503e-01f, 6.38099e-01f, 5.81063e-01f, -1.40136e-01f, 3.90083e-02f, -6.71985e-03f}, // 67/128
|
||||
{-6.80007e-03f, 3.97458e-02f, -1.44963e-01f, 6.47394e-01f, 5.71370e-01f, -1.39150e-01f, 3.88151e-02f, -6.69300e-03f}, // 68/128
|
||||
{-6.79588e-03f, 3.97610e-02f, -1.45343e-01f, 6.56621e-01f, 5.61631e-01f, -1.38094e-01f, 3.86006e-02f, -6.66238e-03f}, // 69/128
|
||||
{-6.78771e-03f, 3.97532e-02f, -1.45641e-01f, 6.65779e-01f, 5.51849e-01f, -1.36969e-01f, 3.83650e-02f, -6.62802e-03f}, // 70/128
|
||||
{-6.77557e-03f, 3.97222e-02f, -1.45859e-01f, 6.74865e-01f, 5.42025e-01f, -1.35775e-01f, 3.81085e-02f, -6.58996e-03f}, // 71/128
|
||||
{-6.75943e-03f, 3.96678e-02f, -1.45993e-01f, 6.83875e-01f, 5.32164e-01f, -1.34515e-01f, 3.78315e-02f, -6.54823e-03f}, // 72/128
|
||||
{-6.73929e-03f, 3.95900e-02f, -1.46043e-01f, 6.92808e-01f, 5.22267e-01f, -1.33190e-01f, 3.75341e-02f, -6.50285e-03f}, // 73/128
|
||||
{-6.71514e-03f, 3.94886e-02f, -1.46009e-01f, 7.01661e-01f, 5.12337e-01f, -1.31800e-01f, 3.72167e-02f, -6.45388e-03f}, // 74/128
|
||||
{-6.68698e-03f, 3.93636e-02f, -1.45889e-01f, 7.10431e-01f, 5.02377e-01f, -1.30347e-01f, 3.68795e-02f, -6.40135e-03f}, // 75/128
|
||||
{-6.65479e-03f, 3.92147e-02f, -1.45682e-01f, 7.19116e-01f, 4.92389e-01f, -1.28832e-01f, 3.65227e-02f, -6.34530e-03f}, // 76/128
|
||||
{-6.61859e-03f, 3.90420e-02f, -1.45387e-01f, 7.27714e-01f, 4.82377e-01f, -1.27258e-01f, 3.61468e-02f, -6.28577e-03f}, // 77/128
|
||||
{-6.57836e-03f, 3.88454e-02f, -1.45004e-01f, 7.36222e-01f, 4.72342e-01f, -1.25624e-01f, 3.57519e-02f, -6.22280e-03f}, // 78/128
|
||||
{-6.53412e-03f, 3.86247e-02f, -1.44531e-01f, 7.44637e-01f, 4.62289e-01f, -1.23933e-01f, 3.53384e-02f, -6.15644e-03f}, // 79/128
|
||||
{-6.48585e-03f, 3.83800e-02f, -1.43968e-01f, 7.52958e-01f, 4.52218e-01f, -1.22185e-01f, 3.49066e-02f, -6.08674e-03f}, // 80/128
|
||||
{-6.43358e-03f, 3.81111e-02f, -1.43313e-01f, 7.61181e-01f, 4.42134e-01f, -1.20382e-01f, 3.44568e-02f, -6.01374e-03f}, // 81/128
|
||||
{-6.37730e-03f, 3.78182e-02f, -1.42566e-01f, 7.69305e-01f, 4.32038e-01f, -1.18526e-01f, 3.39894e-02f, -5.93749e-03f}, // 82/128
|
||||
{-6.31703e-03f, 3.75010e-02f, -1.41727e-01f, 7.77327e-01f, 4.21934e-01f, -1.16618e-01f, 3.35046e-02f, -5.85804e-03f}, // 83/128
|
||||
{-6.25277e-03f, 3.71596e-02f, -1.40794e-01f, 7.85244e-01f, 4.11823e-01f, -1.14659e-01f, 3.30027e-02f, -5.77544e-03f}, // 84/128
|
||||
{-6.18454e-03f, 3.67941e-02f, -1.39767e-01f, 7.93055e-01f, 4.01710e-01f, -1.12650e-01f, 3.24843e-02f, -5.68976e-03f}, // 85/128
|
||||
{-6.11236e-03f, 3.64044e-02f, -1.38644e-01f, 8.00757e-01f, 3.91596e-01f, -1.10593e-01f, 3.19495e-02f, -5.60103e-03f}, // 86/128
|
||||
{-6.03624e-03f, 3.59905e-02f, -1.37426e-01f, 8.08348e-01f, 3.81484e-01f, -1.08490e-01f, 3.13987e-02f, -5.50931e-03f}, // 87/128
|
||||
{-5.95620e-03f, 3.55525e-02f, -1.36111e-01f, 8.15826e-01f, 3.71376e-01f, -1.06342e-01f, 3.08323e-02f, -5.41467e-03f}, // 88/128
|
||||
{-5.87227e-03f, 3.50903e-02f, -1.34699e-01f, 8.23188e-01f, 3.61276e-01f, -1.04150e-01f, 3.02507e-02f, -5.31716e-03f}, // 89/128
|
||||
{-5.78446e-03f, 3.46042e-02f, -1.33189e-01f, 8.30432e-01f, 3.51186e-01f, -1.01915e-01f, 2.96543e-02f, -5.21683e-03f}, // 90/128
|
||||
{-5.69280e-03f, 3.40940e-02f, -1.31581e-01f, 8.37557e-01f, 3.41109e-01f, -9.96402e-02f, 2.90433e-02f, -5.11376e-03f}, // 91/128
|
||||
{-5.59731e-03f, 3.35600e-02f, -1.29874e-01f, 8.44559e-01f, 3.31048e-01f, -9.73254e-02f, 2.84182e-02f, -5.00800e-03f}, // 92/128
|
||||
{-5.49804e-03f, 3.30021e-02f, -1.28068e-01f, 8.51437e-01f, 3.21004e-01f, -9.49727e-02f, 2.77794e-02f, -4.89961e-03f}, // 93/128
|
||||
{-5.39500e-03f, 3.24205e-02f, -1.26161e-01f, 8.58189e-01f, 3.10980e-01f, -9.25834e-02f, 2.71272e-02f, -4.78866e-03f}, // 94/128
|
||||
{-5.28823e-03f, 3.18153e-02f, -1.24154e-01f, 8.64812e-01f, 3.00980e-01f, -9.01591e-02f, 2.64621e-02f, -4.67520e-03f}, // 95/128
|
||||
{-5.17776e-03f, 3.11866e-02f, -1.22047e-01f, 8.71305e-01f, 2.91006e-01f, -8.77011e-02f, 2.57844e-02f, -4.55932e-03f}, // 96/128
|
||||
{-5.06363e-03f, 3.05345e-02f, -1.19837e-01f, 8.77666e-01f, 2.81060e-01f, -8.52109e-02f, 2.50946e-02f, -4.44107e-03f}, // 97/128
|
||||
{-4.94589e-03f, 2.98593e-02f, -1.17526e-01f, 8.83893e-01f, 2.71144e-01f, -8.26900e-02f, 2.43930e-02f, -4.32052e-03f}, // 98/128
|
||||
{-4.82456e-03f, 2.91609e-02f, -1.15113e-01f, 8.89984e-01f, 2.61263e-01f, -8.01399e-02f, 2.36801e-02f, -4.19774e-03f}, // 99/128
|
||||
{-4.69970e-03f, 2.84397e-02f, -1.12597e-01f, 8.95936e-01f, 2.51417e-01f, -7.75620e-02f, 2.29562e-02f, -4.07279e-03f}, // 100/128
|
||||
{-4.57135e-03f, 2.76957e-02f, -1.09978e-01f, 9.01749e-01f, 2.41609e-01f, -7.49577e-02f, 2.22218e-02f, -3.94576e-03f}, // 101/128
|
||||
{-4.43955e-03f, 2.69293e-02f, -1.07256e-01f, 9.07420e-01f, 2.31843e-01f, -7.23286e-02f, 2.14774e-02f, -3.81671e-03f}, // 102/128
|
||||
{-4.30435e-03f, 2.61404e-02f, -1.04430e-01f, 9.12947e-01f, 2.22120e-01f, -6.96762e-02f, 2.07233e-02f, -3.68570e-03f}, // 103/128
|
||||
{-4.16581e-03f, 2.53295e-02f, -1.01501e-01f, 9.18329e-01f, 2.12443e-01f, -6.70018e-02f, 1.99599e-02f, -3.55283e-03f}, // 104/128
|
||||
{-4.02397e-03f, 2.44967e-02f, -9.84679e-02f, 9.23564e-01f, 2.02814e-01f, -6.43069e-02f, 1.91877e-02f, -3.41815e-03f}, // 105/128
|
||||
{-3.87888e-03f, 2.36423e-02f, -9.53307e-02f, 9.28650e-01f, 1.93236e-01f, -6.15931e-02f, 1.84071e-02f, -3.28174e-03f}, // 106/128
|
||||
{-3.73062e-03f, 2.27664e-02f, -9.20893e-02f, 9.33586e-01f, 1.83711e-01f, -5.88617e-02f, 1.76185e-02f, -3.14367e-03f}, // 107/128
|
||||
{-3.57923e-03f, 2.18695e-02f, -8.87435e-02f, 9.38371e-01f, 1.74242e-01f, -5.61142e-02f, 1.68225e-02f, -3.00403e-03f}, // 108/128
|
||||
{-3.42477e-03f, 2.09516e-02f, -8.52933e-02f, 9.43001e-01f, 1.64831e-01f, -5.33522e-02f, 1.60193e-02f, -2.86289e-03f}, // 109/128
|
||||
{-3.26730e-03f, 2.00132e-02f, -8.17385e-02f, 9.47477e-01f, 1.55480e-01f, -5.05770e-02f, 1.52095e-02f, -2.72032e-03f}, // 110/128
|
||||
{-3.10689e-03f, 1.90545e-02f, -7.80792e-02f, 9.51795e-01f, 1.46192e-01f, -4.77900e-02f, 1.43934e-02f, -2.57640e-03f}, // 111/128
|
||||
{-2.94361e-03f, 1.80759e-02f, -7.43154e-02f, 9.55956e-01f, 1.36968e-01f, -4.49929e-02f, 1.35716e-02f, -2.43121e-03f}, // 112/128
|
||||
{-2.77751e-03f, 1.70776e-02f, -7.04471e-02f, 9.59958e-01f, 1.27812e-01f, -4.21869e-02f, 1.27445e-02f, -2.28483e-03f}, // 113/128
|
||||
{-2.60868e-03f, 1.60599e-02f, -6.64743e-02f, 9.63798e-01f, 1.18725e-01f, -3.93735e-02f, 1.19125e-02f, -2.13733e-03f}, // 114/128
|
||||
{-2.43718e-03f, 1.50233e-02f, -6.23972e-02f, 9.67477e-01f, 1.09710e-01f, -3.65541e-02f, 1.10760e-02f, -1.98880e-03f}, // 115/128
|
||||
{-2.26307e-03f, 1.39681e-02f, -5.82159e-02f, 9.70992e-01f, 1.00769e-01f, -3.37303e-02f, 1.02356e-02f, -1.83931e-03f}, // 116/128
|
||||
{-2.08645e-03f, 1.28947e-02f, -5.39305e-02f, 9.74342e-01f, 9.19033e-02f, -3.09033e-02f, 9.39154e-03f, -1.68894e-03f}, // 117/128
|
||||
{-1.90738e-03f, 1.18034e-02f, -4.95412e-02f, 9.77526e-01f, 8.31162e-02f, -2.80746e-02f, 8.54441e-03f, -1.53777e-03f}, // 118/128
|
||||
{-1.72594e-03f, 1.06946e-02f, -4.50483e-02f, 9.80543e-01f, 7.44095e-02f, -2.52457e-02f, 7.69462e-03f, -1.38589e-03f}, // 119/128
|
||||
{-1.54221e-03f, 9.56876e-03f, -4.04519e-02f, 9.83392e-01f, 6.57852e-02f, -2.24178e-02f, 6.84261e-03f, -1.23337e-03f}, // 120/128
|
||||
{-1.35627e-03f, 8.42626e-03f, -3.57525e-02f, 9.86071e-01f, 5.72454e-02f, -1.95925e-02f, 5.98883e-03f, -1.08030e-03f}, // 121/128
|
||||
{-1.16820e-03f, 7.26755e-03f, -3.09503e-02f, 9.88580e-01f, 4.87921e-02f, -1.67710e-02f, 5.13372e-03f, -9.26747e-04f}, // 122/128
|
||||
{-9.78093e-04f, 6.09305e-03f, -2.60456e-02f, 9.90917e-01f, 4.04274e-02f, -1.39548e-02f, 4.27773e-03f, -7.72802e-04f}, // 123/128
|
||||
{-7.86031e-04f, 4.90322e-03f, -2.10389e-02f, 9.93082e-01f, 3.21531e-02f, -1.11453e-02f, 3.42130e-03f, -6.18544e-04f}, // 124/128
|
||||
{-5.92100e-04f, 3.69852e-03f, -1.59305e-02f, 9.95074e-01f, 2.39714e-02f, -8.34364e-03f, 2.56486e-03f, -4.64053e-04f}, // 125/128
|
||||
{-3.96391e-04f, 2.47942e-03f, -1.07209e-02f, 9.96891e-01f, 1.58840e-02f, -5.55134e-03f, 1.70888e-03f, -3.09412e-04f}, // 126/128
|
||||
{-1.98993e-04f, 1.24642e-03f, -5.41054e-03f, 9.98534e-01f, 7.89295e-03f, -2.76968e-03f, 8.53777e-04f, -1.54700e-04f}, // 127/128
|
||||
{0.00000e+00f, 0.00000e+00f, 0.00000e+00f, 1.00000e+00f, 0.00000e+00f, 0.00000e+00f, 0.00000e+00f, 0.00000e+00f}, // 128/128
|
||||
};
|
||||
|
||||
/* Instrumentation Taps */
|
||||
private static final String INSTRUMENT_SYMBOL_SPREAD = "Tap Point: Symbol Spread (Goal=2.0)";
|
||||
private FloatTap mSymbolSpreadTap;
|
||||
private static final String INSTRUMENT_SYMBOL_SPREAD = "Tap Point: Symbol Spread (Goal=2.0)";
|
||||
private FloatTap mSymbolSpreadTap;
|
||||
private List<TapGroup> mAvailableTaps;
|
||||
|
||||
private static final int NUMBER_FILTER_TAPS = 8;
|
||||
private static final int NUMBER_FILTER_STEPS = 128;
|
||||
|
||||
private static final int SAMPLE_RATE = 48000;
|
||||
private static final int SYMBOL_RATE = 4800;
|
||||
|
||||
/* Tracking loop gain constant */
|
||||
private static final double K_SYMBOL_SPREAD = 0.0100;
|
||||
|
||||
/* Constraints on symbol spreading */
|
||||
private static final float SYMBOL_SPREAD_MAX = 2.4f; // upper range limit: +20%
|
||||
private static final float SYMBOL_SPREAD_MIN = 1.6f; // lower range limit: -20%
|
||||
private static final int NUMBER_FILTER_TAPS = 8;
|
||||
private static final int NUMBER_FILTER_STEPS = 128;
|
||||
|
||||
/* Symbol clock tracking loop gain */
|
||||
private static final double K_SYMBOL_TIMING = 0.025;
|
||||
|
||||
/* Coarse and fine frequency tracking constants */
|
||||
private static final double K_COARSE_FREQUENCY = 0.00125;
|
||||
private static final double K_FINE_FREQUENCY = 0.125;
|
||||
|
||||
/* Frequency correction broadcast threshold */
|
||||
private static final int SAMPLE_RATE = 48000;
|
||||
private static final int SYMBOL_RATE = 4800;
|
||||
|
||||
/* Tracking loop gain constant */
|
||||
private static final double K_SYMBOL_SPREAD = 0.0100;
|
||||
|
||||
/* Constraints on symbol spreading */
|
||||
private static final float SYMBOL_SPREAD_MAX = 2.4f; // upper range limit: +20%
|
||||
private static final float SYMBOL_SPREAD_MIN = 1.6f; // lower range limit: -20%
|
||||
|
||||
/* Symbol clock tracking loop gain */
|
||||
private static final double K_SYMBOL_TIMING = 0.025;
|
||||
|
||||
/* Coarse and fine frequency tracking constants */
|
||||
private static final double K_COARSE_FREQUENCY = 0.00125;
|
||||
private static final double K_FINE_FREQUENCY = 0.125;
|
||||
|
||||
/* Frequency correction broadcast threshold */
|
||||
// private static final double COARSE_FREQUENCY_DEADBAND = 1.66;
|
||||
private static final double COARSE_FREQUENCY_THRESHOLD = 1.20;
|
||||
|
||||
/* 2.0 symbol spread gives -3, -1, 1, 3 */
|
||||
private float mSymbolSpread = 2.0f;
|
||||
private float mSymbolClock = 0.0f;
|
||||
private float mSymbolTime = (float)SYMBOL_RATE / (float)SAMPLE_RATE;
|
||||
|
||||
private float mFineFrequencyCorrection = 0.0f;
|
||||
private float mCoarseFrequencyCorrection = 0.0f;
|
||||
private static final double COARSE_FREQUENCY_THRESHOLD = 1.20;
|
||||
|
||||
private float mHistory[] = new float[ NUMBER_FILTER_TAPS ];
|
||||
private int mHistoryLast = 0;
|
||||
|
||||
private RealSampleListener mListener;
|
||||
|
||||
private DirectGainControl mGainController = new DirectGainControl( 15.0f, 0.1f, 35.0f, 0.3f );
|
||||
/* 2.0 symbol spread gives -3, -1, 1, 3 */
|
||||
private float mSymbolSpread = 2.0f;
|
||||
private float mSymbolClock = 0.0f;
|
||||
private float mSymbolTime = (float)SYMBOL_RATE / (float)SAMPLE_RATE;
|
||||
|
||||
private int mFrequencyAdjustmentRequested = 0;
|
||||
private int mFrequencyCorrection = 0;
|
||||
private int mFrequencyCorrectionMaximum = 3000;
|
||||
private boolean mResetFrequencyTracker = false;
|
||||
private FrequencyCorrectionProcessor mFrequencyCorrectionProcessor;
|
||||
private Listener<FrequencyChangeEvent> mFrequencyChangeListener;
|
||||
|
||||
/**
|
||||
* C4FM Symbol Filter
|
||||
*
|
||||
* Sample Gain values - the gain of incoming sample values will critically
|
||||
* impact the performance of this filter. Gain should be adjusted to
|
||||
* optimize the value of mSymbolSpread toward a value of 2.0. If the value
|
||||
* is less than 2.0, then increase gain. If the value is over 2.0, then
|
||||
* decrease gain.
|
||||
*
|
||||
* When preceded by the RealAutomaticGainFilter class, an optimal gain
|
||||
* setting for this filter is 15.4 and that yields a symbol spread that
|
||||
* is centered on 2.0, ranging 1.92 to 2.08.
|
||||
*/
|
||||
public C4FMSymbolFilter( int frequencyCorrectionMaximum )
|
||||
{
|
||||
mFrequencyCorrectionMaximum = frequencyCorrectionMaximum;
|
||||
}
|
||||
|
||||
public void dispose()
|
||||
{
|
||||
mGainController = null;
|
||||
private float mFineFrequencyCorrection = 0.0f;
|
||||
private float mCoarseFrequencyCorrection = 0.0f;
|
||||
|
||||
private float mHistory[] = new float[NUMBER_FILTER_TAPS];
|
||||
private int mHistoryLast = 0;
|
||||
|
||||
private RealSampleListener mListener;
|
||||
|
||||
private DirectGainControl mGainController = new DirectGainControl(15.0f, 0.1f, 35.0f, 0.3f);
|
||||
|
||||
private int mFrequencyAdjustmentRequested = 0;
|
||||
private int mFrequencyCorrection = 0;
|
||||
private int mFrequencyCorrectionMaximum = 3000;
|
||||
private boolean mResetFrequencyTracker = false;
|
||||
private FrequencyCorrectionProcessor mFrequencyCorrectionProcessor;
|
||||
private Listener<SourceEvent> mFrequencyChangeListener;
|
||||
|
||||
/**
|
||||
* C4FM Symbol Filter
|
||||
*
|
||||
* Sample Gain values - the gain of incoming sample values will critically
|
||||
* impact the performance of this filter. Gain should be adjusted to
|
||||
* optimize the value of mSymbolSpread toward a value of 2.0. If the value
|
||||
* is less than 2.0, then increase gain. If the value is over 2.0, then
|
||||
* decrease gain.
|
||||
*
|
||||
* When preceded by the RealAutomaticGainFilter class, an optimal gain
|
||||
* setting for this filter is 15.4 and that yields a symbol spread that
|
||||
* is centered on 2.0, ranging 1.92 to 2.08.
|
||||
*/
|
||||
public C4FMSymbolFilter(int frequencyCorrectionMaximum)
|
||||
{
|
||||
mFrequencyCorrectionMaximum = frequencyCorrectionMaximum;
|
||||
}
|
||||
|
||||
public void dispose()
|
||||
{
|
||||
mGainController = null;
|
||||
// mFrequencyCorrectionControl = null;
|
||||
mListener = null;
|
||||
}
|
||||
|
||||
@Override
|
||||
public void receive( RealBuffer buffer )
|
||||
{
|
||||
for( float sample: buffer.getSamples() )
|
||||
{
|
||||
receive( sample );
|
||||
}
|
||||
mListener = null;
|
||||
}
|
||||
|
||||
@Override
|
||||
public void receive(RealBuffer buffer)
|
||||
{
|
||||
for(float sample : buffer.getSamples())
|
||||
{
|
||||
receive(sample);
|
||||
}
|
||||
|
||||
/* If a frequency correction was requested during the processing of this
|
||||
* buffer, we'll apply the change and it will be reflected in the next
|
||||
* buffer, we'll apply the change and it will be reflected in the next
|
||||
* arriving buffer. Reset the lock on frequency correction and reset
|
||||
* the internal frequency correction tracker */
|
||||
if( mFrequencyAdjustmentRequested != 0 )
|
||||
{
|
||||
int correction = mFrequencyCorrection + mFrequencyAdjustmentRequested;
|
||||
|
||||
if( correction > mFrequencyCorrectionMaximum )
|
||||
{
|
||||
correction = mFrequencyCorrectionMaximum;
|
||||
}
|
||||
else if( correction < -mFrequencyCorrectionMaximum )
|
||||
{
|
||||
correction = -mFrequencyCorrectionMaximum;
|
||||
}
|
||||
if(mFrequencyAdjustmentRequested != 0)
|
||||
{
|
||||
int correction = mFrequencyCorrection + mFrequencyAdjustmentRequested;
|
||||
|
||||
broadcast( new FrequencyChangeEvent(Event.REQUEST_CHANNEL_FREQUENCY_CORRECTION_CHANGE, correction ) );
|
||||
if(correction > mFrequencyCorrectionMaximum)
|
||||
{
|
||||
correction = mFrequencyCorrectionMaximum;
|
||||
}
|
||||
else if(correction < -mFrequencyCorrectionMaximum)
|
||||
{
|
||||
correction = -mFrequencyCorrectionMaximum;
|
||||
}
|
||||
|
||||
mFrequencyAdjustmentRequested = 0;
|
||||
}
|
||||
}
|
||||
broadcast(SourceEvent.channelFrequencyCorrectionRequest(correction));
|
||||
|
||||
public void receive( float sample )
|
||||
mFrequencyAdjustmentRequested = 0;
|
||||
}
|
||||
}
|
||||
|
||||
public void receive(float sample)
|
||||
{
|
||||
sample = mGainController.correct( sample );
|
||||
|
||||
if( mResetFrequencyTracker )
|
||||
{
|
||||
mCoarseFrequencyCorrection = 0.0f;
|
||||
mFineFrequencyCorrection = 0.0f;
|
||||
|
||||
broadcast( new FrequencyChangeEvent(
|
||||
Event.REQUEST_CHANNEL_FREQUENCY_CORRECTION_CHANGE, 0 ));
|
||||
|
||||
mResetFrequencyTracker = false;
|
||||
}
|
||||
|
||||
mSymbolClock += mSymbolTime;
|
||||
|
||||
mHistory[ mHistoryLast++ ] = sample;
|
||||
|
||||
mHistoryLast %= NUMBER_FILTER_TAPS;
|
||||
|
||||
if( mSymbolClock > 1.0f )
|
||||
{
|
||||
mSymbolClock -= 1.0f;
|
||||
|
||||
int imu = (int)Math.floor( 0.5 +
|
||||
( (float)NUMBER_FILTER_STEPS * ( mSymbolClock / mSymbolTime ) ) );
|
||||
|
||||
if( imu >= NUMBER_FILTER_STEPS )
|
||||
{
|
||||
imu = NUMBER_FILTER_STEPS - 1;
|
||||
}
|
||||
sample = mGainController.correct(sample);
|
||||
|
||||
int imu_p1 = imu + 1;
|
||||
if(mResetFrequencyTracker)
|
||||
{
|
||||
mCoarseFrequencyCorrection = 0.0f;
|
||||
mFineFrequencyCorrection = 0.0f;
|
||||
|
||||
int j = mHistoryLast;
|
||||
|
||||
double interp = 0.0;
|
||||
double interp_p1 = 0.0;
|
||||
|
||||
for( int i = 0; i < NUMBER_FILTER_TAPS; i++ )
|
||||
{
|
||||
interp += TAPS[ imu][ i ] * mHistory[ j ];
|
||||
interp_p1 += TAPS[ imu_p1 ][ i ] * mHistory[ j ];
|
||||
|
||||
j = ( j + 1 ) % NUMBER_FILTER_TAPS;
|
||||
}
|
||||
broadcast(SourceEvent.channelFrequencyCorrectionRequest(0l));
|
||||
|
||||
mResetFrequencyTracker = false;
|
||||
}
|
||||
|
||||
mSymbolClock += mSymbolTime;
|
||||
|
||||
mHistory[mHistoryLast++] = sample;
|
||||
|
||||
mHistoryLast %= NUMBER_FILTER_TAPS;
|
||||
|
||||
if(mSymbolClock > 1.0f)
|
||||
{
|
||||
mSymbolClock -= 1.0f;
|
||||
|
||||
int imu = (int)Math.floor(0.5 +
|
||||
((float)NUMBER_FILTER_STEPS * (mSymbolClock / mSymbolTime)));
|
||||
|
||||
if(imu >= NUMBER_FILTER_STEPS)
|
||||
{
|
||||
imu = NUMBER_FILTER_STEPS - 1;
|
||||
}
|
||||
|
||||
int imu_p1 = imu + 1;
|
||||
|
||||
int j = mHistoryLast;
|
||||
|
||||
double interp = 0.0;
|
||||
double interp_p1 = 0.0;
|
||||
|
||||
for(int i = 0; i < NUMBER_FILTER_TAPS; i++)
|
||||
{
|
||||
interp += TAPS[imu][i] * mHistory[j];
|
||||
interp_p1 += TAPS[imu_p1][i] * mHistory[j];
|
||||
|
||||
j = (j + 1) % NUMBER_FILTER_TAPS;
|
||||
}
|
||||
|
||||
/* Output symbol will be interpolated value corrected for symbol
|
||||
* spread and frequency offset */
|
||||
interp -= mFineFrequencyCorrection;
|
||||
interp_p1 -= mFineFrequencyCorrection;
|
||||
interp -= mFineFrequencyCorrection;
|
||||
interp_p1 -= mFineFrequencyCorrection;
|
||||
|
||||
/* Correct output for symbol deviation (spread) */
|
||||
float output = (float)( 2.0 * interp / mSymbolSpread );
|
||||
float output = (float)(2.0 * interp / mSymbolSpread);
|
||||
|
||||
/* Detect received symbol error: basically use a hard decision and
|
||||
* subtract off expected position nominal symbol level which will be
|
||||
* +/- 0.5 * symbol spread and +/- 1.5 symbol spread. Remember that
|
||||
* nominal symbol spread will be 2.0 */
|
||||
double symbolError;
|
||||
|
||||
if( interp < -mSymbolSpread )
|
||||
{
|
||||
double symbolError;
|
||||
|
||||
if(interp < -mSymbolSpread)
|
||||
{
|
||||
/* symbol is -3: Expected at -1.5 * symbol spread */
|
||||
symbolError = interp + ( 1.5 * mSymbolSpread );
|
||||
mSymbolSpread -= ( symbolError * 0.5 * K_SYMBOL_SPREAD );
|
||||
}
|
||||
else if( interp < 0.0 )
|
||||
{
|
||||
symbolError = interp + (1.5 * mSymbolSpread);
|
||||
mSymbolSpread -= (symbolError * 0.5 * K_SYMBOL_SPREAD);
|
||||
}
|
||||
else if(interp < 0.0)
|
||||
{
|
||||
/* symbol is -1: Expected at -0.5 * symbol_spread */
|
||||
symbolError = interp + (0.5 * mSymbolSpread );
|
||||
mSymbolSpread -= ( symbolError * K_SYMBOL_SPREAD );
|
||||
}
|
||||
else if( interp < mSymbolSpread )
|
||||
{
|
||||
symbolError = interp + (0.5 * mSymbolSpread);
|
||||
mSymbolSpread -= (symbolError * K_SYMBOL_SPREAD);
|
||||
}
|
||||
else if(interp < mSymbolSpread)
|
||||
{
|
||||
/* symbol is +1: Expected at +0.5 * symbol_spread */
|
||||
symbolError = interp - ( 0.5 * mSymbolSpread );
|
||||
mSymbolSpread += ( symbolError * K_SYMBOL_SPREAD );
|
||||
}
|
||||
else
|
||||
{
|
||||
symbolError = interp - (0.5 * mSymbolSpread);
|
||||
mSymbolSpread += (symbolError * K_SYMBOL_SPREAD);
|
||||
}
|
||||
else
|
||||
{
|
||||
/* symbol is +3: Expected at +1.5 * symbol_spread */
|
||||
symbolError = interp - ( 1.5 * mSymbolSpread );
|
||||
mSymbolSpread += ( symbolError * 0.5 * K_SYMBOL_SPREAD );
|
||||
}
|
||||
symbolError = interp - (1.5 * mSymbolSpread);
|
||||
mSymbolSpread += (symbolError * 0.5 * K_SYMBOL_SPREAD);
|
||||
}
|
||||
|
||||
/* Symbol clock tracking loop adjustment */
|
||||
if( interp_p1 < interp )
|
||||
{
|
||||
mSymbolClock += symbolError * K_SYMBOL_TIMING;
|
||||
}
|
||||
else
|
||||
{
|
||||
mSymbolClock -= symbolError * K_SYMBOL_TIMING;
|
||||
}
|
||||
|
||||
if( mSymbolSpread < SYMBOL_SPREAD_MIN )
|
||||
{
|
||||
mGainController.increase();
|
||||
|
||||
mSymbolSpread = SYMBOL_SPREAD_MIN;
|
||||
}
|
||||
else if( mSymbolSpread > SYMBOL_SPREAD_MAX )
|
||||
{
|
||||
mGainController.decrease();
|
||||
if(interp_p1 < interp)
|
||||
{
|
||||
mSymbolClock += symbolError * K_SYMBOL_TIMING;
|
||||
}
|
||||
else
|
||||
{
|
||||
mSymbolClock -= symbolError * K_SYMBOL_TIMING;
|
||||
}
|
||||
|
||||
mSymbolSpread = SYMBOL_SPREAD_MAX;
|
||||
}
|
||||
if(mSymbolSpread < SYMBOL_SPREAD_MIN)
|
||||
{
|
||||
mGainController.increase();
|
||||
|
||||
mCoarseFrequencyCorrection += ( ( mFineFrequencyCorrection -
|
||||
mCoarseFrequencyCorrection ) * K_COARSE_FREQUENCY );
|
||||
|
||||
mFineFrequencyCorrection += ( symbolError * K_FINE_FREQUENCY );
|
||||
mSymbolSpread = SYMBOL_SPREAD_MIN;
|
||||
}
|
||||
else if(mSymbolSpread > SYMBOL_SPREAD_MAX)
|
||||
{
|
||||
mGainController.decrease();
|
||||
|
||||
mSymbolSpread = SYMBOL_SPREAD_MAX;
|
||||
}
|
||||
|
||||
mCoarseFrequencyCorrection += ((mFineFrequencyCorrection -
|
||||
mCoarseFrequencyCorrection) * K_COARSE_FREQUENCY);
|
||||
|
||||
mFineFrequencyCorrection += (symbolError * K_FINE_FREQUENCY);
|
||||
|
||||
/* Queue a frequency adjustment (once per buffer) as needed */
|
||||
if( Math.abs( mCoarseFrequencyCorrection ) > COARSE_FREQUENCY_THRESHOLD )
|
||||
{
|
||||
mFrequencyAdjustmentRequested =
|
||||
500 * ( mCoarseFrequencyCorrection > 0 ? 1 : -1 );
|
||||
}
|
||||
|
||||
if( mSymbolSpreadTap != null )
|
||||
{
|
||||
mSymbolSpreadTap.receive( mSymbolSpread );
|
||||
}
|
||||
if(Math.abs(mCoarseFrequencyCorrection) > COARSE_FREQUENCY_THRESHOLD)
|
||||
{
|
||||
mFrequencyAdjustmentRequested =
|
||||
500 * (mCoarseFrequencyCorrection > 0 ? 1 : -1);
|
||||
}
|
||||
|
||||
if(mSymbolSpreadTap != null)
|
||||
{
|
||||
mSymbolSpreadTap.receive(mSymbolSpread);
|
||||
}
|
||||
|
||||
/* dispatch the interpolated value to the listener */
|
||||
if( mListener != null )
|
||||
{
|
||||
mListener.receive( output );
|
||||
}
|
||||
}
|
||||
if(mListener != null)
|
||||
{
|
||||
mListener.receive(output);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@Override
|
||||
public void setListener( RealSampleListener listener )
|
||||
@Override
|
||||
public void setListener(RealSampleListener listener)
|
||||
{
|
||||
mListener = listener;
|
||||
mListener = listener;
|
||||
}
|
||||
|
||||
@Override
|
||||
public void removeListener( RealSampleListener listener )
|
||||
@Override
|
||||
public void removeListener(RealSampleListener listener)
|
||||
{
|
||||
mListener = null;
|
||||
mListener = null;
|
||||
}
|
||||
|
||||
@Override
|
||||
|
||||
@Override
|
||||
public List<TapGroup> getTapGroups()
|
||||
{
|
||||
if( mAvailableTaps == null )
|
||||
{
|
||||
mAvailableTaps = new ArrayList<>();
|
||||
if(mAvailableTaps == null)
|
||||
{
|
||||
mAvailableTaps = new ArrayList<>();
|
||||
|
||||
/*
|
||||
* Since the target is 2.0, we subtract 2.0 from the value so that
|
||||
* a good value is plotted right on the zero center line
|
||||
*/
|
||||
TapGroup group = new TapGroup( "C4FM Symbol Filter" );
|
||||
|
||||
group.add(
|
||||
new AdditiveFloatTap( INSTRUMENT_SYMBOL_SPREAD, 0, 0.1f, -2.0f ) );
|
||||
|
||||
mAvailableTaps.add( group );
|
||||
}
|
||||
|
||||
return mAvailableTaps;
|
||||
TapGroup group = new TapGroup("C4FM Symbol Filter");
|
||||
|
||||
group.add(
|
||||
new AdditiveFloatTap(INSTRUMENT_SYMBOL_SPREAD, 0, 0.1f, -2.0f));
|
||||
|
||||
mAvailableTaps.add(group);
|
||||
}
|
||||
|
||||
return mAvailableTaps;
|
||||
}
|
||||
|
||||
@Override
|
||||
public void registerTap( Tap tap )
|
||||
@Override
|
||||
public void registerTap(Tap tap)
|
||||
{
|
||||
if( tap.getName().contentEquals( INSTRUMENT_SYMBOL_SPREAD ) )
|
||||
{
|
||||
mSymbolSpreadTap = (FloatTap)tap;
|
||||
}
|
||||
if(tap.getName().contentEquals(INSTRUMENT_SYMBOL_SPREAD))
|
||||
{
|
||||
mSymbolSpreadTap = (FloatTap)tap;
|
||||
}
|
||||
}
|
||||
|
||||
@Override
|
||||
public void unregisterTap( Tap tap )
|
||||
@Override
|
||||
public void unregisterTap(Tap tap)
|
||||
{
|
||||
if( tap.getName().contentEquals( INSTRUMENT_SYMBOL_SPREAD ) )
|
||||
{
|
||||
mSymbolSpreadTap = null;
|
||||
}
|
||||
if(tap.getName().contentEquals(INSTRUMENT_SYMBOL_SPREAD))
|
||||
{
|
||||
mSymbolSpreadTap = null;
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Broadcasts a frequency change event to the registered listener
|
||||
*
|
||||
* @param event
|
||||
*/
|
||||
public void broadcast( FrequencyChangeEvent event )
|
||||
{
|
||||
if( mFrequencyChangeListener != null )
|
||||
{
|
||||
mFrequencyChangeListener.receive( event );
|
||||
}
|
||||
}
|
||||
public void broadcast(SourceEvent event)
|
||||
{
|
||||
if(mFrequencyChangeListener != null)
|
||||
{
|
||||
mFrequencyChangeListener.receive(event);
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Sets the listener for frequency change events.
|
||||
*
|
||||
* @param listener
|
||||
*/
|
||||
public void setFrequencyChangeListener( Listener<FrequencyChangeEvent> listener )
|
||||
{
|
||||
mFrequencyChangeListener = listener;
|
||||
}
|
||||
|
||||
@Override
|
||||
public Listener<FrequencyChangeEvent> getFrequencyChangeListener()
|
||||
{
|
||||
if( mFrequencyCorrectionProcessor == null )
|
||||
{
|
||||
mFrequencyCorrectionProcessor = new FrequencyCorrectionProcessor();
|
||||
}
|
||||
|
||||
return mFrequencyCorrectionProcessor;
|
||||
}
|
||||
public void setFrequencyChangeListener(Listener<SourceEvent> listener)
|
||||
{
|
||||
mFrequencyChangeListener = listener;
|
||||
}
|
||||
|
||||
/**
|
||||
* Receives notifications that the channel frequency correction has been
|
||||
* applied and updates internal tracking value.
|
||||
*/
|
||||
public class FrequencyCorrectionProcessor implements Listener<FrequencyChangeEvent>
|
||||
{
|
||||
@Override
|
||||
public void receive( FrequencyChangeEvent event )
|
||||
{
|
||||
switch( event.getEvent() )
|
||||
{
|
||||
case NOTIFICATION_CHANNEL_FREQUENCY_CORRECTION_CHANGE:
|
||||
mFrequencyCorrection = event.getValue().intValue();
|
||||
@Override
|
||||
public Listener<SourceEvent> getSourceEventListener()
|
||||
{
|
||||
if(mFrequencyCorrectionProcessor == null)
|
||||
{
|
||||
mFrequencyCorrectionProcessor = new FrequencyCorrectionProcessor();
|
||||
}
|
||||
|
||||
//Reset internal frequency tracking
|
||||
mCoarseFrequencyCorrection = 0.0f;
|
||||
mFineFrequencyCorrection = 0.0f;
|
||||
break;
|
||||
case NOTIFICATION_FREQUENCY_CORRECTION_CHANGE:
|
||||
case NOTIFICATION_SAMPLE_RATE_CHANGE:
|
||||
mResetFrequencyTracker = true;
|
||||
break;
|
||||
default:
|
||||
break;
|
||||
}
|
||||
}
|
||||
}
|
||||
return mFrequencyCorrectionProcessor;
|
||||
}
|
||||
|
||||
/**
|
||||
* Receives notifications that the channel frequency correction has been
|
||||
* applied and updates internal tracking value.
|
||||
*/
|
||||
public class FrequencyCorrectionProcessor implements Listener<SourceEvent>
|
||||
{
|
||||
@Override
|
||||
public void receive(SourceEvent event)
|
||||
{
|
||||
switch(event.getEvent())
|
||||
{
|
||||
case NOTIFICATION_CHANNEL_FREQUENCY_CORRECTION_CHANGE:
|
||||
mFrequencyCorrection = event.getValue().intValue();
|
||||
|
||||
//Reset internal frequency tracking
|
||||
mCoarseFrequencyCorrection = 0.0f;
|
||||
mFineFrequencyCorrection = 0.0f;
|
||||
break;
|
||||
case NOTIFICATION_FREQUENCY_CORRECTION_CHANGE:
|
||||
case NOTIFICATION_SAMPLE_RATE_CHANGE:
|
||||
mResetFrequencyTracker = true;
|
||||
break;
|
||||
default:
|
||||
break;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -4,10 +4,10 @@ import io.github.dsheirer.alias.AliasList;
|
||||
import io.github.dsheirer.instrument.Instrumentable;
|
||||
import io.github.dsheirer.module.decode.Decoder;
|
||||
import io.github.dsheirer.module.decode.DecoderType;
|
||||
import io.github.dsheirer.source.tuner.frequency.IFrequencyChangeListener;
|
||||
import io.github.dsheirer.source.tuner.frequency.IFrequencyChangeProvider;
|
||||
import io.github.dsheirer.source.ISourceEventListener;
|
||||
import io.github.dsheirer.source.ISourceEventProvider;
|
||||
|
||||
public abstract class P25Decoder extends Decoder implements IFrequencyChangeListener, IFrequencyChangeProvider, Instrumentable
|
||||
public abstract class P25Decoder extends Decoder implements ISourceEventListener, ISourceEventProvider, Instrumentable
|
||||
{
|
||||
private P25MessageProcessor mMessageProcessor;
|
||||
|
||||
|
||||
@@ -142,7 +142,6 @@ import java.util.List;
|
||||
import java.util.Map;
|
||||
import java.util.Set;
|
||||
import java.util.TreeSet;
|
||||
import java.util.concurrent.ScheduledExecutorService;
|
||||
|
||||
public class P25DecoderState extends DecoderState
|
||||
{
|
||||
@@ -223,11 +222,6 @@ public class P25DecoderState extends DecoderState
|
||||
return DecoderType.P25_PHASE1;
|
||||
}
|
||||
|
||||
@Override
|
||||
public void stop()
|
||||
{
|
||||
}
|
||||
|
||||
/**
|
||||
* Performs a full reset to prepare this object for reuse on a new channel
|
||||
*/
|
||||
@@ -241,6 +235,12 @@ public class P25DecoderState extends DecoderState
|
||||
mSystem = null;
|
||||
}
|
||||
|
||||
@Override
|
||||
public void init()
|
||||
{
|
||||
|
||||
}
|
||||
|
||||
/**
|
||||
* Resets any temporal state details
|
||||
*/
|
||||
@@ -259,14 +259,6 @@ public class P25DecoderState extends DecoderState
|
||||
mCurrentCallEvent = null;
|
||||
}
|
||||
|
||||
/**
|
||||
* Performs any initialization operations to prepare for use
|
||||
*/
|
||||
@Override
|
||||
public void init()
|
||||
{
|
||||
}
|
||||
|
||||
/**
|
||||
* Indicates if there a call event exists for the specified channel and
|
||||
* from and to talkgroup identifiers.
|
||||
@@ -3625,7 +3617,7 @@ public class P25DecoderState extends DecoderState
|
||||
}
|
||||
|
||||
@Override
|
||||
public void start(ScheduledExecutorService executor)
|
||||
public void start()
|
||||
{
|
||||
//Change the default (45-second) traffic channel timeout to 1 second
|
||||
if(mChannelType == ChannelType.TRAFFIC)
|
||||
@@ -3633,4 +3625,10 @@ public class P25DecoderState extends DecoderState
|
||||
broadcast(new ChangeChannelTimeoutEvent(this, ChannelType.TRAFFIC, 1000));
|
||||
}
|
||||
}
|
||||
|
||||
@Override
|
||||
public void stop()
|
||||
{
|
||||
|
||||
}
|
||||
}
|
||||
|
||||
@@ -26,13 +26,12 @@ import io.github.dsheirer.instrument.tap.stream.FloatTap;
|
||||
import io.github.dsheirer.sample.Listener;
|
||||
import io.github.dsheirer.sample.real.IFilteredRealBufferListener;
|
||||
import io.github.dsheirer.sample.real.RealBuffer;
|
||||
import io.github.dsheirer.source.tuner.frequency.FrequencyChangeEvent;
|
||||
import io.github.dsheirer.source.SourceEvent;
|
||||
import org.slf4j.Logger;
|
||||
import org.slf4j.LoggerFactory;
|
||||
|
||||
import java.util.ArrayList;
|
||||
import java.util.List;
|
||||
import java.util.concurrent.ScheduledExecutorService;
|
||||
|
||||
public class P25_C4FMDecoder extends P25Decoder implements IFilteredRealBufferListener
|
||||
{
|
||||
@@ -60,7 +59,7 @@ public class P25_C4FMDecoder extends P25Decoder implements IFilteredRealBufferLi
|
||||
* infrastructure and subscriber identities that will be included in each
|
||||
* decoded message
|
||||
*/
|
||||
public P25_C4FMDecoder(AliasList aliasList, int frequencyCorrectionMaximum )
|
||||
public P25_C4FMDecoder( AliasList aliasList, int frequencyCorrectionMaximum )
|
||||
{
|
||||
super( aliasList );
|
||||
|
||||
@@ -193,28 +192,7 @@ public class P25_C4FMDecoder extends P25Decoder implements IFilteredRealBufferLi
|
||||
}
|
||||
|
||||
@Override
|
||||
public void reset()
|
||||
{
|
||||
// TODO Auto-generated method stub
|
||||
|
||||
}
|
||||
|
||||
@Override
|
||||
public void start( ScheduledExecutorService executor )
|
||||
{
|
||||
// TODO Auto-generated method stub
|
||||
|
||||
}
|
||||
|
||||
@Override
|
||||
public void stop()
|
||||
{
|
||||
// TODO Auto-generated method stub
|
||||
|
||||
}
|
||||
|
||||
@Override
|
||||
public void setFrequencyChangeListener( Listener<FrequencyChangeEvent> listener )
|
||||
public void setSourceEventListener(Listener<SourceEvent> listener )
|
||||
{
|
||||
if( mSymbolFilter != null )
|
||||
{
|
||||
@@ -223,7 +201,7 @@ public class P25_C4FMDecoder extends P25Decoder implements IFilteredRealBufferLi
|
||||
}
|
||||
|
||||
@Override
|
||||
public void removeFrequencyChangeListener()
|
||||
public void removeSourceEventListener()
|
||||
{
|
||||
if( mSymbolFilter != null )
|
||||
{
|
||||
@@ -232,11 +210,11 @@ public class P25_C4FMDecoder extends P25Decoder implements IFilteredRealBufferLi
|
||||
}
|
||||
|
||||
@Override
|
||||
public Listener<FrequencyChangeEvent> getFrequencyChangeListener()
|
||||
public Listener<SourceEvent> getSourceEventListener()
|
||||
{
|
||||
if( mSymbolFilter != null )
|
||||
{
|
||||
return mSymbolFilter.getFrequencyChangeListener();
|
||||
return mSymbolFilter.getSourceEventListener();
|
||||
}
|
||||
|
||||
return null;
|
||||
|
||||
@@ -34,13 +34,12 @@ import io.github.dsheirer.sample.Listener;
|
||||
import io.github.dsheirer.sample.complex.ComplexBuffer;
|
||||
import io.github.dsheirer.sample.complex.ComplexBufferToStreamConverter;
|
||||
import io.github.dsheirer.sample.complex.IComplexBufferListener;
|
||||
import io.github.dsheirer.source.tuner.frequency.FrequencyChangeEvent;
|
||||
import io.github.dsheirer.source.SourceEvent;
|
||||
import org.slf4j.Logger;
|
||||
import org.slf4j.LoggerFactory;
|
||||
|
||||
import java.util.ArrayList;
|
||||
import java.util.List;
|
||||
import java.util.concurrent.ScheduledExecutorService;
|
||||
|
||||
public class P25_LSMDecoder extends P25Decoder implements IComplexBufferListener
|
||||
{
|
||||
@@ -67,7 +66,7 @@ public class P25_LSMDecoder extends P25Decoder implements IComplexBufferListener
|
||||
super( aliasList );
|
||||
|
||||
mBasebandFilter = new ComplexFIRFilter_CB_CB( FilterFactory.getLowPass(
|
||||
48000, 7250, 8000, 60, WindowType.HANNING, true ), 1.0f );
|
||||
48000, 7250, 8000, 60, WindowType.HANN, true ), 1.0f );
|
||||
|
||||
mBasebandFilter.setListener( mStreamConverter );
|
||||
|
||||
@@ -106,29 +105,6 @@ public class P25_LSMDecoder extends P25Decoder implements IComplexBufferListener
|
||||
mMessageFramer = null;
|
||||
}
|
||||
|
||||
@Override
|
||||
public void setFrequencyChangeListener(Listener<FrequencyChangeEvent> listener)
|
||||
{
|
||||
}
|
||||
|
||||
@Override
|
||||
public void removeFrequencyChangeListener()
|
||||
{
|
||||
}
|
||||
|
||||
@Override
|
||||
public Listener<FrequencyChangeEvent> getFrequencyChangeListener()
|
||||
{
|
||||
return new Listener<FrequencyChangeEvent>()
|
||||
{
|
||||
@Override
|
||||
public void receive(FrequencyChangeEvent frequencyChangeEvent)
|
||||
{
|
||||
//Ignored
|
||||
}
|
||||
};
|
||||
}
|
||||
|
||||
@Override
|
||||
public Listener<ComplexBuffer> getComplexBufferListener()
|
||||
{
|
||||
@@ -237,23 +213,20 @@ public class P25_LSMDecoder extends P25Decoder implements IComplexBufferListener
|
||||
}
|
||||
|
||||
@Override
|
||||
public void reset()
|
||||
public Listener<SourceEvent> getSourceEventListener()
|
||||
{
|
||||
// TODO Auto-generated method stub
|
||||
|
||||
return null;
|
||||
}
|
||||
|
||||
@Override
|
||||
public void start( ScheduledExecutorService executor )
|
||||
public void setSourceEventListener(Listener<SourceEvent> listener)
|
||||
{
|
||||
// TODO Auto-generated method stub
|
||||
|
||||
|
||||
}
|
||||
|
||||
@Override
|
||||
public void stop()
|
||||
public void removeSourceEventListener()
|
||||
{
|
||||
// TODO Auto-generated method stub
|
||||
|
||||
|
||||
}
|
||||
}
|
||||
|
||||
@@ -20,8 +20,6 @@ import jmbe.iface.AudioConverter;
|
||||
import org.slf4j.Logger;
|
||||
import org.slf4j.LoggerFactory;
|
||||
|
||||
import java.util.concurrent.ScheduledExecutorService;
|
||||
|
||||
public class P25AudioModule extends Module implements Listener<Message>, IAudioPacketProvider, IMessageListener,
|
||||
ISquelchStateListener
|
||||
{
|
||||
@@ -71,7 +69,7 @@ public class P25AudioModule extends Module implements Listener<Message>, IAudioP
|
||||
}
|
||||
|
||||
@Override
|
||||
public void start(ScheduledExecutorService executor)
|
||||
public void start()
|
||||
{
|
||||
|
||||
}
|
||||
|
||||
@@ -33,7 +33,6 @@ import io.github.dsheirer.sample.real.RealBuffer;
|
||||
|
||||
import java.util.ArrayList;
|
||||
import java.util.List;
|
||||
import java.util.concurrent.ScheduledExecutorService;
|
||||
|
||||
public class PassportDecoder extends Decoder
|
||||
implements IUnFilteredRealBufferListener, Instrumentable
|
||||
@@ -108,14 +107,4 @@ public class PassportDecoder extends Decoder
|
||||
{
|
||||
mPassportMessageFramer.reset();
|
||||
}
|
||||
|
||||
@Override
|
||||
public void start( ScheduledExecutorService executor )
|
||||
{
|
||||
}
|
||||
|
||||
@Override
|
||||
public void stop()
|
||||
{
|
||||
}
|
||||
}
|
||||
|
||||
@@ -40,7 +40,6 @@ import java.util.HashMap;
|
||||
import java.util.HashSet;
|
||||
import java.util.Iterator;
|
||||
import java.util.TreeSet;
|
||||
import java.util.concurrent.ScheduledExecutorService;
|
||||
|
||||
public class PassportDecoderState extends DecoderState
|
||||
{
|
||||
@@ -76,21 +75,6 @@ public class PassportDecoderState extends DecoderState
|
||||
return DecoderType.PASSPORT;
|
||||
}
|
||||
|
||||
@Override
|
||||
public void start(ScheduledExecutorService executor)
|
||||
{
|
||||
}
|
||||
|
||||
@Override
|
||||
public void init()
|
||||
{
|
||||
}
|
||||
|
||||
@Override
|
||||
public void stop()
|
||||
{
|
||||
}
|
||||
|
||||
private void logTalkgroup(String talkgroup)
|
||||
{
|
||||
if(mTalkgroupsFirstHeard.contains(talkgroup))
|
||||
@@ -519,6 +503,24 @@ public class PassportDecoderState extends DecoderState
|
||||
resetState();
|
||||
}
|
||||
|
||||
@Override
|
||||
public void start()
|
||||
{
|
||||
|
||||
}
|
||||
|
||||
@Override
|
||||
public void stop()
|
||||
{
|
||||
|
||||
}
|
||||
|
||||
@Override
|
||||
public void init()
|
||||
{
|
||||
|
||||
}
|
||||
|
||||
private void resetState()
|
||||
{
|
||||
mToTalkgroupAttribute.reset();
|
||||
|
||||
@@ -40,7 +40,6 @@ import io.github.dsheirer.sample.real.RealBuffer;
|
||||
|
||||
import java.util.ArrayList;
|
||||
import java.util.List;
|
||||
import java.util.concurrent.ScheduledExecutorService;
|
||||
|
||||
/**
|
||||
* TAIT 1200 - 1200 baud 2FSK decoder
|
||||
@@ -189,25 +188,4 @@ public class Tait1200Decoder extends Decoder implements IFilteredRealBufferListe
|
||||
{
|
||||
return DecoderType.TAIT_1200;
|
||||
}
|
||||
|
||||
@Override
|
||||
public void reset()
|
||||
{
|
||||
// TODO Auto-generated method stub
|
||||
|
||||
}
|
||||
|
||||
@Override
|
||||
public void start( ScheduledExecutorService executor )
|
||||
{
|
||||
// TODO Auto-generated method stub
|
||||
|
||||
}
|
||||
|
||||
@Override
|
||||
public void stop()
|
||||
{
|
||||
// TODO Auto-generated method stub
|
||||
|
||||
}
|
||||
}
|
||||
|
||||
@@ -38,7 +38,6 @@ import java.util.ArrayList;
|
||||
import java.util.Collections;
|
||||
import java.util.List;
|
||||
import java.util.TreeSet;
|
||||
import java.util.concurrent.ScheduledExecutorService;
|
||||
|
||||
public class Tait1200DecoderState extends DecoderState
|
||||
{
|
||||
@@ -65,16 +64,6 @@ public class Tait1200DecoderState extends DecoderState
|
||||
return DecoderType.TAIT_1200;
|
||||
}
|
||||
|
||||
@Override
|
||||
public void start(ScheduledExecutorService executor)
|
||||
{
|
||||
}
|
||||
|
||||
@Override
|
||||
public void stop()
|
||||
{
|
||||
}
|
||||
|
||||
@Override
|
||||
public void reset()
|
||||
{
|
||||
@@ -83,6 +72,24 @@ public class Tait1200DecoderState extends DecoderState
|
||||
resetState();
|
||||
}
|
||||
|
||||
@Override
|
||||
public void start()
|
||||
{
|
||||
|
||||
}
|
||||
|
||||
@Override
|
||||
public void stop()
|
||||
{
|
||||
|
||||
}
|
||||
|
||||
@Override
|
||||
public void init()
|
||||
{
|
||||
|
||||
}
|
||||
|
||||
private void resetState()
|
||||
{
|
||||
mFromAttribute.reset();
|
||||
@@ -176,12 +183,6 @@ public class Tait1200DecoderState extends DecoderState
|
||||
return sb.toString();
|
||||
}
|
||||
|
||||
@Override
|
||||
public void init()
|
||||
{
|
||||
/* No initialization required */
|
||||
}
|
||||
|
||||
/**
|
||||
* Responds to reset events issued by the channel state
|
||||
*/
|
||||
|
||||
@@ -30,95 +30,81 @@ import io.github.dsheirer.sample.complex.IComplexBufferListener;
|
||||
import io.github.dsheirer.sample.real.IFilteredRealBufferProvider;
|
||||
import io.github.dsheirer.sample.real.RealBuffer;
|
||||
|
||||
import java.util.concurrent.ScheduledExecutorService;
|
||||
|
||||
public class AMDemodulatorModule extends Module
|
||||
implements IComplexBufferListener, IFilteredRealBufferProvider
|
||||
implements IComplexBufferListener, IFilteredRealBufferProvider
|
||||
{
|
||||
private static final int SAMPLE_RATE = 48000;
|
||||
|
||||
private ComplexFIRFilter_CB_CB mIQFilter;
|
||||
private AMDemodulator_CB mDemodulator;
|
||||
private RealFIRFilter_RB_RB mLowPassFilter;
|
||||
private static final int SAMPLE_RATE = 48000;
|
||||
|
||||
private ComplexFIRFilter_CB_CB mIQFilter;
|
||||
private AMDemodulator_CB mDemodulator;
|
||||
private RealFIRFilter_RB_RB mLowPassFilter;
|
||||
private AutomaticGainControl_RB mAGC;
|
||||
|
||||
/**
|
||||
* FM Demodulator with integrated DC removal and automatic frequency
|
||||
* correction control.
|
||||
*
|
||||
* @param pass - pass frequency for IQ filtering prior to demodulation. This
|
||||
* frequency should be half of the signal bandwidth since the filter will
|
||||
* be applied against each of the inphase and quadrature signals and the
|
||||
* combined pass bandwidth will be twice this value.
|
||||
*
|
||||
* @param stop - stop frequency for IQ filtering prior to demodulation.
|
||||
*
|
||||
* @param maxFrequencyCorrection - defines the maximum frequency +/- that
|
||||
* the correction controller can adjust from the center tuned frequency.
|
||||
* Set this value to 0 for no frequency correction control.
|
||||
*/
|
||||
public AMDemodulatorModule()
|
||||
{
|
||||
mIQFilter = new ComplexFIRFilter_CB_CB( FilterFactory.getLowPass(
|
||||
SAMPLE_RATE, 5000, 73, WindowType.HAMMING ), 1.0f );
|
||||
|
||||
mDemodulator = new AMDemodulator_CB( 500.0f );
|
||||
mIQFilter.setListener( mDemodulator );
|
||||
/**
|
||||
* AM Demodulator Module
|
||||
*/
|
||||
public AMDemodulatorModule()
|
||||
{
|
||||
mIQFilter = new ComplexFIRFilter_CB_CB(FilterFactory.getLowPass(
|
||||
SAMPLE_RATE, 5000, 73, WindowType.HAMMING), 1.0f);
|
||||
|
||||
mLowPassFilter = new RealFIRFilter_RB_RB(
|
||||
FilterFactory.getLowPass( 48000, 3000, 31, WindowType.COSINE ), 1.0f );
|
||||
mDemodulator.setListener( mLowPassFilter );
|
||||
|
||||
mAGC = new AutomaticGainControl_RB();
|
||||
mLowPassFilter.setListener( mAGC );
|
||||
}
|
||||
mDemodulator = new AMDemodulator_CB(500.0f);
|
||||
mIQFilter.setListener(mDemodulator);
|
||||
|
||||
@Override
|
||||
public Listener<ComplexBuffer> getComplexBufferListener()
|
||||
{
|
||||
return mIQFilter;
|
||||
}
|
||||
mLowPassFilter = new RealFIRFilter_RB_RB(
|
||||
FilterFactory.getLowPass(48000, 3000, 31, WindowType.COSINE), 1.0f);
|
||||
mDemodulator.setListener(mLowPassFilter);
|
||||
|
||||
@Override
|
||||
public void dispose()
|
||||
{
|
||||
mIQFilter.dispose();
|
||||
mIQFilter = null;
|
||||
|
||||
mDemodulator.dispose();
|
||||
mDemodulator = null;
|
||||
|
||||
mLowPassFilter.dispose();
|
||||
mLowPassFilter = null;
|
||||
|
||||
mAGC.dispose();
|
||||
mAGC = null;
|
||||
}
|
||||
mAGC = new AutomaticGainControl_RB();
|
||||
mLowPassFilter.setListener(mAGC);
|
||||
}
|
||||
|
||||
@Override
|
||||
public void reset()
|
||||
{
|
||||
}
|
||||
@Override
|
||||
public Listener<ComplexBuffer> getComplexBufferListener()
|
||||
{
|
||||
return mIQFilter;
|
||||
}
|
||||
|
||||
@Override
|
||||
public void setFilteredRealBufferListener( Listener<RealBuffer> listener )
|
||||
{
|
||||
mAGC.setListener( listener );
|
||||
}
|
||||
@Override
|
||||
public void dispose()
|
||||
{
|
||||
mIQFilter.dispose();
|
||||
mIQFilter = null;
|
||||
|
||||
@Override
|
||||
public void removeFilteredRealBufferListener()
|
||||
{
|
||||
mAGC.removeListener();
|
||||
}
|
||||
mDemodulator.dispose();
|
||||
mDemodulator = null;
|
||||
|
||||
@Override
|
||||
public void start( ScheduledExecutorService executor )
|
||||
{
|
||||
}
|
||||
mLowPassFilter.dispose();
|
||||
mLowPassFilter = null;
|
||||
|
||||
@Override
|
||||
public void stop()
|
||||
{
|
||||
}
|
||||
mAGC.dispose();
|
||||
mAGC = null;
|
||||
}
|
||||
|
||||
@Override
|
||||
public void reset()
|
||||
{
|
||||
}
|
||||
|
||||
@Override
|
||||
public void setFilteredRealBufferListener(Listener<RealBuffer> listener)
|
||||
{
|
||||
mAGC.setListener(listener);
|
||||
}
|
||||
|
||||
@Override
|
||||
public void removeFilteredRealBufferListener()
|
||||
{
|
||||
mAGC.removeListener();
|
||||
}
|
||||
|
||||
@Override
|
||||
public void start()
|
||||
{
|
||||
}
|
||||
|
||||
@Override
|
||||
public void stop()
|
||||
{
|
||||
}
|
||||
}
|
||||
|
||||
@@ -30,8 +30,6 @@ import io.github.dsheirer.sample.real.RealBuffer;
|
||||
import org.slf4j.Logger;
|
||||
import org.slf4j.LoggerFactory;
|
||||
|
||||
import java.util.concurrent.ScheduledExecutorService;
|
||||
|
||||
public class DemodulatedAudioFilterModule extends Module implements IUnFilteredRealBufferListener, IFilteredRealBufferProvider
|
||||
{
|
||||
private final static Logger mLog = LoggerFactory.getLogger(DemodulatedAudioFilterModule.class);
|
||||
@@ -53,7 +51,7 @@ public class DemodulatedAudioFilterModule extends Module implements IUnFilteredR
|
||||
|
||||
//TODO: change this to a band pass filter
|
||||
mBandPassFilter = new RealFIRFilter_RB_RB(FilterFactory.getLowPass(48000, pass, stop, 60,
|
||||
WindowType.HANNING, true), 1.0f);
|
||||
WindowType.HANN, true), 1.0f);
|
||||
|
||||
mDCFilter.setListener(mBandPassFilter);
|
||||
}
|
||||
@@ -97,7 +95,7 @@ public class DemodulatedAudioFilterModule extends Module implements IUnFilteredR
|
||||
}
|
||||
|
||||
@Override
|
||||
public void start(ScheduledExecutorService executor)
|
||||
public void start()
|
||||
{
|
||||
}
|
||||
|
||||
|
||||
@@ -30,8 +30,6 @@ import io.github.dsheirer.sample.real.RealBuffer;
|
||||
import org.slf4j.Logger;
|
||||
import org.slf4j.LoggerFactory;
|
||||
|
||||
import java.util.concurrent.ScheduledExecutorService;
|
||||
|
||||
public class FMDemodulatorModule extends Module implements IComplexBufferListener, IUnFilteredRealBufferProvider
|
||||
{
|
||||
private final static Logger mLog = LoggerFactory.getLogger(FMDemodulatorModule.class);
|
||||
@@ -104,7 +102,7 @@ public class FMDemodulatorModule extends Module implements IComplexBufferListene
|
||||
}
|
||||
|
||||
@Override
|
||||
public void start(ScheduledExecutorService executor)
|
||||
public void start()
|
||||
{
|
||||
}
|
||||
|
||||
|
||||
@@ -30,7 +30,6 @@ import java.io.IOException;
|
||||
import java.io.OutputStreamWriter;
|
||||
import java.io.Writer;
|
||||
import java.nio.file.Path;
|
||||
import java.util.concurrent.ScheduledExecutorService;
|
||||
|
||||
public abstract class EventLogger extends Module
|
||||
{
|
||||
@@ -65,7 +64,7 @@ public abstract class EventLogger extends Module
|
||||
public abstract String getHeader();
|
||||
|
||||
@Override
|
||||
public void start(ScheduledExecutorService executor)
|
||||
public void start()
|
||||
{
|
||||
if(mLogFile == null)
|
||||
{
|
||||
@@ -80,6 +79,8 @@ public abstract class EventLogger extends Module
|
||||
|
||||
mLogFileName = sb.toString();
|
||||
|
||||
mLog.info("Creating log file:" + mLogFileName);
|
||||
|
||||
mLogFile = new OutputStreamWriter(new FileOutputStream(mLogFileName));
|
||||
|
||||
write(getHeader());
|
||||
@@ -123,17 +124,14 @@ public abstract class EventLogger extends Module
|
||||
|
||||
protected void write(String eventLogEntry)
|
||||
{
|
||||
if(mLogFile != null)
|
||||
try
|
||||
{
|
||||
try
|
||||
{
|
||||
mLogFile.write(eventLogEntry + "\n");
|
||||
mLogFile.flush();
|
||||
}
|
||||
catch(IOException e)
|
||||
{
|
||||
mLog.error("Error writing entry to event log file", e);
|
||||
}
|
||||
mLogFile.write(eventLogEntry + "\n");
|
||||
mLogFile.flush();
|
||||
}
|
||||
catch(IOException e)
|
||||
{
|
||||
mLog.error("Error writing entry to event log file", e);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -23,6 +23,7 @@ import io.github.dsheirer.audio.IAudioPacketListener;
|
||||
import io.github.dsheirer.channel.metadata.Metadata;
|
||||
import io.github.dsheirer.module.Module;
|
||||
import io.github.dsheirer.sample.Listener;
|
||||
import io.github.dsheirer.util.ThreadPool;
|
||||
import org.slf4j.Logger;
|
||||
import org.slf4j.LoggerFactory;
|
||||
|
||||
@@ -33,7 +34,6 @@ import java.nio.file.Path;
|
||||
import java.util.ArrayList;
|
||||
import java.util.List;
|
||||
import java.util.concurrent.LinkedBlockingQueue;
|
||||
import java.util.concurrent.ScheduledExecutorService;
|
||||
import java.util.concurrent.ScheduledFuture;
|
||||
import java.util.concurrent.TimeUnit;
|
||||
import java.util.concurrent.atomic.AtomicBoolean;
|
||||
@@ -193,10 +193,8 @@ public abstract class AudioRecorder extends Module implements Listener<AudioPack
|
||||
|
||||
/**
|
||||
* Starts this recorder as a scheduled thread running under the executor argument
|
||||
*
|
||||
* @param executor to use in scheduling audio conversion and file writes.
|
||||
*/
|
||||
public void start(ScheduledExecutorService executor)
|
||||
public void start()
|
||||
{
|
||||
if(mRunning.compareAndSet(false, true))
|
||||
{
|
||||
@@ -212,7 +210,7 @@ public abstract class AudioRecorder extends Module implements Listener<AudioPack
|
||||
mFileOutputStream = new FileOutputStream(mPath.toFile());
|
||||
|
||||
/* Schedule the handler to run every half second */
|
||||
mProcessorHandle = executor.scheduleAtFixedRate(mBufferProcessor, 0, 500, TimeUnit.MILLISECONDS);
|
||||
mProcessorHandle = ThreadPool.SCHEDULED.scheduleAtFixedRate(mBufferProcessor, 0, 500, TimeUnit.MILLISECONDS);
|
||||
}
|
||||
catch(IOException io)
|
||||
{
|
||||
|
||||
@@ -144,7 +144,7 @@ public class RecorderManager implements Listener<AudioPacket>
|
||||
{
|
||||
recorder = new AudioPacketWaveRecorder(filePrefix, audioPacket.getMetadata());
|
||||
|
||||
recorder.start(ThreadPool.SCHEDULED);
|
||||
recorder.start();
|
||||
|
||||
recorder.receive(audioPacket);
|
||||
mRecorders.put(identifier, recorder);
|
||||
|
||||
@@ -22,6 +22,7 @@ import io.github.dsheirer.channel.metadata.Metadata;
|
||||
import io.github.dsheirer.module.Module;
|
||||
import io.github.dsheirer.sample.ConversionUtils;
|
||||
import io.github.dsheirer.sample.Listener;
|
||||
import io.github.dsheirer.util.ThreadPool;
|
||||
import io.github.dsheirer.util.TimeStamp;
|
||||
import org.slf4j.Logger;
|
||||
import org.slf4j.LoggerFactory;
|
||||
@@ -31,7 +32,6 @@ import java.io.IOException;
|
||||
import java.nio.file.Path;
|
||||
import java.nio.file.Paths;
|
||||
import java.util.concurrent.LinkedBlockingQueue;
|
||||
import java.util.concurrent.ScheduledExecutorService;
|
||||
import java.util.concurrent.ScheduledFuture;
|
||||
import java.util.concurrent.TimeUnit;
|
||||
import java.util.concurrent.atomic.AtomicBoolean;
|
||||
@@ -89,7 +89,7 @@ public class AudioPacketWaveRecorder extends Module implements Listener<AudioPac
|
||||
return mFile;
|
||||
}
|
||||
|
||||
public void start(ScheduledExecutorService executor)
|
||||
public void start()
|
||||
{
|
||||
if(mRunning.compareAndSet(false, true))
|
||||
{
|
||||
@@ -111,7 +111,7 @@ public class AudioPacketWaveRecorder extends Module implements Listener<AudioPac
|
||||
mWriter = new WaveWriter(mAudioFormat, mFile);
|
||||
|
||||
/* Schedule the processor to run every 500 milliseconds */
|
||||
mProcessorHandle = executor.scheduleAtFixedRate(mBufferProcessor, 0, 500, TimeUnit.MILLISECONDS);
|
||||
mProcessorHandle = ThreadPool.SCHEDULED.scheduleAtFixedRate(mBufferProcessor, 0, 500, TimeUnit.MILLISECONDS);
|
||||
}
|
||||
catch(IOException io)
|
||||
{
|
||||
|
||||
@@ -23,6 +23,7 @@ import io.github.dsheirer.sample.ConversionUtils;
|
||||
import io.github.dsheirer.sample.Listener;
|
||||
import io.github.dsheirer.sample.complex.ComplexBuffer;
|
||||
import io.github.dsheirer.sample.complex.IComplexBufferListener;
|
||||
import io.github.dsheirer.util.ThreadPool;
|
||||
import io.github.dsheirer.util.TimeStamp;
|
||||
import org.slf4j.Logger;
|
||||
import org.slf4j.LoggerFactory;
|
||||
@@ -32,7 +33,6 @@ import java.io.IOException;
|
||||
import java.nio.file.Path;
|
||||
import java.nio.file.Paths;
|
||||
import java.util.concurrent.LinkedBlockingQueue;
|
||||
import java.util.concurrent.ScheduledExecutorService;
|
||||
import java.util.concurrent.ScheduledFuture;
|
||||
import java.util.concurrent.TimeUnit;
|
||||
import java.util.concurrent.atomic.AtomicBoolean;
|
||||
@@ -43,8 +43,7 @@ import java.util.concurrent.atomic.AtomicBoolean;
|
||||
public class ComplexBufferWaveRecorder extends Module
|
||||
implements IComplexBufferListener, Listener<ComplexBuffer>
|
||||
{
|
||||
private final static Logger mLog =
|
||||
LoggerFactory.getLogger( ComplexBufferWaveRecorder.class );
|
||||
private final static Logger mLog = LoggerFactory.getLogger( ComplexBufferWaveRecorder.class );
|
||||
|
||||
private WaveWriter mWriter;
|
||||
private String mFilePrefix;
|
||||
@@ -68,19 +67,19 @@ public class ComplexBufferWaveRecorder extends Module
|
||||
mFilePrefix = filePrefix;
|
||||
}
|
||||
|
||||
public Path getFile()
|
||||
{
|
||||
return mFile;
|
||||
}
|
||||
|
||||
public void start( ScheduledExecutorService executor )
|
||||
{
|
||||
if( mRunning.compareAndSet( false, true ) )
|
||||
{
|
||||
if( mBufferProcessor == null )
|
||||
{
|
||||
mBufferProcessor = new BufferProcessor();
|
||||
}
|
||||
public Path getFile()
|
||||
{
|
||||
return mFile;
|
||||
}
|
||||
|
||||
public void start()
|
||||
{
|
||||
if(mRunning.compareAndSet(false, true))
|
||||
{
|
||||
if(mBufferProcessor == null)
|
||||
{
|
||||
mBufferProcessor = new BufferProcessor();
|
||||
}
|
||||
|
||||
try
|
||||
{
|
||||
@@ -95,7 +94,7 @@ public class ComplexBufferWaveRecorder extends Module
|
||||
mWriter = new WaveWriter( mAudioFormat, mFile );
|
||||
|
||||
/* Schedule the processor to run every 500 milliseconds */
|
||||
mProcessorHandle = executor.scheduleAtFixedRate(
|
||||
mProcessorHandle = ThreadPool.SCHEDULED.scheduleAtFixedRate(
|
||||
mBufferProcessor, 0, 500, TimeUnit.MILLISECONDS );
|
||||
}
|
||||
catch( IOException io )
|
||||
|
||||
@@ -23,6 +23,7 @@ import io.github.dsheirer.sample.ConversionUtils;
|
||||
import io.github.dsheirer.sample.Listener;
|
||||
import io.github.dsheirer.sample.real.IFilteredRealBufferListener;
|
||||
import io.github.dsheirer.sample.real.RealBuffer;
|
||||
import io.github.dsheirer.util.ThreadPool;
|
||||
import io.github.dsheirer.util.TimeStamp;
|
||||
import org.slf4j.Logger;
|
||||
import org.slf4j.LoggerFactory;
|
||||
@@ -32,7 +33,6 @@ import java.io.IOException;
|
||||
import java.nio.file.Path;
|
||||
import java.nio.file.Paths;
|
||||
import java.util.concurrent.LinkedBlockingQueue;
|
||||
import java.util.concurrent.ScheduledExecutorService;
|
||||
import java.util.concurrent.ScheduledFuture;
|
||||
import java.util.concurrent.TimeUnit;
|
||||
import java.util.concurrent.atomic.AtomicBoolean;
|
||||
@@ -40,8 +40,7 @@ import java.util.concurrent.atomic.AtomicBoolean;
|
||||
/**
|
||||
* WAVE audio recorder module for recording real sample buffers to a wave file
|
||||
*/
|
||||
public class RealBufferWaveRecorder extends Module
|
||||
implements IFilteredRealBufferListener, Listener<RealBuffer>
|
||||
public class RealBufferWaveRecorder extends Module implements IFilteredRealBufferListener, Listener<RealBuffer>
|
||||
{
|
||||
private final static Logger mLog = LoggerFactory.getLogger(RealBufferWaveRecorder.class);
|
||||
|
||||
@@ -89,7 +88,7 @@ public class RealBufferWaveRecorder extends Module
|
||||
return mFile;
|
||||
}
|
||||
|
||||
public void start(ScheduledExecutorService executor)
|
||||
public void start()
|
||||
{
|
||||
if(mRunning.compareAndSet(false, true))
|
||||
{
|
||||
@@ -111,7 +110,7 @@ public class RealBufferWaveRecorder extends Module
|
||||
mWriter = new WaveWriter(mAudioFormat, mFile);
|
||||
|
||||
/* Schedule the processor to run every 500 milliseconds */
|
||||
mProcessorHandle = executor.scheduleAtFixedRate(mBufferProcessor, 0, 500, TimeUnit.MILLISECONDS);
|
||||
mProcessorHandle = ThreadPool.SCHEDULED.scheduleAtFixedRate(mBufferProcessor, 0, 500, TimeUnit.MILLISECONDS);
|
||||
}
|
||||
catch(IOException io)
|
||||
{
|
||||
|
||||
@@ -25,62 +25,100 @@ import java.util.concurrent.CopyOnWriteArrayList;
|
||||
*/
|
||||
public class Broadcaster<T> implements Listener<T>
|
||||
{
|
||||
private List<Listener<T>> mListeners = new CopyOnWriteArrayList<>();
|
||||
private List<Listener<T>> mListeners = new CopyOnWriteArrayList<>();
|
||||
|
||||
@Override
|
||||
public void receive( T t )
|
||||
/**
|
||||
* Implements the Listener<T> interface to receive an element and broadcast that element to all registered
|
||||
* listeners.
|
||||
*
|
||||
* @param t element to broadcast
|
||||
*/
|
||||
@Override
|
||||
public void receive(T t)
|
||||
{
|
||||
broadcast( t );
|
||||
broadcast(t);
|
||||
}
|
||||
|
||||
/**
|
||||
* Clear listeners to prepare for garbage collection
|
||||
*/
|
||||
public void dispose()
|
||||
{
|
||||
mListeners.clear();
|
||||
}
|
||||
|
||||
public boolean hasListeners()
|
||||
{
|
||||
return !mListeners.isEmpty();
|
||||
}
|
||||
|
||||
public int getListenerCount()
|
||||
{
|
||||
return mListeners.size();
|
||||
}
|
||||
|
||||
public List<Listener<T>> getListeners()
|
||||
{
|
||||
return mListeners;
|
||||
}
|
||||
|
||||
public void addListener( Listener<T> listener )
|
||||
{
|
||||
if(listener == null)
|
||||
{
|
||||
throw new IllegalArgumentException("Listener cannot be null");
|
||||
}
|
||||
|
||||
mListeners.add( listener );
|
||||
}
|
||||
|
||||
public void removeListener( Listener<T> listener )
|
||||
{
|
||||
mListeners.remove( listener );
|
||||
}
|
||||
|
||||
public void clear()
|
||||
{
|
||||
mListeners.clear();
|
||||
}
|
||||
|
||||
public void broadcast( T t )
|
||||
/**
|
||||
* Clear listeners to prepare for garbage collection
|
||||
*/
|
||||
public void dispose()
|
||||
{
|
||||
for( Listener<T> listener: mListeners )
|
||||
{
|
||||
listener.receive( t );
|
||||
}
|
||||
mListeners.clear();
|
||||
}
|
||||
|
||||
/**
|
||||
* Indicates if this broadcaster has any listeners registered
|
||||
*/
|
||||
public boolean hasListeners()
|
||||
{
|
||||
return !mListeners.isEmpty();
|
||||
}
|
||||
|
||||
/**
|
||||
* Indicates if the listener is currently registered with this broadcaster
|
||||
*/
|
||||
public boolean hasListener(Listener<T> listener)
|
||||
{
|
||||
return listener != null && mListeners.contains(listener);
|
||||
}
|
||||
|
||||
/**
|
||||
* The count of listeners currently registered with this broadcaster
|
||||
*/
|
||||
public int getListenerCount()
|
||||
{
|
||||
return mListeners.size();
|
||||
}
|
||||
|
||||
/**
|
||||
* The list of listeners currently registered with this broadcaster
|
||||
*/
|
||||
public List<Listener<T>> getListeners()
|
||||
{
|
||||
return mListeners;
|
||||
}
|
||||
|
||||
/**
|
||||
* Registers the listener to receive elements from this broadcaster
|
||||
*
|
||||
* @param listener
|
||||
*/
|
||||
public void addListener(Listener<T> listener)
|
||||
{
|
||||
if(listener != null && !mListeners.contains(listener))
|
||||
{
|
||||
mListeners.add(listener);
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Deregisters the listener from receiving elements from this broadcaster
|
||||
*/
|
||||
public void removeListener(Listener<T> listener)
|
||||
{
|
||||
if(listener != null && mListeners.contains(listener))
|
||||
{
|
||||
mListeners.remove(listener);
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Deregisters all listeners from this broadcaster
|
||||
*/
|
||||
public void clear()
|
||||
{
|
||||
mListeners.clear();
|
||||
}
|
||||
|
||||
/**
|
||||
* Broadcasts the element to all registered listeners
|
||||
*/
|
||||
public void broadcast(T t)
|
||||
{
|
||||
for(Listener<T> listener : mListeners)
|
||||
{
|
||||
listener.receive(t);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -19,6 +19,7 @@
|
||||
package io.github.dsheirer.sample;
|
||||
|
||||
import io.github.dsheirer.sample.real.IOverflowListener;
|
||||
import io.github.dsheirer.source.Source;
|
||||
import org.slf4j.Logger;
|
||||
import org.slf4j.LoggerFactory;
|
||||
|
||||
@@ -33,6 +34,7 @@ public class OverflowableTransferQueue<E>
|
||||
|
||||
public enum State {NORMAL, OVERFLOW};
|
||||
private IOverflowListener mOverflowListener;
|
||||
private Source mSourceOverflowListener;
|
||||
|
||||
private LinkedTransferQueue<E> mQueue = new LinkedTransferQueue<E>();
|
||||
private AtomicInteger mCounter = new AtomicInteger();
|
||||
@@ -73,6 +75,14 @@ public class OverflowableTransferQueue<E>
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Removes and returns a single element from the head of the queue or null if the queue is empty
|
||||
*/
|
||||
public E poll()
|
||||
{
|
||||
return mQueue.poll();
|
||||
}
|
||||
|
||||
/**
|
||||
* Retrieves elements from the queue into the collection up to the maximum number of elements specified
|
||||
*/
|
||||
@@ -91,13 +101,21 @@ public class OverflowableTransferQueue<E>
|
||||
}
|
||||
|
||||
/**
|
||||
* Sets a listener to receive overflow state change events
|
||||
* Sets a listener to receive overflow state change events.
|
||||
*/
|
||||
public void setOverflowListener(IOverflowListener listener)
|
||||
{
|
||||
mOverflowListener = listener;
|
||||
}
|
||||
|
||||
/**
|
||||
* Sets the source to receive overflow state change events (in addition to an IOverflow listener)
|
||||
*/
|
||||
public void setSourceOverflowListener(Source source)
|
||||
{
|
||||
mSourceOverflowListener = source;
|
||||
}
|
||||
|
||||
|
||||
/**
|
||||
* Toggles the overflow state and broadcast state change to listener
|
||||
@@ -110,6 +128,11 @@ public class OverflowableTransferQueue<E>
|
||||
{
|
||||
mOverflowListener.sourceOverflow(overflow);
|
||||
}
|
||||
|
||||
if(mSourceOverflowListener != null)
|
||||
{
|
||||
mSourceOverflowListener.broadcastOverflowState(overflow);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -125,4 +148,4 @@ public class OverflowableTransferQueue<E>
|
||||
mOverflow.set(false);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -2,7 +2,6 @@ package io.github.dsheirer.sample;
|
||||
|
||||
public enum SampleType
|
||||
{
|
||||
COMPLEX, //Baseband
|
||||
REAL, //Demodulated
|
||||
COMPLEX_AND_REAL; //Baseband and Demodulated
|
||||
COMPLEX, // I/Q
|
||||
REAL; // Real
|
||||
}
|
||||
|
||||
@@ -0,0 +1,39 @@
|
||||
/*******************************************************************************
|
||||
* sdrtrunk
|
||||
* Copyright (C) 2014-2017 Dennis Sheirer
|
||||
*
|
||||
* This program is free software: you can redistribute it and/or modify
|
||||
* it under the terms of the GNU General Public License as published by
|
||||
* the Free Software Foundation, either version 3 of the License, or
|
||||
* (at your option) any later version.
|
||||
*
|
||||
* This program is distributed in the hope that it will be useful,
|
||||
* but WITHOUT ANY WARRANTY; without even the implied warranty of
|
||||
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
|
||||
* GNU General Public License for more details.
|
||||
*
|
||||
* You should have received a copy of the GNU General Public License
|
||||
* along with this program. If not, see <http://www.gnu.org/licenses/>
|
||||
*
|
||||
******************************************************************************/
|
||||
package io.github.dsheirer.sample.complex;
|
||||
|
||||
import io.github.dsheirer.sample.Listener;
|
||||
|
||||
public interface IComplexBufferProvider
|
||||
{
|
||||
/**
|
||||
* Adds the listener to receive complex buffer samples
|
||||
*/
|
||||
void addComplexBufferListener(Listener<ComplexBuffer> listener);
|
||||
|
||||
/**
|
||||
* Removes the listener from receiving complex buffer samples
|
||||
*/
|
||||
void removeComplexBufferListener(Listener<ComplexBuffer> listener);
|
||||
|
||||
/**
|
||||
* Indicates if there are any complex buffer listeners registered
|
||||
*/
|
||||
boolean hasComplexBufferListeners();
|
||||
}
|
||||
@@ -6,8 +6,9 @@ import io.github.dsheirer.sample.complex.ComplexBuffer;
|
||||
|
||||
public abstract class ComplexSource extends Source implements Provider<ComplexBuffer>
|
||||
{
|
||||
public ComplexSource()
|
||||
{
|
||||
super( SampleType.COMPLEX );
|
||||
}
|
||||
@Override
|
||||
public SampleType getSampleType()
|
||||
{
|
||||
return SampleType.COMPLEX;
|
||||
}
|
||||
}
|
||||
|
||||
@@ -0,0 +1,35 @@
|
||||
/*******************************************************************************
|
||||
* sdrtrunk
|
||||
* Copyright (C) 2014-2017 Dennis Sheirer
|
||||
*
|
||||
* This program is free software: you can redistribute it and/or modify
|
||||
* it under the terms of the GNU General Public License as published by
|
||||
* the Free Software Foundation, either version 3 of the License, or
|
||||
* (at your option) any later version.
|
||||
*
|
||||
* This program is distributed in the hope that it will be useful,
|
||||
* but WITHOUT ANY WARRANTY; without even the implied warranty of
|
||||
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
|
||||
* GNU General Public License for more details.
|
||||
*
|
||||
* You should have received a copy of the GNU General Public License
|
||||
* along with this program. If not, see <http://www.gnu.org/licenses/>
|
||||
*
|
||||
******************************************************************************/
|
||||
package io.github.dsheirer.source;
|
||||
|
||||
import io.github.dsheirer.sample.Listener;
|
||||
|
||||
|
||||
/**
|
||||
* Interface for broadcasting changes to frequency, bandwidth, sample rate,
|
||||
* and actual sample rate values to all registered listeners.
|
||||
*
|
||||
* Note: broadcasted frequency should be the uncorrected, or displayable
|
||||
* frequency value. All frequency correction aspects should be handled within
|
||||
* the device that implements the tuning of the frequency.
|
||||
*/
|
||||
public interface ISourceEventListener
|
||||
{
|
||||
Listener<SourceEvent> getSourceEventListener();
|
||||
}
|
||||
@@ -0,0 +1,24 @@
|
||||
/*******************************************************************************
|
||||
* sdrtrunk
|
||||
* Copyright (C) 2014-2017 Dennis Sheirer
|
||||
*
|
||||
* This program is free software: you can redistribute it and/or modify
|
||||
* it under the terms of the GNU General Public License as published by
|
||||
* the Free Software Foundation, either version 3 of the License, or
|
||||
* (at your option) any later version.
|
||||
*
|
||||
* This program is distributed in the hope that it will be useful,
|
||||
* but WITHOUT ANY WARRANTY; without even the implied warranty of
|
||||
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
|
||||
* GNU General Public License for more details.
|
||||
*
|
||||
* You should have received a copy of the GNU General Public License
|
||||
* along with this program. If not, see <http://www.gnu.org/licenses/>
|
||||
*
|
||||
******************************************************************************/
|
||||
package io.github.dsheirer.source;
|
||||
|
||||
public interface ISourceEventProcessor
|
||||
{
|
||||
void process(SourceEvent event) throws SourceException;
|
||||
}
|
||||
@@ -16,15 +16,15 @@
|
||||
* along with this program. If not, see <http://www.gnu.org/licenses/>
|
||||
*
|
||||
******************************************************************************/
|
||||
package io.github.dsheirer.source.tuner.frequency;
|
||||
package io.github.dsheirer.source;
|
||||
|
||||
import io.github.dsheirer.sample.Listener;
|
||||
|
||||
/**
|
||||
* Provider of Frequency Change Events
|
||||
* Provider of Source Change Events
|
||||
*/
|
||||
public interface IFrequencyChangeProvider
|
||||
public interface ISourceEventProvider
|
||||
{
|
||||
void setFrequencyChangeListener( Listener<FrequencyChangeEvent> listener );
|
||||
void removeFrequencyChangeListener();
|
||||
void setSourceEventListener(Listener<SourceEvent> listener );
|
||||
void removeSourceEventListener();
|
||||
}
|
||||
@@ -54,4 +54,11 @@ public class InvalidFrequencyException extends SourceException
|
||||
{
|
||||
return mValidFrequency;
|
||||
}
|
||||
|
||||
@Override
|
||||
public String getMessage()
|
||||
{
|
||||
return super.getMessage() + " Invalid Frequency: " + getInvalidFrequency() +
|
||||
" - current valid frequency:" + getValidFrequency();
|
||||
}
|
||||
}
|
||||
|
||||
@@ -7,8 +7,9 @@ import io.github.dsheirer.sample.real.RealBuffer;
|
||||
|
||||
public abstract class RealSource extends Source implements Provider<RealBuffer>
|
||||
{
|
||||
public RealSource()
|
||||
{
|
||||
super( SampleType.REAL );
|
||||
}
|
||||
@Override
|
||||
public SampleType getSampleType()
|
||||
{
|
||||
return SampleType.REAL;
|
||||
}
|
||||
}
|
||||
|
||||
@@ -17,42 +17,53 @@
|
||||
******************************************************************************/
|
||||
package io.github.dsheirer.source;
|
||||
|
||||
import io.github.dsheirer.channel.heartbeat.IHeartbeatProvider;
|
||||
import io.github.dsheirer.module.Module;
|
||||
import io.github.dsheirer.sample.SampleType;
|
||||
import io.github.dsheirer.sample.real.IOverflowListener;
|
||||
import io.github.dsheirer.source.tuner.frequency.IFrequencyChangeListener;
|
||||
import io.github.dsheirer.source.tuner.frequency.IFrequencyChangeProvider;
|
||||
import io.github.dsheirer.source.heartbeat.HeartbeatManager;
|
||||
import io.github.dsheirer.source.heartbeat.IHeartbeatProvider;
|
||||
|
||||
/**
|
||||
* Abstract class to define the minimum functionality of a sample data provider.
|
||||
*/
|
||||
public abstract class Source extends Module implements IFrequencyChangeListener, IFrequencyChangeProvider,
|
||||
IHeartbeatProvider
|
||||
public abstract class Source extends Module implements ISourceEventListener, ISourceEventProvider, IHeartbeatProvider
|
||||
{
|
||||
protected SampleType mSampleType;
|
||||
private HeartbeatManager mHeartbeatManager = new HeartbeatManager();
|
||||
|
||||
/**
|
||||
* Heartbeat manager for this source
|
||||
*/
|
||||
public HeartbeatManager getHeartbeatManager()
|
||||
{
|
||||
return mHeartbeatManager;
|
||||
}
|
||||
|
||||
|
||||
protected IOverflowListener mOverflowListener;
|
||||
|
||||
public Source( SampleType sampleType )
|
||||
{
|
||||
mSampleType = sampleType;
|
||||
}
|
||||
|
||||
public abstract int getSampleRate() throws SourceException;
|
||||
|
||||
public abstract long getFrequency() throws SourceException;
|
||||
|
||||
/**
|
||||
* Indicates the type of samples provided by this source: real or complex
|
||||
*/
|
||||
public abstract SampleType getSampleType();
|
||||
|
||||
/**
|
||||
* Sample rate provided by this source
|
||||
*
|
||||
* @throws SourceException if there is an issue determining the sample rate for this source
|
||||
*/
|
||||
public abstract double getSampleRate();
|
||||
|
||||
/**
|
||||
* Center frequency for this source in Hertz
|
||||
*
|
||||
* @throws SourceException if there is an issue in determining the center frequency for this source
|
||||
*/
|
||||
public abstract long getFrequency();
|
||||
|
||||
/**
|
||||
* Process any cleanup actions to prepare for garbage collection of this source
|
||||
*/
|
||||
public abstract void dispose();
|
||||
|
||||
public SampleType getSampleType()
|
||||
{
|
||||
return mSampleType;
|
||||
}
|
||||
|
||||
public void setSampleType( SampleType sampleType )
|
||||
{
|
||||
mSampleType = sampleType;
|
||||
}
|
||||
|
||||
/**
|
||||
* Registers the listener to receive overflow state changes. Use null argument to clear the listener
|
||||
@@ -65,9 +76,9 @@ public abstract class Source extends Module implements IFrequencyChangeListener,
|
||||
/**
|
||||
* Broadcasts an overflow state
|
||||
*
|
||||
* @param overflow true if overlow, false if normal
|
||||
* @param overflow true if overflow, false if normal
|
||||
*/
|
||||
protected void broadcastOverflowState(boolean overflow)
|
||||
public void broadcastOverflowState(boolean overflow)
|
||||
{
|
||||
if(mOverflowListener != null)
|
||||
{
|
||||
|
||||
202
src/main/java/io/github/dsheirer/source/SourceEvent.java
Normal file
202
src/main/java/io/github/dsheirer/source/SourceEvent.java
Normal file
@@ -0,0 +1,202 @@
|
||||
/*******************************************************************************
|
||||
* sdrtrunk
|
||||
* Copyright (C) 2014-2017 Dennis Sheirer
|
||||
*
|
||||
* This program is free software: you can redistribute it and/or modify
|
||||
* it under the terms of the GNU General Public License as published by
|
||||
* the Free Software Foundation, either version 3 of the License, or
|
||||
* (at your option) any later version.
|
||||
*
|
||||
* This program is distributed in the hope that it will be useful,
|
||||
* but WITHOUT ANY WARRANTY; without even the implied warranty of
|
||||
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
|
||||
* GNU General Public License for more details.
|
||||
*
|
||||
* You should have received a copy of the GNU General Public License
|
||||
* along with this program. If not, see <http://www.gnu.org/licenses/>
|
||||
*
|
||||
******************************************************************************/
|
||||
package io.github.dsheirer.source;
|
||||
|
||||
public class SourceEvent
|
||||
{
|
||||
public enum Event
|
||||
{
|
||||
NOTIFICATION_CHANNEL_FREQUENCY_CORRECTION_CHANGE,
|
||||
NOTIFICATION_CHANNEL_SAMPLE_RATE_CHANGE,
|
||||
NOTIFICATION_FREQUENCY_CHANGE,
|
||||
NOTIFICATION_FREQUENCY_CORRECTION_CHANGE,
|
||||
NOTIFICATION_POLYPHASE_CHANNEL_COUNT_CHANGE,
|
||||
NOTIFICATION_SAMPLE_RATE_CHANGE,
|
||||
|
||||
REQUEST_CHANNEL_FREQUENCY_CORRECTION_CHANGE,
|
||||
REQUEST_FREQUENCY_CHANGE,
|
||||
REQUEST_SOURCE_DISPOSE,
|
||||
REQUEST_START_SAMPLE_STREAM,
|
||||
REQUEST_STOP_SAMPLE_STREAM;
|
||||
}
|
||||
|
||||
private Event mEvent;
|
||||
private Number mValue;
|
||||
private Source mSource;
|
||||
|
||||
/**
|
||||
* Private constructor. Use the static constructor methods to create an event.
|
||||
*/
|
||||
private SourceEvent(Event event, Number value)
|
||||
{
|
||||
mEvent = event;
|
||||
mValue = value;
|
||||
}
|
||||
|
||||
/**
|
||||
* Private constructor. Use the static constructor methods to create an event.
|
||||
*/
|
||||
private SourceEvent(Event event, Source source)
|
||||
{
|
||||
mEvent = event;
|
||||
mSource = source;
|
||||
}
|
||||
|
||||
/**
|
||||
* Event description.
|
||||
*/
|
||||
public Event getEvent()
|
||||
{
|
||||
return mEvent;
|
||||
}
|
||||
|
||||
/**
|
||||
* Value associated with the event.
|
||||
*/
|
||||
public Number getValue()
|
||||
{
|
||||
return mValue;
|
||||
}
|
||||
|
||||
public boolean hasValue()
|
||||
{
|
||||
return mValue != null;
|
||||
}
|
||||
|
||||
/**
|
||||
* Source associated with the event
|
||||
*/
|
||||
public Source getSource()
|
||||
{
|
||||
return mSource;
|
||||
}
|
||||
|
||||
/**
|
||||
* Indicates if this event contains an optional source
|
||||
*/
|
||||
public boolean hasSource()
|
||||
{
|
||||
return mSource != null;
|
||||
}
|
||||
|
||||
/**
|
||||
* Creates a new frequency change event
|
||||
*
|
||||
* @param frequency in hertz
|
||||
*/
|
||||
public static SourceEvent frequencyChange(long frequency)
|
||||
{
|
||||
return new SourceEvent(Event.NOTIFICATION_FREQUENCY_CHANGE, frequency);
|
||||
}
|
||||
|
||||
/**
|
||||
* Creates a new frequency correction change event
|
||||
*
|
||||
* @param frequencyCorrection in hertz
|
||||
*/
|
||||
public static SourceEvent frequencyCorrectionChange(long frequencyCorrection)
|
||||
{
|
||||
return new SourceEvent(Event.NOTIFICATION_FREQUENCY_CORRECTION_CHANGE, frequencyCorrection);
|
||||
}
|
||||
|
||||
/**
|
||||
* Creates a new channel frequency correction change event
|
||||
*
|
||||
* @param frequencyCorrection in hertz
|
||||
*/
|
||||
public static SourceEvent channelFrequencyCorrectionChange(long frequencyCorrection)
|
||||
{
|
||||
return new SourceEvent(Event.NOTIFICATION_CHANNEL_FREQUENCY_CORRECTION_CHANGE, frequencyCorrection);
|
||||
}
|
||||
|
||||
/**
|
||||
* Creates a new sample rate change event
|
||||
*
|
||||
* @param sampleRate in hertz
|
||||
*/
|
||||
public static SourceEvent sampleRateChange(double sampleRate)
|
||||
{
|
||||
return new SourceEvent(Event.NOTIFICATION_SAMPLE_RATE_CHANGE, sampleRate);
|
||||
}
|
||||
|
||||
/**
|
||||
* Creates a new channel sample rate change event
|
||||
*
|
||||
* @param sampleRate in hertz
|
||||
*/
|
||||
public static SourceEvent channelSampleRateChange(double sampleRate)
|
||||
{
|
||||
return new SourceEvent(Event.NOTIFICATION_CHANNEL_SAMPLE_RATE_CHANGE, sampleRate);
|
||||
}
|
||||
|
||||
/**
|
||||
* Creates a new channel count change event
|
||||
*
|
||||
* @param channelCount
|
||||
*/
|
||||
public static SourceEvent channelCountChange(int channelCount)
|
||||
{
|
||||
return new SourceEvent(Event.NOTIFICATION_POLYPHASE_CHANNEL_COUNT_CHANGE, channelCount);
|
||||
}
|
||||
|
||||
/**
|
||||
* Creates a new frequency change request event
|
||||
*
|
||||
* @param frequency requested
|
||||
*/
|
||||
public static SourceEvent frequencyRequest(long frequency)
|
||||
{
|
||||
return new SourceEvent(Event.REQUEST_FREQUENCY_CHANGE, frequency);
|
||||
}
|
||||
|
||||
/**
|
||||
* Creates a new channel frequency correction change request event
|
||||
*
|
||||
* @param frequency requested
|
||||
*/
|
||||
public static SourceEvent channelFrequencyCorrectionRequest(long frequency)
|
||||
{
|
||||
return new SourceEvent(Event.REQUEST_CHANNEL_FREQUENCY_CORRECTION_CHANGE, frequency);
|
||||
}
|
||||
|
||||
/**
|
||||
* Creates a new start sample stream request event
|
||||
*/
|
||||
public static SourceEvent startSampleStream(Source source)
|
||||
{
|
||||
return new SourceEvent(Event.REQUEST_START_SAMPLE_STREAM, source);
|
||||
}
|
||||
|
||||
/**
|
||||
* Creates a new stop sample stream request event
|
||||
*/
|
||||
public static SourceEvent stopSampleStream(Source source)
|
||||
{
|
||||
return new SourceEvent(Event.REQUEST_STOP_SAMPLE_STREAM, source);
|
||||
}
|
||||
|
||||
/**
|
||||
* Creates a request to dispose of the source. This is normally used for TunerChannelSource to notify the
|
||||
* parent source manager that the channel is no longer needed
|
||||
*/
|
||||
public static SourceEvent sourceDisposeRequest(Source source)
|
||||
{
|
||||
return new SourceEvent(Event.REQUEST_SOURCE_DISPOSE, source);
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,60 @@
|
||||
/*******************************************************************************
|
||||
* sdrtrunk
|
||||
* Copyright (C) 2014-2017 Dennis Sheirer
|
||||
*
|
||||
* This program is free software: you can redistribute it and/or modify
|
||||
* it under the terms of the GNU General Public License as published by
|
||||
* the Free Software Foundation, either version 3 of the License, or
|
||||
* (at your option) any later version.
|
||||
*
|
||||
* This program is distributed in the hope that it will be useful,
|
||||
* but WITHOUT ANY WARRANTY; without even the implied warranty of
|
||||
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
|
||||
* GNU General Public License for more details.
|
||||
*
|
||||
* You should have received a copy of the GNU General Public License
|
||||
* along with this program. If not, see <http://www.gnu.org/licenses/>
|
||||
*
|
||||
******************************************************************************/
|
||||
package io.github.dsheirer.source;
|
||||
|
||||
import io.github.dsheirer.sample.Listener;
|
||||
import org.slf4j.Logger;
|
||||
import org.slf4j.LoggerFactory;
|
||||
|
||||
public class SourceEventListenerToProcessorAdapter implements Listener<SourceEvent>
|
||||
{
|
||||
private final static Logger mLog = LoggerFactory.getLogger(SourceEventListenerToProcessorAdapter.class);
|
||||
|
||||
private ISourceEventProcessor mSourceEventProcessor;
|
||||
|
||||
/**
|
||||
* Adapter to implement ISourceEventListener/Listener<SourceEvent> and adapt to a source event processor
|
||||
*/
|
||||
public SourceEventListenerToProcessorAdapter(ISourceEventProcessor sourceEventProcessor)
|
||||
{
|
||||
mSourceEventProcessor = sourceEventProcessor;
|
||||
}
|
||||
|
||||
public void dispose()
|
||||
{
|
||||
mSourceEventProcessor = null;
|
||||
}
|
||||
|
||||
@Override
|
||||
public void receive(SourceEvent sourceEvent)
|
||||
{
|
||||
if(mSourceEventProcessor != null)
|
||||
{
|
||||
try
|
||||
{
|
||||
mSourceEventProcessor.process(sourceEvent);
|
||||
}
|
||||
catch(SourceException e)
|
||||
{
|
||||
mLog.error("Error while transferring source event to source event processor [" +
|
||||
mSourceEventProcessor.getClass() + "]", e);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -1,26 +1,23 @@
|
||||
/*******************************************************************************
|
||||
* SDR Trunk
|
||||
* Copyright (C) 2014 Dennis Sheirer
|
||||
*
|
||||
* This program is free software: you can redistribute it and/or modify
|
||||
* it under the terms of the GNU General Public License as published by
|
||||
* the Free Software Foundation, either version 3 of the License, or
|
||||
* (at your option) any later version.
|
||||
*
|
||||
* This program is distributed in the hope that it will be useful,
|
||||
* but WITHOUT ANY WARRANTY; without even the implied warranty of
|
||||
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
|
||||
* GNU General Public License for more details.
|
||||
*
|
||||
* You should have received a copy of the GNU General Public License
|
||||
* along with this program. If not, see <http://www.gnu.org/licenses/>
|
||||
* sdr-trunk
|
||||
* Copyright (C) 2014-2017 Dennis Sheirer
|
||||
*
|
||||
* This program is free software: you can redistribute it and/or modify it under the terms of the GNU General Public
|
||||
* License as published by the Free Software Foundation, either version 3 of the License, or (at your option) any
|
||||
* later version.
|
||||
*
|
||||
* This program is distributed in the hope that it will be useful, but WITHOUT ANY WARRANTY; without even the implied
|
||||
* warranty of MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU General Public License for more details.
|
||||
*
|
||||
* You should have received a copy of the GNU General Public License along with this program.
|
||||
* If not, see <http://www.gnu.org/licenses/>
|
||||
*
|
||||
******************************************************************************/
|
||||
package io.github.dsheirer.source.config;
|
||||
|
||||
import com.fasterxml.jackson.dataformat.xml.annotation.JacksonXmlProperty;
|
||||
import io.github.dsheirer.source.SourceType;
|
||||
import io.github.dsheirer.source.tuner.TunerChannel;
|
||||
import io.github.dsheirer.source.tuner.TunerChannel.Type;
|
||||
import io.github.dsheirer.source.tuner.channel.TunerChannel;
|
||||
|
||||
public class SourceConfigRecording extends SourceConfiguration
|
||||
{
|
||||
@@ -59,11 +56,11 @@ public class SourceConfigRecording extends SourceConfiguration
|
||||
{
|
||||
return getRecordingAlias();
|
||||
}
|
||||
|
||||
public TunerChannel getTunerChannel()
|
||||
{
|
||||
/* Create a tuner channel with a default bandwidth of 15 kHz */
|
||||
return new TunerChannel( Type.LOCKED, mFrequency, 15000 );
|
||||
}
|
||||
|
||||
|
||||
public TunerChannel getTunerChannel()
|
||||
{
|
||||
/* Create a tuner channel with a default bandwidth of 12.5 kHz */
|
||||
return new TunerChannel(mFrequency, 12500);
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
@@ -21,15 +21,14 @@ import com.fasterxml.jackson.annotation.JsonIgnore;
|
||||
import com.fasterxml.jackson.annotation.JsonSubTypes;
|
||||
import com.fasterxml.jackson.dataformat.xml.annotation.JacksonXmlProperty;
|
||||
import io.github.dsheirer.source.SourceType;
|
||||
import io.github.dsheirer.source.tuner.TunerChannel;
|
||||
import io.github.dsheirer.source.tuner.TunerChannel.Type;
|
||||
import io.github.dsheirer.source.tuner.channel.TunerChannel;
|
||||
|
||||
import java.text.DecimalFormat;
|
||||
|
||||
@JsonSubTypes.Type(value = SourceConfigTuner.class, name = "sourceConfigTuner")
|
||||
public class SourceConfigTuner extends SourceConfiguration
|
||||
{
|
||||
private static DecimalFormat sFORMAT = new DecimalFormat("0.00000");
|
||||
private static DecimalFormat FREQUENCY_FORMAT = new DecimalFormat("0.00000");
|
||||
|
||||
private long mFrequency = 0;
|
||||
private int mBandwidth = 12500;
|
||||
@@ -62,12 +61,12 @@ public class SourceConfigTuner extends SourceConfiguration
|
||||
@Override
|
||||
public String getDescription()
|
||||
{
|
||||
return sFORMAT.format((double)mFrequency / 1000000.0d) + " MHz";
|
||||
return FREQUENCY_FORMAT.format((double)mFrequency / 1000000.0d) + " MHz";
|
||||
}
|
||||
|
||||
@JsonIgnore
|
||||
public TunerChannel getTunerChannel()
|
||||
{
|
||||
return new TunerChannel(Type.LOCKED, mFrequency, mBandwidth);
|
||||
return new TunerChannel(mFrequency, mBandwidth);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -16,7 +16,7 @@
|
||||
* along with this program. If not, see <http://www.gnu.org/licenses/>
|
||||
*
|
||||
******************************************************************************/
|
||||
package io.github.dsheirer.channel.heartbeat;
|
||||
package io.github.dsheirer.source.heartbeat;
|
||||
|
||||
public class Heartbeat
|
||||
{
|
||||
@@ -0,0 +1,60 @@
|
||||
/*******************************************************************************
|
||||
* sdrtrunk
|
||||
* Copyright (C) 2014-2017 Dennis Sheirer
|
||||
*
|
||||
* This program is free software: you can redistribute it and/or modify
|
||||
* it under the terms of the GNU General Public License as published by
|
||||
* the Free Software Foundation, either version 3 of the License, or
|
||||
* (at your option) any later version.
|
||||
*
|
||||
* This program is distributed in the hope that it will be useful,
|
||||
* but WITHOUT ANY WARRANTY; without even the implied warranty of
|
||||
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
|
||||
* GNU General Public License for more details.
|
||||
*
|
||||
* You should have received a copy of the GNU General Public License
|
||||
* along with this program. If not, see <http://www.gnu.org/licenses/>
|
||||
*
|
||||
******************************************************************************/
|
||||
package io.github.dsheirer.source.heartbeat;
|
||||
|
||||
import io.github.dsheirer.sample.Broadcaster;
|
||||
import io.github.dsheirer.sample.Listener;
|
||||
|
||||
public class HeartbeatManager
|
||||
{
|
||||
private static final Heartbeat HEARTBEAT = new Heartbeat();
|
||||
private Broadcaster<Heartbeat> mHeartbeatBroadcaster = new Broadcaster<>();
|
||||
|
||||
/**
|
||||
* Broadcasts heartbeat as commanded and handles listener registration details.
|
||||
*/
|
||||
public HeartbeatManager()
|
||||
{
|
||||
}
|
||||
|
||||
/**
|
||||
* Sends a heartbeat to all registered listeners
|
||||
*/
|
||||
public void broadcast()
|
||||
{
|
||||
mHeartbeatBroadcaster.broadcast(HEARTBEAT);
|
||||
}
|
||||
|
||||
/**
|
||||
* Adds the listener to receive heartbeats
|
||||
*/
|
||||
public void addHeartbeatListener(Listener<Heartbeat> listener)
|
||||
{
|
||||
mHeartbeatBroadcaster.addListener(listener);
|
||||
}
|
||||
|
||||
/**
|
||||
* Removes the listener from receiving heartbeats
|
||||
* @param listener
|
||||
*/
|
||||
public void removeHeartbeatListener(Listener<Heartbeat> listener)
|
||||
{
|
||||
mHeartbeatBroadcaster.removeListener(listener);
|
||||
}
|
||||
}
|
||||
@@ -16,7 +16,7 @@
|
||||
* along with this program. If not, see <http://www.gnu.org/licenses/>
|
||||
*
|
||||
******************************************************************************/
|
||||
package io.github.dsheirer.channel.heartbeat;
|
||||
package io.github.dsheirer.source.heartbeat;
|
||||
|
||||
import io.github.dsheirer.sample.Listener;
|
||||
|
||||
@@ -16,13 +16,9 @@
|
||||
* along with this program. If not, see <http://www.gnu.org/licenses/>
|
||||
*
|
||||
******************************************************************************/
|
||||
package io.github.dsheirer.channel.heartbeat;
|
||||
|
||||
import io.github.dsheirer.sample.Listener;
|
||||
package io.github.dsheirer.source.heartbeat;
|
||||
|
||||
public interface IHeartbeatProvider
|
||||
{
|
||||
public void setHeartbeatListener(Listener<Heartbeat> listener);
|
||||
|
||||
public void removeHeartbeatListener();
|
||||
public HeartbeatManager getHeartbeatManager();
|
||||
}
|
||||
@@ -17,14 +17,11 @@
|
||||
******************************************************************************/
|
||||
package io.github.dsheirer.source.mixer;
|
||||
|
||||
import io.github.dsheirer.channel.heartbeat.Heartbeat;
|
||||
import io.github.dsheirer.dsp.gain.AutomaticGainControl;
|
||||
import io.github.dsheirer.sample.Listener;
|
||||
import io.github.dsheirer.sample.adapter.ISampleAdapter;
|
||||
import io.github.dsheirer.sample.real.RealBuffer;
|
||||
import io.github.dsheirer.source.RealSource;
|
||||
import io.github.dsheirer.source.SourceException;
|
||||
import io.github.dsheirer.source.tuner.frequency.FrequencyChangeEvent;
|
||||
import io.github.dsheirer.source.SourceEvent;
|
||||
import org.slf4j.Logger;
|
||||
import org.slf4j.LoggerFactory;
|
||||
|
||||
@@ -34,7 +31,6 @@ import javax.sound.sampled.LineUnavailableException;
|
||||
import javax.sound.sampled.TargetDataLine;
|
||||
import java.util.Iterator;
|
||||
import java.util.concurrent.CopyOnWriteArrayList;
|
||||
import java.util.concurrent.ScheduledExecutorService;
|
||||
import java.util.concurrent.atomic.AtomicBoolean;
|
||||
|
||||
public class RealMixerSource extends RealSource
|
||||
@@ -49,9 +45,6 @@ public class RealMixerSource extends RealSource
|
||||
private AudioFormat mAudioFormat;
|
||||
private int mBytesPerFrame = 0;
|
||||
private ISampleAdapter mSampleAdapter;
|
||||
private AutomaticGainControl mAGC = new AutomaticGainControl();
|
||||
private Listener<Heartbeat> mHeartbeatListener;
|
||||
private static final Heartbeat HEARTBEAT = new Heartbeat();
|
||||
|
||||
private CopyOnWriteArrayList<Listener<RealBuffer>> mSampleListeners = new CopyOnWriteArrayList<Listener<RealBuffer>>();
|
||||
|
||||
@@ -76,44 +69,24 @@ public class RealMixerSource extends RealSource
|
||||
mSampleAdapter = sampleAdapter;
|
||||
}
|
||||
|
||||
@Override
|
||||
public void setFrequencyChangeListener(Listener<FrequencyChangeEvent> listener)
|
||||
{
|
||||
//Not implemented
|
||||
}
|
||||
@Override
|
||||
public void setSourceEventListener(Listener<SourceEvent> listener)
|
||||
{
|
||||
//Not implemented
|
||||
}
|
||||
|
||||
@Override
|
||||
public void removeFrequencyChangeListener()
|
||||
{
|
||||
//Not implemented
|
||||
}
|
||||
@Override
|
||||
public void removeSourceEventListener()
|
||||
{
|
||||
//Not implemented
|
||||
}
|
||||
|
||||
@Override
|
||||
public Listener<FrequencyChangeEvent> getFrequencyChangeListener()
|
||||
{
|
||||
//Not implemented
|
||||
return null;
|
||||
}
|
||||
|
||||
/**
|
||||
* Registers the listener to receive heartbeats from this source.
|
||||
*
|
||||
* @param listener to receive heartbeats
|
||||
*/
|
||||
@Override
|
||||
public void setHeartbeatListener(Listener<Heartbeat> listener)
|
||||
{
|
||||
mHeartbeatListener = listener;
|
||||
}
|
||||
|
||||
/**
|
||||
* Removes the currently registered heartbeat listener
|
||||
*/
|
||||
@Override
|
||||
public void removeHeartbeatListener()
|
||||
{
|
||||
mHeartbeatListener = null;
|
||||
}
|
||||
@Override
|
||||
public Listener<SourceEvent> getSourceEventListener()
|
||||
{
|
||||
//Not implemented
|
||||
return null;
|
||||
}
|
||||
|
||||
@Override
|
||||
public void reset()
|
||||
@@ -121,7 +94,7 @@ public class RealMixerSource extends RealSource
|
||||
}
|
||||
|
||||
@Override
|
||||
public void start(ScheduledExecutorService executor)
|
||||
public void start()
|
||||
{
|
||||
}
|
||||
|
||||
@@ -176,11 +149,11 @@ public class RealMixerSource extends RealSource
|
||||
}
|
||||
}
|
||||
|
||||
public int getSampleRate()
|
||||
public double getSampleRate()
|
||||
{
|
||||
if(mTargetDataLine != null)
|
||||
{
|
||||
return (int)mTargetDataLine.getFormat().getSampleRate();
|
||||
return mTargetDataLine.getFormat().getSampleRate();
|
||||
}
|
||||
else
|
||||
{
|
||||
@@ -191,7 +164,7 @@ public class RealMixerSource extends RealSource
|
||||
/**
|
||||
* Returns the frequency of this source. Default is 0.
|
||||
*/
|
||||
public long getFrequency() throws SourceException
|
||||
public long getFrequency()
|
||||
{
|
||||
return mFrequency;
|
||||
}
|
||||
@@ -220,11 +193,7 @@ public class RealMixerSource extends RealSource
|
||||
{
|
||||
if(mRunning.compareAndSet(false, true))
|
||||
{
|
||||
//Send a heartbeat every time we run
|
||||
if(mHeartbeatListener != null)
|
||||
{
|
||||
mHeartbeatListener.receive(HEARTBEAT);
|
||||
}
|
||||
getHeartbeatManager().broadcast();
|
||||
|
||||
if(mTargetDataLine == null)
|
||||
{
|
||||
|
||||
@@ -1,26 +1,37 @@
|
||||
/*******************************************************************************
|
||||
* sdr-trunk
|
||||
* Copyright (C) 2014-2017 Dennis Sheirer
|
||||
*
|
||||
* This program is free software: you can redistribute it and/or modify it under the terms of the GNU General Public
|
||||
* License as published by the Free Software Foundation, either version 3 of the License, or (at your option) any
|
||||
* later version.
|
||||
*
|
||||
* This program is distributed in the hope that it will be useful, but WITHOUT ANY WARRANTY; without even the implied
|
||||
* warranty of MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU General Public License for more details.
|
||||
*
|
||||
* You should have received a copy of the GNU General Public License along with this program.
|
||||
* If not, see <http://www.gnu.org/licenses/>
|
||||
*
|
||||
******************************************************************************/
|
||||
package io.github.dsheirer.source.recording;
|
||||
|
||||
import io.github.dsheirer.settings.SettingsManager;
|
||||
import io.github.dsheirer.source.ISourceEventProcessor;
|
||||
import io.github.dsheirer.source.SourceEvent;
|
||||
import io.github.dsheirer.source.SourceException;
|
||||
import io.github.dsheirer.source.tuner.ITunerChannelProvider;
|
||||
import io.github.dsheirer.source.tuner.TunerChannel;
|
||||
import io.github.dsheirer.source.tuner.TunerChannelSource;
|
||||
import io.github.dsheirer.source.tuner.frequency.FrequencyChangeEvent;
|
||||
import io.github.dsheirer.source.tuner.frequency.FrequencyChangeEvent.Event;
|
||||
import io.github.dsheirer.source.tuner.frequency.IFrequencyChangeProcessor;
|
||||
import io.github.dsheirer.source.tuner.channel.TunerChannel;
|
||||
import io.github.dsheirer.source.tuner.channel.TunerChannelSource;
|
||||
|
||||
import java.io.File;
|
||||
import java.util.ArrayList;
|
||||
import java.util.concurrent.RejectedExecutionException;
|
||||
import java.util.List;
|
||||
|
||||
public class Recording implements Comparable<Recording>,
|
||||
IFrequencyChangeProcessor,
|
||||
ITunerChannelProvider
|
||||
public class Recording implements Comparable<Recording>, ISourceEventProcessor, ITunerChannelProvider
|
||||
{
|
||||
private RecordingConfiguration mConfiguration;
|
||||
|
||||
private ArrayList<TunerChannelSource> mTunerChannels =
|
||||
new ArrayList<TunerChannelSource>();
|
||||
private List<TunerChannelSource> mTunerChannels = new ArrayList<TunerChannelSource>();
|
||||
|
||||
@SuppressWarnings( "unused" )
|
||||
private long mCenterFrequency;
|
||||
@@ -65,8 +76,7 @@ public class Recording implements Comparable<Recording>,
|
||||
}
|
||||
|
||||
@Override
|
||||
public TunerChannelSource getChannel( TunerChannel channel )
|
||||
throws RejectedExecutionException, SourceException
|
||||
public TunerChannelSource getChannel( TunerChannel channel )
|
||||
{
|
||||
// TODO Auto-generated method stub
|
||||
return null;
|
||||
@@ -89,9 +99,9 @@ public class Recording implements Comparable<Recording>,
|
||||
}
|
||||
|
||||
@Override
|
||||
public void frequencyChanged( FrequencyChangeEvent event ) throws SourceException
|
||||
public void process(SourceEvent event ) throws SourceException
|
||||
{
|
||||
if( event.getEvent() == Event.NOTIFICATION_FREQUENCY_CHANGE )
|
||||
if( event.getEvent() == SourceEvent.Event.NOTIFICATION_FREQUENCY_CHANGE )
|
||||
{
|
||||
long frequency = event.getValue().longValue();
|
||||
|
||||
@@ -103,7 +113,7 @@ public class Recording implements Comparable<Recording>,
|
||||
|
||||
for( TunerChannelSource channel: mTunerChannels )
|
||||
{
|
||||
channel.frequencyChanged( event );
|
||||
channel.process( event );
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -22,8 +22,8 @@ import io.github.dsheirer.source.Source;
|
||||
import io.github.dsheirer.source.SourceException;
|
||||
import io.github.dsheirer.source.config.SourceConfigRecording;
|
||||
import io.github.dsheirer.source.config.SourceConfiguration;
|
||||
import io.github.dsheirer.source.tuner.TunerChannel;
|
||||
import io.github.dsheirer.source.tuner.TunerChannelSource;
|
||||
import io.github.dsheirer.source.tuner.channel.TunerChannel;
|
||||
import io.github.dsheirer.source.tuner.channel.TunerChannelSource;
|
||||
import org.slf4j.Logger;
|
||||
import org.slf4j.LoggerFactory;
|
||||
|
||||
@@ -33,8 +33,7 @@ import java.util.List;
|
||||
|
||||
public class RecordingSourceManager
|
||||
{
|
||||
private final static Logger mLog =
|
||||
LoggerFactory.getLogger( RecordingSourceManager.class );
|
||||
private final static Logger mLog = LoggerFactory.getLogger( RecordingSourceManager.class );
|
||||
|
||||
private ArrayList<Recording> mRecordings = new ArrayList<Recording>();
|
||||
|
||||
@@ -50,12 +49,12 @@ public class RecordingSourceManager
|
||||
* Iterates current recordings to get a tuner channel source for the frequency
|
||||
* specified in the channel config's source config object
|
||||
*/
|
||||
public Source getSource(SourceConfiguration config, int bandwidth )
|
||||
public Source getSource( SourceConfiguration config, int bandwidth )
|
||||
throws SourceException
|
||||
{
|
||||
TunerChannelSource retVal = null;
|
||||
|
||||
if( config instanceof SourceConfigRecording)
|
||||
if( config instanceof SourceConfigRecording )
|
||||
{
|
||||
SourceConfigRecording configRecording = (SourceConfigRecording)config;
|
||||
|
||||
|
||||
@@ -18,6 +18,8 @@
|
||||
package io.github.dsheirer.source.tuner;
|
||||
|
||||
import io.github.dsheirer.source.SourceException;
|
||||
import io.github.dsheirer.source.tuner.channel.TunerChannel;
|
||||
import io.github.dsheirer.source.tuner.channel.TunerChannelSource;
|
||||
|
||||
import java.util.concurrent.RejectedExecutionException;
|
||||
|
||||
@@ -25,18 +27,14 @@ public interface ITunerChannelProvider
|
||||
{
|
||||
/**
|
||||
* Returns a tuner frequency channel source, tuned to the correct frequency
|
||||
*
|
||||
* @param frequency - desired frequency
|
||||
*
|
||||
*
|
||||
* @return - source for 48k sample rate
|
||||
*/
|
||||
public abstract TunerChannelSource getChannel( TunerChannel channel )
|
||||
public abstract TunerChannelSource getChannel(TunerChannel channel )
|
||||
throws RejectedExecutionException, SourceException;
|
||||
|
||||
/**
|
||||
* Releases the tuned channel resources
|
||||
*
|
||||
* @param channel - previously obtained tuner channel
|
||||
*/
|
||||
public abstract void releaseChannel( TunerChannelSource source );
|
||||
|
||||
|
||||
@@ -1,129 +0,0 @@
|
||||
/*******************************************************************************
|
||||
* SDR Trunk
|
||||
* Copyright (C) 2014 Dennis Sheirer
|
||||
*
|
||||
* This program is free software: you can redistribute it and/or modify
|
||||
* it under the terms of the GNU General Public License as published by
|
||||
* the Free Software Foundation, either version 3 of the License, or
|
||||
* (at your option) any later version.
|
||||
*
|
||||
* This program is distributed in the hope that it will be useful,
|
||||
* but WITHOUT ANY WARRANTY; without even the implied warranty of
|
||||
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
|
||||
* GNU General Public License for more details.
|
||||
*
|
||||
* You should have received a copy of the GNU General Public License
|
||||
* along with this program. If not, see <http://www.gnu.org/licenses/>
|
||||
******************************************************************************/
|
||||
package io.github.dsheirer.source.tuner;
|
||||
|
||||
import io.github.dsheirer.sample.Listener;
|
||||
import io.github.dsheirer.sample.adapter.ISampleAdapter;
|
||||
import io.github.dsheirer.sample.complex.ComplexBuffer;
|
||||
import io.github.dsheirer.sample.complex.ComplexSampleListListener;
|
||||
import io.github.dsheirer.source.mixer.ComplexMixer;
|
||||
import org.slf4j.Logger;
|
||||
import org.slf4j.LoggerFactory;
|
||||
|
||||
import javax.sound.sampled.TargetDataLine;
|
||||
import java.util.ArrayList;
|
||||
|
||||
public abstract class MixerTuner extends Tuner implements Listener<ComplexBuffer>
|
||||
{
|
||||
private final static Logger mLog =
|
||||
LoggerFactory.getLogger( MixerTuner.class );
|
||||
|
||||
protected MixerTunerType mMixerTunerType;
|
||||
protected ComplexMixer mComplexMixer;
|
||||
protected ArrayList<ComplexSampleListListener> mListeners =
|
||||
new ArrayList<ComplexSampleListListener>();
|
||||
|
||||
/**
|
||||
* Wrapper class to add basic Tuner functionality to the ComplexMixer.
|
||||
* Subclasses should couple this functionality with a tuner controller class
|
||||
* to support tuning.
|
||||
*/
|
||||
public MixerTuner( String name,
|
||||
TunerController tunerController,
|
||||
MixerTunerType mixerTunerType,
|
||||
TargetDataLine targetDataLine,
|
||||
ISampleAdapter sampleAdapter )
|
||||
{
|
||||
super( name, tunerController );
|
||||
|
||||
mMixerTunerType = mixerTunerType;
|
||||
|
||||
mComplexMixer = new ComplexMixer( targetDataLine,
|
||||
mMixerTunerType.getAudioFormat(),
|
||||
name,
|
||||
sampleAdapter,
|
||||
(Listener<ComplexBuffer>)this );
|
||||
}
|
||||
|
||||
public MixerTuner( String name,
|
||||
TunerController tunerController,
|
||||
MixerTunerDataLine mixerTuner,
|
||||
ISampleAdapter sampleAdapter )
|
||||
{
|
||||
this( name,
|
||||
tunerController,
|
||||
mixerTuner.getMixerTunerType(),
|
||||
mixerTuner.getTargetDataLine(),
|
||||
sampleAdapter );
|
||||
}
|
||||
|
||||
public void dispose()
|
||||
{
|
||||
mComplexMixer.stop();
|
||||
|
||||
super.dispose();
|
||||
}
|
||||
|
||||
public MixerTunerType getMixerTunerType()
|
||||
{
|
||||
return mMixerTunerType;
|
||||
}
|
||||
|
||||
public TargetDataLine getTargetDataLine()
|
||||
{
|
||||
return mComplexMixer.getTargetDataLine();
|
||||
}
|
||||
|
||||
/**
|
||||
* Dispatches the received complex buffers from the mComplexMixer to all
|
||||
* listeners registered on the parent Tuner class
|
||||
*/
|
||||
@Override
|
||||
public void receive( ComplexBuffer buffer )
|
||||
{
|
||||
super.broadcast( buffer );
|
||||
}
|
||||
|
||||
/**
|
||||
* Overrides the parent Tuner class listener management method to allow
|
||||
* starting the buffer processing thread
|
||||
*/
|
||||
@Override
|
||||
public void addListener( Listener<ComplexBuffer> listener )
|
||||
{
|
||||
super.addListener( listener );
|
||||
|
||||
/* We can call start multiple times -- it ignores additional requests */
|
||||
mComplexMixer.start();
|
||||
}
|
||||
|
||||
/**
|
||||
* Overrides the parent Tuner class listener management method to allow
|
||||
* stopping the buffer processing thread when there are no more listeners
|
||||
*/
|
||||
@Override
|
||||
public void removeListener( Listener<ComplexBuffer> listener )
|
||||
{
|
||||
super.removeListener( listener );
|
||||
|
||||
if( mSampleListeners.isEmpty() )
|
||||
{
|
||||
mComplexMixer.stop();
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -17,196 +17,145 @@
|
||||
******************************************************************************/
|
||||
package io.github.dsheirer.source.tuner;
|
||||
|
||||
import io.github.dsheirer.sample.Broadcaster;
|
||||
import io.github.dsheirer.sample.Listener;
|
||||
import io.github.dsheirer.sample.complex.ComplexBuffer;
|
||||
import io.github.dsheirer.source.SourceException;
|
||||
import io.github.dsheirer.source.tuner.frequency.FrequencyChangeEvent;
|
||||
import io.github.dsheirer.source.tuner.frequency.IFrequencyChangeProcessor;
|
||||
import io.github.dsheirer.source.ISourceEventProcessor;
|
||||
import io.github.dsheirer.source.SourceEvent;
|
||||
import io.github.dsheirer.source.tuner.TunerEvent.Event;
|
||||
import io.github.dsheirer.source.tuner.manager.AbstractSourceManager;
|
||||
import io.github.dsheirer.source.tuner.manager.TunerSourceManager;
|
||||
import org.slf4j.Logger;
|
||||
import org.slf4j.LoggerFactory;
|
||||
|
||||
import java.util.ArrayList;
|
||||
import java.util.List;
|
||||
import java.util.concurrent.CopyOnWriteArrayList;
|
||||
import java.util.concurrent.RejectedExecutionException;
|
||||
|
||||
/**
|
||||
* Tuner - provides tuner channel sources, representing a channel frequency
|
||||
*/
|
||||
public abstract class Tuner implements ITunerChannelProvider
|
||||
public abstract class Tuner
|
||||
{
|
||||
private final static Logger mLog = LoggerFactory.getLogger( Tuner.class );
|
||||
|
||||
private String mName;
|
||||
private TunerController mTunerController;
|
||||
/**
|
||||
* Sample Listeners - these will typically be the DFT processor for spectral
|
||||
* display, or it will be one or more tuner channel sources
|
||||
*/
|
||||
protected List<Listener<ComplexBuffer>> mSampleListeners =
|
||||
new CopyOnWriteArrayList<>();
|
||||
|
||||
protected List<Listener<TunerEvent>> mTunerChangeListeners =
|
||||
new ArrayList<>();
|
||||
|
||||
public Tuner( String name, TunerController tunerController )
|
||||
{
|
||||
mName = name;
|
||||
mTunerController = tunerController;
|
||||
private final static Logger mLog = LoggerFactory.getLogger(Tuner.class);
|
||||
|
||||
//Rebroadcast frequency and sample rate change events as tuner events
|
||||
mTunerController.addListener( new IFrequencyChangeProcessor()
|
||||
{
|
||||
@Override
|
||||
public void frequencyChanged( FrequencyChangeEvent event )
|
||||
{
|
||||
switch( event.getEvent() )
|
||||
{
|
||||
case NOTIFICATION_FREQUENCY_CHANGE:
|
||||
broadcast( new TunerEvent( Tuner.this,
|
||||
TunerEvent.Event.FREQUENCY ));
|
||||
break;
|
||||
case NOTIFICATION_SAMPLE_RATE_CHANGE:
|
||||
broadcast( new TunerEvent( Tuner.this,
|
||||
TunerEvent.Event.SAMPLE_RATE ));
|
||||
break;
|
||||
default:
|
||||
break;
|
||||
}
|
||||
}
|
||||
} );
|
||||
}
|
||||
|
||||
public TunerController getTunerController()
|
||||
{
|
||||
return mTunerController;
|
||||
}
|
||||
|
||||
public String toString()
|
||||
{
|
||||
return mName;
|
||||
}
|
||||
|
||||
public void dispose()
|
||||
{
|
||||
mSampleListeners.clear();
|
||||
}
|
||||
|
||||
public void setName( String name )
|
||||
{
|
||||
mName = name;
|
||||
}
|
||||
|
||||
/**
|
||||
* Unique identifier for this tuner, used to lookup a tuner configuration
|
||||
* from the settings manager.
|
||||
*
|
||||
* @return - unique identifier like a serial number, or a usb bus location
|
||||
* or ip address and port. Return some form of unique identification that
|
||||
* allows this tuner to be identified from among several of the same types
|
||||
* of tuners.
|
||||
*/
|
||||
public abstract String getUniqueID();
|
||||
protected Broadcaster<TunerEvent> mTunerEventBroadcaster = new Broadcaster<>();
|
||||
private TunerController mTunerController;
|
||||
private String mName;
|
||||
private AbstractSourceManager mTunerSourceManager;
|
||||
|
||||
/**
|
||||
* @return - tuner class enum entry
|
||||
*/
|
||||
public abstract TunerClass getTunerClass();
|
||||
|
||||
/**
|
||||
* @return - tuner type enum entry
|
||||
*/
|
||||
public abstract TunerType getTunerType();
|
||||
|
||||
/**
|
||||
* Name of this tuner object
|
||||
* @return - string name of this tuner object
|
||||
*/
|
||||
public String getName()
|
||||
{
|
||||
return mName;
|
||||
}
|
||||
|
||||
/**
|
||||
* Sample size in bits
|
||||
*/
|
||||
public abstract double getSampleSize();
|
||||
|
||||
/**
|
||||
* Returns a tuner frequency channel source, tuned to the correct frequency
|
||||
*
|
||||
* Note: ensure the concrete implementation registers the returned
|
||||
* tuner channel source as a listener on the tuner, to receive frequency
|
||||
* change events.
|
||||
*
|
||||
* @param frequency - desired frequency
|
||||
*
|
||||
* @return - source for 48k sample rate
|
||||
*/
|
||||
public abstract TunerChannelSource getChannel( TunerChannel channel )
|
||||
throws RejectedExecutionException, SourceException;
|
||||
|
||||
/**
|
||||
* Releases the tuned channel resources
|
||||
*
|
||||
* Note: ensure the concrete implementation unregisters the tuner channel
|
||||
* source from the tuner as a frequency change listener
|
||||
*
|
||||
* @param channel - previously obtained tuner channel
|
||||
*/
|
||||
public abstract void releaseChannel( TunerChannelSource source );
|
||||
|
||||
/**
|
||||
* Registers the listener to receive complex float sample arrays
|
||||
*/
|
||||
public void addListener( Listener<ComplexBuffer> listener )
|
||||
/**
|
||||
* Abstract tuner class.
|
||||
* @param name of the tuner
|
||||
* @param tunerController for the tuner
|
||||
*/
|
||||
public Tuner(String name, TunerController tunerController)
|
||||
{
|
||||
mSampleListeners.add( listener );
|
||||
}
|
||||
mName = name;
|
||||
mTunerController = tunerController;
|
||||
|
||||
/**
|
||||
* Removes the registered listener
|
||||
*/
|
||||
public void removeListener( Listener<ComplexBuffer> listener )
|
||||
{
|
||||
mSampleListeners.remove( listener );
|
||||
mTunerSourceManager = new TunerSourceManager(mTunerController);
|
||||
|
||||
//Rebroadcast frequency and sample rate change events as tuner events
|
||||
mTunerController.addListener(new ISourceEventProcessor()
|
||||
{
|
||||
@Override
|
||||
public void process(SourceEvent event)
|
||||
{
|
||||
switch(event.getEvent())
|
||||
{
|
||||
case NOTIFICATION_FREQUENCY_CHANGE:
|
||||
broadcast(new TunerEvent(Tuner.this, Event.FREQUENCY));
|
||||
break;
|
||||
case NOTIFICATION_SAMPLE_RATE_CHANGE:
|
||||
broadcast(new TunerEvent(Tuner.this, Event.SAMPLE_RATE));
|
||||
break;
|
||||
default:
|
||||
break;
|
||||
}
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
/**
|
||||
* Broadcasts the samples to all registered listeners
|
||||
* Source Manager. Provides access to registering for complex buffer samples and source event notifications.
|
||||
*/
|
||||
public void broadcast( ComplexBuffer sampleBuffer )
|
||||
public AbstractSourceManager getSourceManager()
|
||||
{
|
||||
for( Listener<ComplexBuffer> listener: mSampleListeners )
|
||||
{
|
||||
listener.receive( sampleBuffer );
|
||||
}
|
||||
return mTunerSourceManager;
|
||||
}
|
||||
|
||||
/**
|
||||
* Registers the listener
|
||||
*/
|
||||
public void addTunerChangeListener( Listener<TunerEvent> listener )
|
||||
public TunerController getTunerController()
|
||||
{
|
||||
mTunerChangeListeners.add( listener );
|
||||
return mTunerController;
|
||||
}
|
||||
|
||||
/**
|
||||
* Removes the registered listener
|
||||
*/
|
||||
public void removeTunerChangeListener( Listener<TunerEvent> listener )
|
||||
public String toString()
|
||||
{
|
||||
mTunerChangeListeners.remove( listener );
|
||||
return mName;
|
||||
}
|
||||
|
||||
public void dispose()
|
||||
{
|
||||
}
|
||||
|
||||
public void setName(String name)
|
||||
{
|
||||
mName = name;
|
||||
}
|
||||
|
||||
/**
|
||||
* Unique identifier for this tuner, used to lookup a tuner configuration
|
||||
* from the settings manager.
|
||||
*
|
||||
* @return - unique identifier like a serial number, or a usb bus location
|
||||
* or ip address and port. Return some form of unique identification that
|
||||
* allows this tuner to be identified from among several of the same types
|
||||
* of tuners.
|
||||
*/
|
||||
public abstract String getUniqueID();
|
||||
|
||||
/**
|
||||
* @return - tuner class enum entry
|
||||
*/
|
||||
public abstract TunerClass getTunerClass();
|
||||
|
||||
/**
|
||||
* @return - tuner type enum entry
|
||||
*/
|
||||
public abstract TunerType getTunerType();
|
||||
|
||||
/**
|
||||
* Name of this tuner object
|
||||
*
|
||||
* @return - string name of this tuner object
|
||||
*/
|
||||
public String getName()
|
||||
{
|
||||
return mName;
|
||||
}
|
||||
|
||||
/**
|
||||
* Sample size in bits
|
||||
*/
|
||||
public abstract double getSampleSize();
|
||||
|
||||
/**
|
||||
* Registers the listener
|
||||
*/
|
||||
public void addTunerChangeListener(Listener<TunerEvent> listener)
|
||||
{
|
||||
mTunerEventBroadcaster.addListener(listener);
|
||||
}
|
||||
|
||||
/**
|
||||
* Removes the registered listener
|
||||
*/
|
||||
public void removeTunerChangeListener(Listener<TunerEvent> listener)
|
||||
{
|
||||
mTunerEventBroadcaster.removeListener(listener);
|
||||
}
|
||||
|
||||
/**
|
||||
* Broadcasts the tuner change event
|
||||
*/
|
||||
public void broadcast( TunerEvent event )
|
||||
public void broadcast(TunerEvent tunerEvent)
|
||||
{
|
||||
for( Listener<TunerEvent> listener: mTunerChangeListeners )
|
||||
{
|
||||
listener.receive( event );
|
||||
}
|
||||
mTunerEventBroadcaster.broadcast(tunerEvent);
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -1,122 +0,0 @@
|
||||
/*******************************************************************************
|
||||
* SDR Trunk
|
||||
* Copyright (C) 2014,2015 Dennis Sheirer
|
||||
*
|
||||
* This program is free software: you can redistribute it and/or modify
|
||||
* it under the terms of the GNU General Public License as published by
|
||||
* the Free Software Foundation, either version 3 of the License, or
|
||||
* (at your option) any later version.
|
||||
*
|
||||
* This program is distributed in the hope that it will be useful,
|
||||
* but WITHOUT ANY WARRANTY; without even the implied warranty of
|
||||
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
|
||||
* GNU General Public License for more details.
|
||||
*
|
||||
* You should have received a copy of the GNU General Public License
|
||||
* along with this program. If not, see <http://www.gnu.org/licenses/>
|
||||
******************************************************************************/
|
||||
package io.github.dsheirer.source.tuner;
|
||||
|
||||
public class TunerChannel implements Comparable<TunerChannel>
|
||||
{
|
||||
private Type mType;
|
||||
private long mFrequency;
|
||||
private int mBandwidth;
|
||||
|
||||
public TunerChannel( Type type, long frequency, int bandwidth )
|
||||
{
|
||||
mType = type;
|
||||
mFrequency = frequency;
|
||||
mBandwidth = bandwidth;
|
||||
}
|
||||
|
||||
public Type getType()
|
||||
{
|
||||
return mType;
|
||||
}
|
||||
|
||||
public void setType( Type type )
|
||||
{
|
||||
mType = type;
|
||||
}
|
||||
|
||||
public long getFrequency()
|
||||
{
|
||||
return mFrequency;
|
||||
}
|
||||
|
||||
public void setFrequency( long frequency )
|
||||
{
|
||||
mFrequency = frequency;
|
||||
}
|
||||
|
||||
public int getBandwidth()
|
||||
{
|
||||
return mBandwidth;
|
||||
}
|
||||
|
||||
public void setBandwidth( int bandwidth )
|
||||
{
|
||||
mBandwidth = bandwidth;
|
||||
}
|
||||
|
||||
public long getMinFrequency()
|
||||
{
|
||||
return mFrequency - ( mBandwidth / 2 );
|
||||
}
|
||||
|
||||
public long getMaxFrequency()
|
||||
{
|
||||
return mFrequency + ( mBandwidth / 2 );
|
||||
}
|
||||
|
||||
/**
|
||||
* Indicates if any part of this tuner channel overlaps the specified range
|
||||
*/
|
||||
public boolean overlaps( long minimum, long maximum )
|
||||
{
|
||||
long channelMin = getMinFrequency();
|
||||
long channelMax = getMaxFrequency();
|
||||
|
||||
return ( ( minimum <= channelMin && channelMin <= maximum ) ||
|
||||
( minimum <= channelMax && channelMax <= maximum ) ||
|
||||
( minimum <= channelMin && channelMax <= maximum ) ||
|
||||
( channelMin <= minimum && maximum <= channelMax ) );
|
||||
}
|
||||
|
||||
public String toString()
|
||||
{
|
||||
StringBuilder sb = new StringBuilder();
|
||||
|
||||
sb.append( "Channel: " );
|
||||
sb.append( getMinFrequency() );
|
||||
sb.append( "-" );
|
||||
sb.append( getMaxFrequency() );
|
||||
|
||||
return sb.toString();
|
||||
}
|
||||
|
||||
@Override
|
||||
public int compareTo( TunerChannel otherLock )
|
||||
{
|
||||
if( this.equals( otherLock ) )
|
||||
{
|
||||
return 0;
|
||||
}
|
||||
else if( mFrequency < otherLock.getFrequency() )
|
||||
{
|
||||
return -1;
|
||||
}
|
||||
else
|
||||
{
|
||||
return 1;
|
||||
}
|
||||
}
|
||||
|
||||
public enum Type
|
||||
{
|
||||
LOCKED, //Use for decoding channels, so that they cannot be tuned out of range
|
||||
TRAFFIC, //Like locked, but for temporary allocated traffic channels
|
||||
TUNABLE; //Tunable -- can be tuned out of range
|
||||
}
|
||||
}
|
||||
@@ -1,27 +0,0 @@
|
||||
package io.github.dsheirer.source.tuner;
|
||||
|
||||
import io.github.dsheirer.source.SourceException;
|
||||
|
||||
import java.util.concurrent.RejectedExecutionException;
|
||||
|
||||
public interface TunerChannelProvider
|
||||
{
|
||||
/**
|
||||
* Returns a tuner frequency channel source, tuned to the correct frequency
|
||||
*
|
||||
* @param frequency - desired frequency
|
||||
*
|
||||
* @return - source for 48k sample rate
|
||||
*/
|
||||
public abstract TunerChannelSource getChannel( TunerChannel channel )
|
||||
throws RejectedExecutionException, SourceException;
|
||||
|
||||
/**
|
||||
* Releases the tuned channel resources
|
||||
*
|
||||
* @param channel - previously obtained tuner channel
|
||||
*/
|
||||
public abstract void releaseChannel( TunerChannelSource source );
|
||||
|
||||
|
||||
}
|
||||
@@ -1,572 +0,0 @@
|
||||
/*******************************************************************************
|
||||
* sdrtrunk
|
||||
* Copyright (C) 2014-2017 Dennis Sheirer
|
||||
*
|
||||
* This program is free software: you can redistribute it and/or modify
|
||||
* it under the terms of the GNU General Public License as published by
|
||||
* the Free Software Foundation, either version 3 of the License, or
|
||||
* (at your option) any later version.
|
||||
*
|
||||
* This program is distributed in the hope that it will be useful,
|
||||
* but WITHOUT ANY WARRANTY; without even the implied warranty of
|
||||
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
|
||||
* GNU General Public License for more details.
|
||||
*
|
||||
* You should have received a copy of the GNU General Public License
|
||||
* along with this program. If not, see <http://www.gnu.org/licenses/>
|
||||
*
|
||||
******************************************************************************/
|
||||
package io.github.dsheirer.source.tuner;
|
||||
|
||||
import io.github.dsheirer.channel.heartbeat.Heartbeat;
|
||||
import io.github.dsheirer.dsp.filter.FilterFactory;
|
||||
import io.github.dsheirer.dsp.filter.Window.WindowType;
|
||||
import io.github.dsheirer.dsp.filter.cic.ComplexPrimeCICDecimate;
|
||||
import io.github.dsheirer.dsp.mixer.Oscillator;
|
||||
import io.github.dsheirer.sample.Buffer;
|
||||
import io.github.dsheirer.sample.Listener;
|
||||
import io.github.dsheirer.sample.OverflowableTransferQueue;
|
||||
import io.github.dsheirer.sample.complex.Complex;
|
||||
import io.github.dsheirer.sample.complex.ComplexBuffer;
|
||||
import io.github.dsheirer.sample.real.IOverflowListener;
|
||||
import io.github.dsheirer.source.ComplexSource;
|
||||
import io.github.dsheirer.source.SourceException;
|
||||
import io.github.dsheirer.source.tuner.frequency.FrequencyChangeEvent;
|
||||
import io.github.dsheirer.source.tuner.frequency.FrequencyChangeEvent.Event;
|
||||
import io.github.dsheirer.source.tuner.frequency.IFrequencyChangeListener;
|
||||
import io.github.dsheirer.source.tuner.frequency.IFrequencyChangeProcessor;
|
||||
import io.github.dsheirer.source.tuner.frequency.IFrequencyChangeProvider;
|
||||
import org.slf4j.Logger;
|
||||
import org.slf4j.LoggerFactory;
|
||||
|
||||
import java.util.ArrayList;
|
||||
import java.util.List;
|
||||
import java.util.concurrent.RejectedExecutionException;
|
||||
import java.util.concurrent.ScheduledExecutorService;
|
||||
import java.util.concurrent.ScheduledFuture;
|
||||
import java.util.concurrent.TimeUnit;
|
||||
import java.util.concurrent.atomic.AtomicBoolean;
|
||||
|
||||
public class TunerChannelSource extends ComplexSource implements IFrequencyChangeProcessor, Listener<ComplexBuffer>
|
||||
{
|
||||
private final static Logger mLog = LoggerFactory.getLogger(TunerChannelSource.class);
|
||||
|
||||
//Maximum number of filled buffers for the blocking queue
|
||||
private static final int BUFFER_MAX_CAPACITY = 300;
|
||||
|
||||
//Threshold for resetting buffer overflow condition
|
||||
private static final int BUFFER_OVERFLOW_RESET_THRESHOLD = 100;
|
||||
|
||||
private static int CHANNEL_RATE = 48000;
|
||||
private static int CHANNEL_PASS_FREQUENCY = 12000;
|
||||
private static final Heartbeat HEARTBEAT = new Heartbeat();
|
||||
|
||||
private OverflowableTransferQueue<ComplexBuffer> mBuffer;
|
||||
|
||||
private Tuner mTuner;
|
||||
private TunerChannel mTunerChannel;
|
||||
private Oscillator mMixer;
|
||||
private ComplexPrimeCICDecimate mDecimationFilter;
|
||||
private Listener<ComplexBuffer> mListener;
|
||||
private Listener<Heartbeat> mHeartbeatListener;
|
||||
private IFrequencyChangeProcessor mFrequencyChangeProcessor;
|
||||
private DownstreamProcessor mDownstreamFrequencyEventProcessor = new DownstreamProcessor();
|
||||
private ScheduledFuture<?> mTaskHandle;
|
||||
|
||||
private long mTunerFrequency = 0;
|
||||
private int mTunerSampleRate;
|
||||
private int mChannelFrequencyCorrection = 0;
|
||||
|
||||
private DecimationProcessor mDecimationProcessor = new DecimationProcessor();
|
||||
|
||||
private AtomicBoolean mRunning = new AtomicBoolean();
|
||||
private boolean mExpended = false;
|
||||
|
||||
/**
|
||||
* Provides a Digital Drop Channel (DDC) to decimate the IQ output from a
|
||||
* tuner down to a 48 kHz IQ channel rate.
|
||||
*
|
||||
* Note: this class can only be used once (started and stopped) and a new
|
||||
* tuner channel source must be requested from the tuner once this object
|
||||
* has been stopped. This is because channels are managed dynamically and
|
||||
* center tuned frequency may have changed since this source was obtained
|
||||
* and thus the tuner might no longer be able to source this channel once it
|
||||
* has been stopped.
|
||||
*
|
||||
* @param tuner to obtain wideband IQ samples from
|
||||
* @param tunerChannel specifying the center frequency for the DDC
|
||||
* @throws RejectedExecutionException if the thread pool manager cannot
|
||||
* accept the decimation processing task
|
||||
* @throws SourceException if the tuner has an issue providing IQ samples
|
||||
*/
|
||||
public TunerChannelSource(Tuner tuner, TunerChannel tunerChannel) throws RejectedExecutionException, SourceException
|
||||
{
|
||||
mTuner = tuner;
|
||||
mTunerChannel = tunerChannel;
|
||||
mTuner.getTunerController().addListener((IFrequencyChangeProcessor) this);
|
||||
mTunerFrequency = mTuner.getTunerController().getFrequency();
|
||||
|
||||
mBuffer = new OverflowableTransferQueue<>(BUFFER_MAX_CAPACITY, BUFFER_OVERFLOW_RESET_THRESHOLD);
|
||||
|
||||
/* Setup the frequency translator to the current source frequency */
|
||||
long frequencyOffset = mTunerFrequency - mTunerChannel.getFrequency();
|
||||
|
||||
mMixer = new Oscillator(frequencyOffset, mTuner.getTunerController().getSampleRate());
|
||||
|
||||
/* Fire a sample rate change event to setup the decimation chain */
|
||||
frequencyChanged(new FrequencyChangeEvent(Event.NOTIFICATION_SAMPLE_RATE_CHANGE,
|
||||
mTuner.getTunerController().getSampleRate()));
|
||||
}
|
||||
|
||||
/**
|
||||
* Overrides the default source overflow listener management to delegate responsibility to the overflow buffer
|
||||
*/
|
||||
@Override
|
||||
public void setOverflowListener(IOverflowListener listener)
|
||||
{
|
||||
mBuffer.setOverflowListener(listener);
|
||||
}
|
||||
|
||||
public void start(ScheduledExecutorService executor)
|
||||
{
|
||||
if(mExpended)
|
||||
{
|
||||
throw new IllegalStateException("Attempt to re-start an expended tuner channel source. TunerChannelSource" +
|
||||
" objects can only be used once. ");
|
||||
}
|
||||
|
||||
if(mRunning.compareAndSet(false, true))
|
||||
{
|
||||
//Broadcast current frequency and sample rate settings so all downstream components are aware
|
||||
mDownstreamFrequencyEventProcessor.broadcastCurrentFrequency();
|
||||
mDownstreamFrequencyEventProcessor.broadcastCurrentSampleRate();
|
||||
|
||||
//Schedule the decimation task to run every 9 ms (111 iterations/second), an odd periodicity relative
|
||||
//to the inbound periodicity of 20 ms, to attempt to avoid thread queue contention
|
||||
mTaskHandle = executor.scheduleAtFixedRate(mDecimationProcessor, 0, 9, TimeUnit.MILLISECONDS);
|
||||
|
||||
/* Finally, register to receive samples from the tuner */
|
||||
mTuner.addListener((Listener<ComplexBuffer>) this);
|
||||
}
|
||||
else
|
||||
{
|
||||
mLog.warn("Attempt to start() an already running tuner channel source was ignored");
|
||||
}
|
||||
}
|
||||
|
||||
@Override
|
||||
public void reset()
|
||||
{
|
||||
}
|
||||
|
||||
@Override
|
||||
public void stop()
|
||||
{
|
||||
if(mRunning.compareAndSet(true, false))
|
||||
{
|
||||
mTuner.releaseChannel(this);
|
||||
mDecimationProcessor.shutdown();
|
||||
|
||||
if(mTaskHandle != null)
|
||||
{
|
||||
mTaskHandle.cancel(true);
|
||||
mTaskHandle = null;
|
||||
}
|
||||
|
||||
mBuffer.clear();
|
||||
|
||||
mExpended = true;
|
||||
}
|
||||
else
|
||||
{
|
||||
mLog.warn("Attempt to stop() an already stopped tuner channel source was ignored");
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@Override
|
||||
public void dispose()
|
||||
{
|
||||
if(!mRunning.get())
|
||||
{
|
||||
/* Tell the tuner to release/unregister our resources */
|
||||
mTuner.getTunerController().removeListener(this);
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Changes the frequency correction value and broadcasts the change to the registered downstream listener.
|
||||
* @param correction current frequency correction value.
|
||||
*/
|
||||
private void setFrequencyCorrection(int correction)
|
||||
{
|
||||
mChannelFrequencyCorrection = correction;
|
||||
|
||||
updateMixerFrequencyOffset();
|
||||
|
||||
mDownstreamFrequencyEventProcessor.broadcast(
|
||||
new FrequencyChangeEvent( Event.NOTIFICATION_CHANNEL_FREQUENCY_CORRECTION_CHANGE, mChannelFrequencyCorrection));
|
||||
}
|
||||
|
||||
public Tuner getTuner()
|
||||
{
|
||||
return mTuner;
|
||||
}
|
||||
|
||||
public TunerChannel getTunerChannel()
|
||||
{
|
||||
return mTunerChannel;
|
||||
}
|
||||
|
||||
@Override
|
||||
public void receive(ComplexBuffer buffer)
|
||||
{
|
||||
if(mRunning.get())
|
||||
{
|
||||
mBuffer.offer(buffer);
|
||||
}
|
||||
}
|
||||
|
||||
public void setFrequencyChangeListener(IFrequencyChangeProcessor processor)
|
||||
{
|
||||
mFrequencyChangeProcessor = processor;
|
||||
}
|
||||
|
||||
@Override
|
||||
public void setListener(Listener<ComplexBuffer> listener)
|
||||
{
|
||||
/* Save a pointer to the listener so that if we have to change the
|
||||
* decimation filter, we can re-add the listener */
|
||||
mListener = listener;
|
||||
|
||||
mDecimationFilter.setListener(listener);
|
||||
}
|
||||
|
||||
@Override
|
||||
public void removeListener(Listener<ComplexBuffer> listener)
|
||||
{
|
||||
mDecimationFilter.removeListener();
|
||||
}
|
||||
|
||||
/**
|
||||
* Handler for frequency change events received from the tuner and channel
|
||||
* frequency correction events received from the channel consumer/listener
|
||||
*/
|
||||
@Override
|
||||
public void frequencyChanged(FrequencyChangeEvent event) throws SourceException
|
||||
{
|
||||
// Echo the event to the registered event listener
|
||||
if(mFrequencyChangeProcessor != null)
|
||||
{
|
||||
mFrequencyChangeProcessor.frequencyChanged(event);
|
||||
}
|
||||
|
||||
switch(event.getEvent())
|
||||
{
|
||||
case NOTIFICATION_FREQUENCY_CHANGE:
|
||||
mTunerFrequency = event.getValue().longValue();
|
||||
updateMixerFrequencyOffset();
|
||||
|
||||
//Reset frequency correction so that downstream components can recalculate the value
|
||||
setFrequencyCorrection(0);
|
||||
break;
|
||||
case NOTIFICATION_SAMPLE_RATE_CHANGE:
|
||||
int sampleRate = event.getValue().intValue();
|
||||
setSampleRate(sampleRate);
|
||||
break;
|
||||
default:
|
||||
break;
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Updates the sample rate to the requested value and notifies any downstream components of the change
|
||||
* @param sampleRate to set
|
||||
*/
|
||||
private void setSampleRate(int sampleRate)
|
||||
{
|
||||
if(mTunerSampleRate != sampleRate)
|
||||
{
|
||||
mMixer.setSampleRate(sampleRate);
|
||||
|
||||
/* Get new decimation filter */
|
||||
mDecimationFilter = FilterFactory.getDecimationFilter(sampleRate, CHANNEL_RATE, 1,
|
||||
CHANNEL_PASS_FREQUENCY, 60, WindowType.HAMMING);
|
||||
|
||||
/* re-add the original output listener */
|
||||
mDecimationFilter.setListener(mListener);
|
||||
|
||||
mTunerSampleRate = sampleRate;
|
||||
|
||||
mDownstreamFrequencyEventProcessor.broadcastCurrentSampleRate();
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Calculates the local mixer frequency offset from the tuned frequency,
|
||||
* channel's requested frequency, and channel frequency correction.
|
||||
*/
|
||||
private void updateMixerFrequencyOffset()
|
||||
{
|
||||
long offset = mTunerFrequency - mTunerChannel.getFrequency() - mChannelFrequencyCorrection;
|
||||
mMixer.setFrequency(offset);
|
||||
}
|
||||
|
||||
public int getSampleRate() throws SourceException
|
||||
{
|
||||
return CHANNEL_RATE;
|
||||
}
|
||||
|
||||
public long getFrequency() throws SourceException
|
||||
{
|
||||
return mTunerChannel.getFrequency();
|
||||
}
|
||||
|
||||
/**
|
||||
* Implements IFrequencyChangeProvider to enable this source to broadcast frequency change events to downstream
|
||||
* listeners.
|
||||
*
|
||||
* @param listener to receive downstream events
|
||||
*/
|
||||
@Override
|
||||
public void setFrequencyChangeListener(Listener<FrequencyChangeEvent> listener)
|
||||
{
|
||||
mDownstreamFrequencyEventProcessor.setFrequencyChangeListener(listener);
|
||||
}
|
||||
|
||||
/**
|
||||
* Implements IFrequencyChangeProvider to remove the frequency change listener from receiving down-stream frequency
|
||||
* change events.
|
||||
*/
|
||||
@Override
|
||||
public void removeFrequencyChangeListener()
|
||||
{
|
||||
mDownstreamFrequencyEventProcessor.removeFrequencyChangeListener();
|
||||
}
|
||||
|
||||
/**
|
||||
* Implements IFrequencyChangeListener to receive frequency change events containing requests from downstream
|
||||
* listeners to change frequency values.
|
||||
* @return listener
|
||||
*/
|
||||
@Override
|
||||
public Listener<FrequencyChangeEvent> getFrequencyChangeListener()
|
||||
{
|
||||
return mDownstreamFrequencyEventProcessor.getFrequencyChangeListener();
|
||||
}
|
||||
|
||||
/**
|
||||
* Registers the listener to receive heartbeats from this source.
|
||||
* @param listener to receive heartbeats
|
||||
*/
|
||||
@Override
|
||||
public void setHeartbeatListener(Listener<Heartbeat> listener)
|
||||
{
|
||||
mHeartbeatListener = listener;
|
||||
}
|
||||
|
||||
/**
|
||||
* Removes the currently registered heartbeat listener
|
||||
*/
|
||||
@Override
|
||||
public void removeHeartbeatListener()
|
||||
{
|
||||
mHeartbeatListener = null;
|
||||
}
|
||||
|
||||
/**
|
||||
* Managers frequency change requests and notifications from/to any downstream component. Downstream
|
||||
* components are those that receive samples from this tuner channel source. These downstream components will be
|
||||
* notified of any frequency or sample rate change events and will also be able to request frequency correction
|
||||
* updates.
|
||||
*/
|
||||
public class DownstreamProcessor implements IFrequencyChangeListener, IFrequencyChangeProvider,
|
||||
Listener<FrequencyChangeEvent>
|
||||
{
|
||||
//Listener to receive downstream events
|
||||
private Listener<FrequencyChangeEvent> mListener;
|
||||
|
||||
/**
|
||||
* Broadcasts the frequency change event to the downstream frequency change listener
|
||||
* @param event to broadcast
|
||||
*/
|
||||
public void broadcast(FrequencyChangeEvent event)
|
||||
{
|
||||
if(mListener != null)
|
||||
{
|
||||
mListener.receive(event);
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Broadcasts the current frequency of this tuner channel source to the downstream listener
|
||||
*/
|
||||
public void broadcastCurrentFrequency()
|
||||
{
|
||||
try
|
||||
{
|
||||
long frequency = getFrequency();
|
||||
broadcast(new FrequencyChangeEvent(Event.NOTIFICATION_FREQUENCY_CHANGE, frequency));
|
||||
}
|
||||
catch(SourceException se)
|
||||
{
|
||||
mLog.error("Error obtaining frequency from tuner to broadcast downstream");
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Broadcasts the current decimated sample rate of this tuner channel source
|
||||
*/
|
||||
public void broadcastCurrentSampleRate()
|
||||
{
|
||||
try
|
||||
{
|
||||
//Note: downstream sample rate is currently a fixed value -- it will change in the future
|
||||
broadcast(new FrequencyChangeEvent(Event.NOTIFICATION_SAMPLE_RATE_CHANGE, getSampleRate()));
|
||||
}
|
||||
catch(SourceException se)
|
||||
{
|
||||
mLog.error("Error obtaining sample rate from tuner to broadcast downstream");
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Sets the downstream listener to receive frequency change events from this tuner channel source
|
||||
* @param listener to receive events
|
||||
*/
|
||||
@Override
|
||||
public void setFrequencyChangeListener(Listener<FrequencyChangeEvent> listener)
|
||||
{
|
||||
mListener = listener;
|
||||
}
|
||||
|
||||
/**
|
||||
* Removes the downstream listener from receiving frequency change events.
|
||||
*/
|
||||
@Override
|
||||
public void removeFrequencyChangeListener()
|
||||
{
|
||||
mListener = null;
|
||||
}
|
||||
|
||||
/**
|
||||
* Listener for receiving frequency change events from downstream components
|
||||
*/
|
||||
@Override
|
||||
public Listener<FrequencyChangeEvent> getFrequencyChangeListener()
|
||||
{
|
||||
return this;
|
||||
}
|
||||
|
||||
/**
|
||||
* Processes frequency change events from downstream components.
|
||||
* @param event to process
|
||||
*/
|
||||
@Override
|
||||
public void receive(FrequencyChangeEvent event)
|
||||
{
|
||||
switch(event.getEvent())
|
||||
{
|
||||
//Frequency correction requests are the only change requests supported from downstream components
|
||||
case REQUEST_CHANNEL_FREQUENCY_CORRECTION_CHANGE:
|
||||
setFrequencyCorrection(event.getValue().intValue());
|
||||
break;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Decimates an inbound buffer of I/Q samples from the source down to the
|
||||
* standard 48000 channel sample rate
|
||||
*/
|
||||
public class DecimationProcessor implements Runnable
|
||||
{
|
||||
private boolean mProcessing = true;
|
||||
private List<ComplexBuffer> mSampleBuffers = new ArrayList<ComplexBuffer>();
|
||||
|
||||
public void shutdown()
|
||||
{
|
||||
mProcessing = false;
|
||||
}
|
||||
|
||||
@Override
|
||||
public void run()
|
||||
{
|
||||
/* General exception handler so that any errors won't kill the
|
||||
* decimation thread and cause the input buffers to fill up and
|
||||
* run the program out of memory */
|
||||
try
|
||||
{
|
||||
if(mProcessing)
|
||||
{
|
||||
//Send a heartbeat every time this runs to allow downstream components to perform periodic
|
||||
//state monitoring functions on this thread
|
||||
if(mHeartbeatListener != null)
|
||||
{
|
||||
mHeartbeatListener.receive(HEARTBEAT);
|
||||
}
|
||||
|
||||
mBuffer.drainTo(mSampleBuffers, 20);
|
||||
|
||||
for(Buffer buffer : mSampleBuffers)
|
||||
{
|
||||
/* Check to see if we've been shutdown */
|
||||
if(!mProcessing)
|
||||
{
|
||||
mBuffer.clear();
|
||||
return;
|
||||
}
|
||||
else
|
||||
{
|
||||
float[] samples = buffer.getSamples();
|
||||
|
||||
/* We make a copy of the buffer so that we don't affect
|
||||
* anyone else that is using the same buffer, like other
|
||||
* channels or the spectral display */
|
||||
float[] translated = new float[samples.length];
|
||||
|
||||
/* Perform frequency translation */
|
||||
for(int x = 0; x < samples.length; x += 2)
|
||||
{
|
||||
mMixer.rotate();
|
||||
|
||||
translated[x] = Complex.multiplyInphase(
|
||||
samples[x], samples[x + 1], mMixer.inphase(), mMixer.quadrature());
|
||||
|
||||
translated[x + 1] = Complex.multiplyQuadrature(
|
||||
samples[x], samples[x + 1], mMixer.inphase(), mMixer.quadrature());
|
||||
}
|
||||
|
||||
if(mProcessing)
|
||||
{
|
||||
final ComplexPrimeCICDecimate filter = mDecimationFilter;
|
||||
filter.receive(new ComplexBuffer(translated));
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
mSampleBuffers.clear();
|
||||
}
|
||||
}
|
||||
catch(Exception e)
|
||||
{
|
||||
/* Only log the stack trace if we're still processing */
|
||||
if(mProcessing)
|
||||
{
|
||||
mLog.error("Error encountered during decimation process", e);
|
||||
}
|
||||
}
|
||||
catch(Throwable throwable)
|
||||
{
|
||||
mLog.error("Code error encountered during decimation process - channel thread will probably die", throwable);
|
||||
}
|
||||
|
||||
/* Check to see if we've been shutdown */
|
||||
if(!mProcessing)
|
||||
{
|
||||
mBuffer.clear();
|
||||
mSampleBuffers.clear();
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
Some files were not shown because too many files have changed in this diff Show More
Reference in New Issue
Block a user