Compare commits

..

8 Commits

Author SHA1 Message Date
Denny Sheirer
ea46a8b0e8 Merge pull request #225 from DSheirer/222-spectral-display-of-frequency-correction-offset
Frequency change event handling in the processing chain
2017-03-11 17:03:34 +00:00
Denny
d510c34356 Adds frequency change event listener handler to LSM decoder. 2017-03-11 12:01:45 -05:00
Denny
ae8a44caed Removes cic cleanup filter from processing chain. 2017-03-11 11:53:12 -05:00
Denny
1aa85a86ca Resolves issue with channel frequency correction events not being broadcast within the processing chain and rendering in the spectral display. 2017-03-11 10:16:08 -05:00
Denny Sheirer
17539d4d55 Merge pull request #224 from DSheirer/223-now-playing-frequency-format
Now Playing frequency display formatting
2017-03-11 11:14:58 +00:00
Denny
1db1d28390 Resolves issue with frequency display formating in the Now Playing window. 2017-03-11 06:13:59 -05:00
Denny Sheirer
37a941e904 Merge pull request #219 from DSheirer/218-audio-priority-
Resolves issue with audio playback allowing higher priority audio to preempt lower priority audio
2017-03-05 12:34:09 +00:00
Denny
57f6655824 Resolves issue where higher priority audio was not preempting lower priority audio. 2017-03-05 07:32:42 -05:00
22 changed files with 1787 additions and 1406 deletions

View File

@@ -86,6 +86,9 @@ public class AudioManager implements Listener<AudioPacket>, IAudioController
loadSettings(); loadSettings();
} }
/**
* Loads the saved mixer configuration or a default configuration for audio playback.
*/
private void loadSettings() private void loadSettings()
{ {
MixerChannelConfiguration configuration = null; MixerChannelConfiguration configuration = null;
@@ -132,6 +135,9 @@ public class AudioManager implements Listener<AudioPacket>, IAudioController
} }
} }
/**
* Creates a default audio playback configuration with a mono audio playback channel.
*/
private MixerChannelConfiguration getDefaultConfiguration() private MixerChannelConfiguration getDefaultConfiguration()
{ {
/* Use the system default mixer and mono channel as default startup */ /* Use the system default mixer and mono channel as default startup */
@@ -161,6 +167,9 @@ public class AudioManager implements Listener<AudioPacket>, IAudioController
mAudioOutputConnections.clear(); mAudioOutputConnections.clear();
} }
/**
* Primary ingest point for audio produced by all decoding channels, for distribution to audio playback devices.
*/
@Override @Override
public synchronized void receive(AudioPacket packet) public synchronized void receive(AudioPacket packet)
{ {
@@ -176,11 +185,11 @@ public class AudioManager implements Listener<AudioPacket>, IAudioController
for(AudioOutputConnection connection : mAudioOutputConnections) for(AudioOutputConnection connection : mAudioOutputConnections)
{ {
if(connection.isInactive() && if(connection.isInactive() && mChannelConnectionMap.containsKey(connection.getChannelMetadataID()))
mChannelConnectionMap.containsKey(connection.getChannelMetadataID()))
{ {
mChannelConnectionMap.remove(connection.getChannelMetadataID()); mChannelConnectionMap.remove(connection.getChannelMetadataID());
connection.disconnect(); connection.disconnect();
mAvailableConnectionCount++;
changed = true; changed = true;
} }
} }
@@ -192,7 +201,7 @@ public class AudioManager implements Listener<AudioPacket>, IAudioController
} }
/** /**
* Identifies the lowest priority channel connection * Identifies the lowest priority channel connection where the a higher value indicates a lower priority.
*/ */
private void updateLowestPriorityAssignment() private void updateLowestPriorityAssignment()
{ {
@@ -201,19 +210,32 @@ public class AudioManager implements Listener<AudioPacket>, IAudioController
for(AudioOutputConnection connection : mAudioOutputConnections) for(AudioOutputConnection connection : mAudioOutputConnections)
{ {
if(connection.isConnected() && if(connection.isConnected() &&
(mLowestPriorityConnection == null || mLowestPriorityConnection.getPriority() > connection.getPriority())) (mLowestPriorityConnection == null || mLowestPriorityConnection.getPriority() < connection.getPriority()))
{ {
mLowestPriorityConnection = connection; mLowestPriorityConnection = connection;
} }
} }
} }
/**
* Configures audio playback to use the configuration specified in the entry argument.
*
* @param entry to use in configuring the audio playback setup.
* @throws AudioException if there is an error
*/
@Override @Override
public void setMixerChannelConfiguration(MixerChannelConfiguration entry) throws AudioException public void setMixerChannelConfiguration(MixerChannelConfiguration entry) throws AudioException
{ {
setMixerChannelConfiguration(entry, true); setMixerChannelConfiguration(entry, true);
} }
/**
* Configures audio playback to use the configuration specified in the entry argument.
*
* @param entry to use in configuring the audio playback setup.
* @param saveSettings to save the audio playback configuration settings in the properties file.
* @throws AudioException if there is an error
*/
public void setMixerChannelConfiguration(MixerChannelConfiguration entry, boolean saveSettings) throws AudioException public void setMixerChannelConfiguration(MixerChannelConfiguration entry, boolean saveSettings) throws AudioException
{ {
if(entry != null && (entry.getMixerChannel() == MixerChannel.MONO || entry.getMixerChannel() == MixerChannel.STEREO)) if(entry != null && (entry.getMixerChannel() == MixerChannel.MONO || entry.getMixerChannel() == MixerChannel.STEREO))
@@ -266,9 +288,8 @@ public class AudioManager implements Listener<AudioPacket>, IAudioController
} }
/** /**
* Clears all channel assignments and terminates all audio outputs in * Clears all channel assignments and terminates all audio outputs in preparation for complete shutdown or change
* preparation for complete shutdown or change to another mixer/channel * to another mixer/channel configuration
* configuration
*/ */
private void disposeCurrentConfiguration() private void disposeCurrentConfiguration()
{ {
@@ -288,12 +309,18 @@ public class AudioManager implements Listener<AudioPacket>, IAudioController
mLowestPriorityConnection = null; mLowestPriorityConnection = null;
} }
/**
* Current audio playback mixer channel configuration setting.
*/
@Override @Override
public MixerChannelConfiguration getMixerChannelConfiguration() throws AudioException public MixerChannelConfiguration getMixerChannelConfiguration() throws AudioException
{ {
return mMixerChannelConfiguration; return mMixerChannelConfiguration;
} }
/**
* List of audio outputs available for the current mixer channel configuration
*/
@Override @Override
public List<AudioOutput> getAudioOutputs() public List<AudioOutput> getAudioOutputs()
{ {
@@ -311,12 +338,18 @@ public class AudioManager implements Listener<AudioPacket>, IAudioController
return outputs; return outputs;
} }
/**
* Adds an audio event listener to receive audio event notifications.
*/
@Override @Override
public void addControllerListener(Listener<AudioEvent> listener) public void addControllerListener(Listener<AudioEvent> listener)
{ {
mControllerBroadcaster.addListener(listener); mControllerBroadcaster.addListener(listener);
} }
/**
* Removes an audio event listener from receiving audio event notifications.
*/
@Override @Override
public void removeControllerListener(Listener<AudioEvent> listener) public void removeControllerListener(Listener<AudioEvent> listener)
{ {
@@ -324,16 +357,15 @@ public class AudioManager implements Listener<AudioPacket>, IAudioController
} }
/** /**
* Returns an audio output connection for the packet if one is * Returns an audio output connection for the packet if one is available, or overrides an existing lower priority
* available, or overrides an existing lower priority connection. * connection. Returns null if no connection is available for the audio packet.
* Returns null if no connection is available for the audio packet.
* *
* @param packet from a source * @param audioPacket from a decoding channel source
* @return an audio output connection or null * @return an audio output connection or null
*/ */
private AudioOutputConnection getConnection(AudioPacket packet) private AudioOutputConnection getConnection(AudioPacket audioPacket)
{ {
int channelMetadataID = packet.getMetadata().getMetadataID(); int channelMetadataID = audioPacket.getMetadata().getMetadataID();
//Use an existing connection //Use an existing connection
if(mChannelConnectionMap.containsKey(channelMetadataID)) if(mChannelConnectionMap.containsKey(channelMetadataID))
@@ -341,37 +373,35 @@ public class AudioManager implements Listener<AudioPacket>, IAudioController
return mChannelConnectionMap.get(channelMetadataID); return mChannelConnectionMap.get(channelMetadataID);
} }
//Connect to and use an available connection //Connect to an unused, available connection
if(mAvailableConnectionCount > 0) if(mAvailableConnectionCount > 0)
{ {
for(AudioOutputConnection connection : mAudioOutputConnections) for(AudioOutputConnection connection : mAudioOutputConnections)
{ {
if(connection.isDisconnected()) if(connection.isDisconnected())
{ {
connection.connect(channelMetadataID); connection.connect(channelMetadataID, audioPacket.getMetadata().getAudioPriority());
mChannelConnectionMap.put(channelMetadataID, connection); mChannelConnectionMap.put(channelMetadataID, connection);
mAvailableConnectionCount--;
return connection; return connection;
} }
} }
} }
//Preempt an existing connection and connect it to this higher priority source //Preempt an existing lower priority connection and connect when this is a higher priority packet
else //Check for a channel priority override else
{ {
int priority = packet.getMetadata().getAudioPriority(); int priority = audioPacket.getMetadata().getAudioPriority();
if(mLowestPriorityConnection != null && AudioOutputConnection connection = mLowestPriorityConnection;
priority < mLowestPriorityConnection.getPriority())
if(connection != null && priority < connection.getPriority())
{ {
AudioOutputConnection connection = mLowestPriorityConnection;
mChannelConnectionMap.remove(connection.getChannelMetadataID()); mChannelConnectionMap.remove(connection.getChannelMetadataID());
connection.connect(channelMetadataID); connection.connect(channelMetadataID, priority);
mChannelConnectionMap.put(channelMetadataID, connection); mChannelConnectionMap.put(channelMetadataID, connection);
updateLowestPriorityAssignment();
return connection; return connection;
} }
} }
@@ -396,7 +426,7 @@ public class AudioManager implements Listener<AudioPacket>, IAudioController
for(AudioPacket packet : packets) for(AudioPacket packet : packets)
{ {
/* Don't process any packet's marked as do not monitor */ /* Don't process any packet's marked as do not monitor */
if(!packet.getMetadata().isDoNotMonitor() && packet.getType() == AudioPacket.Type.AUDIO) if(!packet.getMetadata().isDoNotMonitor() && packet.getType() == AudioPacket.Type.AUDIO)
{ {
AudioOutputConnection connection = getConnection(packet); AudioOutputConnection connection = getConnection(packet);
@@ -494,9 +524,11 @@ public class AudioManager implements Listener<AudioPacket>, IAudioController
* Connects this assignment to the indicated source so that audio * Connects this assignment to the indicated source so that audio
* packets from this source can be sent to the audio output * packets from this source can be sent to the audio output
*/ */
public void connect(int source) public void connect(int source, int priority)
{ {
mChannelMetadataID = source; mChannelMetadataID = source;
mPriority = priority;
updateLowestPriorityAssignment();
mAudioOutput.updateTimestamp(); mAudioOutput.updateTimestamp();
} }

View File

@@ -32,7 +32,7 @@ import java.util.Map;
public class ChannelMetadataModel extends AbstractTableModel implements Listener<MutableMetadataChangeEvent> public class ChannelMetadataModel extends AbstractTableModel implements Listener<MutableMetadataChangeEvent>
{ {
private final DecimalFormat FREQUENCY_FORMATTER = new DecimalFormat( "#.0000" ); private final DecimalFormat FREQUENCY_FORMATTER = new DecimalFormat( "#.00000" );
public static final int COLUMN_STATE = 0; public static final int COLUMN_STATE = 0;
public static final int COLUMN_DECODER = 1; public static final int COLUMN_DECODER = 1;

View File

@@ -36,14 +36,19 @@ import org.slf4j.Logger;
import org.slf4j.LoggerFactory; import org.slf4j.LoggerFactory;
import sample.Listener; import sample.Listener;
import sample.real.IOverflowListener; import sample.real.IOverflowListener;
import source.tuner.frequency.FrequencyChangeEvent;
import source.tuner.frequency.IFrequencyChangeListener;
import java.util.concurrent.RejectedExecutionException; import java.util.concurrent.RejectedExecutionException;
import java.util.concurrent.ScheduledExecutorService; import java.util.concurrent.ScheduledExecutorService;
import java.util.concurrent.ScheduledFuture; import java.util.concurrent.ScheduledFuture;
import java.util.concurrent.TimeUnit; import java.util.concurrent.TimeUnit;
import static source.tuner.frequency.FrequencyChangeEvent.Event.NOTIFICATION_FREQUENCY_CHANGE;
public class ChannelState extends Module implements ICallEventProvider, IDecoderStateEventListener, public class ChannelState extends Module implements ICallEventProvider, IDecoderStateEventListener,
IDecoderStateEventProvider, ISquelchStateProvider, IAttributeChangeRequestListener, IOverflowListener IDecoderStateEventProvider, IFrequencyChangeListener, ISquelchStateProvider, IAttributeChangeRequestListener,
IOverflowListener
{ {
private final static Logger mLog = LoggerFactory.getLogger(ChannelState.class); private final static Logger mLog = LoggerFactory.getLogger(ChannelState.class);
@@ -61,6 +66,7 @@ public class ChannelState extends Module implements ICallEventProvider, IDecoder
private ChannelType mChannelType; private ChannelType mChannelType;
private TrafficChannelManager mTrafficChannelEndListener; private TrafficChannelManager mTrafficChannelEndListener;
private CallEvent mTrafficChannelCallEvent; private CallEvent mTrafficChannelCallEvent;
private FrequencyChangeListener mFrequencyChangeListener;
private boolean mSquelchLocked = false; private boolean mSquelchLocked = false;
private boolean mSelected = false; private boolean mSelected = false;
@@ -189,6 +195,17 @@ public class ChannelState extends Module implements ICallEventProvider, IDecoder
mStateMonitor = null; mStateMonitor = null;
} }
@Override
public Listener<FrequencyChangeEvent> getFrequencyChangeListener()
{
if(mFrequencyChangeListener == null)
{
mFrequencyChangeListener = new FrequencyChangeListener();
}
return mFrequencyChangeListener;
}
private boolean isStandardChannel() private boolean isStandardChannel()
{ {
return mChannelType == ChannelType.STANDARD; return mChannelType == ChannelType.STANDARD;
@@ -532,6 +549,23 @@ public class ChannelState extends Module implements ICallEventProvider, IDecoder
broadcast(mTrafficChannelCallEvent); broadcast(mTrafficChannelCallEvent);
} }
/**
* Listener to receive frequency change events and rebroadcast them to the decoder states
*/
public class FrequencyChangeListener implements Listener<FrequencyChangeEvent>
{
@Override
public void receive(FrequencyChangeEvent frequencyChangeEvent)
{
if(frequencyChangeEvent.getEvent() == NOTIFICATION_FREQUENCY_CHANGE)
{
long frequency = frequencyChangeEvent.getValue().longValue();
broadcast(new DecoderStateEvent(this, Event.SOURCE_FREQUENCY, getState(), frequency));
}
}
}
/** /**
* DecoderStateEvent receiver wrapper * DecoderStateEvent receiver wrapper
*/ */

View File

@@ -1,28 +1,24 @@
/******************************************************************************* /*******************************************************************************
* SDR Trunk * sdrtrunk
* Copyright (C) 2014-2016 Dennis Sheirer * Copyright (C) 2014-2017 Dennis Sheirer
* *
* This program is free software: you can redistribute it and/or modify * 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 * it under the terms of the GNU General Public License as published by
* the Free Software Foundation, either version 3 of the License, or * the Free Software Foundation, either version 3 of the License, or
* (at your option) any later version. * (at your option) any later version.
* *
* This program is distributed in the hope that it will be useful, * This program is distributed in the hope that it will be useful,
* but WITHOUT ANY WARRANTY; without even the implied warranty of * but WITHOUT ANY WARRANTY; without even the implied warranty of
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
* GNU General Public License for more details. * GNU General Public License for more details.
* *
* You should have received a copy of the GNU General Public License * You should have received a copy of the GNU General Public License
* along with this program. If not, see <http://www.gnu.org/licenses/> * along with this program. If not, see <http://www.gnu.org/licenses/>
*
******************************************************************************/ ******************************************************************************/
package controller.channel; package controller.channel;
import javax.xml.bind.annotation.XmlAttribute; import controller.config.Configuration;
import javax.xml.bind.annotation.XmlElement;
import javax.xml.bind.annotation.XmlRootElement;
import javax.xml.bind.annotation.XmlSeeAlso;
import javax.xml.bind.annotation.XmlTransient;
import module.decode.DecoderFactory; import module.decode.DecoderFactory;
import module.decode.DecoderType; import module.decode.DecoderType;
import module.decode.config.AuxDecodeConfiguration; import module.decode.config.AuxDecodeConfiguration;
@@ -31,6 +27,7 @@ import module.log.EventLogType;
import module.log.config.EventLogConfiguration; import module.log.config.EventLogConfiguration;
import record.RecorderType; import record.RecorderType;
import record.config.RecordConfiguration; import record.config.RecordConfiguration;
import sample.Listener;
import source.SourceType; import source.SourceType;
import source.config.SourceConfigFactory; import source.config.SourceConfigFactory;
import source.config.SourceConfigRecording; import source.config.SourceConfigRecording;
@@ -40,424 +37,444 @@ import source.tuner.TunerChannel;
import source.tuner.TunerChannel.Type; import source.tuner.TunerChannel.Type;
import source.tuner.frequency.FrequencyChangeEvent; import source.tuner.frequency.FrequencyChangeEvent;
import source.tuner.frequency.FrequencyChangeEvent.Event; import source.tuner.frequency.FrequencyChangeEvent.Event;
import source.tuner.frequency.IFrequencyChangeProcessor;
import controller.config.Configuration;
@XmlSeeAlso( { Configuration.class } ) import javax.xml.bind.annotation.XmlAttribute;
@XmlRootElement( name = "channel" ) import javax.xml.bind.annotation.XmlElement;
public class Channel extends Configuration implements IFrequencyChangeProcessor import javax.xml.bind.annotation.XmlRootElement;
import javax.xml.bind.annotation.XmlSeeAlso;
import javax.xml.bind.annotation.XmlTransient;
@XmlSeeAlso({Configuration.class})
@XmlRootElement(name = "channel")
public class Channel extends Configuration implements Listener<FrequencyChangeEvent>
{ {
// Standard channels are persisted and traffic channels are temporary // Standard channels are persisted and traffic channels are temporary
public enum ChannelType { STANDARD, TRAFFIC }; public enum ChannelType
{
STANDARD, TRAFFIC
}
// Static unique channel identifier tracking ;
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 RecordConfiguration mRecordConfiguration = new RecordConfiguration();
private String mAliasListName; // Static unique channel identifier tracking
private String mSystem = "System"; 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 RecordConfiguration mRecordConfiguration = new RecordConfiguration();
private String mAliasListName;
private String mSystem = "System";
private String mSite = "Site"; private String mSite = "Site";
private String mName = "Channel"; private String mName = "Channel";
private boolean mEnabled;
private boolean mSelected;
private TunerChannel mTunerChannel = null;
private ChannelType mChannelType = ChannelType.STANDARD;
private int mChannelID; private boolean mEnabled;
private int mChannelFrequencyCorrection = 0; private boolean mSelected;
private TunerChannel mTunerChannel = null;
/**
* Channel represents a complete set of configurations needed to setup and
* manage a processing chain and/or manage as a set of business objects that
* can be displayed in graphical components.
*
* @param channelName
* @param channelType
*/
public Channel( String channelName, ChannelType channelType )
{
this( channelName );
mChannelType = channelType;
}
/** private ChannelType mChannelType = ChannelType.STANDARD;
* Constructs a new standard channel with the specified channel name
*/
public Channel( String channelName )
{
this();
mName = channelName;
}
/** private int mChannelID;
* Constructs a new standard channel with a default name of "Channel" private int mChannelFrequencyCorrection = 0;
*/
public Channel()
{
mChannelID = UNIQUE_ID++;
}
/** /**
* Creates a (new) deep copy of this channel * Channel represents a complete set of configurations needed to setup and
*/ * manage a processing chain and/or manage as a set of business objects that
public Channel copyOf() * can be displayed in graphical components.
{ *
Channel channel = new Channel( mName ); * @param channelName
channel.setSystem( mSystem ); * @param channelType
channel.setSite( mSite ); */
channel.setAliasListName( mAliasListName ); public Channel(String channelName, ChannelType channelType)
{
this(channelName);
mChannelType = channelType;
}
AuxDecodeConfiguration aux = new AuxDecodeConfiguration(); /**
* Constructs a new standard channel with the specified channel name
for( DecoderType auxType: aux.getAuxDecoders() ) */
{ public Channel(String channelName)
aux.addAuxDecoder( auxType ); {
} this();
mName = channelName;
channel.setAuxDecodeConfiguration( aux ); }
channel.setDecodeConfiguration( DecoderFactory.copy( mDecodeConfiguration ) );
EventLogConfiguration log = new EventLogConfiguration();
for( EventLogType logType: mEventLogConfiguration.getLoggers() )
{
log.addLogger( logType );
}
channel.setEventLogConfiguration( log );
RecordConfiguration record = new RecordConfiguration(); /**
* Constructs a new standard channel with a default name of "Channel"
for( RecorderType recordType: mRecordConfiguration.getRecorders() ) */
{ public Channel()
record.addRecorder( recordType ); {
} mChannelID = UNIQUE_ID++;
}
channel.setRecordConfiguration( record );
/**
* Creates a (new) deep copy of this channel
*/
public Channel copyOf()
{
Channel channel = new Channel(mName);
channel.setSystem(mSystem);
channel.setSite(mSite);
channel.setAliasListName(mAliasListName);
AuxDecodeConfiguration aux = new AuxDecodeConfiguration();
for(DecoderType auxType : aux.getAuxDecoders())
{
aux.addAuxDecoder(auxType);
}
channel.setAuxDecodeConfiguration(aux);
channel.setDecodeConfiguration(DecoderFactory.copy(mDecodeConfiguration));
EventLogConfiguration log = new EventLogConfiguration();
for(EventLogType logType : mEventLogConfiguration.getLoggers())
{
log.addLogger(logType);
}
channel.setEventLogConfiguration(log);
RecordConfiguration record = new RecordConfiguration();
for(RecorderType recordType : mRecordConfiguration.getRecorders())
{
record.addRecorder(recordType);
}
channel.setRecordConfiguration(record);
channel.setSourceConfiguration(SourceConfigFactory.copy(mSourceConfiguration));
return channel;
}
/**
* Unique identifier for this channel. Value is transient (not persisted).
*/
@XmlTransient
public int getChannelID()
{
return mChannelID;
}
/**
* Indicates if this channel has been selected for prioritized audio output
* and display of processing chain products
*/
@XmlTransient
public boolean isSelected()
{
return mSelected;
}
/**
* Sets selection status of the channel.
*
* NOTE: this method is package private so that only the ChannelSelectionManager
* can toggle the selection state. Use the channel model to broadcast a
* request select/deselect channel event.
*
* @param selected
*/
void setSelected(boolean selected)
{
mSelected = selected;
}
@XmlTransient
public ChannelType getChannelType()
{
return mChannelType;
}
channel.setSourceConfiguration( SourceConfigFactory.copy( mSourceConfiguration ) );
return channel;
}
/**
* Unique identifier for this channel. Value is transient (not persisted).
*/
@XmlTransient
public int getChannelID()
{
return mChannelID;
}
/**
* Indicates if this channel has been selected for prioritized audio output
* and display of processing chain products
*/
@XmlTransient
public boolean isSelected()
{
return mSelected;
}
/**
* Sets selection status of the channel.
*
* NOTE: this method is package private so that only the ChannelSelectionManager
* can toggle the selection state. Use the channel model to broadcast a
* request select/deselect channel event.
*
* @param selected
*/
void setSelected( boolean selected )
{
mSelected = selected;
}
@XmlTransient
public ChannelType getChannelType()
{
return mChannelType;
}
/** /**
* Returns the owning system for this channel. * Returns the owning system for this channel.
*/ */
@XmlAttribute( name = "system" ) @XmlAttribute(name = "system")
public String getSystem() public String getSystem()
{ {
return mSystem; return mSystem;
} }
public void setSystem( String system )
{
mSystem = system;
}
public boolean hasSystem() public void setSystem(String system)
{ {
return mSystem != null; mSystem = system;
} }
/** public boolean hasSystem()
* Returns the owning site for this channel. {
*/ return mSystem != null;
@XmlAttribute( name = "site" ) }
public String getSite()
{ /**
return mSite; * Returns the owning site for this channel.
} */
@XmlAttribute(name = "site")
public void setSite( String site ) public String getSite()
{ {
mSite = site; return mSite;
} }
public boolean hasSite() public void setSite(String site)
{ {
return mSite != null; mSite = site;
} }
/** public boolean hasSite()
* Returns the name of this channel. {
*/ return mSite != null;
@XmlAttribute( name = "name" ) }
public String getName()
{ /**
return mName; * Returns the name of this channel.
} */
@XmlAttribute(name = "name")
/** public String getName()
* Sets the name of this channel. {
*/ return mName;
public void setName( String name ) }
{
mName = name; /**
} * Sets the name of this channel.
*/
public void setName(String name)
{
mName = name;
}
/** /**
* Default display string for this channel: SYSTEM_SITE_NAME * Default display string for this channel: SYSTEM_SITE_NAME
*/ */
public String toString() public String toString()
{ {
StringBuilder sb = new StringBuilder(); StringBuilder sb = new StringBuilder();
sb.append( hasSystem() ? getSystem() : "SYSTEM" ); sb.append(hasSystem() ? getSystem() : "SYSTEM");
sb.append( "_" ); sb.append("_");
sb.append( hasSite() ? getSite() : "SITE" ); sb.append(hasSite() ? getSite() : "SITE");
sb.append( "_" ); sb.append("_");
sb.append( getName() ); sb.append(getName());
return sb.toString();
}
/** return sb.toString();
* Indicates if this channel is enabled for processing. }
*/
@XmlAttribute( name = "enabled" )
public boolean getEnabled()
{
return mEnabled;
}
/** /**
* Indicates if this channel will automatically start processing on * Indicates if this channel is enabled for processing.
* application startup or if this channel is currently processing after */
* application startup. @XmlAttribute(name = "enabled")
* public boolean getEnabled()
* Note: this method is package private and is intended to be managed {
* by a co-package central processing manager return mEnabled;
* }
* @see controller.ChannelProcessingManager
*/
void setEnabled( boolean enabled )
{
mEnabled = enabled;
}
/** /**
* Returns the alias list that is used by this channel for looking up alias * Indicates if this channel will automatically start processing on
* values for the various identifiers produced by the decoder(s). * application startup or if this channel is currently processing after
*/ * application startup.
@XmlElement( name = "alias_list_name" ) *
public String getAliasListName() * Note: this method is package private and is intended to be managed
{ * by a co-package central processing manager
return mAliasListName; *
} * @see controller.channel.ChannelProcessingManager
*/
void setEnabled(boolean enabled)
{
mEnabled = enabled;
}
/** /**
* Sets the alias list to be used for looking up the alias values for the * Returns the alias list that is used by this channel for looking up alias
* various identifiers produced by the decoder * values for the various identifiers produced by the decoder(s).
*/ */
public void setAliasListName( String name ) @XmlElement(name = "alias_list_name")
{ public String getAliasListName()
mAliasListName = name; {
} return mAliasListName;
}
/** /**
* Gets the primary decoder configuration used by this channel * Sets the alias list to be used for looking up the alias values for the
*/ * various identifiers produced by the decoder
@XmlElement( name = "decode_configuration" ) */
public DecodeConfiguration getDecodeConfiguration() public void setAliasListName(String name)
{ {
return mDecodeConfiguration; mAliasListName = name;
} }
/** /**
* Sets the primary decoder configuration used by this channel * Gets the primary decoder configuration used by this channel
*/ */
public void setDecodeConfiguration( DecodeConfiguration config ) @XmlElement(name = "decode_configuration")
{ public DecodeConfiguration getDecodeConfiguration()
mDecodeConfiguration = config; {
} return mDecodeConfiguration;
}
/** /**
* Gets the aux decoder configuration used by this channel. Aux decoders * Sets the primary decoder configuration used by this channel
* operate on demodulated audio */
*/ public void setDecodeConfiguration(DecodeConfiguration config)
@XmlElement( name = "aux_decode_configuration" ) {
public AuxDecodeConfiguration getAuxDecodeConfiguration() mDecodeConfiguration = config;
{ }
return mAuxDecodeConfiguration;
}
/** /**
* Sets the decoder configuration used by this channel * Gets the aux decoder configuration used by this channel. Aux decoders
*/ * operate on demodulated audio
public void setAuxDecodeConfiguration( AuxDecodeConfiguration config ) */
{ @XmlElement(name = "aux_decode_configuration")
mAuxDecodeConfiguration = config; public AuxDecodeConfiguration getAuxDecodeConfiguration()
} {
return mAuxDecodeConfiguration;
}
/** /**
* Returns the source configuration for this channel. A channel source * Sets the decoder configuration used by this channel
* identifies where the processing chain will get source sample data from. */
*/ public void setAuxDecodeConfiguration(AuxDecodeConfiguration config)
@XmlElement( name = "source_configuration" ) {
public SourceConfiguration getSourceConfiguration() mAuxDecodeConfiguration = config;
{ }
return mSourceConfiguration;
}
/** /**
* Sets the source configuration for this channel. * Returns the source configuration for this channel. A channel source
*/ * identifies where the processing chain will get source sample data from.
public void setSourceConfiguration( SourceConfiguration config ) */
{ @XmlElement(name = "source_configuration")
mSourceConfiguration = config; public SourceConfiguration getSourceConfiguration()
{
return mSourceConfiguration;
}
//Clear the tune channel object so that it can be recreated if the /**
//source configuration changes * Sets the source configuration for this channel.
mTunerChannel = null; */
} public void setSourceConfiguration(SourceConfiguration config)
{
mSourceConfiguration = config;
/** //Clear the tune channel object so that it can be recreated if the
* Returns the event logger configuration for this channel. //source configuration changes
*/ mTunerChannel = null;
@XmlElement( name = "event_log_configuration" ) }
public EventLogConfiguration getEventLogConfiguration()
{
return mEventLogConfiguration;
}
/** /**
* Sets the event logger configuration for this channel. * Returns the event logger configuration for this channel.
*/ */
public void setEventLogConfiguration( EventLogConfiguration config ) @XmlElement(name = "event_log_configuration")
{ public EventLogConfiguration getEventLogConfiguration()
mEventLogConfiguration = config; {
} return mEventLogConfiguration;
}
/** /**
* Returns the recorder configuration for this channel. * Sets the event logger configuration for this channel.
*/ */
@XmlElement( name = "record_configuration" ) public void setEventLogConfiguration(EventLogConfiguration config)
public RecordConfiguration getRecordConfiguration() {
{ mEventLogConfiguration = config;
return mRecordConfiguration; }
}
/** /**
* Sets the recorder configuration for this channel. * Returns the recorder configuration for this channel.
*/ */
public void setRecordConfiguration( RecordConfiguration config ) @XmlElement(name = "record_configuration")
{ public RecordConfiguration getRecordConfiguration()
mRecordConfiguration = config; {
} return mRecordConfiguration;
}
/** /**
* Convenience method to construct a tuner channel object representing a * Sets the recorder configuration for this channel.
* tuner or recording source frequency and bandwidth that can be used by */
* application components for graphically representing this channel. public void setRecordConfiguration(RecordConfiguration config)
* {
* If the source configuration is not a tuner or recording, this method mRecordConfiguration = config;
* returns null. }
*/
@XmlTransient /**
* Convenience method to construct a tuner channel object representing a
* tuner or recording source frequency and bandwidth that can be used by
* application components for graphically representing this channel.
*
* If the source configuration is not a tuner or recording, this method
* returns null.
*/
@XmlTransient
public TunerChannel getTunerChannel() public TunerChannel getTunerChannel()
{ {
if( mTunerChannel == null ) if(mTunerChannel == null)
{ {
if( mSourceConfiguration.getSourceType() == SourceType.TUNER ) if(mSourceConfiguration.getSourceType() == SourceType.TUNER)
{ {
SourceConfigTuner config = (SourceConfigTuner)mSourceConfiguration; SourceConfigTuner config = (SourceConfigTuner)mSourceConfiguration;
mTunerChannel = new TunerChannel( Type.LOCKED, config.getFrequency(), mTunerChannel = new TunerChannel(Type.LOCKED, config.getFrequency(),
mDecodeConfiguration.getDecoderType().getChannelBandwidth() ); mDecodeConfiguration.getDecoderType().getChannelBandwidth());
} }
else if( mSourceConfiguration.getSourceType() == SourceType.RECORDING ) else if(mSourceConfiguration.getSourceType() == SourceType.RECORDING)
{ {
SourceConfigRecording config = SourceConfigRecording config =
(SourceConfigRecording)mSourceConfiguration; (SourceConfigRecording)mSourceConfiguration;
mTunerChannel = new TunerChannel(Type.LOCKED, config.getFrequency(),
mDecodeConfiguration.getDecoderType().getChannelBandwidth());
}
}
mTunerChannel = new TunerChannel( Type.LOCKED, config.getFrequency(),
mDecodeConfiguration.getDecoderType().getChannelBandwidth() );
}
}
return mTunerChannel; return mTunerChannel;
} }
/** /**
* Convenience method to indicate if any part of this channel is contained * Convenience method to indicate if any part of this channel is contained
* within the minimum and maximum frequency values. * within the minimum and maximum frequency values.
*/ */
public boolean isWithin( long minimum, long maximum ) public boolean isWithin(long minimum, long maximum)
{ {
TunerChannel tunerChannel = getTunerChannel(); TunerChannel tunerChannel = getTunerChannel();
return tunerChannel != null &&
tunerChannel.overlaps( minimum, maximum );
}
/**
* Convenience method to make the current channel frequency correction value
* available for use outside of a constructed processing chain while maintaining
* linkage to the source channel. This hack allows the correction value to
* be used to visually show in the spectral display.
*/
@Override
public void frequencyChanged( FrequencyChangeEvent event )
{
if( event.getEvent() == Event.NOTIFICATION_CHANNEL_FREQUENCY_CORRECTION_CHANGE )
{
mChannelFrequencyCorrection = event.getValue().intValue();
}
}
@XmlTransient return tunerChannel != null && tunerChannel.overlaps(minimum, maximum);
public int getChannelFrequencyCorrection() }
{
return mChannelFrequencyCorrection; /**
} * Convenience method to make the current channel frequency correction value
* available for use outside of a constructed processing chain while maintaining
* linkage to the source channel. This hack allows the correction value to
* be used to visually show in the spectral display.
*/
@Override
public void receive(FrequencyChangeEvent event)
{
if(event.getEvent() == Event.NOTIFICATION_CHANNEL_FREQUENCY_CORRECTION_CHANGE)
{
mChannelFrequencyCorrection = event.getValue().intValue();
}
}
/**
* Current channel frequency correction value (when this channel is processing)
* @return
*/
@XmlTransient
public int getChannelFrequencyCorrection()
{
return mChannelFrequencyCorrection;
}
/**
* Resets frequency correction value to 0
*/
public void resetFrequencyCorrection()
{
mChannelFrequencyCorrection = 0;
}
} }

View File

@@ -231,7 +231,7 @@ public class ChannelProcessingManager implements ChannelEventListener
if(processingChain == null) if(processingChain == null)
{ {
processingChain = new ProcessingChain(channel.getName(), channel.getChannelType()); processingChain = new ProcessingChain(channel.getChannelType());
/* Register global listeners */ /* Register global listeners */
for(Listener<AudioPacket> listener : mAudioPacketListeners) for(Listener<AudioPacket> listener : mAudioPacketListeners)
@@ -244,6 +244,9 @@ public class ChannelProcessingManager implements ChannelEventListener
processingChain.addMessageListener(listener); processingChain.addMessageListener(listener);
} }
//Register channel to receive frequency correction events to show in the spectral display (hack!)
processingChain.addFrequencyChangeListener(channel);
/* Processing Modules */ /* Processing Modules */
List<Module> modules = DecoderFactory.getModules(mChannelModel, mChannelMapModel, this, List<Module> modules = DecoderFactory.getModules(mChannelModel, mChannelMapModel, this,
mAliasModel, channel, processingChain.getChannelState().getMutableMetadata()); mAliasModel, channel, processingChain.getChannelState().getMutableMetadata());
@@ -347,6 +350,9 @@ public class ChannelProcessingManager implements ChannelEventListener
processingChain.removeRecordingModules(); processingChain.removeRecordingModules();
//Deregister channel from receive frequency correction events to show in the spectral display (hack!)
processingChain.removeFrequencyChangeListener(channel);
channel.resetFrequencyCorrection();
mChannelModel.broadcast(new ChannelEvent(channel, Event.NOTIFICATION_PROCESSING_STOP)); mChannelModel.broadcast(new ChannelEvent(channel, Event.NOTIFICATION_PROCESSING_STOP));

View File

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

View File

@@ -1,65 +1,82 @@
/*******************************************************************************
* 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 instrument; package instrument;
import controller.channel.Channel.ChannelType;
import instrument.tap.Tap; import instrument.tap.Tap;
import instrument.tap.TapGroup; import instrument.tap.TapGroup;
import module.Module;
import module.ProcessingChain;
import source.Source;
import java.util.ArrayList; import java.util.ArrayList;
import java.util.List; import java.util.List;
import module.Module;
import module.ProcessingChain;
import source.Source;
import controller.channel.Channel.ChannelType;
public class InstrumentableProcessingChain extends ProcessingChain implements Instrumentable public class InstrumentableProcessingChain extends ProcessingChain implements Instrumentable
{ {
public InstrumentableProcessingChain() public InstrumentableProcessingChain()
{ {
super( "instrumented", ChannelType.STANDARD ); super(ChannelType.STANDARD);
} }
public void setSource( Source source ) throws IllegalStateException public void setSource(Source source) throws IllegalStateException
{ {
mSource = source; mSource = source;
} }
@Override @Override
public List<TapGroup> getTapGroups() public List<TapGroup> getTapGroups()
{ {
List<TapGroup> groups = new ArrayList<>(); List<TapGroup> groups = new ArrayList<>();
for( Module module: getModules() )
{
if( module instanceof Instrumentable )
{
groups.addAll( ((Instrumentable)module).getTapGroups() );
}
}
return groups;
}
@Override for(Module module : getModules())
public void registerTap( Tap tap ) {
{ if(module instanceof Instrumentable)
for( Module module: getModules() ) {
{ groups.addAll(((Instrumentable)module).getTapGroups());
if( module instanceof Instrumentable ) }
{ }
((Instrumentable)module).registerTap( tap );
}
}
}
@Override return groups;
public void unregisterTap( Tap tap ) }
{
for( Module module: getModules() ) @Override
{ public void registerTap(Tap tap)
if( module instanceof Instrumentable ) {
{ for(Module module : getModules())
((Instrumentable)module).unregisterTap( tap ); {
} if(module instanceof Instrumentable)
} {
} ((Instrumentable)module).registerTap(tap);
}
}
}
@Override
public void unregisterTap(Tap tap)
{
for(Module module : getModules())
{
if(module instanceof Instrumentable)
{
((Instrumentable)module).unregisterTap(tap);
}
}
}
} }

View File

@@ -108,7 +108,6 @@ public class ProcessingChain implements IChannelEventListener
private Broadcaster<RealBuffer> mUnFilteredRealBufferBroadcaster = new Broadcaster<>(); private Broadcaster<RealBuffer> mUnFilteredRealBufferBroadcaster = new Broadcaster<>();
private Broadcaster<SquelchState> mSquelchStateBroadcaster = new Broadcaster<>(); private Broadcaster<SquelchState> mSquelchStateBroadcaster = new Broadcaster<>();
private String mName;
private AtomicBoolean mRunning = new AtomicBoolean(); private AtomicBoolean mRunning = new AtomicBoolean();
protected Source mSource; protected Source mSource;
@@ -120,13 +119,10 @@ public class ProcessingChain implements IChannelEventListener
/** /**
* Creates a processing chain for managing a set of modules * Creates a processing chain for managing a set of modules
* *
* @param name for thread pool naming each thread
* @param channelType * @param channelType
*/ */
public ProcessingChain(String name, ChannelType channelType) public ProcessingChain(ChannelType channelType)
{ {
mName = name;
mChannelState = new ChannelState(channelType); mChannelState = new ChannelState(channelType);
addModule(mChannelState); addModule(mChannelState);
@@ -211,6 +207,8 @@ public class ProcessingChain implements IChannelEventListener
} }
mSource = source; mSource = source;
addModule(mSource);
} }
/** /**
@@ -557,25 +555,6 @@ public class ProcessingChain implements IChannelEventListener
+ "sample type - cannot start processing chain"); + "sample type - cannot start processing chain");
} }
/* If this is a tuner source, broadcast the frequency to all
* of the decoder state's and start the samples flowing */
if(mSource instanceof TunerChannelSource)
{
TunerChannelSource tcs = (TunerChannelSource) mSource;
try
{
long frequency = tcs.getFrequency();
mDecoderStateEventBroadcaster.broadcast(
new DecoderStateEvent(this, Event.SOURCE_FREQUENCY, State.IDLE, frequency));
}
catch(SourceException e)
{
mLog.error("Error getting frequency from tuner channel source", e);
}
}
/* Start each of the modules */ /* Start each of the modules */
for(Module module : mModules) for(Module module : mModules)
{ {
@@ -588,8 +567,6 @@ public class ProcessingChain implements IChannelEventListener
mLog.error("Error starting module", e); mLog.error("Error starting module", e);
} }
} }
mSource.start(ThreadPool.SCHEDULED);
} }
else else
{ {
@@ -606,9 +583,16 @@ public class ProcessingChain implements IChannelEventListener
{ {
if(mRunning.compareAndSet(true, false)) if(mRunning.compareAndSet(true, false))
{ {
/* Stop each of the modules */
for(Module module : mModules)
{
module.stop();
}
if(mSource != null) if(mSource != null)
{ {
mSource.stop(); removeModule(mSource);
mSource.setOverflowListener(null); mSource.setOverflowListener(null);
switch(mSource.getSampleType()) switch(mSource.getSampleType())
@@ -629,11 +613,6 @@ public class ProcessingChain implements IChannelEventListener
mSource = null; mSource = null;
} }
/* Stop each of the modules */
for(Module module : mModules)
{
module.stop();
}
} }
} }
@@ -760,6 +739,24 @@ public class ProcessingChain implements IChannelEventListener
mMessageBroadcaster.removeListener(listener); mMessageBroadcaster.removeListener(listener);
} }
/**
* Adds the listener to receive frequency change events from the processing chain
* @param listener to receive events
*/
public void addFrequencyChangeListener(Listener<FrequencyChangeEvent> listener)
{
mFrequencyChangeEventBroadcaster.addListener(listener);
}
/**
* Removes the listener from receiving frequency change events
* @param listener to remove
*/
public void removeFrequencyChangeListener(Listener<FrequencyChangeEvent> listener)
{
mFrequencyChangeEventBroadcaster.removeListener(listener);
}
/** /**
* Adds the listener to receive call events from all modules. * Adds the listener to receive call events from all modules.
*/ */

View File

@@ -9,21 +9,21 @@ import instrument.tap.stream.FloatTap;
import java.util.ArrayList; import java.util.ArrayList;
import java.util.List; import java.util.List;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import sample.Listener; import sample.Listener;
import sample.real.RealBuffer; import sample.real.RealBuffer;
import sample.real.RealSampleListener; import sample.real.RealSampleListener;
import sample.real.RealSampleProvider; import sample.real.RealSampleProvider;
import source.tuner.TunerChannelSource;
import source.tuner.frequency.FrequencyChangeEvent; import source.tuner.frequency.FrequencyChangeEvent;
import source.tuner.frequency.FrequencyChangeEvent.Event; import source.tuner.frequency.FrequencyChangeEvent.Event;
import source.tuner.frequency.IFrequencyChangeListener; import source.tuner.frequency.IFrequencyChangeListener;
import dsp.gain.DirectGainControl; import dsp.gain.DirectGainControl;
public class C4FMSymbolFilter implements Listener<RealBuffer>, public class C4FMSymbolFilter implements Listener<RealBuffer>, IFrequencyChangeListener, RealSampleProvider, Instrumentable
IFrequencyChangeListener,
RealSampleProvider,
Instrumentable
{ {
private static final float TAPS[][] = 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 { 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 { -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
@@ -198,8 +198,7 @@ public class C4FMSymbolFilter implements Listener<RealBuffer>,
private RealSampleListener mListener; private RealSampleListener mListener;
private DirectGainControl mGainController = private DirectGainControl mGainController = new DirectGainControl( 15.0f, 0.1f, 35.0f, 0.3f );
new DirectGainControl( 15.0f, 0.1f, 35.0f, 0.3f );
private int mFrequencyAdjustmentRequested = 0; private int mFrequencyAdjustmentRequested = 0;
private int mFrequencyCorrection = 0; private int mFrequencyCorrection = 0;
@@ -258,9 +257,8 @@ public class C4FMSymbolFilter implements Listener<RealBuffer>,
correction = -mFrequencyCorrectionMaximum; correction = -mFrequencyCorrectionMaximum;
} }
broadcast( new FrequencyChangeEvent( broadcast( new FrequencyChangeEvent(Event.REQUEST_CHANNEL_FREQUENCY_CORRECTION_CHANGE, correction ) );
Event.REQUEST_CHANNEL_FREQUENCY_CORRECTION_CHANGE, correction ) );
mFrequencyAdjustmentRequested = 0; mFrequencyAdjustmentRequested = 0;
} }
} }
@@ -452,6 +450,10 @@ public class C4FMSymbolFilter implements Listener<RealBuffer>,
} }
} }
/**
* Broadcasts a frequency change event to the registered listener
* @param event
*/
public void broadcast( FrequencyChangeEvent event ) public void broadcast( FrequencyChangeEvent event )
{ {
if( mFrequencyChangeListener != null ) if( mFrequencyChangeListener != null )
@@ -459,7 +461,11 @@ public class C4FMSymbolFilter implements Listener<RealBuffer>,
mFrequencyChangeListener.receive( event ); mFrequencyChangeListener.receive( event );
} }
} }
/**
* Sets the listener for frequency change events.
* @param listener
*/
public void setFrequencyChangeListener( Listener<FrequencyChangeEvent> listener ) public void setFrequencyChangeListener( Listener<FrequencyChangeEvent> listener )
{ {
mFrequencyChangeListener = listener; mFrequencyChangeListener = listener;

View File

@@ -4,8 +4,10 @@ import alias.AliasList;
import instrument.Instrumentable; import instrument.Instrumentable;
import module.decode.Decoder; import module.decode.Decoder;
import module.decode.DecoderType; import module.decode.DecoderType;
import source.tuner.frequency.IFrequencyChangeListener;
import source.tuner.frequency.IFrequencyChangeProvider;
public abstract class P25Decoder extends Decoder implements Instrumentable public abstract class P25Decoder extends Decoder implements IFrequencyChangeListener, IFrequencyChangeProvider, Instrumentable
{ {
private P25MessageProcessor mMessageProcessor; private P25MessageProcessor mMessageProcessor;

View File

@@ -689,8 +689,7 @@ public class P25MessageFramer implements Listener<Dibit>
private LSMDemodulator mDemodulator; private LSMDemodulator mDemodulator;
private double mCorrection; private double mCorrection;
public CostasPhaseErrorDetector( FrameSync frameSync, public CostasPhaseErrorDetector( FrameSync frameSync, LSMDemodulator demodulator, double correction )
LSMDemodulator demodulator, double correction )
{ {
super( frameSync.getSync() ); super( frameSync.getSync() );

View File

@@ -1,19 +1,20 @@
/******************************************************************************* /*******************************************************************************
* SDR Trunk * sdrtrunk
* Copyright (C) 2014,2015 Dennis Sheirer * Copyright (C) 2014-2017 Dennis Sheirer
* *
* This program is free software: you can redistribute it and/or modify * 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 * it under the terms of the GNU General Public License as published by
* the Free Software Foundation, either version 3 of the License, or * the Free Software Foundation, either version 3 of the License, or
* (at your option) any later version. * (at your option) any later version.
* *
* This program is distributed in the hope that it will be useful, * This program is distributed in the hope that it will be useful,
* but WITHOUT ANY WARRANTY; without even the implied warranty of * but WITHOUT ANY WARRANTY; without even the implied warranty of
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
* GNU General Public License for more details. * GNU General Public License for more details.
* *
* You should have received a copy of the GNU General Public License * You should have received a copy of the GNU General Public License
* along with this program. If not, see <http://www.gnu.org/licenses/> * along with this program. If not, see <http://www.gnu.org/licenses/>
*
******************************************************************************/ ******************************************************************************/
package module.decode.p25; package module.decode.p25;
@@ -41,8 +42,7 @@ import dsp.filter.FilterFactory;
import dsp.filter.Window.WindowType; import dsp.filter.Window.WindowType;
import dsp.filter.fir.real.RealFIRFilter_RB_RB; import dsp.filter.fir.real.RealFIRFilter_RB_RB;
public class P25_C4FMDecoder extends P25Decoder public class P25_C4FMDecoder extends P25Decoder implements IFilteredRealBufferListener
implements IFrequencyChangeListener, IFrequencyChangeProvider,IFilteredRealBufferListener
{ {
private final static Logger mLog = LoggerFactory.getLogger( P25_C4FMDecoder.class ); private final static Logger mLog = LoggerFactory.getLogger( P25_C4FMDecoder.class );

View File

@@ -42,6 +42,7 @@ import dsp.filter.fir.complex.ComplexFIRFilter_CB_CB;
import dsp.gain.ComplexFeedForwardGainControl; import dsp.gain.ComplexFeedForwardGainControl;
import dsp.psk.LSMDemodulator; import dsp.psk.LSMDemodulator;
import dsp.psk.QPSKPolarSlicer; import dsp.psk.QPSKPolarSlicer;
import source.tuner.frequency.FrequencyChangeEvent;
public class P25_LSMDecoder extends P25Decoder implements IComplexBufferListener public class P25_LSMDecoder extends P25Decoder implements IComplexBufferListener
{ {
@@ -56,8 +57,7 @@ public class P25_LSMDecoder extends P25Decoder implements IComplexBufferListener
private List<TapGroup> mAvailableTaps; private List<TapGroup> mAvailableTaps;
private ComplexFIRFilter_CB_CB mBasebandFilter; private ComplexFIRFilter_CB_CB mBasebandFilter;
private ComplexBufferToStreamConverter mStreamConverter = private ComplexBufferToStreamConverter mStreamConverter = new ComplexBufferToStreamConverter();
new ComplexBufferToStreamConverter();
private ComplexFeedForwardGainControl mAGC = private ComplexFeedForwardGainControl mAGC =
new ComplexFeedForwardGainControl( 32 ); new ComplexFeedForwardGainControl( 32 );
private LSMDemodulator mLSMDemodulator = new LSMDemodulator(); private LSMDemodulator mLSMDemodulator = new LSMDemodulator();
@@ -107,8 +107,31 @@ public class P25_LSMDecoder extends P25Decoder implements IComplexBufferListener
mMessageFramer.dispose(); mMessageFramer.dispose();
mMessageFramer = null; mMessageFramer = null;
} }
@Override @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() public Listener<ComplexBuffer> getComplexBufferListener()
{ {
return mBasebandFilter; return mBasebandFilter;

View File

@@ -58,6 +58,11 @@ public class Broadcaster<T> implements Listener<T>
public void addListener( Listener<T> listener ) public void addListener( Listener<T> listener )
{ {
if(listener == null)
{
throw new IllegalArgumentException("Listener cannot be null");
}
mListeners.add( listener ); mListeners.add( listener );
} }

View File

@@ -20,11 +20,13 @@ package source;
import module.Module; import module.Module;
import sample.SampleType; import sample.SampleType;
import sample.real.IOverflowListener; import sample.real.IOverflowListener;
import source.tuner.frequency.IFrequencyChangeListener;
import source.tuner.frequency.IFrequencyChangeProvider;
/** /**
* Abstract class to define the minimum functionality of a sample data provider. * Abstract class to define the minimum functionality of a sample data provider.
*/ */
public abstract class Source extends Module public abstract class Source extends Module implements IFrequencyChangeListener, IFrequencyChangeProvider
{ {
protected SampleType mSampleType; protected SampleType mSampleType;
protected IOverflowListener mOverflowListener; protected IOverflowListener mOverflowListener;

View File

@@ -36,11 +36,11 @@ import sample.real.RealBuffer;
import source.RealSource; import source.RealSource;
import source.SourceException; import source.SourceException;
import dsp.gain.AutomaticGainControl; import dsp.gain.AutomaticGainControl;
import source.tuner.frequency.FrequencyChangeEvent;
public class RealMixerSource extends RealSource public class RealMixerSource extends RealSource
{ {
private final static Logger mLog = private final static Logger mLog = LoggerFactory.getLogger( RealMixerSource.class );
LoggerFactory.getLogger( RealMixerSource.class );
private long mFrequency = 0; private long mFrequency = 0;
private int mBufferSize = 16384; private int mBufferSize = 16384;
@@ -48,13 +48,11 @@ public class RealMixerSource extends RealSource
private BufferReader mBufferReader = new BufferReader(); private BufferReader mBufferReader = new BufferReader();
private TargetDataLine mTargetDataLine; private TargetDataLine mTargetDataLine;
private AudioFormat mAudioFormat; private AudioFormat mAudioFormat;
private String mDisplayName;
private int mBytesPerFrame = 0; private int mBytesPerFrame = 0;
private ISampleAdapter mSampleAdapter; private ISampleAdapter mSampleAdapter;
private AutomaticGainControl mAGC = new AutomaticGainControl(); private AutomaticGainControl mAGC = new AutomaticGainControl();
CopyOnWriteArrayList<Listener<RealBuffer>> mSampleListeners = CopyOnWriteArrayList<Listener<RealBuffer>> mSampleListeners = new CopyOnWriteArrayList<Listener<RealBuffer>>();
new CopyOnWriteArrayList<Listener<RealBuffer>>();
/** /**
* Real Mixer Source - constructs a reader on the mixer/sound card target * Real Mixer Source - constructs a reader on the mixer/sound card target
@@ -80,8 +78,27 @@ public class RealMixerSource extends RealSource
mAudioFormat = format; mAudioFormat = format;
mSampleAdapter = sampleAdapter; mSampleAdapter = sampleAdapter;
} }
@Override @Override
public void setFrequencyChangeListener(Listener<FrequencyChangeEvent> listener)
{
//Not implemented
}
@Override
public void removeFrequencyChangeListener()
{
//Not implemented
}
@Override
public Listener<FrequencyChangeEvent> getFrequencyChangeListener()
{
//Not implemented
return null;
}
@Override
public void reset() public void reset()
{ {
} }
@@ -133,23 +150,15 @@ public class RealMixerSource extends RealSource
* buffer, otherwise send him a copy of the buffer */ * buffer, otherwise send him a copy of the buffer */
if( it.hasNext() ) if( it.hasNext() )
{ {
// next.receive( mAGC.process( samples.copyOf() ) );
next.receive( samples.copyOf() ); next.receive( samples.copyOf() );
} }
else else
{ {
// next.receive( mAGC.process( samples ) );
next.receive( samples ); next.receive( samples );
} }
} }
} }
@Override
public String toString()
{
return mDisplayName;
}
public int getSampleRate() public int getSampleRate()
{ {
if( mTargetDataLine != null ) if( mTargetDataLine != null )

View File

@@ -34,7 +34,9 @@ import source.ComplexSource;
import source.SourceException; import source.SourceException;
import source.tuner.frequency.FrequencyChangeEvent; import source.tuner.frequency.FrequencyChangeEvent;
import source.tuner.frequency.FrequencyChangeEvent.Event; import source.tuner.frequency.FrequencyChangeEvent.Event;
import source.tuner.frequency.IFrequencyChangeListener;
import source.tuner.frequency.IFrequencyChangeProcessor; import source.tuner.frequency.IFrequencyChangeProcessor;
import source.tuner.frequency.IFrequencyChangeProvider;
import java.util.ArrayList; import java.util.ArrayList;
import java.util.List; import java.util.List;
@@ -65,6 +67,7 @@ public class TunerChannelSource extends ComplexSource implements IFrequencyChang
private ComplexPrimeCICDecimate mDecimationFilter; private ComplexPrimeCICDecimate mDecimationFilter;
private Listener<ComplexBuffer> mListener; private Listener<ComplexBuffer> mListener;
private IFrequencyChangeProcessor mFrequencyChangeProcessor; private IFrequencyChangeProcessor mFrequencyChangeProcessor;
private DownstreamProcessor mDownstreamFrequencyEventProcessor = new DownstreamProcessor();
private ScheduledFuture<?> mTaskHandle; private ScheduledFuture<?> mTaskHandle;
private long mTunerFrequency = 0; private long mTunerFrequency = 0;
@@ -75,7 +78,6 @@ public class TunerChannelSource extends ComplexSource implements IFrequencyChang
private AtomicBoolean mRunning = new AtomicBoolean(); private AtomicBoolean mRunning = new AtomicBoolean();
private boolean mExpended = false; private boolean mExpended = false;
private int mCount;
/** /**
* Provides a Digital Drop Channel (DDC) to decimate the IQ output from a * Provides a Digital Drop Channel (DDC) to decimate the IQ output from a
@@ -109,7 +111,8 @@ public class TunerChannelSource extends ComplexSource implements IFrequencyChang
mMixer = new Oscillator(frequencyOffset, mTuner.getTunerController().getSampleRate()); mMixer = new Oscillator(frequencyOffset, mTuner.getTunerController().getSampleRate());
/* Fire a sample rate change event to setup the decimation chain */ /* Fire a sample rate change event to setup the decimation chain */
frequencyChanged(new FrequencyChangeEvent(Event.NOTIFICATION_SAMPLE_RATE_CHANGE, mTuner.getTunerController().getSampleRate())); frequencyChanged(new FrequencyChangeEvent(Event.NOTIFICATION_SAMPLE_RATE_CHANGE,
mTuner.getTunerController().getSampleRate()));
} }
/** /**
@@ -131,6 +134,10 @@ public class TunerChannelSource extends ComplexSource implements IFrequencyChang
if(mRunning.compareAndSet(false, true)) 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 //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 //to the inbound periodicity of 20 ms, to attempt to avoid thread queue contention
mTaskHandle = executor.scheduleAtFixedRate(mDecimationProcessor, 0, 9, TimeUnit.MILLISECONDS); mTaskHandle = executor.scheduleAtFixedRate(mDecimationProcessor, 0, 9, TimeUnit.MILLISECONDS);
@@ -184,6 +191,20 @@ public class TunerChannelSource extends ComplexSource implements IFrequencyChang
} }
} }
/**
* 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() public Tuner getTuner()
{ {
return mTuner; return mTuner;
@@ -242,50 +263,49 @@ public class TunerChannelSource extends ComplexSource implements IFrequencyChang
case NOTIFICATION_FREQUENCY_CHANGE: case NOTIFICATION_FREQUENCY_CHANGE:
mTunerFrequency = event.getValue().longValue(); mTunerFrequency = event.getValue().longValue();
updateMixerFrequencyOffset(); updateMixerFrequencyOffset();
break;
case REQUEST_CHANNEL_FREQUENCY_CORRECTION_CHANGE:
mChannelFrequencyCorrection = event.getValue().intValue();
updateMixerFrequencyOffset(); //Reset frequency correction so that downstream components can recalculate the value
setFrequencyCorrection(0);
if(mFrequencyChangeProcessor != null)
{
mFrequencyChangeProcessor.frequencyChanged(new FrequencyChangeEvent(
Event.NOTIFICATION_CHANNEL_FREQUENCY_CORRECTION_CHANGE, mChannelFrequencyCorrection));
}
break; break;
case NOTIFICATION_SAMPLE_RATE_CHANGE: case NOTIFICATION_SAMPLE_RATE_CHANGE:
int sampleRate = event.getValue().intValue(); int sampleRate = event.getValue().intValue();
setSampleRate(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;
}
break; break;
default: default:
break; 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, * Calculates the local mixer frequency offset from the tuned frequency,
* channel's requested frequency, and channel frequency correction. * channel's requested frequency, and channel frequency correction.
*/ */
private void updateMixerFrequencyOffset() private void updateMixerFrequencyOffset()
{ {
long offset = mTunerFrequency - long offset = mTunerFrequency - mTunerChannel.getFrequency() - mChannelFrequencyCorrection;
mTunerChannel.getFrequency() -
mChannelFrequencyCorrection;
mMixer.setFrequency(offset); mMixer.setFrequency(offset);
} }
@@ -299,6 +319,140 @@ public class TunerChannelSource extends ComplexSource implements IFrequencyChang
return mTunerChannel.getFrequency(); 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();
}
/**
* 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 * Decimates an inbound buffer of I/Q samples from the source down to the
* standard 48000 channel sample rate * standard 48000 channel sample rate

View File

@@ -1,9 +1,30 @@
/*******************************************************************************
* 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 source.tuner.frequency; package source.tuner.frequency;
import sample.Listener; import sample.Listener;
/**
* Provider of Frequency Change Events
*/
public interface IFrequencyChangeProvider public interface IFrequencyChangeProvider
{ {
public void setFrequencyChangeListener( Listener<FrequencyChangeEvent> listener ); void setFrequencyChangeListener( Listener<FrequencyChangeEvent> listener );
public void removeFrequencyChangeListener(); void removeFrequencyChangeListener();
} }

View File

@@ -1,19 +1,20 @@
/******************************************************************************* /*******************************************************************************
* SDR Trunk * sdrtrunk
* Copyright (C) 2014,2015 Dennis Sheirer * Copyright (C) 2014-2017 Dennis Sheirer
* *
* This program is free software: you can redistribute it and/or modify * 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 * it under the terms of the GNU General Public License as published by
* the Free Software Foundation, either version 3 of the License, or * the Free Software Foundation, either version 3 of the License, or
* (at your option) any later version. * (at your option) any later version.
* *
* This program is distributed in the hope that it will be useful, * This program is distributed in the hope that it will be useful,
* but WITHOUT ANY WARRANTY; without even the implied warranty of * but WITHOUT ANY WARRANTY; without even the implied warranty of
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
* GNU General Public License for more details. * GNU General Public License for more details.
* *
* You should have received a copy of the GNU General Public License * You should have received a copy of the GNU General Public License
* along with this program. If not, see <http://www.gnu.org/licenses/> * along with this program. If not, see <http://www.gnu.org/licenses/>
*
******************************************************************************/ ******************************************************************************/
package source.wave; package source.wave;
@@ -36,6 +37,7 @@ import sample.complex.ComplexBuffer;
import source.ComplexSource; import source.ComplexSource;
import source.IControllableFileSource; import source.IControllableFileSource;
import source.IFrameLocationListener; import source.IFrameLocationListener;
import source.tuner.frequency.FrequencyChangeEvent;
public class ComplexWaveSource extends ComplexSource implements IControllableFileSource public class ComplexWaveSource extends ComplexSource implements IControllableFileSource
{ {
@@ -54,8 +56,27 @@ public class ComplexWaveSource extends ComplexSource implements IControllableFil
{ {
mFile = file; mFile = file;
} }
@Override @Override
public void setFrequencyChangeListener(Listener<FrequencyChangeEvent> listener)
{
//Not implemented
}
@Override
public void removeFrequencyChangeListener()
{
//Not implemented
}
@Override
public Listener<FrequencyChangeEvent> getFrequencyChangeListener()
{
//Not implemented
return null;
}
@Override
public void reset() public void reset()
{ {
stop(); stop();

View File

@@ -36,6 +36,7 @@ import sample.real.RealBuffer;
import source.IControllableFileSource; import source.IControllableFileSource;
import source.IFrameLocationListener; import source.IFrameLocationListener;
import source.RealSource; import source.RealSource;
import source.tuner.frequency.FrequencyChangeEvent;
public class RealWaveSource extends RealSource public class RealWaveSource extends RealSource
implements IControllableFileSource, AutoCloseable implements IControllableFileSource, AutoCloseable
@@ -55,8 +56,27 @@ public class RealWaveSource extends RealSource
{ {
mFile = file; mFile = file;
} }
@Override
public void setFrequencyChangeListener(Listener<FrequencyChangeEvent> listener)
{
//Not implemented
}
@Override
public void removeFrequencyChangeListener()
{
//Not implemented
}
@Override
public Listener<FrequencyChangeEvent> getFrequencyChangeListener()
{
//Not implemented
return null;
}
@Override @Override
public void reset() public void reset()
{ {

View File

@@ -50,6 +50,7 @@ public class OverlayPanel extends JPanel implements ChannelEventListener, IFrequ
private static final long serialVersionUID = 1L; private static final long serialVersionUID = 1L;
private final static Logger mLog = LoggerFactory.getLogger(OverlayPanel.class); private final static Logger mLog = LoggerFactory.getLogger(OverlayPanel.class);
private final DecimalFormat PPM_FORMATTER = new DecimalFormat( "#.0" );
private final static RenderingHints RENDERING_HINTS = new RenderingHints(RenderingHints.KEY_ANTIALIASING, private final static RenderingHints RENDERING_HINTS = new RenderingHints(RenderingHints.KEY_ANTIALIASING,
RenderingHints.VALUE_ANTIALIAS_ON); RenderingHints.VALUE_ANTIALIAS_ON);
@@ -187,7 +188,6 @@ public class OverlayPanel extends JPanel implements ChannelEventListener, IFrequ
* 6 64x Zoom * 6 64x Zoom
* *
* @param zoom level, 0 - 6. * @param zoom level, 0 - 6.
* @param offset into the FFT bins for start zoom window
*/ */
public void setZoom(int zoom) public void setZoom(int zoom)
{ {
@@ -412,20 +412,41 @@ public class OverlayPanel extends JPanel implements ChannelEventListener, IFrequ
} }
/** /**
* Draws a vertical line at the xaxis * Draws the Automatic Frequency Control (AFC) channel center offset
*/ */
private void drawAFC(Graphics2D graphics, double frequencyAxis, double errorAxis) private void drawAFC(Graphics2D graphics, double frequencyAxis, double errorAxis, double bandwidth,
int correction, long frequency)
{ {
double height = getSize().getHeight() - mSpectrumInset; double height = getSize().getHeight() - mSpectrumInset;
double lineHeight = height * 0.75d; double verticalAxisTop = height * 0.88d;
double verticalAxisBottom = height * 0.98d;
double halfBandwidth = bandwidth / 2.0;
double errorEdgeStart = errorAxis - halfBandwidth;
double errorEdgeStop = errorAxis + halfBandwidth;
graphics.setColor(Color.YELLOW); graphics.setColor(Color.YELLOW);
//Vertical frequency error line //Horizontal line connecting frequency and error line
graphics.draw(new Line2D.Double(errorAxis, lineHeight, errorAxis, height - 1.0d)); graphics.draw(new Line2D.Double(errorEdgeStart, verticalAxisBottom, errorEdgeStop, verticalAxisBottom));
//Horizontal line connecting frequency and error lines //Vertical band edge lines
graphics.draw(new Line2D.Double(frequencyAxis, lineHeight, errorAxis, lineHeight)); graphics.draw(new Line2D.Double(errorEdgeStart, verticalAxisTop, errorEdgeStart, verticalAxisBottom));
graphics.draw(new Line2D.Double(errorEdgeStop, verticalAxisTop, errorEdgeStop, verticalAxisBottom));
double ppm = (double)correction / ((double)frequency / 1E6d);
String label = "PPM " + PPM_FORMATTER.format(ppm) ;
FontMetrics fontMetrics = graphics.getFontMetrics(this.getFont());
Rectangle2D rect = fontMetrics.getStringBounds(label, graphics);
//Only render the correction value label if the spacing is large enough
if(rect.getWidth() <= bandwidth && rect.getHeight() * 5 <= height)
{
graphics.drawString(label, (float)(errorEdgeStart + 1.0), (float)(verticalAxisBottom - 2.0));
}
} }
/** /**
@@ -564,7 +585,8 @@ public class OverlayPanel extends JPanel implements ChannelEventListener, IFrequ
{ {
long error = frequency + correction; long error = frequency + correction;
drawAFC(graphics, frequencyAxis, getAxisFromFrequency(error)); drawAFC(graphics, frequencyAxis, getAxisFromFrequency(error), width, correction,
tunerChannel.getFrequency());
} }
} }
} }

View File

@@ -1,493 +1,483 @@
/******************************************************************************* /*******************************************************************************
* SDR Trunk * sdrtrunk
* Copyright (C) 2014,2015 Dennis Sheirer * Copyright (C) 2014-2017 Dennis Sheirer
* *
* This program is free software: you can redistribute it and/or modify * 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 * it under the terms of the GNU General Public License as published by
* the Free Software Foundation, either version 3 of the License, or * the Free Software Foundation, either version 3 of the License, or
* (at your option) any later version. * (at your option) any later version.
* *
* This program is distributed in the hope that it will be useful, * This program is distributed in the hope that it will be useful,
* but WITHOUT ANY WARRANTY; without even the implied warranty of * but WITHOUT ANY WARRANTY; without even the implied warranty of
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
* GNU General Public License for more details. * GNU General Public License for more details.
* *
* You should have received a copy of the GNU General Public License * You should have received a copy of the GNU General Public License
* along with this program. If not, see <http://www.gnu.org/licenses/> * along with this program. If not, see <http://www.gnu.org/licenses/>
*
******************************************************************************/ ******************************************************************************/
package spectrum; package spectrum;
import java.awt.Color;
import java.awt.Dimension;
import java.awt.GradientPaint;
import java.awt.Graphics;
import java.awt.Graphics2D;
import java.awt.Rectangle;
import java.awt.RenderingHints;
import java.awt.geom.GeneralPath;
import java.awt.geom.Line2D;
import java.util.Arrays;
import javax.swing.JPanel;
import org.apache.commons.lang3.Validate;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import settings.ColorSetting;
import settings.ColorSetting.ColorSettingName;
import settings.Setting;
import settings.SettingChangeListener;
import settings.SettingsManager;
import dsp.filter.smoothing.GaussianSmoothingFilter; import dsp.filter.smoothing.GaussianSmoothingFilter;
import dsp.filter.smoothing.NoSmoothingFilter; import dsp.filter.smoothing.NoSmoothingFilter;
import dsp.filter.smoothing.RectangularSmoothingFilter; import dsp.filter.smoothing.RectangularSmoothingFilter;
import dsp.filter.smoothing.SmoothingFilter; import dsp.filter.smoothing.SmoothingFilter;
import dsp.filter.smoothing.SmoothingFilter.SmoothingType; import dsp.filter.smoothing.SmoothingFilter.SmoothingType;
import dsp.filter.smoothing.TriangularSmoothingFilter; import dsp.filter.smoothing.TriangularSmoothingFilter;
import org.apache.commons.lang3.Validate;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import settings.ColorSetting;
import settings.ColorSetting.ColorSettingName;
import settings.Setting;
import settings.SettingChangeListener;
import settings.SettingsManager;
public class SpectrumPanel extends JPanel import javax.swing.*;
implements DFTResultsListener, import java.awt.*;
SettingChangeListener, import java.awt.geom.GeneralPath;
SpectralDisplayAdjuster import java.awt.geom.Line2D;
import java.util.Arrays;
public class SpectrumPanel extends JPanel implements DFTResultsListener, SettingChangeListener, SpectralDisplayAdjuster
{ {
private static final long serialVersionUID = 1L; private static final long serialVersionUID = 1L;
private final static Logger mLog = private final static Logger mLog = LoggerFactory.getLogger(SpectrumPanel.class);
LoggerFactory.getLogger( SpectrumPanel.class );
private static final RenderingHints RENDERING_HINTS =
new RenderingHints( RenderingHints.KEY_ANTIALIASING,
RenderingHints.VALUE_ANTIALIAS_ON );
static private static final RenderingHints RENDERING_HINTS =
{ new RenderingHints(RenderingHints.KEY_ANTIALIASING, RenderingHints.VALUE_ANTIALIAS_ON);
RENDERING_HINTS.put( RenderingHints.KEY_RENDERING,
RenderingHints.VALUE_RENDER_QUALITY );
}
private Color mColorSpectrumBackground;
private Color mColorSpectrumGradientTop;
private Color mColorSpectrumGradientBottom;
private Color mColorSpectrumLine;
//Defines the panel inset along the bottom for frequency display static
private float mSpectrumInset = 20.0f;
//Current DFT output bins in dB
private float[] mDisplayFFTBins = new float[ 1 ];
//Averaging across multiple DFT result sets
private int mAveraging = 4;
//Smoothing across bins in the same DFT result set
private SmoothingFilter mSmoothingFilter = new GaussianSmoothingFilter();
//Reference dB value set according to the source sample size
private float mDBScale;
private int mZoom = 0;
private int mZoomWindowOffset = 0;;
private SettingsManager mSettingsManager;
public SpectrumPanel( SettingsManager settingsManager )
{ {
mSettingsManager = settingsManager; RENDERING_HINTS.put(RenderingHints.KEY_RENDERING, RenderingHints.VALUE_RENDER_QUALITY);
if( mSettingsManager != null )
{
mSettingsManager.addListener( this );
}
setSampleSize( 16.0 );
mSmoothingFilter.setPointSize( SmoothingFilter.SMOOTHING_DEFAULT );
getColors();
setAveraging( mAveraging );
} }
public void dispose() private Color mColorSpectrumBackground;
{ private Color mColorSpectrumGradientTop;
if( mSettingsManager != null ) private Color mColorSpectrumGradientBottom;
{ private Color mColorSpectrumLine;
mSettingsManager.removeListener( this );
} //Defines the panel inset along the bottom for frequency display
private float mSpectrumInset = 20.0f;
mSettingsManager = null;
} //Current DFT output bins in dB
private float[] mDisplayFFTBins = new float[1];
//Averaging across multiple DFT result sets
private int mAveraging = 4;
//Smoothing across bins in the same DFT result set
private SmoothingFilter mSmoothingFilter = new GaussianSmoothingFilter();
//Reference dB value set according to the source sample size
private float mDBScale;
private int mZoom = 0;
private int mZoomWindowOffset = 0;
;
private SettingsManager mSettingsManager;
public SpectrumPanel(SettingsManager settingsManager)
{
mSettingsManager = settingsManager;
if(mSettingsManager != null)
{
mSettingsManager.addListener(this);
}
setSampleSize(16.0);
mSmoothingFilter.setPointSize(SmoothingFilter.SMOOTHING_DEFAULT);
getColors();
setAveraging(mAveraging);
}
public void dispose()
{
if(mSettingsManager != null)
{
mSettingsManager.removeListener(this);
}
mSettingsManager = null;
}
/** /**
* DFTResultsListener interface for receiving the processed data * DFTResultsListener interface for receiving the processed data
* to display * to display
*/ */
public void receive( float[] currentFFTBins ) public void receive(float[] currentFFTBins)
{ {
//Prevent arrays of NaN values from being rendered. The first few //Prevent arrays of NaN values from being rendered. The first few
//DFT result sets on startup will contain NaN values //DFT result sets on startup will contain NaN values
if( Float.isInfinite( currentFFTBins[ 0 ] ) || Float.isNaN( currentFFTBins[ 0 ] ) ) if(Float.isInfinite(currentFFTBins[0]) || Float.isNaN(currentFFTBins[0]))
{ {
currentFFTBins = new float[ currentFFTBins.length ]; currentFFTBins = new float[currentFFTBins.length];
} }
//Construct and/or resize our DFT results variables //Construct and/or resize our DFT results variables
if( mDisplayFFTBins == null || if(mDisplayFFTBins == null ||
mDisplayFFTBins.length != currentFFTBins.length ) mDisplayFFTBins.length != currentFFTBins.length)
{ {
mDisplayFFTBins = currentFFTBins; mDisplayFFTBins = currentFFTBins;
} }
//Apply smoothing across the bins of the DFT results //Apply smoothing across the bins of the DFT results
float[] smoothedBins = mSmoothingFilter.filter( currentFFTBins ); float[] smoothedBins = mSmoothingFilter.filter(currentFFTBins);
//Apply averaging over multiple DFT output frames //Apply averaging over multiple DFT output frames
if( mAveraging > 1 ) if(mAveraging > 1)
{ {
float gain = 1.0f / (float)mAveraging; float gain = 1.0f / (float)mAveraging;
for( int x = 0; x < mDisplayFFTBins.length; x++ ) for(int x = 0; x < mDisplayFFTBins.length; x++)
{ {
mDisplayFFTBins[ x ] += mDisplayFFTBins[x] +=
( smoothedBins[ x ] - mDisplayFFTBins[ x ] ) * gain; (smoothedBins[x] - mDisplayFFTBins[x]) * gain;
} }
} }
else else
{ {
mDisplayFFTBins = smoothedBins; mDisplayFFTBins = smoothedBins;
} }
repaint(); repaint();
} }
@Override @Override
public void paintComponent( Graphics g ) public void paintComponent(Graphics g)
{ {
super.paintComponent( g ); super.paintComponent(g);
Graphics2D graphics = (Graphics2D) g;
graphics.setBackground( mColorSpectrumBackground ); Graphics2D graphics = (Graphics2D)g;
graphics.setRenderingHints( RENDERING_HINTS ); graphics.setBackground(mColorSpectrumBackground);
drawSpectrum( graphics ); graphics.setRenderingHints(RENDERING_HINTS);
drawSpectrum(graphics);
} }
/** /**
* Draws the current fft spectrum with a line and a gradient fill. * Draws the current fft spectrum with a line and a gradient fill.
*/ */
private void drawSpectrum( Graphics2D graphics ) private void drawSpectrum(Graphics2D graphics)
{ {
Dimension size = getSize(); Dimension size = getSize();
//Draw the background //Draw the background
Rectangle background = new Rectangle( 0, 0, size.width, size.height ); Rectangle background = new Rectangle(0, 0, size.width, size.height);
graphics.setColor( mColorSpectrumBackground ); graphics.setColor(mColorSpectrumBackground);
graphics.draw( background ); graphics.draw(background);
graphics.fill( background ); graphics.fill(background);
//Define the gradient //Define the gradient
GradientPaint gradient = new GradientPaint( 0, GradientPaint gradient = new GradientPaint(0,
( getSize().height - mSpectrumInset ) / 2, (getSize().height - mSpectrumInset) / 2,
mColorSpectrumGradientTop, mColorSpectrumGradientTop,
0, 0,
getSize().height, getSize().height,
mColorSpectrumGradientBottom ); mColorSpectrumGradientBottom);
graphics.setBackground( mColorSpectrumBackground );
GeneralPath spectrumShape = new GeneralPath();
//Start at the lower right inset point graphics.setBackground(mColorSpectrumBackground);
spectrumShape.moveTo( size.getWidth(),
size.getHeight() - mSpectrumInset );
//Draw to the lower left
spectrumShape.lineTo( 0, size.getHeight() - mSpectrumInset );
float[] bins = getBins(); GeneralPath spectrumShape = new GeneralPath();
//If we have FFT data to display ...
if( bins != null )
{
float insideHeight = size.height - mSpectrumInset;
float scalor = insideHeight / -mDBScale; //Start at the lower right inset point
spectrumShape.moveTo(size.getWidth(),
size.getHeight() - mSpectrumInset);
//Draw to the lower left
spectrumShape.lineTo(0, size.getHeight() - mSpectrumInset);
float[] bins = getBins();
//If we have FFT data to display ...
if(bins != null)
{
float insideHeight = size.height - mSpectrumInset;
float scalor = insideHeight / -mDBScale;
/* Calculate based on bin size - 1, since bin 0 is rendered at zero /* Calculate based on bin size - 1, since bin 0 is rendered at zero
* and the last bin is rendered at the width */ * and the last bin is rendered at the width */
float binSize = (float)size.width / ( (float)( bins.length ) ); float binSize = (float)size.width / ((float)(bins.length));
for( int x = 0; x < bins.length; x++ ) for(int x = 0; x < bins.length; x++)
{ {
float height; float height;
height = bins[ x ] * scalor;
if( height > insideHeight )
{
height = insideHeight;
}
if( height < 0 )
{
height = 0;
}
float xAxis = (float)x * binSize;
spectrumShape.lineTo( xAxis, height ); height = bins[x] * scalor;
}
}
//Otherwise show an empty spectrum
else
{
//Draw Left Size
graphics.setPaint( gradient );
spectrumShape.lineTo( 0, size.getHeight() - mSpectrumInset );
//Draw Middle
spectrumShape.lineTo( size.getWidth(),
size.getHeight() - mSpectrumInset );
}
//Draw Right Side
spectrumShape.lineTo( size.getWidth(),
size.getHeight() - mSpectrumInset );
graphics.setPaint( gradient );
graphics.draw( spectrumShape );
graphics.fill( spectrumShape );
graphics.setPaint( mColorSpectrumLine );
//Draw the bottom line under the spectrum if(height > insideHeight)
graphics.draw( new Line2D.Float( 0, {
size.height - mSpectrumInset, height = insideHeight;
size.width, }
size.height - mSpectrumInset ) );
if(height < 0)
{
height = 0;
}
float xAxis = (float)x * binSize;
spectrumShape.lineTo(xAxis, height);
}
}
//Otherwise show an empty spectrum
else
{
//Draw Left Size
graphics.setPaint(gradient);
spectrumShape.lineTo(0, size.getHeight() - mSpectrumInset);
//Draw Middle
spectrumShape.lineTo(size.getWidth(),
size.getHeight() - mSpectrumInset);
}
//Draw Right Side
spectrumShape.lineTo(size.getWidth(),
size.getHeight() - mSpectrumInset);
graphics.setPaint(gradient);
graphics.draw(spectrumShape);
graphics.fill(spectrumShape);
graphics.setPaint(mColorSpectrumLine);
//Draw the bottom line under the spectrum
graphics.draw(new Line2D.Float(0,
size.height - mSpectrumInset,
size.width,
size.height - mSpectrumInset));
} }
/** /**
* Sets the current zoom level * Sets the current zoom level
* *
* 0 No Zoom * 0 No Zoom
* 1 2x Zoom * 1 2x Zoom
* 2 4x Zoom * 2 4x Zoom
* 3 8x Zoom * 3 8x Zoom
* 4 16x Zoom * 4 16x Zoom
* 5 32x Zoom * 5 32x Zoom
* * 6 64x Zoom
* @param zoom level, 0 - 5. *
* @param zoom level, 0 - 6.
*/ */
public void setZoom( int zoom ) public void setZoom(int zoom)
{ {
Validate.isTrue( 0 <= zoom && zoom <= 5 ); Validate.isTrue(0 <= zoom && zoom <= 6, "Unrecognized Zoom Level: " + zoom);
mZoom = zoom; mZoom = zoom;
} }
/** /**
* Sets the offset to define which subset of zoomed bins to display * Sets the offset to define which subset of zoomed bins to display
* *
* @param offset * @param offset
*/ */
public void setZoomWindowOffset( int offset ) public void setZoomWindowOffset(int offset)
{ {
mZoomWindowOffset = offset; mZoomWindowOffset = offset;
} }
/** /**
* Sets the source sample size in bits which defines the lowest dB value to * Sets the source sample size in bits which defines the lowest dB value to
* display in the spectrum * display in the spectrum
* *
* @param sampleSize in bits * @param sampleSize in bits
*/ */
public void setSampleSize( double sampleSize ) public void setSampleSize(double sampleSize)
{
Validate.isTrue(2.0 <= sampleSize && sampleSize <= 32.0);
mDBScale = (float)( 20.0 * Math.log10( Math.pow( 2.0, sampleSize - 1 ) ) );
}
/**
* Defines the number of DFT result sets to average across
*/
public void setAveraging( int size )
{
mAveraging = size;
}
/**
* DFT result sets averaging value
*/
public int getAveraging()
{
return mAveraging;
}
/**
* Clears the spectral display
*/
public void clearSpectrum()
{
mDisplayFFTBins = null;
repaint();
}
private void getColors()
{
mColorSpectrumBackground =
getColor( ColorSettingName.SPECTRUM_BACKGROUND );
mColorSpectrumGradientBottom =
getColor( ColorSettingName.SPECTRUM_GRADIENT_BOTTOM );
mColorSpectrumGradientTop =
getColor( ColorSettingName.SPECTRUM_GRADIENT_TOP );
mColorSpectrumLine = getColor( ColorSettingName.SPECTRUM_LINE );
}
/**
* Retrieves the current setting for the named color setting
*/
private Color getColor( ColorSettingName name )
{
ColorSetting setting = mSettingsManager.getColorSetting( name );
return setting.getColor();
}
/**
* Listener interface to receive setting change notifications
*/
@Override
public void settingChanged( Setting setting )
{ {
if( setting instanceof ColorSetting ) Validate.isTrue(2.0 <= sampleSize && sampleSize <= 32.0);
{
ColorSetting colorSetting = (ColorSetting)setting; mDBScale = (float)(20.0 * Math.log10(Math.pow(2.0, sampleSize - 1)));
switch( ( (ColorSetting) setting ).getColorSettingName() )
{
case SPECTRUM_BACKGROUND:
mColorSpectrumBackground = colorSetting.getColor();
break;
case SPECTRUM_GRADIENT_BOTTOM:
mColorSpectrumGradientBottom = colorSetting.getColor();
break;
case SPECTRUM_GRADIENT_TOP:
mColorSpectrumGradientTop = colorSetting.getColor();
break;
case SPECTRUM_LINE:
mColorSpectrumLine = colorSetting.getColor();
break;
default:
break;
}
}
} }
private int getZoomMultiplier() /**
{ * Defines the number of DFT result sets to average across
return (int)Math.pow( 2.0, mZoom ); */
} public void setAveraging(int size)
{
/** mAveraging = size;
* Returns the DFT result bins, or a zoomed and offset version of the bins }
* when the display is zoomed.
*/ /**
* DFT result sets averaging value
*/
public int getAveraging()
{
return mAveraging;
}
/**
* Clears the spectral display
*/
public void clearSpectrum()
{
mDisplayFFTBins = null;
repaint();
}
private void getColors()
{
mColorSpectrumBackground =
getColor(ColorSettingName.SPECTRUM_BACKGROUND);
mColorSpectrumGradientBottom =
getColor(ColorSettingName.SPECTRUM_GRADIENT_BOTTOM);
mColorSpectrumGradientTop =
getColor(ColorSettingName.SPECTRUM_GRADIENT_TOP);
mColorSpectrumLine = getColor(ColorSettingName.SPECTRUM_LINE);
}
/**
* Retrieves the current setting for the named color setting
*/
private Color getColor(ColorSettingName name)
{
ColorSetting setting = mSettingsManager.getColorSetting(name);
return setting.getColor();
}
/**
* Listener interface to receive setting change notifications
*/
@Override
public void settingChanged(Setting setting)
{
if(setting instanceof ColorSetting)
{
ColorSetting colorSetting = (ColorSetting)setting;
switch(((ColorSetting)setting).getColorSettingName())
{
case SPECTRUM_BACKGROUND:
mColorSpectrumBackground = colorSetting.getColor();
break;
case SPECTRUM_GRADIENT_BOTTOM:
mColorSpectrumGradientBottom = colorSetting.getColor();
break;
case SPECTRUM_GRADIENT_TOP:
mColorSpectrumGradientTop = colorSetting.getColor();
break;
case SPECTRUM_LINE:
mColorSpectrumLine = colorSetting.getColor();
break;
default:
break;
}
}
}
private int getZoomMultiplier()
{
return (int)Math.pow(2.0, mZoom);
}
/**
* Returns the DFT result bins, or a zoomed and offset version of the bins
* when the display is zoomed.
*/
private float[] getBins() private float[] getBins()
{ {
if( mZoom == 0 ) if(mZoom == 0)
{ {
return mDisplayFFTBins; return mDisplayFFTBins;
} }
else else
{ {
int length = mDisplayFFTBins.length / getZoomMultiplier(); int length = mDisplayFFTBins.length / getZoomMultiplier();
int offset = mZoomWindowOffset;
if( ( offset + length ) >= mDisplayFFTBins.length )
{
offset = mDisplayFFTBins.length - length;
}
if( offset < 0 )
{
offset = 0;
}
return Arrays.copyOfRange( mDisplayFFTBins, offset, offset + length ); int offset = mZoomWindowOffset;
}
if((offset + length) >= mDisplayFFTBins.length)
{
offset = mDisplayFFTBins.length - length;
}
if(offset < 0)
{
offset = 0;
}
return Arrays.copyOfRange(mDisplayFFTBins, offset, offset + length);
}
} }
@Override @Override
public void settingDeleted( Setting setting ) { /* not implemented */ } public void settingDeleted(Setting setting)
{ /* not implemented */ }
/** /**
* Returns the current inter-bin smoothing setting * Returns the current inter-bin smoothing setting
*/ */
@Override @Override
public int getSmoothing() public int getSmoothing()
{ {
return mSmoothingFilter.getPointSize(); return mSmoothingFilter.getPointSize();
} }
/** /**
* Sets the bin smoothing value to define how wide the smoothing window is * Sets the bin smoothing value to define how wide the smoothing window is
* when averaging across DFT bins * when averaging across DFT bins
*/ */
@Override @Override
public void setSmoothing( int smoothing ) public void setSmoothing(int smoothing)
{ {
mSmoothingFilter.setPointSize( smoothing ); mSmoothingFilter.setPointSize(smoothing);
} }
/** /**
* Returns the current smoothing filter type * Returns the current smoothing filter type
*/ */
@Override @Override
public SmoothingType getSmoothingType() public SmoothingType getSmoothingType()
{ {
return mSmoothingFilter.getSmoothingType(); return mSmoothingFilter.getSmoothingType();
} }
/** /**
* Sets the smoothing filter type * Sets the smoothing filter type
*/ */
@Override @Override
public void setSmoothingType( SmoothingType type ) public void setSmoothingType(SmoothingType type)
{ {
if( mSmoothingFilter.getSmoothingType() != type ) if(mSmoothingFilter.getSmoothingType() != type)
{ {
int pointSize = getSmoothing(); int pointSize = getSmoothing();
synchronized ( mSmoothingFilter ) synchronized(mSmoothingFilter)
{ {
switch( type ) switch(type)
{ {
case GAUSSIAN: case GAUSSIAN:
mSmoothingFilter = new GaussianSmoothingFilter(); mSmoothingFilter = new GaussianSmoothingFilter();
break; break;
case RECTANGLE: case RECTANGLE:
mSmoothingFilter = new RectangularSmoothingFilter(); mSmoothingFilter = new RectangularSmoothingFilter();
break; break;
case TRIANGLE: case TRIANGLE:
mSmoothingFilter = new TriangularSmoothingFilter(); mSmoothingFilter = new TriangularSmoothingFilter();
break; break;
case NONE: case NONE:
default: default:
mSmoothingFilter = new NoSmoothingFilter(); mSmoothingFilter = new NoSmoothingFilter();
break; break;
} }
} }
setSmoothing( pointSize ); setSmoothing(pointSize);
} }
} }
} }