Compare commits

..

49 Commits

Author SHA1 Message Date
denny
03e641525c Updating version to 0.2.0 (final). 2016-03-29 20:11:41 -04:00
denny
f2b189ee2b Adds support to TunerManager for the RAD1O (HackRF compatible) tuner. 2016-03-21 18:39:59 -04:00
denny
3f0581dad1 Adds support for Munich hackerspace Rad1o tuner - a HackRF compatible
tuner.
2016-03-21 18:33:56 -04:00
denny
daf2b58cf2 Updates build to Version 0.2.0 Beta 8 2016-02-27 06:21:51 -05:00
denny
cf2c0ca07d Resolves significant memory usage issue - updates Airspy and HackRF with
the recent changes applied to RTL2832 Tuner controller for processing of
libusb transfer buffers.
2016-02-27 06:19:35 -05:00
denny
eead22ffad Issue #59 - more changes to improve handling of transfer buffers between
tuner start/stop iterations.
2016-02-26 14:52:31 -05:00
denny
1550c62782 Adds null checking to channel state panel. 2016-02-26 14:37:12 -05:00
denny
5f2d8c8fe6 Issue #59 - reorganizes management of the libusb transfers queue to
better handle processing and shutdowns.

-Reuses transfers between tuner start/stop invocations

-Moves libusb transfer status direct byte buffer allocation out of
buffer processing thread and reuses it, saving a lot of memory
allocations over time.
2016-02-26 14:35:34 -05:00
denny
22d3871d0b Restructures transfer buffer handling to better handle shutdown and
canceling of transfers across multiple threads.
2016-02-26 13:28:59 -05:00
denny
455e45269c Issue #58 - DFT processor queue - removed logging that indicates the
queue is full and samples are being purged.
2016-02-26 10:08:52 -05:00
denny
ca8406bdc0 Resolves #58 - DFT processor sample queue may fill up over time causing
the spectral display to be slow to reflect changes to tuner settings.

Changed the max size of the sample queue based on the highest rate tuner
sample rate 10 MHz (airspy) and the lowest available spectral update
frame rate (12).  Sized the buffer queue based on the maximum number of
buffers that should be available per DFT processing iteration.  If the
queue backs up, new buffers will be discarded until the DFT processor
consumes the queue and catches up.
2016-02-26 06:35:03 -05:00
denny
399d164bd0 Resolves #57 - default tuner configuration per tuner type was being
shared across multiple tuners of the same type, unless the user created
a new tuner configuration for each tuner. 

Updated code to create a default tuner configuration based on the tuner
type, but name the configuration as 'Default-UNIQUETUNERID', so that any
changes to that uniquely identified tuner will persist across sessions.

Note: this does not avoid the situation where multiple tuners of the
same type are all using the same default serial number (e.g. 0000001).
In this case, they will all still share the same configuraiton.  User
must change the serial number of the dongle in order for it to be
uniquely identifiable, so that we can correctly lookup the configuration
for the specific tuner.
2016-02-26 06:07:21 -05:00
denny
6f2a7e89a9 Resolves issue with RTL2832 Tuner cancelling transfer buffers that
haven't yet been initialized.  This occurs when there is a channel
allocated from the tuner that is the first/only listener and then is
immediately released.  Some of the buffers are in motion and the others
simply haven't yet been initialized.
2016-02-22 20:49:20 -05:00
denny
b821588e54 Removes debug message from trafficchannelmanager 2016-02-20 17:20:15 -05:00
denny
62aca27e66 Resolves issue with resetting audio metadata between traffic channel
reuse.
2016-02-20 10:26:50 -05:00
denny
cdcc487095 Resolves potential threading issue in tuner channel source. 2016-02-20 10:21:47 -05:00
denny
d5b6589e36 Updates build to 0.2.0 beta 7 2016-02-20 10:05:22 -05:00
denny
be7ab871d1 Resolves issue with audio metadata updates that was causing the audio
playback panel to not detect changes in FROM/TO values for the current
audio playback.
2016-02-20 09:55:39 -05:00
denny
294a830aaf Updates TDU message decoded message text. 2016-02-20 08:54:02 -05:00
denny
a7da60cd27 Resolves issue with audio playback where audio playback stops working.
Potential existed for the audio output class to become unsynchronized
with the underlying source data line and attempt to perform a blocking
write to the data line when the data line doesn't have the capacity to
accept the full buffer, and the data line is not started.  Thus,
playback could potentially stay in a persitent blocked state.

Refactored Mono and Stereo audio output implementations to push the
audio buffer queue processing into the parent abstract class.

AudioManager was updated to remove the last time active timestamp out 
of the audio manager and down into the audio output to remove duplicate
timestamp accounting.
2016-02-20 08:53:09 -05:00
denny
894ebeb040 Removes logging of C4FM filter coefficients. 2016-02-19 21:03:34 -05:00
denny
682fe0c779 Updates linux launch script to allow clean exit on program exit. 2016-02-15 21:06:20 -05:00
denny
88d3088ebc Resolves issue with concurrent access of channel panels in channel list 2016-02-15 14:58:07 -05:00
denny
dc3390460a Updates build to beta.6 2016-02-15 10:01:11 -05:00
denny
8db226cd7b Adds broadcasting of unknown P25 message types. If a P25 message passes
the NID crc check but uses an unrecognized data unit id, it will now be
broadcast as NID:UNKN, but allows for detection of new DUID types when
we have a large number of occurances of unknonwn duid types.
2016-02-15 10:00:29 -05:00
denny
b7a54598b1 Changes audio panel call timeout from 2 seconds to 1 second in order to
increase audio output cycle time.
2016-02-15 09:59:04 -05:00
denny
dd22397f75 Resolves issue with application memory growth and thread pool
saturation.

Each tunerchannelsource was sharing a thread pool and as the processing
load increased, the thread pool was not expanding and the processing
tasks were backing up, causing the buffers to grow with unprocessed
data.

Changed so that each tuner channel source is handed a thread pool to use
for processing incoming sample data.  Processing chain maintains the
thread pool and starts the channel source.  This allows traffic channels
to reuse the thread pool across multiple iterations.
2016-02-15 09:46:33 -05:00
denny
d8a20b5adb Updating build to beta 5 2016-02-13 09:10:06 -05:00
denny
c670f1d855 Resolves issue with audio playback state becoming disconnected with
AudioManager connection class monitoring of the playback state.  Adds a
line listener to monitor when the line is started/stopped and responds
accordingly.

Removes squelch control from mono and stereo audio playback components
which was a holdover -- squelch state is now managed within the 
audio component for each processing chain.
2016-02-13 09:07:37 -05:00
denny
1dc0125ef4 Resolves issue with LTR standard calls - ending state was not monitoring
the correct channel number on call ends.
2016-02-12 20:54:14 -05:00
denny
225e747d6e Updated README 2016-02-07 10:04:23 -05:00
denny
36f9953156 Updated README 2016-02-07 09:40:25 -05:00
denny
47c452e7e6 README update 2016-02-07 09:28:09 -05:00
denny
06c8bba97e Updated README with version 0.2.0 wiki updates. 2016-02-07 09:26:25 -05:00
denny
a25bb83dea Talkgroup ID Editor - updates help text in editor window 2016-02-06 09:34:13 -05:00
denny
8e11cb4554 Removes audio inversion from talkgroup id and editor. The audio
processing architecture will be updated in the next version and will
handle identifying and processing audio inversion on a per-alias basis.
2016-02-06 09:30:52 -05:00
denny
2892e4609e Removes the 'No Tuner' color change sub-menu from the decoding channels
list.  This is no longer a valid channel state since any decoding
channels that fail to get a tuner source will not be displayed.
2016-02-06 08:24:46 -05:00
denny
d6e161f3e9 Updating build to 0.2.0.beta4 2016-02-05 09:11:37 -05:00
denny
ba952af721 Added support for parsing additional Packet Data Unit unrecognized types
and removed logging of unrecognized PDUC messages.
2016-02-05 09:10:23 -05:00
denny
d9be346d7f Added new message for Packet Data Unit Confirmed messages with unknown
PDUType.
2016-02-04 21:14:16 -05:00
denny
133adec27a Adds support for PacketData messages that were being flagged as Packet
Data Unit - Confirmed unknown message types.
2016-02-04 20:51:13 -05:00
denny
981a8fec55 Removes logger from TrafficChannelManager that was logging when a
traffic channel is allocated but fails to get a source and is
subsequently torn down.  Logging was ambiguous and unneeded.
2016-02-04 20:50:17 -05:00
denny
9c94266adb Resolves issue with Audio Channel display showing a call in progress
with TO/FROM identifiers displayed, but no audio playing.  This occurred
when the decoder sent a few audio buffers which caused the display to
update from the metadata, however the decoder stopped sending buffers
and the audio start threshold is never met.  Clear display is invoked
upon audio stop, so the panel was never started or stopped or cleared
until the next call came in.

Changed to store the incoming metadata and only display it once the
audio playback starts and reset the display once the audio stops.
2016-02-04 20:49:11 -05:00
denny
4460f1ee30 Suppresses logging of attempts to change to invalid channel state. The
state enumeration is designed to block these attempts which can occur
when a non-standard call sequence of events occurs, normally due to
intermittent decoding.
2016-02-02 20:14:33 -05:00
denny
ddfefe4b85 Resolves issue with ChannelState FADE monitor - changes from
starting/stopping the monitor for each call to simply leaving the
monitor task running continuously.  Was causing issue with scheduled
thread executor not shutting down the task in a timely manner -- new
approach should avoid start/stop related issues altogether.

Updates P25DecoderState with reordering of timing of when we send
updates for the TO/FROM identifiers so that the audio manager receives
up-to-date metadata and hopefully resolves issue with 'Do Not Monitor'
talkgroups being monitored by the audio manager.

Updates the AudioManager with a try/catch block around the processing
task so that we can catch any audio processing exceptions that occur
during the timer task and might get suppressed by the scheduled thread
executor.
2016-02-02 19:44:18 -05:00
Denny Sheirer
debdc39a5f Resolves threading issue in ChannelState.
Resolves issue in tuner controller where controller was not releasing
sourced tuner channels, preventing re-tuning to other frequencies.
Resolves issue with channel timeout on MPT1327 monitored traffic
channels
2016-02-01 09:17:56 +00:00
Denny Sheirer
6f76f6fca4 Resolves issue with Tuner retaining released TunerChannels, preventing
the tuner from sourcing additional out-of-band channels.
2016-01-30 16:58:52 +00:00
denny
1290b8c4d3 Adds VSELP message and unknown Data Unit ID handling to P25 decoder. 2016-01-28 19:18:05 -05:00
denny
b95b3ff415 Updated readme 2016-01-23 21:19:54 -05:00
68 changed files with 1984 additions and 1731 deletions

1
.gitignore vendored
View File

@@ -1,3 +1,4 @@
/*.class /*.class
/classes* /classes*
/product* /product*
/bin/

View File

@@ -1,19 +1,22 @@
# sdrtrunk # sdrtrunk #
A cross-platform java application for decoding, monitoring, recording and streaming trunked mobile and related radio protocols using Software Defined Radios (SDR). A cross-platform java application for decoding, monitoring and recording trunked
mobile and related radio protocols using Software Defined Radios (SDR).
# Documentation #
* [Getting Started](https://github.com/DSheirer/sdrtrunk/wiki/GettingStarted) * [Getting Started](https://github.com/DSheirer/sdrtrunk/wiki/GettingStarted)
* [User's Manual](https://github.com/DSheirer/sdrtrunk/wiki/UserManual) * [User's Manual (Version 0.2.0)](https://github.com/DSheirer/sdrtrunk/wiki/UserManual)
* [Download](https://drive.google.com/folderview?id=0B7BHsssXUq8eOHBTNndCczZTd0k&usp=sharing) * [Wiki](https://github.com/DSheirer/sdrtrunk/wiki)
# Support Group and Error Reporting #
You can get help with setting up the sdrtrunk application or report any errors
that you encounter while running the application in the sdrtrunk Google Group at:
* [Support](https://groups.google.com/forum/#!forum/sdrtrunk) * [Support](https://groups.google.com/forum/#!forum/sdrtrunk)
# Updates #
Subscribe to this group to receive updates about new releases and the ongoing
application development effort.
* [Subscribe for Updates](https://groups.google.com/forum/#!forum/sdrtrunk)
![](https://github.com/DSheirer/sdrtrunk/wiki/images/SDRTrunk_application.png) ![](https://github.com/DSheirer/sdrtrunk/wiki/images/SDRTrunk_application.png)
<script>
(function(i,s,o,g,r,a,m){i['GoogleAnalyticsObject']=r;i[r]=i[r]||function(){
(i[r].q=i[r].q||[]).push(arguments)},i[r].l=1*new Date();a=s.createElement(o),
m=s.getElementsByTagName(o)[0];a.async=1;a.src=g;m.parentNode.insertBefore(a,m)
})(window,document,'script','//www.google-analytics.com/analytics.js','ga');
ga('create', 'UA-67489234-1', 'auto');
ga('send', 'pageview');
</script>

View File

@@ -7,8 +7,8 @@
<property name="major" value="0"/> <property name="major" value="0"/>
<property name="minor" value="2"/> <property name="minor" value="2"/>
<property name="patch" value="0"/> <property name="patch" value="0"/>
<property name="release" value="beta.3"/> <property name="release" value=""/>
<property name="filename" value="sdrtrunk_${major}.${minor}.${patch}-${release}"/> <property name="filename" value="sdrtrunk_${major}.${minor}.${patch}${release}"/>
<!-- Build properties --> <!-- Build properties -->
<property name="repo" location=".."/> <property name="repo" location=".."/>

2
scripts/run_sdrtrunk_linux.sh Normal file → Executable file
View File

@@ -1,2 +1,2 @@
#!/bin/bash #!/bin/bash
java -XX:+UseG1GC -cp "*:libs/*" gui.SDRTrunk& java -XX:+UseG1GC -cp "*:libs/*" gui.SDRTrunk

View File

@@ -27,7 +27,6 @@ import audio.inverted.AudioType;
public class TalkgroupID extends AliasID public class TalkgroupID extends AliasID
{ {
private String mTalkgroup; private String mTalkgroup;
private AudioType mAudioType;
public TalkgroupID() public TalkgroupID()
{ {
@@ -44,20 +43,9 @@ public class TalkgroupID extends AliasID
mTalkgroup = talkgroup; mTalkgroup = talkgroup;
} }
@XmlAttribute
public AudioType getAudioType()
{
return mAudioType;
}
public void setAudioType( AudioType type )
{
mAudioType = type;
}
public boolean hasAudioType() public boolean hasAudioType()
{ {
return mAudioType != null; return false;
} }
public String toString() public String toString()
@@ -66,13 +54,6 @@ public class TalkgroupID extends AliasID
sb.append( "Talkgroup: " + mTalkgroup ); sb.append( "Talkgroup: " + mTalkgroup );
if( mAudioType != null )
{
sb.append( " [Audio " );
sb.append( mAudioType.getShortDisplayString() );
sb.append( "]" );
}
return sb.toString(); return sb.toString();
} }

View File

@@ -1,6 +1,6 @@
/******************************************************************************* /*******************************************************************************
* SDR Trunk * SDR Trunk
* Copyright (C) 2014 Dennis Sheirer * Copyright (C) 2014-2016 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
@@ -20,9 +20,7 @@ package alias.id.talkgroup;
import java.awt.event.ActionEvent; import java.awt.event.ActionEvent;
import java.awt.event.ActionListener; import java.awt.event.ActionListener;
import javax.swing.DefaultComboBoxModel;
import javax.swing.JButton; import javax.swing.JButton;
import javax.swing.JComboBox;
import javax.swing.JLabel; import javax.swing.JLabel;
import javax.swing.JOptionPane; import javax.swing.JOptionPane;
import javax.swing.JPanel; import javax.swing.JPanel;
@@ -30,7 +28,6 @@ import javax.swing.JTextArea;
import javax.swing.JTextField; import javax.swing.JTextField;
import net.miginfocom.swing.MigLayout; import net.miginfocom.swing.MigLayout;
import audio.inverted.AudioType;
import controller.ConfigurableNode; import controller.ConfigurableNode;
public class TalkgroupIDEditor extends JPanel implements ActionListener public class TalkgroupIDEditor extends JPanel implements ActionListener
@@ -38,19 +35,15 @@ public class TalkgroupIDEditor extends JPanel implements ActionListener
private static final long serialVersionUID = 1L; private static final long serialVersionUID = 1L;
private TalkgroupIDNode mTalkgroupIDNode; private TalkgroupIDNode mTalkgroupIDNode;
private JLabel mLabelName;
private JTextField mTextTalkgroupID; private JTextField mTextTalkgroupID;
private JLabel mLabelAudio;
private JComboBox<AudioType> mComboAudioTypes;
private String mHelpText = "Enter a formatted talkgroup identifier.\n\n" private String mHelpText = "Enter a formatted talkgroup identifier.\n\n"
+ "P25 Talkgroup Format: hexadecimal value, 4 characters for "
+ "talkgroup or 6 characters for a mobile radio\n\n"
+ "LTR Talkgroup Format: A-HH-TTT (A=Area, H=Home Repeater, " + "LTR Talkgroup Format: A-HH-TTT (A=Area, H=Home Repeater, "
+ "T=Talkgroup)\n\n" + "T=Talkgroup)\n\n"
+ "Passport Talkgroup Format: xxxxx (up to 5-digit number)\n\n" + "Passport Talkgroup Format: xxxxx (up to 5-digit number)\n\n"
+ "Wildcard: use one or more asterisks (*) for any talkgroup digits.\n\n" + "Wildcard: use one or more asterisks (*) for any talkgroup digits.";
+ "Audio: if the talkgroup uses simple frequency inversion, specify"
+ " the inversion frequency from the dropdown list to automatically"
+ " un-invert audio for this specific talkgroup";
public TalkgroupIDEditor( TalkgroupIDNode talkgroupIDNode ) public TalkgroupIDEditor( TalkgroupIDNode talkgroupIDNode )
{ {
@@ -72,22 +65,6 @@ public class TalkgroupIDEditor extends JPanel implements ActionListener
add( mTextTalkgroupID, "growx,push" ); add( mTextTalkgroupID, "growx,push" );
add( new JLabel( "Audio:" ) );
mComboAudioTypes = new JComboBox<AudioType>();
mComboAudioTypes.setModel(
new DefaultComboBoxModel<AudioType>( AudioType.values() ) );
AudioType audioType = mTalkgroupIDNode.getTalkgroupID().getAudioType();
if( audioType != null )
{
mComboAudioTypes.setSelectedItem( audioType );
}
add( mComboAudioTypes, "growx,push" );
JButton btnSave = new JButton( "Save" ); JButton btnSave = new JButton( "Save" );
btnSave.addActionListener( TalkgroupIDEditor.this ); btnSave.addActionListener( TalkgroupIDEditor.this );
add( btnSave, "growx,push" ); add( btnSave, "growx,push" );
@@ -117,14 +94,6 @@ public class TalkgroupIDEditor extends JPanel implements ActionListener
((ConfigurableNode)mTalkgroupIDNode.getParent()).sort(); ((ConfigurableNode)mTalkgroupIDNode.getParent()).sort();
AudioType selected = mComboAudioTypes
.getItemAt( mComboAudioTypes.getSelectedIndex() );
if( selected != null )
{
mTalkgroupIDNode.getTalkgroupID().setAudioType( selected );
}
mTalkgroupIDNode.save(); mTalkgroupIDNode.save();
mTalkgroupIDNode.show(); mTalkgroupIDNode.show();
@@ -139,9 +108,6 @@ public class TalkgroupIDEditor extends JPanel implements ActionListener
{ {
mTextTalkgroupID.setText( mTextTalkgroupID.setText(
mTalkgroupIDNode.getTalkgroupID().getTalkgroup() ); mTalkgroupIDNode.getTalkgroupID().getTalkgroup() );
mComboAudioTypes.setSelectedItem(
mTalkgroupIDNode.getTalkgroupID().getAudioType() );
} }
mTalkgroupIDNode.refresh(); mTalkgroupIDNode.refresh();

View File

@@ -49,7 +49,10 @@ public class AudioChannelPanel extends JPanel
private JLabel mFrom = new JLabel( "" ); private JLabel mFrom = new JLabel( "" );
private JLabel mFromAlias = new JLabel( "" ); private JLabel mFromAlias = new JLabel( "" );
private boolean mConfigured = false; private enum AudioStatus { STARTED,STOPPED };
private AudioStatus mAudioStatus = AudioStatus.STOPPED;
private AudioMetadata mAudioMetadata;
public AudioChannelPanel( SettingsManager settingsManager, AudioOutput audioOutput ) public AudioChannelPanel( SettingsManager settingsManager, AudioOutput audioOutput )
{ {
@@ -134,15 +137,19 @@ public class AudioChannelPanel extends JPanel
{ {
switch( audioEvent.getType() ) switch( audioEvent.getType() )
{ {
case AUDIO_STOPPED: case AUDIO_STARTED:
EventQueue.invokeLater( new Runnable() synchronized( mAudioStatus )
{ {
@Override mAudioStatus = AudioStatus.STARTED;
public void run() updateLabels();
{ }
resetLabels(); break;
} case AUDIO_STOPPED:
} ); synchronized( mAudioStatus )
{
mAudioStatus = AudioStatus.STOPPED;
resetLabels();
}
break; break;
case AUDIO_MUTED: case AUDIO_MUTED:
case AUDIO_UNMUTED: case AUDIO_UNMUTED:
@@ -166,13 +173,55 @@ public class AudioChannelPanel extends JPanel
*/ */
private void resetLabels() private void resetLabels()
{ {
mFrom.setText( "" ); EventQueue.invokeLater( new Runnable()
updateAlias( mFromAlias, null ); {
@Override
public void run()
{
mFrom.setText( "" );
updateAlias( mFromAlias, null );
mTo.setText( "" ); mTo.setText( "" );
updateAlias( mToAlias, null ); updateAlias( mToAlias, null );
}
} );
}
mConfigured = false; private void updateLabels()
{
if( mAudioMetadata != null )
{
final Metadata from = mAudioMetadata.getMetadata( MetadataType.FROM );
final Metadata to = mAudioMetadata.getMetadata( MetadataType.TO );
EventQueue.invokeLater( new Runnable()
{
@Override
public void run()
{
if( from != null )
{
mFrom.setText( from.getValue() );
updateAlias( mFromAlias, from.getAlias() );
}
else
{
mFrom.setText( "-----" );
}
if( to != null )
{
mTo.setText( to.getValue() );
updateAlias( mToAlias, to.getAlias() );
}
else
{
mTo.setText( "-----" );
}
}
} );
}
} }
/** /**
@@ -214,32 +263,17 @@ public class AudioChannelPanel extends JPanel
@Override @Override
public void receive( AudioMetadata audioMetadata ) public void receive( AudioMetadata audioMetadata )
{ {
if( !mConfigured || audioMetadata.isUpdated() ) mAudioMetadata = audioMetadata;
if( audioMetadata.isUpdated() )
{ {
final Metadata from = audioMetadata.getMetadata( MetadataType.FROM ); synchronized( mAudioStatus )
final Metadata to = audioMetadata.getMetadata( MetadataType.TO );
EventQueue.invokeLater( new Runnable()
{ {
@Override if( mAudioStatus == AudioStatus.STARTED )
public void run()
{ {
if( from != null ) updateLabels();
{
mFrom.setText( from.getValue() );
updateAlias( mFromAlias, from.getAlias() );
}
if( to != null )
{
mTo.setText( to.getValue() );
updateAlias( mToAlias, to.getAlias() );
}
} }
} ); }
mConfigured = true;
} }
} }
} }

View File

@@ -1,5 +1,3 @@
package audio;
/******************************************************************************* /*******************************************************************************
* SDR Trunk * SDR Trunk
* Copyright (C) 2014-2016 Dennis Sheirer * Copyright (C) 2014-2016 Dennis Sheirer
@@ -17,6 +15,7 @@ package audio;
* 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 audio;
import java.util.ArrayList; import java.util.ArrayList;
import java.util.Collections; import java.util.Collections;
@@ -41,7 +40,6 @@ import source.mixer.MixerChannel;
import source.mixer.MixerChannelConfiguration; import source.mixer.MixerChannelConfiguration;
import source.mixer.MixerManager; import source.mixer.MixerManager;
import audio.AudioEvent.Type; import audio.AudioEvent.Type;
import audio.AudioPanel.MixerSelectionItem;
import audio.output.AudioOutput; import audio.output.AudioOutput;
import audio.output.MonoAudioOutput; import audio.output.MonoAudioOutput;
import audio.output.StereoAudioOutput; import audio.output.StereoAudioOutput;
@@ -52,7 +50,7 @@ public class AudioManager implements Listener<AudioPacket>, IAudioController
{ {
private static final Logger mLog = LoggerFactory.getLogger( AudioManager.class ); private static final Logger mLog = LoggerFactory.getLogger( AudioManager.class );
public static final int AUDIO_TIMEOUT = 2000; //2 seconds public static final int AUDIO_TIMEOUT = 1000; //1 second
public static final String AUDIO_CHANNELS_PROPERTY = "audio.manager.channels"; public static final String AUDIO_CHANNELS_PROPERTY = "audio.manager.channels";
public static final String AUDIO_MIXER_PROPERTY = "audio.manager.mixer"; public static final String AUDIO_MIXER_PROPERTY = "audio.manager.mixer";
@@ -184,7 +182,8 @@ public class AudioManager implements Listener<AudioPacket>, IAudioController
for( AudioOutputConnection connection: mAudioOutputConnections ) for( AudioOutputConnection connection: mAudioOutputConnections )
{ {
if( connection.isConnected() && connection.isInactive() ) if( connection.isInactive() &&
mChannelConnectionMap.containsKey( connection.getSource() ) )
{ {
mChannelConnectionMap.remove( connection.getSource() ); mChannelConnectionMap.remove( connection.getSource() );
connection.disconnect(); connection.disconnect();
@@ -392,51 +391,55 @@ public class AudioManager implements Listener<AudioPacket>, IAudioController
@Override @Override
public void run() public void run()
{ {
disconnectInactiveChannelAssignments(); try
if( mAudioPacketQueue != null )
{ {
List<AudioPacket> packets = new ArrayList<AudioPacket>(); disconnectInactiveChannelAssignments();
mAudioPacketQueue.drainTo( packets ); if( mAudioPacketQueue != null )
for( AudioPacket packet: packets )
{ {
/* Don't process any packet's marked as do not monitor */ List<AudioPacket> packets = new ArrayList<AudioPacket>();
if( !packet.getAudioMetadata().isDoNotMonitor() &&
packet.getType() == AudioPacket.Type.AUDIO )
{
AudioOutputConnection connection = getConnection( packet );
if( connection != null ) mAudioPacketQueue.drainTo( packets );
for( AudioPacket packet: packets )
{
/* Don't process any packet's marked as do not monitor */
if( !packet.getAudioMetadata().isDoNotMonitor() &&
packet.getType() == AudioPacket.Type.AUDIO )
{ {
connection.receive( packet ); AudioOutputConnection connection = getConnection( packet );
if( connection != null )
{
connection.receive( packet );
}
} }
} }
} }
} }
catch( Exception e )
{
mLog.error( "Encountered error while processing audio packets", e );
}
} }
} }
/** /**
* Audio output connection manages a connection between a soure and an audio * Audio output connection manages a connection between a source and an audio
* output and maintains current state information about the audio activity * output and maintains current state information about the audio activity
* received form the source. * received form the source.
*/ */
public class AudioOutputConnection implements Listener<AudioEvent> public class AudioOutputConnection
{ {
private static final long CALL_END_DELAY = 2000; //2 seconds
private static final int DISCONNECTED = -1; private static final int DISCONNECTED = -1;
private AudioOutput mAudioOutput; private AudioOutput mAudioOutput;
private int mPriority = 0; private int mPriority = 0;
private long mLastActivity = System.currentTimeMillis();
private int mSource = DISCONNECTED; private int mSource = DISCONNECTED;
public AudioOutputConnection( AudioOutput audioOutput ) public AudioOutputConnection( AudioOutput audioOutput )
{ {
mAudioOutput = audioOutput; mAudioOutput = audioOutput;
mAudioOutput.addAudioEventListener( this );
} }
public void receive( AudioPacket packet ) public void receive( AudioPacket packet )
@@ -452,8 +455,6 @@ public class AudioManager implements Listener<AudioPacket>, IAudioController
updateLowestPriorityAssignment(); updateLowestPriorityAssignment();
} }
updateTimestamp();
if( mAudioOutput != null ) if( mAudioOutput != null )
{ {
mAudioOutput.receive( packet ); mAudioOutput.receive( packet );
@@ -476,11 +477,6 @@ public class AudioManager implements Listener<AudioPacket>, IAudioController
} }
} }
private void updateTimestamp()
{
mLastActivity = System.currentTimeMillis();
}
/** /**
* Terminates the audio output and prepares this connection for disposal * Terminates the audio output and prepares this connection for disposal
*/ */
@@ -513,7 +509,8 @@ public class AudioManager implements Listener<AudioPacket>, IAudioController
public void connect( int source ) public void connect( int source )
{ {
mSource = source; mSource = source;
updateTimestamp();
mAudioOutput.updateTimestamp();
} }
/** /**
@@ -531,6 +528,7 @@ public class AudioManager implements Listener<AudioPacket>, IAudioController
public void disconnect() public void disconnect()
{ {
mSource = DISCONNECTED; mSource = DISCONNECTED;
mPriority = 0;
} }
/** /**
@@ -539,28 +537,12 @@ public class AudioManager implements Listener<AudioPacket>, IAudioController
*/ */
public boolean isInactive() public boolean isInactive()
{ {
return mLastActivity != Long.MAX_VALUE && return ( mAudioOutput.getLastActivityTimestamp() + AUDIO_TIMEOUT ) < System.currentTimeMillis();
mLastActivity + CALL_END_DELAY < System.currentTimeMillis();
} }
public int getPriority() public int getPriority()
{ {
return mPriority; return mPriority;
} }
@Override
public void receive( AudioEvent event )
{
switch( event.getType() )
{
case AUDIO_STARTED:
case AUDIO_STOPPED:
case AUDIO_CONTINUATION:
updateTimestamp();
break;
default:
break;
}
}
} }
} }

View File

@@ -64,6 +64,11 @@ public class AudioPacket
return mAudioData; return mAudioData;
} }
public boolean hasAudioBuffer()
{
return mAudioData != null;
}
public enum Type public enum Type
{ {
AUDIO, AUDIO,

View File

@@ -1,19 +1,6 @@
package audio.metadata;
import java.util.ArrayList;
import java.util.Iterator;
import java.util.List;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import sample.Listener;
import alias.Alias;
import alias.priority.Priority;
/******************************************************************************* /*******************************************************************************
* SDR Trunk * SDR Trunk
* Copyright (C) 2014,2015 Dennis Sheirer * Copyright (C) 2014-2016 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
@@ -28,15 +15,30 @@ import alias.priority.Priority;
* 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 audio.metadata;
import java.util.ArrayList;
import java.util.HashMap;
import java.util.List;
import java.util.Map;
import java.util.Map.Entry;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import sample.Listener;
import alias.Alias;
import alias.priority.Priority;
public class AudioMetadata implements Listener<Metadata> public class AudioMetadata implements Listener<Metadata>
{ {
protected static final Logger mLog = LoggerFactory.getLogger( AudioMetadata.class ); protected static final Logger mLog = LoggerFactory.getLogger( AudioMetadata.class );
private int mSource; private int mSource;
private List<Metadata> mMetadata = new ArrayList<>(); private Map<MetadataType,Metadata> mMetadata = new HashMap<>();
private boolean mUpdated = false; private boolean mUpdated = false;
/* Channel Selectect == highest priority */ /* Channel Selected == highest priority */
private boolean mSelected = false; private boolean mSelected = false;
/* Call Priority */ /* Call Priority */
@@ -92,7 +94,8 @@ public class AudioMetadata implements Listener<Metadata>
copy.mPriority = mPriority; copy.mPriority = mPriority;
copy.mSelected = mSelected; copy.mSelected = mSelected;
copy.mRecordable = mRecordable; copy.mRecordable = mRecordable;
copy.mMetadata.addAll( mMetadata );
copy.mMetadata.putAll( mMetadata );
copy.mUpdated = mUpdated; copy.mUpdated = mUpdated;
copy.mIdentifier = new String( mIdentifier ); copy.mIdentifier = new String( mIdentifier );
@@ -106,25 +109,34 @@ public class AudioMetadata implements Listener<Metadata>
*/ */
public void reset() public void reset()
{ {
Iterator<Metadata> it = mMetadata.iterator();
while( it.hasNext() )
{
if( it.next().isTemporal() )
{
it.remove();
}
}
/* Reset recordable state, identifier and audio call priority levels /* Reset recordable state, identifier and audio call priority levels
* and reprocess the metadata */ * and reprocess the metadata */
mRecordable = mSourceRecordable; mRecordable = mSourceRecordable;
setIdentifier( mSource ); setIdentifier( mSource );
mPriority = Priority.DEFAULT_PRIORITY; mPriority = Priority.DEFAULT_PRIORITY;
for( Metadata metadata: mMetadata ) List<MetadataType> toRemove = new ArrayList<>();
for( MetadataType type: mMetadata.keySet() )
{ {
processMetadata( metadata ); Metadata metadata = mMetadata.get( type );
if( metadata != null )
{
if( metadata.isTemporal() )
{
toRemove.add( type );
}
else
{
processMetadata( metadata );
}
}
}
for( MetadataType type: toRemove )
{
mMetadata.remove( type );
} }
mUpdated = true; mUpdated = true;
@@ -185,8 +197,14 @@ public class AudioMetadata implements Listener<Metadata>
} }
else else
{ {
mMetadata.add( metadata ); Metadata existing = mMetadata.get( metadata.getMetadataType() );
processMetadata( metadata );
if( existing == null || !existing.equals( metadata ) )
{
mMetadata.put( metadata.getMetadataType(), metadata );
processMetadata( metadata );
setUpdated( true );
}
} }
mUpdated = true; mUpdated = true;
@@ -195,26 +213,17 @@ public class AudioMetadata implements Listener<Metadata>
/** /**
* Returns the complete set of metadata * Returns the complete set of metadata
*/ */
public List<Metadata> getMetadata() public Map<MetadataType,Metadata> getMetadata()
{ {
return mMetadata; return mMetadata;
} }
/** /**
* Returns the first metadata object matching the type, or null if the * Returns the metadata object matching the type or null
* metadata type is not found.
*/ */
public Metadata getMetadata( MetadataType type ) public Metadata getMetadata( MetadataType type )
{ {
for( Metadata metadata: mMetadata ) return mMetadata.get( type );
{
if( metadata.getMetadataType() == type )
{
return metadata;
}
}
return null;
} }
/** /**
@@ -296,11 +305,11 @@ public class AudioMetadata implements Listener<Metadata>
sb.append( getPriority() ); sb.append( getPriority() );
sb.append( " updated:" + isUpdated() ); sb.append( " updated:" + isUpdated() );
sb.append( " " ); sb.append( " " );
for( Metadata m: getMetadata() ) for( Entry<MetadataType,Metadata> entry: getMetadata().entrySet() )
{ {
sb.append( m.getKey() ); sb.append( entry.getValue().getKey() );
sb.append( ":" ); sb.append( ":" );
sb.append( m.getValue() ); sb.append( entry.getValue().getValue() );
sb.append( " " ); sb.append( " " );
} }

View File

@@ -103,4 +103,44 @@ public class Metadata
return sb.toString(); return sb.toString();
} }
public boolean equals( Metadata other )
{
if( mMetadataType != other.getMetadataType() )
{
return false;
}
if( mTemporal != other.isTemporal() )
{
return false;
}
if( mValue != null && other.getValue() != null &&
!mValue.contentEquals( other.getValue() ))
{
return false;
}
if( ( mValue == null && other.getValue() != null ) ||
( mValue != null && other.getValue() == null ) )
{
return false;
}
if( mValueAlias != null && other.getAlias() != null &&
!mValueAlias.getName().contentEquals( other.getAlias().getName() ))
{
return false;
}
if( ( mValueAlias == null && other.getAlias() != null ) ||
( mValueAlias != null && other.getAlias() == null ) )
{
return false;
}
return true;
}
} }

View File

@@ -1,37 +1,148 @@
package audio.output; package audio.output;
import java.nio.ByteBuffer;
import java.util.ArrayList;
import java.util.List;
import java.util.concurrent.LinkedTransferQueue; import java.util.concurrent.LinkedTransferQueue;
import java.util.concurrent.ScheduledFuture;
import java.util.concurrent.TimeUnit;
import java.util.concurrent.atomic.AtomicBoolean; import java.util.concurrent.atomic.AtomicBoolean;
import javax.sound.sampled.AudioFormat;
import javax.sound.sampled.BooleanControl;
import javax.sound.sampled.Control;
import javax.sound.sampled.FloatControl; import javax.sound.sampled.FloatControl;
import javax.sound.sampled.Line;
import javax.sound.sampled.LineEvent;
import javax.sound.sampled.LineListener;
import javax.sound.sampled.LineUnavailableException;
import javax.sound.sampled.Mixer;
import javax.sound.sampled.SourceDataLine;
import org.slf4j.Logger; import org.slf4j.Logger;
import org.slf4j.LoggerFactory; import org.slf4j.LoggerFactory;
import sample.Broadcaster; import sample.Broadcaster;
import sample.Listener; import sample.Listener;
import source.mixer.MixerChannel;
import audio.AudioEvent; import audio.AudioEvent;
import audio.AudioEvent.Type;
import audio.AudioPacket; import audio.AudioPacket;
import audio.AudioPacket.Type;
import audio.metadata.AudioMetadata; import audio.metadata.AudioMetadata;
import controller.ThreadPoolManager; import controller.ThreadPoolManager;
import controller.ThreadPoolManager.ThreadType;
public abstract class AudioOutput implements Listener<AudioPacket> public abstract class AudioOutput implements Listener<AudioPacket>, LineListener
{ {
private final static Logger mLog = LoggerFactory.getLogger( AudioOutput.class ); private final static Logger mLog = LoggerFactory.getLogger( AudioOutput.class );
private AtomicBoolean mSquelched = new AtomicBoolean( false ); private LinkedTransferQueue<AudioPacket> mBuffer = new LinkedTransferQueue<>();
private int mBufferStartThreshold;
private int mBufferStopThreshold;
private Listener<AudioMetadata> mAudioMetadataListener; private Listener<AudioMetadata> mAudioMetadataListener;
private Broadcaster<AudioEvent> mAudioEventBroadcaster = new Broadcaster<>();
protected Broadcaster<AudioEvent> mAudioEventBroadcaster = new Broadcaster<>(); private ThreadPoolManager mThreadPoolManager;
protected LinkedTransferQueue<AudioPacket> mBuffer = new LinkedTransferQueue<>(); private ScheduledFuture<?> mProcessorTask;
protected ThreadPoolManager mThreadPoolManager;
protected boolean mCanProcessAudio;
protected long mLastActivity;
public AudioOutput( ThreadPoolManager threadPoolManager ) private SourceDataLine mOutput;
private Mixer mMixer;
private MixerChannel mMixerChannel;
private FloatControl mGainControl;
private BooleanControl mMuteControl;
private AudioEvent mAudioStartEvent;
private AudioEvent mAudioStopEvent;
private boolean mCanProcessAudio = false;
private long mLastActivity = System.currentTimeMillis();
/**
* Single audio channel playback with automatic starting and stopping of the
* underlying sourcedataline specified by the mixer and mixer channel
* arguments.
*
* Maintains an internal non-blocking audio packet queue and processes this
* queue 25 times a second (every 40 ms).
*
* @param threadPoolManager for assigning buffer processing schedule task
* @param mixer to obtain source data line
* @param mixerChannel either mono or left/right stereo
* @param audioFormat to use during playback
* @param lineInfo to use when obtaining the source data line
* @param requestedBufferSize of approximately 1 second of audio
*/
public AudioOutput( ThreadPoolManager threadPoolManager,
Mixer mixer,
MixerChannel mixerChannel,
AudioFormat audioFormat,
Line.Info lineInfo,
int requestedBufferSize )
{ {
mThreadPoolManager = threadPoolManager; mThreadPoolManager = threadPoolManager;
mMixer = mixer;
mMixerChannel = mixerChannel;
try
{
mOutput = (SourceDataLine)mMixer.getLine( lineInfo );
if( mOutput != null )
{
mOutput.open( audioFormat, requestedBufferSize );
//Start threshold: buffer is full with 10% or less of capacity remaining
mBufferStartThreshold = (int)( mOutput.getBufferSize() * 0.10 );
//Stop threshold: buffer is empty with 90% or more capacity available
mBufferStopThreshold = (int)( mOutput.getBufferSize() * 0.90 );
mOutput.addLineListener( this );
if( mOutput != null )
{
try
{
Control gain = mOutput.getControl( FloatControl.Type.MASTER_GAIN );
mGainControl = (FloatControl)gain;
}
catch( IllegalArgumentException iae )
{
mLog.warn( "Couldn't obtain MASTER GAIN control for stereo line [" +
mixer.getMixerInfo().getName() + " | " + getChannelName() + "]" );
}
try
{
Control mute = mOutput.getControl( BooleanControl.Type.MUTE );
mMuteControl = (BooleanControl)mute;
}
catch( IllegalArgumentException iae )
{
mLog.warn( "Couldn't obtain MUTE control for stereo line [" +
mixer.getMixerInfo().getName() + " | " + getChannelName() + "]" );
}
/* Run the queue processor task every 40 milliseconds or 25 times a second */
mProcessorTask = mThreadPoolManager.scheduleFixedRate(
ThreadType.AUDIO_PROCESSING, new BufferProcessor(),
40, TimeUnit.MILLISECONDS );
}
mAudioStartEvent = new AudioEvent( AudioEvent.Type.AUDIO_STARTED,
getChannelName() );
mAudioStopEvent = new AudioEvent( AudioEvent.Type.AUDIO_STOPPED,
getChannelName() );
mCanProcessAudio = true;
}
}
catch ( LineUnavailableException e )
{
mLog.error( "Couldn't obtain audio source data line for "
+ "audio output - mixer [" + mMixer.getMixerInfo().getName() + "]" );
}
} }
public void reset() public void reset()
@@ -44,47 +155,50 @@ public abstract class AudioOutput implements Listener<AudioPacket>
{ {
mCanProcessAudio = false; mCanProcessAudio = false;
if( mProcessorTask != null && mThreadPoolManager != null )
{
mThreadPoolManager.cancel( mProcessorTask );
}
mThreadPoolManager = null;
mProcessorTask = null;
mBuffer.clear(); mBuffer.clear();
mAudioEventBroadcaster.dispose(); mAudioEventBroadcaster.dispose();
mAudioEventBroadcaster = null; mAudioEventBroadcaster = null;
mAudioMetadataListener = null; mAudioMetadataListener = null;
mThreadPoolManager = null; if( mOutput != null )
}
public abstract String getChannelName();
public abstract void setMuted( boolean muted );
public abstract boolean isMuted();
public abstract FloatControl getGainControl();
public abstract boolean hasGainControl();
protected void broadcast( AudioMetadata metadata )
{
if( mAudioMetadataListener != null )
{ {
mAudioMetadataListener.receive( metadata ); mOutput.close();
} }
mOutput = null;
mGainControl = null;
mMuteControl = null;
} }
protected void broadcast( AudioEvent audioEvent ) /**
* Converts the audio packet data into a byte buffer format appropriate for
* the underlying source data line.
*/
protected abstract ByteBuffer convert( AudioPacket packet );
/**
* Audio output channel name
*/
public String getChannelName()
{ {
mAudioEventBroadcaster.broadcast( audioEvent ); return mMixerChannel.getLabel();
} }
public void setSquelched( boolean squelched ) /**
* Mixer Channel for this audio output
*/
protected MixerChannel getMixerChannel()
{ {
mSquelched.set( squelched ); return mMixerChannel;
broadcast( new AudioEvent( squelched ? Type.AUDIO_SQUELCHED :
Type.AUDIO_UNSQUELCHED, getChannelName() ) );
}
public boolean isSquelched()
{
return mSquelched.get();
} }
/** /**
@@ -100,6 +214,14 @@ public abstract class AudioOutput implements Listener<AudioPacket>
mAudioEventBroadcaster.removeListener( listener ); mAudioEventBroadcaster.removeListener( listener );
} }
/**
* Broadcasts an audio event to the registered listener
*/
private void broadcast( AudioEvent audioEvent )
{
mAudioEventBroadcaster.broadcast( audioEvent );
}
/** /**
* Registers a single listener to receive the audio metadata from each * Registers a single listener to receive the audio metadata from each
* audio packet * audio packet
@@ -115,21 +237,200 @@ public abstract class AudioOutput implements Listener<AudioPacket>
} }
/** /**
* Timestamp when most recent buffer was received * Broadcasts audio metadata to the registered listener
*/ */
public long getLastBufferReceived() private void broadcast( AudioMetadata metadata )
{
if( mAudioMetadataListener != null )
{
mAudioMetadataListener.receive( metadata );
}
}
/**
* Timestamp of either last buffer received or last buffer processed
*/
public long getLastActivityTimestamp()
{ {
return mLastActivity; return mLastActivity;
} }
/**
* Updates the last activity timestamp to current system time
*/
public void updateTimestamp()
{
mLastActivity = System.currentTimeMillis();
}
@Override @Override
public void receive( AudioPacket packet ) public void receive( AudioPacket packet )
{ {
if( mCanProcessAudio ) if( mCanProcessAudio )
{ {
mLastActivity = System.currentTimeMillis(); //Update the activity timestamp so that this audio output doesn't
//get disconnected before it starts processing the audio stream
updateTimestamp();
mBuffer.add( packet ); mBuffer.add( packet );
} }
} }
public class BufferProcessor implements Runnable
{
private AtomicBoolean mProcessing = new AtomicBoolean();
public BufferProcessor()
{
}
@Override
public void run()
{
try
{
/* The processing flag ensures that only one instance of the
* processor can run at any given time */
if( mProcessing.compareAndSet( false, true ) )
{
List<AudioPacket> packets = new ArrayList<AudioPacket>();
mBuffer.drainTo( packets );
for( AudioPacket packet: packets )
{
if( packet.getType() == Type.AUDIO )
{
broadcast( packet.getAudioMetadata() );
ByteBuffer buffer = convert( packet );
int wrote = 0;
if( !mOutput.isRunning() )
{
int toWrite = mOutput.available();
if( toWrite > buffer.array().length )
{
toWrite = buffer.array().length;
}
//Top off the buffer and check if we can start it
wrote += mOutput.write( buffer.array(), 0, toWrite );
checkStart();
}
if( mOutput.isRunning() && wrote < buffer.array().length )
{
//Blocking write
wrote += mOutput.write( buffer.array(), wrote,
buffer.array().length - wrote );
}
updateTimestamp();
}
}
checkStop();
mProcessing.set( false );
}
}
catch( Exception e )
{
mLog.error( "Error while processing audio buffers", e );
}
}
/**
* Starts audio playback once audio buffer is almost full and remaining
* capacity falls below the start threshold.
*/
private void checkStart()
{
if( mCanProcessAudio &&
!mOutput.isRunning() &&
mOutput.available() <= mBufferStartThreshold )
{
mOutput.start();
}
}
/**
* Stops audio playback and drains the audio buffer to empty when the
* audio buffer is mostly empty and the available buffer capacity
* exceeds the stop threshold
*/
private void checkStop()
{
if( mCanProcessAudio &&
mOutput.isRunning() &&
mOutput.available() >= mBufferStopThreshold )
{
mOutput.drain();
mOutput.stop();
}
}
}
/**
* Sets the mute state for this audio output channel
*/
public void setMuted( boolean muted )
{
if( mMuteControl != null )
{
mMuteControl.setValue( muted );
broadcast( new AudioEvent( muted ? AudioEvent.Type.AUDIO_MUTED :
AudioEvent.Type.AUDIO_UNMUTED, getChannelName() ) );
}
}
/**
* Current mute state for this audio output channel
*/
public boolean isMuted()
{
if( mMuteControl != null )
{
return mMuteControl.getValue();
}
return false;
}
/**
* Gain/volume control for this audio output channel, if one is available.
*/
public FloatControl getGainControl()
{
return mGainControl;
}
public boolean hasGainControl()
{
return mGainControl != null;
}
/**
* Monitors the source data line playback state and broadcasts audio events
* to the registered listener as the state changes
*/
@Override
public void update( LineEvent event )
{
LineEvent.Type type = event.getType();
if( type == LineEvent.Type.START )
{
mAudioEventBroadcaster.broadcast( mAudioStartEvent );
}
else if( type == LineEvent.Type.STOP )
{
mAudioEventBroadcaster.broadcast( mAudioStopEvent );
}
}
} }

View File

@@ -3,273 +3,55 @@ package audio.output;
import java.nio.ByteBuffer; import java.nio.ByteBuffer;
import java.nio.ByteOrder; import java.nio.ByteOrder;
import java.nio.ShortBuffer; import java.nio.ShortBuffer;
import java.util.ArrayList;
import java.util.List;
import java.util.concurrent.ScheduledFuture;
import java.util.concurrent.TimeUnit;
import java.util.concurrent.atomic.AtomicBoolean;
import javax.sound.sampled.BooleanControl;
import javax.sound.sampled.Control;
import javax.sound.sampled.FloatControl;
import javax.sound.sampled.LineUnavailableException;
import javax.sound.sampled.Mixer; import javax.sound.sampled.Mixer;
import javax.sound.sampled.SourceDataLine;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import source.mixer.MixerChannel; import source.mixer.MixerChannel;
import audio.AudioEvent;
import audio.AudioFormats; import audio.AudioFormats;
import audio.AudioPacket; import audio.AudioPacket;
import audio.AudioPacket.Type;
import controller.ThreadPoolManager; import controller.ThreadPoolManager;
import controller.ThreadPoolManager.ThreadType;
/** /**
* Mono Audio output with automatic flow control based on the availability of * Mono Audio output implementation
* audio data packets.
*/ */
public class MonoAudioOutput extends AudioOutput public class MonoAudioOutput extends AudioOutput
{ {
private final static Logger mLog = LoggerFactory.getLogger( MonoAudioOutput.class );
/* Output line buffer size is set at one half second of audio at 48 kHz and
* two bytes per sample */
private final static int BUFFER_SIZE = 48000; private final static int BUFFER_SIZE = 48000;
/* The queue processor will run every 20 milliseconds checking for inbound
* audio and automatically starting or stopping audio playback. We set the
* data line available space threshold to start when we're 20 ms away from
* being full, and the stop threshold when 20 ms away from being empty */
private final static int SAMPLE_SIZE_40_MS = BUFFER_SIZE / 25;
private final static int START_THRESHOLD = SAMPLE_SIZE_40_MS;
private final static int STOP_THRESHOLD = BUFFER_SIZE - SAMPLE_SIZE_40_MS;
private SourceDataLine mOutput;
private FloatControl mGainControl;
private BooleanControl mMuteControl;
private AudioEvent mAudioStartEvent;
private AudioEvent mAudioStopEvent;
private AudioEvent mAudioContinuationEvent;
private ScheduledFuture<?> mProcessorTask;
public MonoAudioOutput( ThreadPoolManager threadPoolManager, Mixer mixer ) public MonoAudioOutput( ThreadPoolManager threadPoolManager, Mixer mixer )
{ {
super( threadPoolManager ); super( threadPoolManager,
mixer,
mAudioStartEvent = new AudioEvent( AudioEvent.Type.AUDIO_STARTED, MixerChannel.MONO,
getChannelName() ); AudioFormats.PCM_SIGNED_48KHZ_16BITS_MONO,
mAudioStopEvent = new AudioEvent( AudioEvent.Type.AUDIO_STOPPED, AudioFormats.MONO_SOURCE_DATALINE_INFO,
getChannelName() ); BUFFER_SIZE );
mAudioContinuationEvent = new AudioEvent( AudioEvent.Type.AUDIO_CONTINUATION,
getChannelName() );
try
{
mOutput = (SourceDataLine)mixer.getLine(
AudioFormats.MONO_SOURCE_DATALINE_INFO );
if( mOutput != null )
{
mOutput.open( AudioFormats.PCM_SIGNED_48KHZ_16BITS_MONO, BUFFER_SIZE );
mCanProcessAudio = true;
}
}
catch ( LineUnavailableException e )
{
mLog.error( "Couldn't obtain source data line for 48kHz PCM mono "
+ "audio output" );
}
if( mOutput != null )
{
try
{
Control gain = mOutput.getControl( FloatControl.Type.MASTER_GAIN );
mGainControl = (FloatControl)gain;
}
catch( IllegalArgumentException iae )
{
mLog.warn( "Couldn't obtain MASTER GAIN control for mono line [" +
mixer.getMixerInfo().getName() + "]" );
}
try
{
Control mute = mOutput.getControl( BooleanControl.Type.MUTE );
mMuteControl = (BooleanControl)mute;
}
catch( IllegalArgumentException iae )
{
mLog.warn( "Couldn't obtain MUTE control for mono line [" +
mixer.getMixerInfo().getName() + "]" );
}
/* Run the queue processor task every 40 milliseconds */
mProcessorTask = mThreadPoolManager.scheduleFixedRate(
ThreadType.AUDIO_PROCESSING, new QueueProcessor(),
40, TimeUnit.MILLISECONDS );
}
} }
@Override /**
public String getChannelName() * Converts the audio packet data into mono audio frames.
*/
protected ByteBuffer convert( AudioPacket packet )
{ {
return MixerChannel.MONO.getLabel(); if( packet.hasAudioBuffer() )
}
public void dispose()
{
if( mProcessorTask != null && mThreadPoolManager != null )
{ {
mThreadPoolManager.cancel( mProcessorTask ); float[] samples = packet.getAudioBuffer().getSamples();
}
mProcessorTask = null; /* Little-endian byte buffer */
ByteBuffer buffer =
ByteBuffer.allocate( samples.length * 2 )
.order( ByteOrder.LITTLE_ENDIAN );
super.dispose(); ShortBuffer shortBuffer = buffer.asShortBuffer();
if( mOutput != null ) for( float sample: samples )
{
mOutput.close();
}
mOutput = null;
mGainControl = null;
mMuteControl = null;
}
public class QueueProcessor implements Runnable
{
private AtomicBoolean mProcessing = new AtomicBoolean();
@Override
public void run()
{
try
{ {
/* The mProcessing flag ensures that only one instance of the shortBuffer.put( (short)( sample * Short.MAX_VALUE ) );
* processor can run at any given time */
if( mProcessing.compareAndSet( false, true ) )
{
List<AudioPacket> packets = new ArrayList<AudioPacket>();
mBuffer.drainTo( packets );
if( packets.size() > 0 )
{
broadcast( mAudioContinuationEvent );
for( AudioPacket packet: packets )
{
if( mCanProcessAudio )
{
if( isSquelched() )
{
checkStop();
}
else if( packet.getType() == Type.AUDIO )
{
float[] samples = packet.getAudioBuffer().getSamples();
/* Little-endian byte buffer */
ByteBuffer buffer =
ByteBuffer.allocate( samples.length * 2 )
.order( ByteOrder.LITTLE_ENDIAN );
ShortBuffer shortBuffer = buffer.asShortBuffer();
for( float sample: samples )
{
shortBuffer.put( (short)( sample * Short.MAX_VALUE ) );
}
//This is a blocking write
mOutput.write( buffer.array(), 0, buffer.array().length );
checkStart();
broadcast( packet.getAudioMetadata() );
mLastActivity = System.currentTimeMillis();
}
}
}
}
else
{
checkStop();
}
mProcessing.set( false );
}
}
catch( Exception e )
{
mLog.error( "Error while processing audio buffers", e );
} }
return buffer;
} }
private void checkStart() return null;
{
if( mCanProcessAudio && !isSquelched() &&
!mOutput.isRunning() && mOutput.available() <= START_THRESHOLD )
{
mOutput.start();
mAudioEventBroadcaster.broadcast( mAudioStartEvent );
}
}
private void checkStop()
{
if( mCanProcessAudio && mOutput.isRunning() &&
( mOutput.available() >= STOP_THRESHOLD || isSquelched() ) )
{
mOutput.drain();
mOutput.stop();
mAudioEventBroadcaster.broadcast( mAudioStopEvent );
}
}
}
@Override
public void setMuted( boolean muted )
{
if( mMuteControl != null )
{
mMuteControl.setValue( muted );
broadcast( new AudioEvent( muted ? AudioEvent.Type.AUDIO_MUTED :
AudioEvent.Type.AUDIO_UNMUTED, getChannelName() ) );
}
}
@Override
public boolean isMuted()
{
if( mMuteControl != null )
{
return mMuteControl.getValue();
}
return false;
}
@Override
public FloatControl getGainControl()
{
return mGainControl;
}
@Override
public boolean hasGainControl()
{
return mGainControl != null;
} }
} }

View File

@@ -3,302 +3,69 @@ package audio.output;
import java.nio.ByteBuffer; import java.nio.ByteBuffer;
import java.nio.ByteOrder; import java.nio.ByteOrder;
import java.nio.ShortBuffer; import java.nio.ShortBuffer;
import java.util.ArrayList;
import java.util.List;
import java.util.concurrent.ScheduledFuture;
import java.util.concurrent.TimeUnit;
import java.util.concurrent.atomic.AtomicBoolean;
import javax.sound.sampled.BooleanControl;
import javax.sound.sampled.Control;
import javax.sound.sampled.FloatControl;
import javax.sound.sampled.LineUnavailableException;
import javax.sound.sampled.Mixer; import javax.sound.sampled.Mixer;
import javax.sound.sampled.SourceDataLine;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import source.mixer.MixerChannel; import source.mixer.MixerChannel;
import audio.AudioEvent;
import audio.AudioFormats; import audio.AudioFormats;
import audio.AudioPacket; import audio.AudioPacket;
import audio.AudioPacket.Type;
import controller.ThreadPoolManager; import controller.ThreadPoolManager;
import controller.ThreadPoolManager.ThreadType;
/** /**
* Mono Audio output in stereo format with automatic flow control based on the * Stereo audio output implementation.
* availability of audio data packets.
*/ */
public class StereoAudioOutput extends AudioOutput public class StereoAudioOutput extends AudioOutput
{ {
private final static Logger mLog = LoggerFactory.getLogger( StereoAudioOutput.class );
/* Output line buffer size is set at one half second of audio at 48 kHz and
* two bytes per sample for two channels */
private final static int BUFFER_SIZE = 96000; private final static int BUFFER_SIZE = 96000;
/* The queue processor will run every 20 milliseconds checking for inbound
* audio and automatically starting or stopping audio playback. We set the
* data line available space threshold to start when we're 20 ms away from
* being full, and the stop threshold when 20 ms away from being empty */
private final static int SAMPLE_SIZE_40_MS = BUFFER_SIZE / 25;
private final static int START_THRESHOLD = SAMPLE_SIZE_40_MS;
private final static int STOP_THRESHOLD = BUFFER_SIZE - SAMPLE_SIZE_40_MS;
private SourceDataLine mOutput;
private FloatControl mGainControl;
private BooleanControl mMuteControl;
private AudioEvent mAudioStartEvent;
private AudioEvent mAudioStopEvent;
private AudioEvent mAudioContinuationEvent;
private MixerChannel mMixerChannel;
private ScheduledFuture<?> mProcessorTask;
public StereoAudioOutput( ThreadPoolManager threadPoolManager, public StereoAudioOutput( ThreadPoolManager threadPoolManager,
Mixer mixer, MixerChannel channel ) Mixer mixer, MixerChannel channel )
{ {
super( threadPoolManager ); super( threadPoolManager,
mixer,
assert( channel == MixerChannel.LEFT || channel == MixerChannel.RIGHT ); channel,
AudioFormats.PCM_SIGNED_48KHZ_16BITS_STEREO,
mMixerChannel = channel; AudioFormats.STEREO_SOURCE_DATALINE_INFO,
BUFFER_SIZE );
mAudioStartEvent = new AudioEvent( AudioEvent.Type.AUDIO_STARTED,
getChannelName() );
mAudioStopEvent = new AudioEvent( AudioEvent.Type.AUDIO_STOPPED,
getChannelName() );
mAudioContinuationEvent = new AudioEvent( AudioEvent.Type.AUDIO_CONTINUATION,
getChannelName() );
try
{
mOutput = (SourceDataLine)mixer.getLine(
AudioFormats.STEREO_SOURCE_DATALINE_INFO );
if( mOutput != null )
{
mOutput.open( AudioFormats.PCM_SIGNED_48KHZ_16BITS_STEREO, BUFFER_SIZE );
mCanProcessAudio = true;
}
}
catch ( LineUnavailableException e )
{
mLog.error( "Couldn't obtain source data line for 48kHz PCM stereo "
+ "audio output - mixer [" + mixer.getMixerInfo().getName() +
"] channel [" + mMixerChannel.name() + "]" );
}
if( mOutput != null )
{
try
{
Control gain = mOutput.getControl( FloatControl.Type.MASTER_GAIN );
mGainControl = (FloatControl)gain;
}
catch( IllegalArgumentException iae )
{
mLog.warn( "Couldn't obtain MASTER GAIN control for stereo line [" +
mixer.getMixerInfo().getName() + " | " + channel.name() + "]" );
}
try
{
Control mute = mOutput.getControl( BooleanControl.Type.MUTE );
mMuteControl = (BooleanControl)mute;
}
catch( IllegalArgumentException iae )
{
mLog.warn( "Couldn't obtain MUTE control for stereo line [" +
mixer.getMixerInfo().getName() + " | " + channel.name() + "]" );
}
/* Run the queue processor task every 40 milliseconds */
mProcessorTask = mThreadPoolManager.scheduleFixedRate(
ThreadType.AUDIO_PROCESSING, new QueueProcessor(),
40, TimeUnit.MILLISECONDS );
}
} }
@Override /**
public String getChannelName() * Converts the audio packet data into stereo audio frames with the mixer
* channel containing the audio and the other channel containing zero
* valued (silent) samples.
*/
protected ByteBuffer convert( AudioPacket packet )
{ {
return mMixerChannel.getLabel(); if( packet.hasAudioBuffer() )
}
public void dispose()
{
if( mProcessorTask != null && mThreadPoolManager != null )
{ {
mThreadPoolManager.cancel( mProcessorTask ); float[] samples = packet.getAudioBuffer().getSamples();
}
mProcessorTask = null; /* Little-endian byte buffer */
ByteBuffer buffer = ByteBuffer.allocate( samples.length * 4 )
.order( ByteOrder.LITTLE_ENDIAN );
super.dispose(); ShortBuffer shortBuffer = buffer.asShortBuffer();
if( mOutput != null ) if( getMixerChannel() == MixerChannel.LEFT )
{
mOutput.close();
}
mOutput = null;
mGainControl = null;
mMuteControl = null;
}
private ByteBuffer convert( float[] samples )
{
/* Little-endian byte buffer */
ByteBuffer buffer = ByteBuffer.allocate( samples.length * 4 )
.order( ByteOrder.LITTLE_ENDIAN );
ShortBuffer shortBuffer = buffer.asShortBuffer();
if( mMixerChannel == MixerChannel.LEFT )
{
for( float sample: samples )
{ {
shortBuffer.put( (short)( sample * Short.MAX_VALUE ) ); for( float sample: samples )
shortBuffer.put( (short)0 );
}
}
else
{
for( float sample: samples )
{
shortBuffer.put( (short)0 );
shortBuffer.put( (short)( sample * Short.MAX_VALUE ) );
}
}
return buffer;
}
public class QueueProcessor implements Runnable
{
private AtomicBoolean mProcessing = new AtomicBoolean();
public QueueProcessor()
{
}
@Override
public void run()
{
try
{
/* The mProcessing flag ensures that only one instance of the
* processor can run at any given time */
if( mProcessing.compareAndSet( false, true ) )
{ {
List<AudioPacket> packets = new ArrayList<AudioPacket>(); shortBuffer.put( (short)( sample * Short.MAX_VALUE ) );
shortBuffer.put( (short)0 );
mBuffer.drainTo( packets );
if( packets.size() > 0 )
{
broadcast( mAudioContinuationEvent );
for( AudioPacket packet: packets )
{
if( mCanProcessAudio )
{
if( isSquelched() )
{
checkStop();
}
else if( packet.getType() == Type.AUDIO )
{
ByteBuffer buffer = convert(
packet.getAudioBuffer().getSamples() );
mOutput.write( buffer.array(), 0,
buffer.array().length );
checkStart();
broadcast( packet.getAudioMetadata() );
mLastActivity = System.currentTimeMillis();
}
}
}
}
else
{
checkStop();
}
mProcessing.set( false );
} }
} }
catch( Exception e ) else
{ {
mLog.error( "Error while processing audio buffers", e ); for( float sample: samples )
{
shortBuffer.put( (short)0 );
shortBuffer.put( (short)( sample * Short.MAX_VALUE ) );
}
} }
return buffer;
} }
private void checkStart() return null;
{
if( mCanProcessAudio && !isSquelched() &&
!mOutput.isRunning() && mOutput.available() <= START_THRESHOLD )
{
mOutput.start();
mAudioEventBroadcaster.broadcast( mAudioStartEvent );
}
}
private void checkStop()
{
if( mCanProcessAudio && mOutput.isRunning() &&
( mOutput.available() >= STOP_THRESHOLD || isSquelched() ) )
{
mOutput.drain();
mOutput.stop();
mAudioEventBroadcaster.broadcast( mAudioStopEvent );
}
}
}
@Override
public void setMuted( boolean muted )
{
if( mMuteControl != null )
{
mMuteControl.setValue( muted );
broadcast( new AudioEvent( muted ? AudioEvent.Type.AUDIO_MUTED :
AudioEvent.Type.AUDIO_UNMUTED, getChannelName() ) );
}
}
@Override
public boolean isMuted()
{
if( mMuteControl != null )
{
return mMuteControl.getValue();
}
return false;
}
@Override
public FloatControl getGainControl()
{
return mGainControl;
}
@Override
public boolean hasGainControl()
{
return mGainControl != null;
} }
} }

View File

@@ -58,6 +58,16 @@ public class MessageFramer implements Listener<Boolean>,
mMessageLength = messageLength; mMessageLength = messageLength;
} }
public void reset()
{
for( MessageAssembler assembler: mMessageAssemblers )
{
assembler.dispose();
}
mMessageAssemblers.clear();
}
public void dispose() public void dispose()
{ {
mBroadcaster.dispose(); mBroadcaster.dispose();

View File

@@ -64,11 +64,13 @@ public class ThreadPoolManager
mExecutor.schedule( command, delay, unit ); mExecutor.schedule( command, delay, unit );
} }
public void cancel( ScheduledFuture<?> task ) public boolean cancel( ScheduledFuture<?> task )
{ {
task.cancel( true ); boolean success = task.cancel( true );
mTasks.remove( task ); mTasks.remove( task );
return success;
} }
public int getTaskCount( ThreadType type ) public int getTaskCount( ThreadType type )

View File

@@ -297,6 +297,9 @@ public class Channel extends Configuration
/* Broadcast channel deleted event */ /* Broadcast channel deleted event */
fireChannelEvent( Event.CHANNEL_DELETED ); fireChannelEvent( Event.CHANNEL_DELETED );
mProcessingChain.dispose();
mProcessingChain = null;
} }
/** /**
@@ -809,8 +812,7 @@ public class Channel extends Configuration
{ {
if( mProcessingChain == null ) if( mProcessingChain == null )
{ {
mProcessingChain = new ProcessingChain( mProcessingChain = new ProcessingChain( getName() );
mResourceManager.getThreadPoolManager() );
/* Get the optional alias list for the decode modules to use */ /* Get the optional alias list for the decode modules to use */
AliasList aliasList = mResourceManager.getPlaylistManager() AliasList aliasList = mResourceManager.getPlaylistManager()
@@ -901,6 +903,8 @@ public class Channel extends Configuration
{ {
if( !mEnabled && mProcessingChain != null ) if( !mEnabled && mProcessingChain != null )
{ {
fireChannelEvent( Event.CHANNEL_PROCESSING_STOPPED );
mProcessingChain.stop(); mProcessingChain.stop();
if( mChannelType == ChannelType.STANDARD ) if( mChannelType == ChannelType.STANDARD )
@@ -910,8 +914,6 @@ public class Channel extends Configuration
} }
mSelected = false; mSelected = false;
fireChannelEvent( Event.CHANNEL_PROCESSING_STOPPED );
} }
} }

View File

@@ -12,8 +12,9 @@ import source.Source;
public class InstrumentableProcessingChain extends ProcessingChain implements Instrumentable public class InstrumentableProcessingChain extends ProcessingChain implements Instrumentable
{ {
public InstrumentableProcessingChain() public InstrumentableProcessingChain( String name )
{ {
super( name );
} }
public void setSource( Source source ) throws IllegalStateException public void setSource( Source source ) throws IllegalStateException

View File

@@ -99,7 +99,7 @@ public class DecoderViewFrame extends JInternalFrame
mDecodeConfig = decodeConfig; mDecodeConfig = decodeConfig;
mProcessingChain = new InstrumentableProcessingChain(); mProcessingChain = new InstrumentableProcessingChain( "instrumented" );
List<Module> modules = DecoderFactory List<Module> modules = DecoderFactory
.getModules( ChannelType.STANDARD, .getModules( ChannelType.STANDARD,

View File

@@ -19,7 +19,8 @@ package module;
import java.util.ArrayList; import java.util.ArrayList;
import java.util.List; import java.util.List;
import java.util.concurrent.ScheduledFuture; import java.util.concurrent.Executors;
import java.util.concurrent.ScheduledExecutorService;
import java.util.concurrent.atomic.AtomicBoolean; import java.util.concurrent.atomic.AtomicBoolean;
import message.IMessageListener; import message.IMessageListener;
@@ -61,7 +62,7 @@ import audio.metadata.Metadata;
import audio.squelch.ISquelchStateListener; import audio.squelch.ISquelchStateListener;
import audio.squelch.ISquelchStateProvider; import audio.squelch.ISquelchStateProvider;
import audio.squelch.SquelchState; import audio.squelch.SquelchState;
import controller.ThreadPoolManager; import controller.NamingThreadFactory;
import controller.channel.ChannelEvent; import controller.channel.ChannelEvent;
import controller.channel.IChannelEventListener; import controller.channel.IChannelEventListener;
import controller.channel.IChannelEventProvider; import controller.channel.IChannelEventProvider;
@@ -99,22 +100,17 @@ public class ProcessingChain implements IChannelEventListener
private Broadcaster<RealBuffer> mRealBufferBroadcaster = new Broadcaster<>(); private Broadcaster<RealBuffer> mRealBufferBroadcaster = new Broadcaster<>();
private Broadcaster<SquelchState> mSquelchStateBroadcaster = new Broadcaster<>(); private Broadcaster<SquelchState> mSquelchStateBroadcaster = new Broadcaster<>();
private ThreadPoolManager mThreadPoolManager; private ScheduledExecutorService mScheduledExecutorService;
private ScheduledFuture<?> mBufferProcessingTask;
private AtomicBoolean mRunning = new AtomicBoolean(); private AtomicBoolean mRunning = new AtomicBoolean();
protected Source mSource; protected Source mSource;
private List<Module> mModules = new ArrayList<>(); private List<Module> mModules = new ArrayList<>();
private IFrequencyCorrectionController mFrequencyCorrectionController; private IFrequencyCorrectionController mFrequencyCorrectionController;
public ProcessingChain() public ProcessingChain( String name )
{ {
this( new ThreadPoolManager() ); mScheduledExecutorService = Executors.newScheduledThreadPool( 1,
} new NamingThreadFactory( "channel " + name ) );
public ProcessingChain( ThreadPoolManager threadManager )
{
mThreadPoolManager = threadManager;
} }
public void dispose() public void dispose()
@@ -128,14 +124,9 @@ public class ProcessingChain implements IChannelEventListener
mModules.clear(); mModules.clear();
// mComplexReceiver.dispose(); mScheduledExecutorService.shutdownNow();
// mComplexReceiver = null; mScheduledExecutorService = null;
//
// mRealReceiver.dispose();
// mRealReceiver = null;
mThreadPoolManager = null;
mBufferProcessingTask = null;
mFrequencyCorrectionController = null; mFrequencyCorrectionController = null;
mAudioPacketBroadcaster.dispose(); mAudioPacketBroadcaster.dispose();
@@ -447,9 +438,11 @@ public class ProcessingChain implements IChannelEventListener
* of the decoder state's */ * of the decoder state's */
if( mSource instanceof TunerChannelSource ) if( mSource instanceof TunerChannelSource )
{ {
TunerChannelSource tunerChannelSource = (TunerChannelSource)mSource;
try try
{ {
long frequency = ((TunerChannelSource)mSource).getFrequency(); long frequency = tunerChannelSource.getFrequency();
mDecoderStateEventBroadcaster.broadcast( mDecoderStateEventBroadcaster.broadcast(
new DecoderStateEvent( this, Event.SOURCE_FREQUENCY, new DecoderStateEvent( this, Event.SOURCE_FREQUENCY,
@@ -459,6 +452,8 @@ public class ProcessingChain implements IChannelEventListener
{ {
mLog.error( "Error getting frequency from tuner channel source", e ); mLog.error( "Error getting frequency from tuner channel source", e );
} }
tunerChannelSource.start( mScheduledExecutorService );
} }
} }
else else
@@ -496,12 +491,6 @@ public class ProcessingChain implements IChannelEventListener
mSource = null; mSource = null;
} }
if( mBufferProcessingTask != null )
{
mThreadPoolManager.cancel( mBufferProcessingTask );
mBufferProcessingTask = null;
}
/* Stop each of the modules */ /* Stop each of the modules */
for( Module module: mModules ) for( Module module: mModules )
{ {

View File

@@ -228,7 +228,8 @@ public class DecoderFactory
Sync sync = mptConfig.getSync(); Sync sync = mptConfig.getSync();
modules.add( new MPT1327Decoder( aliasList, sync ) ); modules.add( new MPT1327Decoder( aliasList, sync ) );
modules.add( new MPT1327DecoderState( aliasList, channelMap, channelType ) ); modules.add( new MPT1327DecoderState( aliasList, channelMap,
channelType, mptConfig.getCallTimeout() * 1000 ) );
if( channelType == ChannelType.STANDARD ) if( channelType == ChannelType.STANDARD )
{ {

View File

@@ -310,6 +310,11 @@ public abstract class CallEvent implements Comparable<CallEvent>
@Override @Override
public int compareTo( CallEvent other ) public int compareTo( CallEvent other )
{ {
if( other != null && other == this )
{
return 0;
}
return Long.compare( mEventStart, other.getEventStartTime() ); return Long.compare( mEventStart, other.getEventStartTime() );
} }
} }

View File

@@ -112,6 +112,7 @@ public class LTRNetDecoder extends Decoder implements IRealBufferListener, Instr
@Override @Override
public void reset() public void reset()
{ {
mLTRMessageFramer.reset();
} }
@Override @Override

View File

@@ -76,6 +76,7 @@ public class LTRStandardDecoder extends Decoder implements IRealBufferListener
@Override @Override
public void reset() public void reset()
{ {
mLTRMessageFramer.reset();
} }
@Override @Override

View File

@@ -153,7 +153,9 @@ public class LTRStandardDecoderState extends DecoderState
case CA_ENDD: case CA_ENDD:
CallEndMessage end = (CallEndMessage)message; CallEndMessage end = (CallEndMessage)message;
int repeater = end.getHomeRepeater(); //Home channel is 31 for call end -- use the free channel
//as the call end channel
int repeater = end.getFree();
mLCNTracker.processCallChannel( repeater ); mLCNTracker.processCallChannel( repeater );

View File

@@ -301,8 +301,8 @@ public class MPT1327Decoder extends Decoder
@Override @Override
public void reset() public void reset()
{ {
// TODO Auto-generated method stub mControlMessageFramer.reset();
mTrafficMessageFramer.reset();
} }
@Override @Override

View File

@@ -26,9 +26,11 @@ import java.util.TreeSet;
import message.Message; import message.Message;
import module.decode.DecoderType; import module.decode.DecoderType;
import module.decode.config.DecodeConfiguration;
import module.decode.event.CallEvent; import module.decode.event.CallEvent;
import module.decode.event.CallEvent.CallEventType; import module.decode.event.CallEvent.CallEventType;
import module.decode.mpt1327.MPT1327Message.IdentType; import module.decode.mpt1327.MPT1327Message.IdentType;
import module.decode.state.ChangeChannelTimeoutEvent;
import module.decode.state.ChangedAttribute; import module.decode.state.ChangedAttribute;
import module.decode.state.DecoderState; import module.decode.state.DecoderState;
import module.decode.state.DecoderStateEvent; import module.decode.state.DecoderStateEvent;
@@ -41,6 +43,8 @@ import org.slf4j.LoggerFactory;
import alias.Alias; import alias.Alias;
import alias.AliasList; import alias.AliasList;
import audio.metadata.Metadata;
import audio.metadata.MetadataType;
import controller.channel.Channel.ChannelType; import controller.channel.Channel.ChannelType;
import controller.channel.map.ChannelMap; import controller.channel.map.ChannelMap;
@@ -60,15 +64,18 @@ public class MPT1327DecoderState extends DecoderState
private ChannelMap mChannelMap; private ChannelMap mChannelMap;
private long mFrequency = 0; private long mFrequency = 0;
private long mCallTimeout;
public MPT1327DecoderState( AliasList aliasList, ChannelMap channelMap, public MPT1327DecoderState( AliasList aliasList,
ChannelType channelType ) ChannelMap channelMap,
ChannelType channelType,
long callTimeout )
{ {
super( aliasList ); super( aliasList );
mChannelMap = channelMap; mChannelMap = channelMap;
mChannelType = channelType; mChannelType = channelType;
mCallTimeout = callTimeout;
} }
@Override @Override
@@ -252,8 +259,42 @@ public class MPT1327DecoderState extends DecoderState
/* Traffic Channel Events */ /* Traffic Channel Events */
case CLEAR: case CLEAR:
mChannelNumber = mpt.getChannel();
broadcast( new DecoderStateEvent( this, Event.END, State.FADE ) );
break;
case MAINT: case MAINT:
broadcast( new DecoderStateEvent( this, Event.END, State.END ) ); if( mChannelType == ChannelType.STANDARD )
{
/**
* When we receive a MAINT message and we're configured
* as a standard channel, we need to apply the call
* timeout specified by the user. Otherwise we'll
* be using the shorter default call timeout
*/
broadcast( new ChangeChannelTimeoutEvent( this,
mChannelType, mCallTimeout ) );
if( mCurrentCallEvent == null )
{
mCurrentCallEvent = new MPT1327CallEvent.Builder( CallEventType.CALL )
.aliasList( getAliasList() )
.channel( String.valueOf( mChannelNumber ) )
.details( "MONITORED TRAFFIC CHANNEL" )
.frequency( mFrequency )
.to( mpt.getToID() )
.build();
broadcast( mCurrentCallEvent );
}
broadcast(new Metadata(MetadataType.TO,
mpt.getToID(), mpt.getToIDAlias(), true));
broadcast( new DecoderStateEvent( this, Event.START, State.CALL ) );
setToTalkgroup( mpt.getToID() );
}
break; break;
default: default:
break; break;
@@ -276,6 +317,25 @@ public class MPT1327DecoderState extends DecoderState
mToTalkgroup = null; mToTalkgroup = null;
broadcast( ChangedAttribute.TO_TALKGROUP ); broadcast( ChangedAttribute.TO_TALKGROUP );
/**
* If this is a standard channel, reset the fade timeout to the default
* timeout. Once processing is underway, if we get a MAINT message,
* this indicates we're processing a traffic channel as a standard
* channel, so we'll issue a different call timeout then.
*/
if( mChannelType == ChannelType.STANDARD )
{
broadcast( new ChangeChannelTimeoutEvent( this, mChannelType,
DecodeConfiguration.DEFAULT_CALL_TIMEOUT_SECONDS * 1000 ) );
if( mCurrentCallEvent != null )
{
mCurrentCallEvent.end();
broadcast( mCurrentCallEvent );
mCurrentCallEvent = null;
}
}
} }
public String getSite() public String getSite()

View File

@@ -42,6 +42,8 @@ import module.decode.p25.message.ldu.lc.TelephoneInterconnectVoiceChannelUser;
import module.decode.p25.message.ldu.lc.UnitToUnitVoiceChannelUser; import module.decode.p25.message.ldu.lc.UnitToUnitVoiceChannelUser;
import module.decode.p25.message.pdu.PDUMessage; import module.decode.p25.message.pdu.PDUMessage;
import module.decode.p25.message.pdu.confirmed.PDUConfirmedMessage; import module.decode.p25.message.pdu.confirmed.PDUConfirmedMessage;
import module.decode.p25.message.pdu.confirmed.PDUTypeUnknown;
import module.decode.p25.message.pdu.confirmed.PacketData;
import module.decode.p25.message.pdu.confirmed.SNDCPActivateTDSContextAccept; import module.decode.p25.message.pdu.confirmed.SNDCPActivateTDSContextAccept;
import module.decode.p25.message.pdu.confirmed.SNDCPActivateTDSContextReject; import module.decode.p25.message.pdu.confirmed.SNDCPActivateTDSContextReject;
import module.decode.p25.message.pdu.confirmed.SNDCPActivateTDSContextRequest; import module.decode.p25.message.pdu.confirmed.SNDCPActivateTDSContextRequest;
@@ -405,12 +407,12 @@ public class P25DecoderState extends DecoderState
{ {
if( hdu.isValid() ) if( hdu.isValid() )
{ {
State state = hdu.isEncrypted() ? State.ENCRYPTED : State.CALL; broadcast( new DecoderStateEvent( this, Event.START, State.CALL ) );
broadcast( new DecoderStateEvent( this, Event.START, state ) );
String to = hdu.getToID(); String to = hdu.getToID();
updateTo( to );
if( mCurrentCallEvent == null ) if( mCurrentCallEvent == null )
{ {
mCurrentCallEvent = new P25CallEvent.Builder( CallEventType.CALL ) mCurrentCallEvent = new P25CallEvent.Builder( CallEventType.CALL )
@@ -424,7 +426,6 @@ public class P25DecoderState extends DecoderState
broadcast( mCurrentCallEvent ); broadcast( mCurrentCallEvent );
} }
updateTo( to );
} }
} }
@@ -436,7 +437,7 @@ public class P25DecoderState extends DecoderState
{ {
if( tdulc.getOpcode() == LinkControlOpcode.CALL_TERMINATION_OR_CANCELLATION ) if( tdulc.getOpcode() == LinkControlOpcode.CALL_TERMINATION_OR_CANCELLATION )
{ {
broadcast( new DecoderStateEvent( this, Event.END, State.END ) ); broadcast( new DecoderStateEvent( this, Event.END, State.FADE ) );
if( mCurrentCallEvent != null ) if( mCurrentCallEvent != null )
{ {
@@ -614,12 +615,14 @@ public class P25DecoderState extends DecoderState
module.decode.p25.message.tdu.lc.GroupVoiceChannelUser gvcuser = module.decode.p25.message.tdu.lc.GroupVoiceChannelUser gvcuser =
(module.decode.p25.message.tdu.lc.GroupVoiceChannelUser)tdulc; (module.decode.p25.message.tdu.lc.GroupVoiceChannelUser)tdulc;
broadcast( new DecoderStateEvent( this, Event.CONTINUATION, broadcast( new DecoderStateEvent( this, Event.CONTINUATION, State.CALL ) );
( gvcuser.isEncrypted() ? State.ENCRYPTED : State.CALL ) ) );
String from = gvcuser.getSourceAddress(); String from = gvcuser.getSourceAddress();
String to = gvcuser.getGroupAddress(); String to = gvcuser.getGroupAddress();
updateFrom( from );
updateTo( to );
if( mCurrentCallEvent == null ) if( mCurrentCallEvent == null )
{ {
mCurrentCallEvent = new P25CallEvent.Builder( CallEventType.GROUP_CALL ) mCurrentCallEvent = new P25CallEvent.Builder( CallEventType.GROUP_CALL )
@@ -635,9 +638,6 @@ public class P25DecoderState extends DecoderState
broadcast( mCurrentCallEvent ); broadcast( mCurrentCallEvent );
} }
updateFrom( from );
updateTo( to );
} }
break; break;
case MESSAGE_UPDATE: case MESSAGE_UPDATE:
@@ -844,6 +844,8 @@ public class P25DecoderState extends DecoderState
String to = tivcu.getAddress(); String to = tivcu.getAddress();
updateTo( to );
if( mCurrentCallEvent == null ) if( mCurrentCallEvent == null )
{ {
mCurrentCallEvent = new P25CallEvent.Builder( CallEventType.TELEPHONE_INTERCONNECT ) mCurrentCallEvent = new P25CallEvent.Builder( CallEventType.TELEPHONE_INTERCONNECT )
@@ -857,8 +859,6 @@ public class P25DecoderState extends DecoderState
broadcast( mCurrentCallEvent ); broadcast( mCurrentCallEvent );
} }
updateTo( to );
} }
break; break;
case UNIT_AUTHENTICATION_COMMAND: case UNIT_AUTHENTICATION_COMMAND:
@@ -921,7 +921,10 @@ public class P25DecoderState extends DecoderState
(module.decode.p25.message.tdu.lc.UnitToUnitVoiceChannelUser)tdulc; (module.decode.p25.message.tdu.lc.UnitToUnitVoiceChannelUser)tdulc;
String from = uuvcu.getSourceAddress(); String from = uuvcu.getSourceAddress();
updateFrom( from );
String to = uuvcu.getTargetAddress(); String to = uuvcu.getTargetAddress();
updateTo( to );
if( mCurrentCallEvent != null ) if( mCurrentCallEvent != null )
{ {
@@ -935,9 +938,6 @@ public class P25DecoderState extends DecoderState
.to( to ) .to( to )
.build(); .build();
} }
updateFrom( from );
updateTo( to );
} }
break; break;
default: default:
@@ -949,35 +949,14 @@ public class P25DecoderState extends DecoderState
* Log optional vendor format messages that we don't yet support so that we * Log optional vendor format messages that we don't yet support so that we
* can understand those messages and eventually add support. * can understand those messages and eventually add support.
*/ */
private void logAlternateVendorMessage( P25Message p25 ) private void logAlternateVendorMessage( P25Message message )
{ {
// if( p25 instanceof TDULinkControlMessage ) // if( message.isValid() )
// { // {
// TDULinkControlMessage tdulc = (TDULinkControlMessage)p25; // mLog.info( "PLEASE NOTIFY DEVELOPER - UNRECOGNIZED P25 VENDOR FORMAT -"
// // + " DUID:" + message.getDUID()
// mLog.debug( "TDULC - VENDOR [" + tdulc.getVendor() + // + " MSG:" + message.getBinaryMessage()
// "] SPECIFIC FORMAT FOR " + tdulc.getVendorOpcode().getLabel() + // + " CLASS:" + message.getClass() );
// " " + tdulc.getBinaryMessage() );
// }
// else if( p25 instanceof LDU1Message )
// {
// LDU1Message ldu1 = (LDU1Message)p25;
//
// mLog.debug( "LDU1 - VENDOR [" + ldu1.getVendor() +
// "] SPECIFIC FORMAT FOR " + ldu1.getVendorOpcode().getLabel() +
// " " + ldu1.getBinaryMessage() );
// }
// else if( p25 instanceof PDUMessage )
// {
// PDUMessage pdu = (PDUMessage)p25;
//
// mLog.debug( "PDU - VENDOR [" + pdu.getVendor() +
// "] SPECIFIC FORMAT FOR " + pdu.getVendorOpcode().getLabel() +
// " " + pdu.getBinaryMessage() );
// }
// else
// {
// mLog.debug( "P25 - ALTERNATE VENDOR MESSAGE [" + p25.getClass() + "]" );
// } // }
} }
@@ -988,15 +967,11 @@ public class P25DecoderState extends DecoderState
*/ */
private void processLDU( LDUMessage ldu ) private void processLDU( LDUMessage ldu )
{ {
State state = ( ldu.isValid() && ldu.isEncrypted() ) ? broadcast( new DecoderStateEvent( this, Event.CONTINUATION, State.CALL ) );
State.ENCRYPTED : State.CALL;
broadcast( new DecoderStateEvent( this, Event.CONTINUATION, state ) );
if( mCurrentCallEvent == null ) if( mCurrentCallEvent == null )
{ {
mCurrentCallEvent = new P25CallEvent.Builder( mCurrentCallEvent = new P25CallEvent.Builder( CallEventType.CALL )
( state == State.CALL ) ? CallEventType.CALL : CallEventType.ENCRYPTED_CALL )
.aliasList( getAliasList() ) .aliasList( getAliasList() )
.channel( mCurrentChannel ) .channel( mCurrentChannel )
.frequency( mCurrentChannelFrequency ) .frequency( mCurrentChannelFrequency )
@@ -1056,7 +1031,7 @@ public class P25DecoderState extends DecoderState
} }
break; break;
case CALL_TERMINATION_OR_CANCELLATION: case CALL_TERMINATION_OR_CANCELLATION:
broadcast( new DecoderStateEvent( this, Event.END, State.END ) ); broadcast( new DecoderStateEvent( this, Event.END, State.FADE ) );
mCurrentCallEvent = null; mCurrentCallEvent = null;
if( !( ldu instanceof CallTermination ) ) if( !( ldu instanceof CallTermination ) )
@@ -1114,42 +1089,43 @@ public class P25DecoderState extends DecoderState
} }
break; break;
case GROUP_VOICE_CHANNEL_UPDATE: case GROUP_VOICE_CHANNEL_UPDATE:
if( mChannelType == ChannelType.STANDARD && if( ldu instanceof module.decode.p25.message.ldu.lc.GroupVoiceChannelUpdate )
ldu instanceof module.decode.p25.message.ldu.lc.GroupVoiceChannelUpdate )
{ {
module.decode.p25.message.ldu.lc.GroupVoiceChannelUpdate gvcu = module.decode.p25.message.ldu.lc.GroupVoiceChannelUpdate gvcu =
(module.decode.p25.message.ldu.lc.GroupVoiceChannelUpdate)ldu; (module.decode.p25.message.ldu.lc.GroupVoiceChannelUpdate)ldu;
String userA = gvcu.getGroupAddressA(); String userA = gvcu.getGroupAddressA();
if( !mCallDetectTalkgroups.contains( userA ) )
{
broadcast( new P25CallEvent.Builder( CallEventType.CALL_DETECT )
.aliasList( getAliasList() )
.channel( gvcu.getChannelA() )
.details( ( gvcu.isEncrypted() ? "ENCRYPTED" : "" ) )
.frequency( gvcu.getDownlinkFrequencyA() )
.to( userA )
.build() );
mCallDetectTalkgroups.add( userA );
}
String userB = gvcu.getGroupAddressB(); String userB = gvcu.getGroupAddressB();
if( userB != null && if( mChannelType == ChannelType.STANDARD )
!userB.contentEquals( "0000" ) &&
!mCallDetectTalkgroups.contains( userB ) )
{ {
broadcast( new P25CallEvent.Builder( CallEventType.CALL_DETECT ) if( !mCallDetectTalkgroups.contains( userA ) )
.aliasList( getAliasList() ) {
.channel( gvcu.getChannelB() ) broadcast( new P25CallEvent.Builder( CallEventType.CALL_DETECT )
.details( ( gvcu.isEncrypted() ? "ENCRYPTED" : "" ) ) .aliasList( getAliasList() )
.frequency( gvcu.getDownlinkFrequencyB() ) .channel( gvcu.getChannelA() )
.to( userB ) .details( ( gvcu.isEncrypted() ? "ENCRYPTED" : "" ) )
.build() ); .frequency( gvcu.getDownlinkFrequencyA() )
.to( userA )
.build() );
mCallDetectTalkgroups.add( userB ); mCallDetectTalkgroups.add( userA );
}
if( userB != null &&
!userB.contentEquals( "0000" ) &&
!mCallDetectTalkgroups.contains( userB ) )
{
broadcast( new P25CallEvent.Builder( CallEventType.CALL_DETECT )
.aliasList( getAliasList() )
.channel( gvcu.getChannelB() )
.details( ( gvcu.isEncrypted() ? "ENCRYPTED" : "" ) )
.frequency( gvcu.getDownlinkFrequencyB() )
.to( userB )
.build() );
mCallDetectTalkgroups.add( userB );
}
} }
} }
else else
@@ -1158,25 +1134,27 @@ public class P25DecoderState extends DecoderState
} }
break; break;
case GROUP_VOICE_CHANNEL_UPDATE_EXPLICIT: case GROUP_VOICE_CHANNEL_UPDATE_EXPLICIT:
if( mChannelType == ChannelType.STANDARD && if( ldu instanceof module.decode.p25.message.ldu.lc.GroupVoiceChannelUpdateExplicit )
ldu instanceof module.decode.p25.message.ldu.lc.GroupVoiceChannelUpdateExplicit )
{ {
module.decode.p25.message.ldu.lc.GroupVoiceChannelUpdateExplicit gvcue = module.decode.p25.message.ldu.lc.GroupVoiceChannelUpdateExplicit gvcue =
(module.decode.p25.message.ldu.lc.GroupVoiceChannelUpdateExplicit)ldu; (module.decode.p25.message.ldu.lc.GroupVoiceChannelUpdateExplicit)ldu;
String group = gvcue.getGroupAddress(); String group = gvcue.getGroupAddress();
if( !mCallDetectTalkgroups.contains( group ) ) if( mChannelType == ChannelType.STANDARD )
{ {
broadcast( new P25CallEvent.Builder( CallEventType.CALL_DETECT ) if( !mCallDetectTalkgroups.contains( group ) )
.aliasList( getAliasList() ) {
.channel( gvcue.getTransmitChannel() ) broadcast( new P25CallEvent.Builder( CallEventType.CALL_DETECT )
.details( ( gvcue.isEncrypted() ? "ENCRYPTED" : "" ) ) .aliasList( getAliasList() )
.frequency( gvcue.getDownlinkFrequency() ) .channel( gvcue.getTransmitChannel() )
.to( group ) .details( ( gvcue.isEncrypted() ? "ENCRYPTED" : "" ) )
.build() ); .frequency( gvcue.getDownlinkFrequency() )
.to( group )
.build() );
mCallDetectTalkgroups.add( group ); mCallDetectTalkgroups.add( group );
}
} }
} }
else else
@@ -1186,29 +1164,32 @@ public class P25DecoderState extends DecoderState
break; break;
case GROUP_VOICE_CHANNEL_USER: case GROUP_VOICE_CHANNEL_USER:
/* Indicates the current user of the current channel */ /* Indicates the current user of the current channel */
if( mChannelType == ChannelType.STANDARD && if( ldu instanceof module.decode.p25.message.ldu.lc.GroupVoiceChannelUser )
ldu instanceof module.decode.p25.message.ldu.lc.GroupVoiceChannelUser )
{ {
module.decode.p25.message.ldu.lc.GroupVoiceChannelUser gvcuser = module.decode.p25.message.ldu.lc.GroupVoiceChannelUser gvcuser =
(module.decode.p25.message.ldu.lc.GroupVoiceChannelUser)ldu; (module.decode.p25.message.ldu.lc.GroupVoiceChannelUser)ldu;
if( mCurrentCallEvent.getCallEventType() != CallEventType.GROUP_CALL )
{
mCurrentCallEvent.setCallEventType( CallEventType.GROUP_CALL );
broadcast( mCurrentCallEvent );
}
if( mCurrentCallEvent.getDetails() == null )
{
mCurrentCallEvent.setDetails(
( gvcuser.isEncrypted() ? "ENCRYPTED " : "" ) +
( gvcuser.isEmergency() ? "EMERGENCY " : "" ) );
broadcast( mCurrentCallEvent );
}
updateFrom( gvcuser.getSourceAddress() ); updateFrom( gvcuser.getSourceAddress() );
updateTo( gvcuser.getGroupAddress() ); updateTo( gvcuser.getGroupAddress() );
if( mChannelType == ChannelType.STANDARD )
{
if( mCurrentCallEvent.getCallEventType() != CallEventType.GROUP_CALL )
{
mCurrentCallEvent.setCallEventType( CallEventType.GROUP_CALL );
broadcast( mCurrentCallEvent );
}
if( mCurrentCallEvent.getDetails() == null )
{
mCurrentCallEvent.setDetails(
( gvcuser.isEncrypted() ? "ENCRYPTED " : "" ) +
( gvcuser.isEmergency() ? "EMERGENCY " : "" ) );
broadcast( mCurrentCallEvent );
}
}
} }
else else
{ {
@@ -1404,6 +1385,8 @@ public class P25DecoderState extends DecoderState
TelephoneInterconnectVoiceChannelUser tivcu = TelephoneInterconnectVoiceChannelUser tivcu =
(TelephoneInterconnectVoiceChannelUser)ldu; (TelephoneInterconnectVoiceChannelUser)ldu;
updateTo( tivcu.getAddress() );
if( mCurrentCallEvent.getCallEventType() != CallEventType.TELEPHONE_INTERCONNECT ) if( mCurrentCallEvent.getCallEventType() != CallEventType.TELEPHONE_INTERCONNECT )
{ {
mCurrentCallEvent.setCallEventType( CallEventType.TELEPHONE_INTERCONNECT ); mCurrentCallEvent.setCallEventType( CallEventType.TELEPHONE_INTERCONNECT );
@@ -1418,8 +1401,6 @@ public class P25DecoderState extends DecoderState
broadcast( mCurrentCallEvent ); broadcast( mCurrentCallEvent );
} }
updateTo( tivcu.getAddress() );
} }
else else
{ {
@@ -1485,6 +1466,10 @@ public class P25DecoderState extends DecoderState
UnitToUnitVoiceChannelUser uuvcu = UnitToUnitVoiceChannelUser uuvcu =
(UnitToUnitVoiceChannelUser)ldu; (UnitToUnitVoiceChannelUser)ldu;
updateFrom( uuvcu.getSourceAddress() );
updateTo( uuvcu.getTargetAddress() );
if( mCurrentCallEvent.getCallEventType() != CallEventType.UNIT_TO_UNIT_CALL ) if( mCurrentCallEvent.getCallEventType() != CallEventType.UNIT_TO_UNIT_CALL )
{ {
mCurrentCallEvent.setCallEventType( CallEventType.UNIT_TO_UNIT_CALL ); mCurrentCallEvent.setCallEventType( CallEventType.UNIT_TO_UNIT_CALL );
@@ -1499,9 +1484,6 @@ public class P25DecoderState extends DecoderState
broadcast( mCurrentCallEvent ); broadcast( mCurrentCallEvent );
} }
updateFrom( uuvcu.getSourceAddress() );
updateTo( uuvcu.getTargetAddress() );
} }
else else
{ {
@@ -1520,7 +1502,7 @@ public class P25DecoderState extends DecoderState
private void processTSBK( TSBKMessage tsbk ) private void processTSBK( TSBKMessage tsbk )
{ {
/* Trunking Signalling Block Messages - indicates Control Channel */ /* Trunking Signalling Block Messages - indicates Control Channel */
broadcast( new DecoderStateEvent( this, Event.START, State.CONTROL ) ); broadcast( new DecoderStateEvent( this, Event.CONTINUATION, State.CONTROL ) );
if( tsbk.getVendor() == Vendor.STANDARD ) if( tsbk.getVendor() == Vendor.STANDARD )
{ {
@@ -1649,7 +1631,11 @@ public class P25DecoderState extends DecoderState
*/ */
private void processPDU( PDUMessage pdu ) private void processPDU( PDUMessage pdu )
{ {
if( pdu instanceof PDUConfirmedMessage ) if( pdu instanceof PacketData || pdu instanceof PDUTypeUnknown )
{
broadcast( new DecoderStateEvent( this, Event.CONTINUATION, State.DATA ) );
}
else if( pdu instanceof PDUConfirmedMessage )
{ {
broadcast( new DecoderStateEvent( this, Event.CONTINUATION, State.DATA ) ); broadcast( new DecoderStateEvent( this, Event.CONTINUATION, State.DATA ) );
@@ -1657,7 +1643,7 @@ public class P25DecoderState extends DecoderState
switch( pduc.getPDUType() ) switch( pduc.getPDUType() )
{ {
case SN_ACTIVATE_TDS_CONTEXT_ACCEPT: case SNDCP_ACTIVATE_TDS_CONTEXT_ACCEPT:
SNDCPActivateTDSContextAccept satca = SNDCPActivateTDSContextAccept satca =
(SNDCPActivateTDSContextAccept)pduc; (SNDCPActivateTDSContextAccept)pduc;
@@ -1669,7 +1655,7 @@ public class P25DecoderState extends DecoderState
.to( satca.getLogicalLinkID() ) .to( satca.getLogicalLinkID() )
.build() ); .build() );
break; break;
case SN_ACTIVATE_TDS_CONTEXT_REJECT: case SNDCP_ACTIVATE_TDS_CONTEXT_REJECT:
SNDCPActivateTDSContextReject satcr = SNDCPActivateTDSContextReject satcr =
(SNDCPActivateTDSContextReject)pduc; (SNDCPActivateTDSContextReject)pduc;
@@ -1694,7 +1680,7 @@ public class P25DecoderState extends DecoderState
.from( satcreq.getLogicalLinkID() ) .from( satcreq.getLogicalLinkID() )
.build() ); .build() );
break; break;
case SN_DEACTIVATE_TDS_CONTEXT_ACCEPT: case SNDCP_DEACTIVATE_TDS_CONTEXT_ACCEPT:
SNDCPDeactivateTDSContext sdtca = SNDCPDeactivateTDSContext sdtca =
(SNDCPDeactivateTDSContext)pduc; (SNDCPDeactivateTDSContext)pduc;
@@ -1706,7 +1692,7 @@ public class P25DecoderState extends DecoderState
.from( sdtca.getLogicalLinkID() ) .from( sdtca.getLogicalLinkID() )
.build() ); .build() );
break; break;
case SN_DEACTIVATE_TDS_CONTEXT_REQUEST: case SNDCP_DEACTIVATE_TDS_CONTEXT_REQUEST:
SNDCPDeactivateTDSContext sdtcreq = SNDCPDeactivateTDSContext sdtcreq =
(SNDCPDeactivateTDSContext)pduc; (SNDCPDeactivateTDSContext)pduc;
@@ -1718,7 +1704,7 @@ public class P25DecoderState extends DecoderState
.from( sdtcreq.getLogicalLinkID() ) .from( sdtcreq.getLogicalLinkID() )
.build() ); .build() );
break; break;
case SN_RF_CONFIRMED_DATA: case SNDCP_RF_CONFIRMED_DATA:
SNDCPUserData sud = (SNDCPUserData)pduc; SNDCPUserData sud = (SNDCPUserData)pduc;
StringBuilder sbFrom = new StringBuilder(); StringBuilder sbFrom = new StringBuilder();
@@ -1747,10 +1733,10 @@ public class P25DecoderState extends DecoderState
.to( pduc.getLogicalLinkID() ) .to( pduc.getLogicalLinkID() )
.build() ); .build() );
break; break;
case SN_RF_UNCONFIRMED_DATA: case SNDCP_RF_UNCONFIRMED_DATA:
break; break;
default: default:
mLog.debug( "PDUC - Unrecognized Message: " + pduc.toString() ); // mLog.debug( "PDUC - Unrecognized Message: " + pduc.toString() );
break; break;
} }
} }
@@ -3101,15 +3087,8 @@ public class P25DecoderState extends DecoderState
if( mToTalkgroup == null || !mToTalkgroup.contentEquals( to ) ) if( mToTalkgroup == null || !mToTalkgroup.contentEquals( to ) )
{ {
mToTalkgroup = to; mToTalkgroup = to;
broadcast( ChangedAttribute.TO_TALKGROUP );
if( mCurrentCallEvent != null && broadcast( ChangedAttribute.TO_TALKGROUP );
( mCurrentCallEvent.getToID() == null ||
!mCurrentCallEvent.getToID().contentEquals( to ) ) )
{
mCurrentCallEvent.setToID( to );
broadcast( mCurrentCallEvent );
}
if( hasAliasList() ) if( hasAliasList() )
{ {
@@ -3121,10 +3100,18 @@ public class P25DecoderState extends DecoderState
} }
broadcast( ChangedAttribute.TO_TALKGROUP_ALIAS ); broadcast( ChangedAttribute.TO_TALKGROUP_ALIAS );
}
/* Send audio metadata update */ /* Send audio metadata update */
broadcast( new Metadata( MetadataType.TO, mToTalkgroup, mToAlias, true ) ); broadcast( new Metadata( MetadataType.TO, mToTalkgroup, mToAlias, true ) );
if( mCurrentCallEvent != null &&
( mCurrentCallEvent.getToID() == null ||
!mCurrentCallEvent.getToID().contentEquals( to ) ) )
{
mCurrentCallEvent.setToID( to );
broadcast( mCurrentCallEvent );
}
}
} }
} }
@@ -3140,14 +3127,6 @@ public class P25DecoderState extends DecoderState
mFromTalkgroup = from; mFromTalkgroup = from;
broadcast( ChangedAttribute.FROM_TALKGROUP ); broadcast( ChangedAttribute.FROM_TALKGROUP );
if( mCurrentCallEvent != null &&
mCurrentCallEvent.getFromID() == null ||
!mCurrentCallEvent.getFromID().contentEquals( from ) )
{
mCurrentCallEvent.setFromID( from );
broadcast( mCurrentCallEvent );
}
if( hasAliasList() ) if( hasAliasList() )
{ {
mFromAlias = getAliasList().getTalkgroupAlias( mFromTalkgroup ); mFromAlias = getAliasList().getTalkgroupAlias( mFromTalkgroup );
@@ -3158,10 +3137,19 @@ public class P25DecoderState extends DecoderState
} }
broadcast( ChangedAttribute.FROM_TALKGROUP_ALIAS ); broadcast( ChangedAttribute.FROM_TALKGROUP_ALIAS );
/* Send audio metadata update */
broadcast( new Metadata( MetadataType.FROM, mFromTalkgroup, mFromAlias, true ) );
if( mCurrentCallEvent != null &&
mCurrentCallEvent.getFromID() == null ||
!mCurrentCallEvent.getFromID().contentEquals( from ) )
{
mCurrentCallEvent.setFromID( from );
broadcast( mCurrentCallEvent );
}
} }
/* Send audio metadata update */
broadcast( new Metadata( MetadataType.FROM, mFromTalkgroup, mFromAlias, true ) );
} }
} }

View File

@@ -289,6 +289,7 @@ public class P25MessageFramer implements Listener<Dibit>
else else
{ {
mComplete = true; mComplete = true;
dispatch( new P25Message( mMessage.copy(), duid, mAliasList ) );
} }
} }
else else

View File

@@ -77,7 +77,6 @@ public class P25_C4FMDecoder extends P25Decoder implements IRealBufferListener
/* Filter demodulated sample buffers */ /* Filter demodulated sample buffers */
float[] filter = FilterFactory.getLowPass( 48000, 2500, 4000, 80, WindowType.HANNING, true ); float[] filter = FilterFactory.getLowPass( 48000, 2500, 4000, 80, WindowType.HANNING, true );
mLog.debug( "Demod Filter tap count:" + filter.length + " coefficients:" + Arrays.toString( filter ) );
mC4FMPreFilter = new RealFIRFilter_RB_RB( filter, 1.0f ); mC4FMPreFilter = new RealFIRFilter_RB_RB( filter, 1.0f );
/* Issue tuned frequency correction commands, remotely controlled by the /* Issue tuned frequency correction commands, remotely controlled by the

View File

@@ -87,6 +87,8 @@ public class P25Message extends Message
sb.append( " " ); sb.append( " " );
sb.append( getDUID().getLabel() ); sb.append( getDUID().getLabel() );
sb.append( " DATA UNIT ID:" + mMessage.getInt( DUID ) );
return sb.toString(); return sb.toString();
} }

View File

@@ -1,5 +1,6 @@
package module.decode.p25.message.pdu; package module.decode.p25.message.pdu;
import module.decode.p25.message.pdu.confirmed.PacketData;
import module.decode.p25.message.pdu.osp.control.AdjacentStatusBroadcastExtended; import module.decode.p25.message.pdu.osp.control.AdjacentStatusBroadcastExtended;
import module.decode.p25.message.pdu.osp.control.CallAlertExtended; import module.decode.p25.message.pdu.osp.control.CallAlertExtended;
import module.decode.p25.message.pdu.osp.control.GroupAffiliationQueryExtended; import module.decode.p25.message.pdu.osp.control.GroupAffiliationQueryExtended;
@@ -121,7 +122,8 @@ public class PDUMessageFactory
} }
return new PDUMessage( message, duid, aliasList ); return new PDUMessage( message, duid, aliasList );
case PACKET_DATA:
return new PacketData( message, aliasList );
case UNCONFIRMED_MULTI_BLOCK_TRUNKING_CONTROL: case UNCONFIRMED_MULTI_BLOCK_TRUNKING_CONTROL:
return new PDUMessage( message, duid, aliasList ); return new PDUMessage( message, duid, aliasList );
default: default:

View File

@@ -48,19 +48,27 @@ public class PDUConfirmedMessageFactory
case UNENCRYPTED_USER_DATA: case UNENCRYPTED_USER_DATA:
switch( message.getPDUType() ) switch( message.getPDUType() )
{ {
case SN_ACTIVATE_TDS_CONTEXT_ACCEPT: case SNDCP_ACTIVATE_TDS_CONTEXT_ACCEPT:
return new SNDCPActivateTDSContextAccept( message ); return new SNDCPActivateTDSContextAccept( message );
case SN_ACTIVATE_TDS_CONTEXT_REJECT: case SNDCP_ACTIVATE_TDS_CONTEXT_REJECT:
return new SNDCPActivateTDSContextReject( message ); return new SNDCPActivateTDSContextReject( message );
case SN_ACTIVATE_TDS_CONTEXT_REQUEST: case SN_ACTIVATE_TDS_CONTEXT_REQUEST:
return new SNDCPActivateTDSContextRequest( message ); return new SNDCPActivateTDSContextRequest( message );
case SN_DEACTIVATE_TDS_CONTEXT_ACCEPT: case SNDCP_DEACTIVATE_TDS_CONTEXT_ACCEPT:
case SN_DEACTIVATE_TDS_CONTEXT_REQUEST: case SNDCP_DEACTIVATE_TDS_CONTEXT_REQUEST:
return new SNDCPDeactivateTDSContext( message ); return new SNDCPDeactivateTDSContext( message );
case SN_RF_CONFIRMED_DATA: case SNDCP_RF_CONFIRMED_DATA:
return new SNDCPUserData( message ); return new SNDCPUserData( message );
case SN_RF_UNCONFIRMED_DATA: case SNDCP_RF_UNCONFIRMED_DATA:
break; break;
case PDU_TYPE_6:
case PDU_TYPE_7:
case PDU_TYPE_8:
case PDU_TYPE_9:
case PDU_TYPE_10:
case PDU_TYPE_11:
return new PDUTypeUnknown( message );
case UNKNOWN:
default: default:
break; break;
} }

View File

@@ -0,0 +1,55 @@
package module.decode.p25.message.pdu.confirmed;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
public class PDUTypeUnknown extends PDUConfirmedMessage
{
public final static Logger mLog =
LoggerFactory.getLogger( PDUTypeUnknown.class );
public PDUTypeUnknown( PDUConfirmedMessage message )
{
super( message );
}
@Override
public String getMessage()
{
StringBuilder sb = new StringBuilder();
sb.append( "NAC:" );
sb.append( getNAC() );
sb.append( " LLID:" );
sb.append( getLogicalLinkID() );
sb.append( " PACKET DATA UNIT CONFIRMED - " );
sb.append( getPDUType().getLabel() );
sb.append( " CRC[" );
sb.append( getErrorStatus() );
sb.append( "]" );
sb.append( " PACKET #" );
sb.append( getPacketSequenceNumber() );
if( isFinalFragment() && getFragmentSequenceNumber() == 0 )
{
sb.append( ".C" );
}
else
{
sb.append( "." );
sb.append( getFragmentSequenceNumber() );
if( isFinalFragment() )
{
sb.append( "C" );
}
}
sb.append( " " );
sb.append( mMessage.toString() );
return sb.toString();
}
}

View File

@@ -0,0 +1,66 @@
package module.decode.p25.message.pdu.confirmed;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import alias.AliasList;
import bits.BinaryMessage;
public class PacketData extends PDUConfirmedMessage
{
public final static Logger mLog =
LoggerFactory.getLogger( PacketData.class );
public static final int DATA_BLOCK_START = 176;
public PacketData( BinaryMessage message, AliasList aliasList )
{
super( message, aliasList );
}
@Override
public String getMessage()
{
StringBuilder sb = new StringBuilder();
sb.append( "NAC:" );
sb.append( getNAC() );
sb.append( " PDUC LLID:" );
sb.append( getLogicalLinkID() );
sb.append( " PACKET DATA" );
sb.append( " CRC[" );
sb.append( getErrorStatus() );
sb.append( "]" );
if( !mMessage.get( FINAL_FRAGMENT_FLAG ) )
{
sb.append( " RESENDING" );
}
sb.append( " PACKET #" );
sb.append( getPacketSequenceNumber() );
if( isFinalFragment() && getFragmentSequenceNumber() == 0 )
{
sb.append( ".C" );
}
else
{
sb.append( "." );
sb.append( getFragmentSequenceNumber() );
if( isFinalFragment() )
{
sb.append( "C" );
}
}
sb.append( " " );
sb.append( mMessage.toString() );
return sb.toString();
}
}

View File

@@ -49,7 +49,7 @@ public class SNDCPDeactivateTDSContext extends PDUConfirmedMessage
public String getDeactivationType() public String getDeactivationType()
{ {
if( getPDUType() == PDUType.SN_DEACTIVATE_TDS_CONTEXT_REQUEST ) if( getPDUType() == PDUType.SNDCP_DEACTIVATE_TDS_CONTEXT_REQUEST )
{ {
return mMessage.getInt( DEACTIVATION_TYPE ) == 0 ? return mMessage.getInt( DEACTIVATION_TYPE ) == 0 ?
"ALL NSAPIS" : "THIS NSAPI"; "ALL NSAPIS" : "THIS NSAPI";

View File

@@ -18,4 +18,19 @@ public class TDUMessage extends P25Message
mCRC = new CRC[ 1 ]; mCRC = new CRC[ 1 ];
mCRC[ 0 ] = CRC.PASSED; mCRC[ 0 ] = CRC.PASSED;
} }
@Override
public String getMessage()
{
StringBuilder sb = new StringBuilder();
sb.append( "NAC:" );
sb.append( getNAC() );
sb.append( " " );
sb.append( getDUID().getLabel() );
sb.append( " TERMINATOR DATA UNIT" );
return sb.toString();
}
} }

View File

@@ -10,9 +10,11 @@ public enum DataUnitID
HDU ( 0, 712, false, "HDU ", "Header Data Unit" ), HDU ( 0, 712, false, "HDU ", "Header Data Unit" ),
TDU ( 3, 64, false, "TDU ", "Simple Terminator Data Unit" ), TDU ( 3, 64, false, "TDU ", "Simple Terminator Data Unit" ),
LDU1 ( 5, 1632, true, "LDU1 ", "Logical Link Data Unit 1" ), LDU1 ( 5, 1632, true, "LDU1 ", "Logical Link Data Unit 1" ),
VSELP1( 6, 1680, false, "VSEL1", "Motorola VSELP Unit 1" ),
TSBK1( 7, 260, false, "TSBK1", "Trunking Signaling Block" ), TSBK1( 7, 260, false, "TSBK1", "Trunking Signaling Block" ),
TSBK2( 7, 260, false, "TSBK2", "Trunking Signaling Block" ), TSBK2( 7, 260, false, "TSBK2", "Trunking Signaling Block" ),
TSBK3( 7, 260, false, "TSBK3", "Trunking Signaling Block" ), TSBK3( 7, 260, false, "TSBK3", "Trunking Signaling Block" ),
VSELP2( 9, 1680, false, "VSEL1", "Motorola VSELP Unit 1" ),
LDU2 ( 10, 1632, true, "LDU2 ", "Logical Link Data Unit 2" ), LDU2 ( 10, 1632, true, "LDU2 ", "Logical Link Data Unit 2" ),
PDU0 ( 12, 260, false, "PDU0 ", "Packet Header Data Unit" ), PDU0 ( 12, 260, false, "PDU0 ", "Packet Header Data Unit" ),
PDU1 ( 12, 356, false, "PDU1 ", "Packet Data Unit-Unconfirmed" ), PDU1 ( 12, 356, false, "PDU1 ", "Packet Data Unit-Unconfirmed" ),

View File

@@ -3,12 +3,18 @@ package module.decode.p25.reference;
public enum PDUType public enum PDUType
{ {
/* Outbound */ /* Outbound */
SN_ACTIVATE_TDS_CONTEXT_ACCEPT( "SN-ACTIVATE TDS CONTEXT ACCEPT", 0 ), SNDCP_ACTIVATE_TDS_CONTEXT_ACCEPT( "SNDCP-ACTIVATE TDS CONTEXT ACCEPT", 0 ),
SN_DEACTIVATE_TDS_CONTEXT_ACCEPT( "SN-DEACTIVATE TDS CONTEXT ACCEPT", 1 ), SNDCP_DEACTIVATE_TDS_CONTEXT_ACCEPT( "SNDCP-DEACTIVATE TDS CONTEXT ACCEPT", 1 ),
SN_DEACTIVATE_TDS_CONTEXT_REQUEST( "SN-DEACTIVATE TDS CONTEXT REQUEST", 2 ), SNDCP_DEACTIVATE_TDS_CONTEXT_REQUEST( "SNDCP-DEACTIVATE TDS CONTEXT REQUEST", 2 ),
SN_ACTIVATE_TDS_CONTEXT_REJECT( "SN-ACTIVATE TDS CONTEXT REJECT", 3 ), SNDCP_ACTIVATE_TDS_CONTEXT_REJECT( "SNDCP-ACTIVATE TDS CONTEXT REJECT", 3 ),
SN_RF_UNCONFIRMED_DATA( "SN-RF UNCONFIRMED DATA", 4 ), SNDCP_RF_UNCONFIRMED_DATA( "SNDCP-RF UNCONFIRMED DATA", 4 ),
SN_RF_CONFIRMED_DATA( "SN-RF CONFIRMED DATA", 5 ), SNDCP_RF_CONFIRMED_DATA( "SNDCP-RF CONFIRMED DATA", 5 ),
PDU_TYPE_6( "PDU TYPE 6 UNKNOWN", 6 ),
PDU_TYPE_7( "PDU TYPE 7 UNKNOWN", 7 ),
PDU_TYPE_8( "PDU TYPE 8 UNKNOWN", 8 ),
PDU_TYPE_9( "PDU TYPE 9 UNKNOWN", 9 ),
PDU_TYPE_10( "PDU TYPE 10 UNKNOWN", 10 ),
PDU_TYPE_11( "PDU TYPE 11 UNKNOWN", 11 ),
/* Inbound */ /* Inbound */
SN_ACTIVATE_TDS_CONTEXT_REQUEST( "SN-ACTIVATE TDS CONTEXT REQUEST", 0 ), SN_ACTIVATE_TDS_CONTEXT_REQUEST( "SN-ACTIVATE TDS CONTEXT REQUEST", 0 ),
@@ -36,7 +42,7 @@ public enum PDUType
public static PDUType fromValue( int value, boolean outbound ) public static PDUType fromValue( int value, boolean outbound )
{ {
if( outbound && 0 <= value && value <= 5 ) if( outbound && 0 <= value && value <= 11 )
{ {
return values()[ value ]; return values()[ value ];
} }

View File

@@ -2,7 +2,7 @@ package module.decode.p25.reference;
public enum ServiceAccessPoint public enum ServiceAccessPoint
{ {
UNENCRYPTED_USER_DATA( "USER DATA", 0 ), UNENCRYPTED_USER_DATA( "UNENCRYPTED USER DATA", 0 ),
ENCRYPTED_USER_DATA( "ENCRYPTED USER DATA", 1 ), ENCRYPTED_USER_DATA( "ENCRYPTED USER DATA", 1 ),
CIRCUIT_DATA( "CIRCUIT DATA", 2 ), CIRCUIT_DATA( "CIRCUIT DATA", 2 ),
CIRCUIT_DATA_CONTROL( "CIRCUIT DATA CONTROL", 3 ), CIRCUIT_DATA_CONTROL( "CIRCUIT DATA CONTROL", 3 ),

View File

@@ -103,6 +103,7 @@ public class PassportDecoder extends Decoder
@Override @Override
public void reset() public void reset()
{ {
mPassportMessageFramer.reset();
} }
@Override @Override

View File

@@ -0,0 +1,29 @@
package module.decode.state;
import controller.channel.Channel.ChannelType;
/**
* Allows decoders to change the call timeout setting in the channel state
*/
public class ChangeChannelTimeoutEvent extends DecoderStateEvent
{
private ChannelType mChannelType;
private long mCallTimeout;
public ChangeChannelTimeoutEvent( Object source, ChannelType channelType, long timeout )
{
super( source, Event.CHANGE_CALL_TIMEOUT, State.IDLE );
mChannelType = channelType;
mCallTimeout = timeout;
}
public ChannelType getChannelType()
{
return mChannelType;
}
public long getCallTimeout()
{
return mCallTimeout;
}
}

View File

@@ -22,7 +22,8 @@ import java.awt.Component;
import java.awt.Point; import java.awt.Point;
import java.awt.event.MouseEvent; import java.awt.event.MouseEvent;
import java.awt.event.MouseListener; import java.awt.event.MouseListener;
import java.util.HashMap; import java.util.Map;
import java.util.concurrent.ConcurrentHashMap;
import javax.swing.JMenu; import javax.swing.JMenu;
import javax.swing.JPanel; import javax.swing.JPanel;
@@ -53,8 +54,8 @@ public class ChannelList extends JPanel implements ChannelEventListener
private final static Logger mLog = LoggerFactory.getLogger( ChannelList.class ); private final static Logger mLog = LoggerFactory.getLogger( ChannelList.class );
private HashMap<Channel,ChannelCollectionPanel> mDisplayedPanels = private Map<Channel,ChannelCollectionPanel> mDisplayedPanels =
new HashMap<Channel,ChannelCollectionPanel>(); new ConcurrentHashMap<Channel,ChannelCollectionPanel>();
private PlaylistManager mPlaylistManager; private PlaylistManager mPlaylistManager;
private SettingsManager mSettingsManager; private SettingsManager mSettingsManager;
@@ -253,20 +254,6 @@ public class ChannelList extends JPanel implements ChannelEventListener
idleMenu.add( new ColorSettingResetMenuItem( mSettingsManager, idleMenu.add( new ColorSettingResetMenuItem( mSettingsManager,
ColorSettingName.CHANNEL_STATE_GRADIENT_TOP_IDLE ) ); ColorSettingName.CHANNEL_STATE_GRADIENT_TOP_IDLE ) );
JMenu noTunerMenu = new JMenu( "No Tuner" );
channelsMenu.add( noTunerMenu );
noTunerMenu.add( new ColorSettingMenuItem( mSettingsManager,
ColorSettingName.CHANNEL_STATE_GRADIENT_MIDDLE_NO_TUNER ) );
noTunerMenu.add( new ColorSettingResetMenuItem( mSettingsManager,
ColorSettingName.CHANNEL_STATE_GRADIENT_MIDDLE_NO_TUNER ) );
noTunerMenu.add( new JSeparator() );
noTunerMenu.add( new ColorSettingMenuItem( mSettingsManager,
ColorSettingName.CHANNEL_STATE_GRADIENT_TOP_NO_TUNER ) );
noTunerMenu.add( new ColorSettingResetMenuItem( mSettingsManager,
ColorSettingName.CHANNEL_STATE_GRADIENT_TOP_NO_TUNER ) );
JMenu labelMenu = new JMenu( "Labels" ); JMenu labelMenu = new JMenu( "Labels" );
colorMenu.add( labelMenu ); colorMenu.add( labelMenu );

View File

@@ -1,6 +1,6 @@
/******************************************************************************* /*******************************************************************************
* SDR Trunk * SDR Trunk
* Copyright (C) 2014,2015 Dennis Sheirer * Copyright (C) 2014-2016 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
@@ -22,16 +22,9 @@ import java.util.concurrent.ScheduledFuture;
import java.util.concurrent.TimeUnit; import java.util.concurrent.TimeUnit;
import java.util.concurrent.atomic.AtomicBoolean; import java.util.concurrent.atomic.AtomicBoolean;
import module.Module;
import module.decode.config.DecodeConfiguration;
import module.decode.event.CallEvent;
import module.decode.event.ICallEventProvider;
import module.decode.state.DecoderStateEvent.Event;
import org.slf4j.Logger; import org.slf4j.Logger;
import org.slf4j.LoggerFactory; import org.slf4j.LoggerFactory;
import sample.Listener;
import audio.metadata.IMetadataProvider; import audio.metadata.IMetadataProvider;
import audio.metadata.Metadata; import audio.metadata.Metadata;
import audio.metadata.MetadataReset; import audio.metadata.MetadataReset;
@@ -41,38 +34,13 @@ import audio.squelch.SquelchState;
import controller.ThreadPoolManager; import controller.ThreadPoolManager;
import controller.ThreadPoolManager.ThreadType; import controller.ThreadPoolManager.ThreadType;
import controller.channel.Channel.ChannelType; import controller.channel.Channel.ChannelType;
import module.Module;
import module.decode.config.DecodeConfiguration;
import module.decode.event.CallEvent;
import module.decode.event.ICallEventProvider;
import module.decode.state.DecoderStateEvent.Event;
import sample.Listener;
/**
* Channel state tracks the overall state of all processing modules and decoders
* configured for the channel and provides squelch control events.
*
* Primary channel state provides a state machine for tracking decode events
* during a voice or data call through it's life cycle of CALL/DATA, FADE, END,
* and then back to an IDLE state. This class also provides squelch control
* to support any audio processing.
*
* State Descriptions:
* IDLE No voice or data call activity
* CALL/DATA A voice or data call has started, or is continuing
* CONTROL Control channel
* FADE The phase after a voice or data call when either an explicit
* call end has been received, or when no new signalling updates
* have been received, and the fade timer has expired. This phase
* allows for gui updates to signal to the user that the call is
* ended, while continuing to display the call details for the user
* END The phase after the voice or data call FADE, when the call is
* now over, and the gui should be reset to IDLE.
*
* When a call is started, invoke mTimerService.startFadeTimer() to start the
* call fade monitoring timer process that will automatically signal a call end
* once no more call signalling has been detected after the fade timeout has
* occurred, by invoking the fade() method.
*
* When a call ends, invoke the mTimerService.stopFadeTimer() and then invoke
* the mTimerService.startResetTimer(). Once the reset timeout occurs,
* the reset() method will be called, allowing the channel state to perform
* any reset actions, like clearing status and id labels on any view panels.
*/
public class ChannelState extends Module implements ICallEventProvider, public class ChannelState extends Module implements ICallEventProvider,
IChangedAttributeProvider, IDecoderStateEventListener, IChangedAttributeProvider, IDecoderStateEventListener,
IDecoderStateEventProvider, IMetadataProvider, ISquelchStateProvider IDecoderStateEventProvider, IMetadataProvider, ISquelchStateProvider
@@ -84,8 +52,6 @@ public class ChannelState extends Module implements ICallEventProvider,
public static final long RESET_TIMEOUT_DELAY = 2000; public static final long RESET_TIMEOUT_DELAY = 2000;
private State mState = State.IDLE; private State mState = State.IDLE;
private long mTrafficChannelTimeout =
DecodeConfiguration.DEFAULT_CALL_TIMEOUT_SECONDS;
private Listener<CallEvent> mCallEventListener; private Listener<CallEvent> mCallEventListener;
private Listener<ChangedAttribute> mChangedAttributeListener; private Listener<ChangedAttribute> mChangedAttributeListener;
@@ -98,12 +64,15 @@ public class ChannelState extends Module implements ICallEventProvider,
private boolean mSquelchLocked = false; private boolean mSquelchLocked = false;
private boolean mSelected = false; private boolean mSelected = false;
private long mStandardChannelFadeTimeout = FADE_TIMEOUT_DELAY;
private long mTrafficChannelFadeTimeout =
DecodeConfiguration.DEFAULT_CALL_TIMEOUT_SECONDS * 1000;
private long mFadeTimeout; private long mFadeTimeout;
private long mResetTimeout; private long mEndTimeout;
private StateMonitor mStateMonitor = new StateMonitor(); private StateMonitor mStateMonitor = new StateMonitor();
private AtomicBoolean mMonitoring = new AtomicBoolean(); private AtomicBoolean mMonitoring = new AtomicBoolean();
private ScheduledFuture<?> mRunningMonitor; private ScheduledFuture<?> mStateMonitorFuture;
private ThreadPoolManager mThreadPoolManager; private ThreadPoolManager mThreadPoolManager;
private ChannelType mChannelType; private ChannelType mChannelType;
@@ -111,32 +80,97 @@ public class ChannelState extends Module implements ICallEventProvider,
private TrafficChannelStatusListener mTrafficChannelStatusListener; private TrafficChannelStatusListener mTrafficChannelStatusListener;
private CallEvent mTrafficChannelCallEvent; private CallEvent mTrafficChannelCallEvent;
/**
* Channel state tracks the overall state of all processing modules and decoders
* configured for the channel and provides squelch control and decoder state
* reset events.
*
* Uses a state enumeration that defines allowable channel state transitions in
* order to track a call or data decode event from start to finish. Uses a
* timer to monitor for inactivity and to provide a FADE period that indicates
* to the user that the activity has stopped while continuing to provide details
* about the call, before the state is reset to IDLE.
*
* Since this class is multi-threaded between decoder events and the internal
* timer that monitors for inactivity, this class uses the mState object for
* thread synchronization.
*
* State Descriptions:
* IDLE Normal state. No voice or data call activity
* CALL/DATA/ENCRYPTED/CONTROL
* Decoding states.
* FADE The phase after a voice or data call when either an explicit
* call end has been received, or when no new signalling updates
* have been received, and the fade timer has expired. This phase
* allows for gui updates to signal to the user that the call is
* ended, while continuing to display the call details for the user
* TEARDOWN Indicates a traffic channel that will be torn down for reuse.
*/
public ChannelState( ThreadPoolManager manager, ChannelType channelType ) public ChannelState( ThreadPoolManager manager, ChannelType channelType )
{ {
mThreadPoolManager = manager; mThreadPoolManager = manager;
mChannelType = channelType; mChannelType = channelType;
} }
/**
* Resets this channel state and prepares it for reuse.
*/
@Override @Override
public void reset() public void reset()
{ {
broadcast( new DecoderStateEvent( this, Event.RESET, State.IDLE ) );
broadcast( new MetadataReset() );
mState = State.IDLE;
broadcast( ChangedAttribute.CHANNEL_STATE );
} }
@Override @Override
public void start() public void start()
{ {
//Start a monitoring task to check for FADE and END event timeouts
if( mMonitoring.compareAndSet( false, true ) )
{
if( mStateMonitorFuture == null && mStateMonitor != null )
{
try
{
mStateMonitorFuture = mThreadPoolManager.scheduleFixedRate(
ThreadType.DECODER, mStateMonitor, 20, TimeUnit.MILLISECONDS );
}
catch( RejectedExecutionException ree )
{
mLog.error( "state monitor scheduled task rejected", ree );
mMonitoring.set( false );
}
}
}
} }
@Override @Override
public void stop() public void stop()
{ {
mSquelchLocked = false; mSquelchLocked = false;
//Shutdown the state montoring task
if( mMonitoring.compareAndSet( true, false ) )
{
if( mStateMonitorFuture != null )
{
boolean success = mThreadPoolManager.cancel( mStateMonitorFuture );
if( !success )
{
mLog.error("Couldn't stop monitoring scheduled future" );
}
}
mStateMonitorFuture = null;
}
} }
public void dispose() public void dispose()
{ {
stopMonitor();
mCallEventListener = null; mCallEventListener = null;
mChangedAttributeListener = null; mChangedAttributeListener = null;
mDecoderStateListener = null; mDecoderStateListener = null;
@@ -145,9 +179,34 @@ public class ChannelState extends Module implements ICallEventProvider,
mStateMonitor = null; mStateMonitor = null;
} }
private boolean isStandardChannel()
{
return mChannelType == ChannelType.STANDARD;
}
private boolean isTrafficChannel()
{
return mChannelType == ChannelType.TRAFFIC;
}
public void setStandardChannelTimeout( long milliseconds )
{
mStandardChannelFadeTimeout = milliseconds;
if( mChannelType == ChannelType.STANDARD )
{
mFadeTimeout = mStandardChannelFadeTimeout;
}
}
public void setTrafficChannelTimeout( long milliseconds ) public void setTrafficChannelTimeout( long milliseconds )
{ {
mTrafficChannelTimeout = milliseconds; mTrafficChannelFadeTimeout = milliseconds;
if( mChannelType == ChannelType.TRAFFIC )
{
mFadeTimeout = mTrafficChannelFadeTimeout;
}
} }
public void setSelected( boolean selected ) public void setSelected( boolean selected )
@@ -165,79 +224,18 @@ public class ChannelState extends Module implements ICallEventProvider,
return mState; return mState;
} }
private boolean isIdleOrEndingState()
{
return mState == State.FADE ||
mState == State.END ||
mState == State.IDLE;
}
/**
* Starts the state monitor running when we're not in IDLE state. Calls to
* start an already started monitor are ignored.
*/
private void startMonitor()
{
if( mMonitoring.compareAndSet( false, true ) )
{
if( mRunningMonitor == null && mStateMonitor != null )
{
try
{
mRunningMonitor = mThreadPoolManager.scheduleFixedRate(
ThreadType.DECODER, mStateMonitor, 20, TimeUnit.MILLISECONDS );
}
catch( RejectedExecutionException ree )
{
mLog.error( "state monitor scheduled task rejected", ree );
mMonitoring.set( false );
}
}
else
{
throw new RuntimeException( "Channel state monitor's scheduled "
+ "future pointer was not null" );
}
}
}
/**
* Stops the state monitor when we're in an IDLE state. Calls to stop an
* already stopped monitor are ignored.
*/
private void stopMonitor()
{
if( mMonitoring.compareAndSet( true, false ) )
{
if( mRunningMonitor != null )
{
mThreadPoolManager.cancel( mRunningMonitor );
}
mRunningMonitor = null;
}
}
/** /**
* Updates the fade timeout threshold to the current time plus delay * Updates the fade timeout threshold to the current time plus delay
*/ */
private void updateFadeTimeout() private void updateFadeTimeout()
{ {
/* Standard channel uses the FADE_TIMEOUT_DELAY and traffic channels if( isTrafficChannel() )
* use the TRAFFIC_FADE_TIMEOUT_DELAY. Some traffic channels (e.g. MPT)
* don't have signaling on the traffic channel that can be used to
* update the fade timeout, so we set the timeout value larger to ensure
* the channel doesn't fade before the end of the call. */
long current = System.currentTimeMillis();
long timeout = current +
( mChannelType == ChannelType.STANDARD ? FADE_TIMEOUT_DELAY :
mTrafficChannelTimeout );
if( timeout > mFadeTimeout )
{ {
mFadeTimeout = timeout; mFadeTimeout = System.currentTimeMillis() + mTrafficChannelFadeTimeout;
}
else
{
mFadeTimeout = System.currentTimeMillis() + mStandardChannelFadeTimeout;
} }
} }
@@ -246,13 +244,13 @@ public class ChannelState extends Module implements ICallEventProvider,
*/ */
private void updateResetTimeout() private void updateResetTimeout()
{ {
if( mChannelType == ChannelType.TRAFFIC ) if( isTrafficChannel() )
{ {
mResetTimeout = System.currentTimeMillis(); mEndTimeout = System.currentTimeMillis();
} }
else else
{ {
mResetTimeout = System.currentTimeMillis() + RESET_TIMEOUT_DELAY; mEndTimeout = System.currentTimeMillis() + RESET_TIMEOUT_DELAY;
} }
} }
@@ -294,134 +292,120 @@ public class ChannelState extends Module implements ICallEventProvider,
*/ */
protected void setState( State state ) protected void setState( State state )
{ {
if( mState == state ) synchronized( mState )
{ {
switch( state ) if( state == mState )
{ {
case CALL: if( State.CALL_STATES.contains( state ) )
case CONTROL: {
case DATA:
case ENCRYPTED:
updateFadeTimeout(); updateFadeTimeout();
break; }
default:
break;
} }
} else if( state != mState && mState.canChangeTo( state ) )
else if( mState.canChangeTo( state ) )
{
switch( state )
{ {
case CONTROL: switch( state )
//Don't allow traffic channels to be control channels, {
//otherwise they won't get call tear down case CONTROL:
if( mChannelType != ChannelType.TRAFFIC ) //Don't allow traffic channels to be control channels,
{ //otherwise they can't transition to call tear down
if( isStandardChannel() )
{
broadcast( SquelchState.SQUELCH );
updateFadeTimeout();
mState = state;
broadcast( ChangedAttribute.CHANNEL_STATE );
}
break;
case DATA:
case ENCRYPTED:
broadcast( SquelchState.SQUELCH ); broadcast( SquelchState.SQUELCH );
updateFadeTimeout(); updateFadeTimeout();
startMonitor();
mState = state; mState = state;
broadcast( ChangedAttribute.CHANNEL_STATE ); broadcast( ChangedAttribute.CHANNEL_STATE );
} break;
break; case CALL:
case DATA: broadcast( SquelchState.UNSQUELCH );
case ENCRYPTED: updateFadeTimeout();
broadcast( SquelchState.SQUELCH ); mState = state;
updateFadeTimeout(); broadcast( ChangedAttribute.CHANNEL_STATE );
startMonitor(); break;
mState = state; case FADE:
broadcast( ChangedAttribute.CHANNEL_STATE ); processFadeState();
break; break;
case CALL: case IDLE:
broadcast( SquelchState.UNSQUELCH ); processIdleState();
updateFadeTimeout(); break;
startMonitor(); case TEARDOWN:
mState = state; processTeardownState();
broadcast( ChangedAttribute.CHANNEL_STATE ); break;
break; case RESET:
case FADE: mState = State.IDLE;
broadcast( SquelchState.SQUELCH ); break;
updateResetTimeout(); default:
startMonitor(); break;
mState = state; }
broadcast( ChangedAttribute.CHANNEL_STATE ); }
break; else
case END: {
broadcast( SquelchState.SQUELCH ); // mLog.debug( "Can't change from [" + mState + "] to [" + state + "]" );
broadcast( new DecoderStateEvent( this, Event.RESET, State.IDLE ) );
broadcast( new MetadataReset() );
mState = state;
broadcast( ChangedAttribute.CHANNEL_STATE );
break;
case IDLE:
broadcast( SquelchState.SQUELCH );
stopMonitor();
if( mChannelType == ChannelType.TRAFFIC )
{
if( mTrafficChannelStatusListener != null )
{
mTrafficChannelStatusListener.callEnd();
mTrafficChannelStatusListener = null;
}
if( mTrafficChannelCallEvent != null )
{
mTrafficChannelCallEvent.end();
broadcast( mTrafficChannelCallEvent );
mTrafficChannelCallEvent = null;
}
}
mState = state;
broadcast( ChangedAttribute.CHANNEL_STATE );
break;
default:
break;
} }
}
else
{
mLog.debug( "Can't change from [" + mState + "] to [" + state + "]" );
} }
} }
public class StateMonitor implements Runnable /**
* Sets the state and processes related actions
*/
private void processFadeState()
{ {
@Override updateResetTimeout();
public void run() mState = State.FADE;
broadcast( SquelchState.SQUELCH );
broadcast( ChangedAttribute.CHANNEL_STATE );
}
private void processIdleState()
{
broadcast( SquelchState.SQUELCH );
if( mState == State.FADE )
{ {
try broadcast( new DecoderStateEvent( this, Event.RESET, State.IDLE ) );
{ broadcast( new MetadataReset() );
if( getState() == State.FADE ) }
{
if( mResetTimeout <= System.currentTimeMillis() ) mState = State.IDLE;
{
setState( State.END ); broadcast( ChangedAttribute.CHANNEL_STATE );
} }
}
else if( getState() == State.END ) private void processTeardownState()
{ {
setState( State.IDLE ); broadcast( SquelchState.SQUELCH );
}
else broadcast( new MetadataReset() );
{
if( mFadeTimeout <= System.currentTimeMillis() ) mState = State.TEARDOWN;
{
setState( State.FADE ); broadcast( ChangedAttribute.CHANNEL_STATE );
}
} if( mTrafficChannelCallEvent != null )
} {
catch( Exception e ) mTrafficChannelCallEvent.end();
{
mLog.error( "Exception thrown while state monitor is running " + broadcast( mTrafficChannelCallEvent );
"- state [" + getState() + mTrafficChannelCallEvent = null;
"] current [" + System.currentTimeMillis() + }
"] mResetTimeout [" + mResetTimeout +
"] mFadeTimeout [" + mFadeTimeout + if( mTrafficChannelStatusListener != null )
"]", e ); {
} mTrafficChannelStatusListener.callEnd();
mTrafficChannelStatusListener = null;
}
else
{
mLog.error( "Traffic channel status listener was null - couldn't "
+ "teardown the channel");
} }
} }
@@ -615,35 +599,95 @@ public class ChannelState extends Module implements ICallEventProvider,
broadcast( SquelchState.UNSQUELCH ); broadcast( SquelchState.UNSQUELCH );
mSquelchLocked = true; mSquelchLocked = true;
break; break;
case CHANGE_CALL_TIMEOUT:
if( event instanceof ChangeChannelTimeoutEvent )
{
ChangeChannelTimeoutEvent timeout = (ChangeChannelTimeoutEvent)event;
if( timeout.getChannelType() == ChannelType.STANDARD )
{
setStandardChannelTimeout( timeout.getCallTimeout() );
}
else
{
setTrafficChannelTimeout( timeout.getCallTimeout() );
}
}
case CONTINUATION: case CONTINUATION:
if( isIdleOrEndingState() )
{
setState( event.getState() );
}
else
{
updateFadeTimeout();
}
break;
case DECODE: case DECODE:
if( getState() == State.IDLE ) case START:
if( State.CALL_STATES.contains( event.getState() ) )
{ {
setState( event.getState() ); setState( event.getState() );
} }
break; break;
case END: case END:
setState( State.FADE ); if( isTrafficChannel() )
{
setState( State.TEARDOWN );
}
else
{
setState( State.FADE );
}
break; break;
case RESET: case RESET:
/* Channel State does not respond to reset events */ /* Channel State does not respond to reset events */
break; break;
case START:
setState( event.getState() );
break;
default: default:
break; break;
} }
} }
} }
} }
/**
* Monitors decoder state events to automatically transition the channel
* state to IDLE (standard channel) or to TEARDOWN (traffic channel) when
* decoding stops or the monitored channel returns to a no signal state.
*
* Provides a FADE transition state to allow for momentary decoding dropouts
* and to allow the user access to call details for a fade period upon
* call end.
*/
public class StateMonitor implements Runnable
{
@Override
public void run()
{
try
{
synchronized( mState )
{
if( State.CALL_STATES.contains( mState ) &&
mFadeTimeout <= System.currentTimeMillis() )
{
mState = State.FADE;
processFadeState();
}
else if( mState == State.FADE &&
mEndTimeout <= System.currentTimeMillis() )
{
if( isTrafficChannel() )
{
processTeardownState();
}
else
{
processIdleState();
}
}
}
}
catch( Exception e )
{
mLog.error( "Exception thrown while state monitor is running " +
"- state [" + getState() +
"] current [" + System.currentTimeMillis() +
"] mResetTimeout [" + mEndTimeout +
"] mFadeTimeout [" + mFadeTimeout +
"]", e );
}
}
}
} }

View File

@@ -14,12 +14,13 @@ import javax.swing.JLabel;
import javax.swing.JMenu; import javax.swing.JMenu;
import javax.swing.JPanel; import javax.swing.JPanel;
import module.decode.DecoderFactory;
import net.miginfocom.swing.MigLayout;
import org.slf4j.Logger; import org.slf4j.Logger;
import org.slf4j.LoggerFactory; import org.slf4j.LoggerFactory;
import controller.channel.Channel;
import controller.channel.ChannelUtils;
import module.decode.DecoderFactory;
import net.miginfocom.swing.MigLayout;
import playlist.PlaylistManager; import playlist.PlaylistManager;
import record.RecorderType; import record.RecorderType;
import record.config.RecordConfiguration; import record.config.RecordConfiguration;
@@ -29,8 +30,6 @@ import settings.ColorSetting.ColorSettingName;
import settings.Setting; import settings.Setting;
import settings.SettingChangeListener; import settings.SettingChangeListener;
import settings.SettingsManager; import settings.SettingsManager;
import controller.channel.Channel;
import controller.channel.ChannelUtils;
public class ChannelStatePanel extends JPanel public class ChannelStatePanel extends JPanel
implements Listener<ChangedAttribute>, SettingChangeListener implements Listener<ChangedAttribute>, SettingChangeListener
@@ -203,7 +202,7 @@ public class ChannelStatePanel extends JPanel
switch( changedAttribute ) switch( changedAttribute )
{ {
case CHANNEL_STATE: case CHANNEL_STATE:
if( state.getState() != null ) if( state != null && state.getState() != null )
{ {
mStateLabel.setText( state.getState().getDisplayValue() ); mStateLabel.setText( state.getState().getDisplayValue() );
} }
@@ -237,25 +236,36 @@ public class ChannelStatePanel extends JPanel
Paint p = null; Paint p = null;
switch( mChannel.getChannelState().getState() ) final Channel channel = mChannel;
final ChannelState channelState = ( channel == null ? null : channel.getChannelState() );
if( channelState != null )
{ {
case CALL: switch( channelState.getState() )
p = getGradient( mColorTopCall, mColorMiddleCall ); {
break; case CALL:
case CONTROL: p = getGradient( mColorTopCall, mColorMiddleCall );
p = getGradient( mColorTopControl, mColorMiddleControl ); break;
break; case CONTROL:
case DATA: p = getGradient( mColorTopControl, mColorMiddleControl );
p = getGradient( mColorTopData, mColorMiddleData ); break;
break; case DATA:
case FADE: p = getGradient( mColorTopData, mColorMiddleData );
p = getGradient( mColorTopFade, mColorMiddleFade ); break;
break; case FADE:
case END: p = getGradient( mColorTopFade, mColorMiddleFade );
case IDLE: break;
default: case IDLE:
p = getGradient( mColorTopIdle, mColorMiddleIdle ); default:
break; p = getGradient( mColorTopIdle, mColorMiddleIdle );
break;
}
}
if( p == null )
{
p = getGradient( mColorTopIdle, mColorMiddleIdle );
} }
g2.setPaint( p ); g2.setPaint( p );

View File

@@ -56,6 +56,7 @@ public class DecoderStateEvent
public enum Event public enum Event
{ {
ALWAYS_UNSQUELCH, ALWAYS_UNSQUELCH,
CHANGE_CALL_TIMEOUT,
CONTINUATION, CONTINUATION,
DECODE, DECODE,
END, END,

View File

@@ -1,5 +1,10 @@
package module.decode.state; package module.decode.state;
import java.util.EnumSet;
/**
* Details the set of states for a channel and the allowable transition states
*/
public enum State public enum State
{ {
IDLE( "IDLE" ) IDLE( "IDLE" )
@@ -7,7 +12,8 @@ public enum State
@Override @Override
public boolean canChangeTo( State state ) public boolean canChangeTo( State state )
{ {
return true; return state != TEARDOWN &&
state != RESET;
} }
}, },
CALL( "CALL" ) CALL( "CALL" )
@@ -15,11 +21,11 @@ public enum State
@Override @Override
public boolean canChangeTo( State state ) public boolean canChangeTo( State state )
{ {
return state == CALL || return state == CONTROL ||
state == CONTROL ||
state == DATA || state == DATA ||
state == ENCRYPTED || state == ENCRYPTED ||
state == FADE; state == FADE ||
state == TEARDOWN;
} }
}, },
DATA( "DATA" ) DATA( "DATA" )
@@ -29,9 +35,9 @@ public enum State
{ {
return state == CALL || return state == CALL ||
state == CONTROL || state == CONTROL ||
state == DATA || state == ENCRYPTED ||
state == State.ENCRYPTED || state == FADE ||
state == FADE; state == TEARDOWN;
} }
}, },
ENCRYPTED( "ENCRYPTED" ) ENCRYPTED( "ENCRYPTED" )
@@ -39,7 +45,7 @@ public enum State
@Override @Override
public boolean canChangeTo( State state ) public boolean canChangeTo( State state )
{ {
return state == FADE; return state == FADE || state == TEARDOWN;
} }
}, },
CONTROL( "CONTROL" ) CONTROL( "CONTROL" )
@@ -47,7 +53,8 @@ public enum State
@Override @Override
public boolean canChangeTo( State state ) public boolean canChangeTo( State state )
{ {
return true; return state == IDLE ||
state == FADE;
} }
}, },
FADE( "FADE" ) FADE( "FADE" )
@@ -55,20 +62,36 @@ public enum State
@Override @Override
public boolean canChangeTo( State state ) public boolean canChangeTo( State state )
{ {
return state != FADE; //All states except fade allowed return state != FADE &&
} state != RESET;
}
}, },
END( "END" ) RESET( "RESET" )
{ {
@Override @Override
public boolean canChangeTo( State state ) public boolean canChangeTo(State state)
{ {
return state != FADE; //Fade is only disallowed state return state == IDLE;
} }
}; },
TEARDOWN( "TEARDOWN" )
{
@Override
public boolean canChangeTo(State state)
{
return state == RESET;
}
};
private String mDisplayValue; private String mDisplayValue;
public static final EnumSet<State> CALL_STATES =
EnumSet.of( CALL, CONTROL, DATA, ENCRYPTED );
public static final EnumSet<State> IDLE_STATES =
EnumSet.of( IDLE, FADE );
private State( String displayValue ) private State( String displayValue )
{ {
mDisplayValue = displayValue; mDisplayValue = displayValue;

View File

@@ -15,6 +15,7 @@ import org.slf4j.Logger;
import org.slf4j.LoggerFactory; import org.slf4j.LoggerFactory;
import record.config.RecordConfiguration; import record.config.RecordConfiguration;
import sample.Broadcaster;
import sample.Listener; import sample.Listener;
import source.config.SourceConfigTuner; import source.config.SourceConfigTuner;
import source.tuner.TunerChannel; import source.tuner.TunerChannel;
@@ -90,12 +91,31 @@ public class TrafficChannelManager extends Module
@Override @Override
public void dispose() public void dispose()
{ {
for( Channel trafficChannel: mTrafficChannelPool )
{
trafficChannel.setEnabled( false );
trafficChannel.dispose();
}
mTrafficChannelPool.clear();
mCallEventListener = null; mCallEventListener = null;
mResourceManager = null; mResourceManager = null;
mDecodeConfiguration = null; mDecodeConfiguration = null;
mPreviousDoNotMonitorCallEvent = null; mPreviousDoNotMonitorCallEvent = null;
} }
/**
* Provides a channel by either reusing an existing channel or constructing
* a new one, limited by the total number of constructed channels allowed.
*
* Note: you must enforce thread safety on the mTrafficChannelsInUse
* external to this method.
*
* @param channelNumber
* @param tunerChannel
* @return
*/
private Channel getChannel( String channelNumber, TunerChannel tunerChannel ) private Channel getChannel( String channelNumber, TunerChannel tunerChannel )
{ {
Channel channel = null; Channel channel = null;
@@ -164,72 +184,79 @@ public class TrafficChannelManager extends Module
*/ */
private void process( TrafficChannelAllocationEvent event ) private void process( TrafficChannelAllocationEvent event )
{ {
if( mCallEventListener != null ) CallEvent callEvent = event.getCallEvent();
{
synchronized( mCallEventListener )
{
CallEvent callEvent = event.getCallEvent();
/* Check for duplicate events and suppress */ /* Check for duplicate events and suppress */
if( mTrafficChannelsInUse.containsKey( callEvent.getChannel() ) ) synchronized( mTrafficChannelsInUse )
{
if( mTrafficChannelsInUse.containsKey( callEvent.getChannel() ) )
{
return;
}
long frequency = callEvent.getFrequency();
/* Check the from/to aliases for do not monitor priority */
if( isDoNotMonitor( callEvent ) )
{
callEvent.setCallEventType( CallEventType.CALL_DO_NOT_MONITOR );
if( isSameCallEvent( mPreviousDoNotMonitorCallEvent, callEvent ) )
{ {
return; return;
} }
else
long frequency = callEvent.getFrequency();
/* Check the from/to aliases for do not monitor priority */
if( isDoNotMonitor( callEvent ) )
{ {
callEvent.setCallEventType( CallEventType.CALL_DO_NOT_MONITOR ); mPreviousDoNotMonitorCallEvent = callEvent;
if( isSameCallEvent( mPreviousDoNotMonitorCallEvent, callEvent ) )
{
return;
}
else
{
mPreviousDoNotMonitorCallEvent = callEvent;
}
} }
else if( frequency > 0 ) }
else if( frequency > 0 )
{
Channel channel = getChannel( callEvent.getChannel(),
new TunerChannel( Type.TRAFFIC, frequency,
mDecodeConfiguration.getDecoderType()
.getChannelBandwidth() ) );
if( channel != null )
{ {
Channel channel = getChannel( callEvent.getChannel(), if( channel.isProcessing() )
new TunerChannel( Type.TRAFFIC, frequency,
mDecodeConfiguration.getDecoderType()
.getChannelBandwidth() ) );
if( channel != null )
{ {
if( channel.isProcessing() ) callEvent.setDetails( "TRAFFIC CHANNEL ALLOCATED" );
{
ChannelState state = channel.getChannelState();
/* Register as a listener to be notified when the call ChannelState state = channel.getChannelState();
* is completed */
state.configureAsTrafficChannel(
new TrafficChannelStatusListener( this,
callEvent.getChannel() ), event );
/* Set state to call so that audio squelch state is correct */ /* Register as a listener to be notified when the call
state.setState( State.CALL ); * is completed */
} state.configureAsTrafficChannel(
else new TrafficChannelStatusListener( this,
{ callEvent.getChannel() ), event );
callEvent.setCallEventType( CallEventType.CALL_DETECT );
} /* Set state to call so that audio squelch state is correct */
state.setState( State.CALL );
} }
else else
{ {
callEvent.setCallEventType( CallEventType.CALL_DETECT ); callEvent.setCallEventType( CallEventType.CALL_DETECT );
callEvent.setDetails( "CHANNEL PROCESSING ERROR" );
} }
} }
else else
{ {
callEvent.setCallEventType( CallEventType.CALL_DETECT ); callEvent.setCallEventType( CallEventType.CALL_DETECT );
callEvent.setDetails( "NO TUNER AVAILABLE" );
} }
}
else
{
callEvent.setCallEventType( CallEventType.CALL_DETECT );
callEvent.setDetails( "UNKNOWN FREQUENCY" );
}
mCallEventListener.receive( callEvent ); final Listener<CallEvent> listener = mCallEventListener;
if( listener != null )
{
listener.receive( callEvent );
} }
} }
} }
@@ -370,14 +397,18 @@ public class TrafficChannelManager extends Module
*/ */
public void callEnd( String channelNumber ) public void callEnd( String channelNumber )
{ {
if( channelNumber != null && mTrafficChannelsInUse.containsKey( channelNumber ) ) synchronized( mTrafficChannelsInUse )
{ {
Channel channel = mTrafficChannelsInUse.get( channelNumber ); if( channelNumber != null &&
mTrafficChannelsInUse.containsKey( channelNumber ) )
{
Channel channel = mTrafficChannelsInUse.get( channelNumber );
mTrafficChannelsInUse.remove( channelNumber ); mTrafficChannelsInUse.remove( channelNumber );
/* Disable the channel and broadcast a notification */ /* Disable the channel and broadcast a notification */
channel.setEnabled( false, true ); channel.setEnabled( false, true );
}
} }
} }

View File

@@ -557,11 +557,6 @@ public class SettingsManager
ArrayList<TunerConfiguration> configs = getSettings() ArrayList<TunerConfiguration> configs = getSettings()
.getTunerConfigurations( type ); .getTunerConfigurations( type );
if( configs.isEmpty() )
{
configs.add( addNewTunerConfiguration( type, "Default" ) );
}
return configs; return configs;
} }

View File

@@ -63,9 +63,8 @@ public class Recording implements Comparable<Recording>,
} }
@Override @Override
public TunerChannelSource getChannel( ThreadPoolManager threadPoolManager, public TunerChannelSource getChannel( TunerChannel channel )
TunerChannel channel ) throws RejectedExecutionException, throws RejectedExecutionException, SourceException
SourceException
{ {
// TODO Auto-generated method stub // TODO Auto-generated method stub
return null; return null;

View File

@@ -71,9 +71,7 @@ public class RecordingSourceManager
if( recording != null ) if( recording != null )
{ {
retVal = recording.getChannel( retVal = recording.getChannel( tunerChannel );
mResourceManager.getThreadPoolManager(),
tunerChannel );
} }
else else
{ {

View File

@@ -30,10 +30,9 @@ import sample.complex.ComplexBuffer;
import source.SourceException; import source.SourceException;
import source.tuner.frequency.FrequencyChangeBroadcaster; import source.tuner.frequency.FrequencyChangeBroadcaster;
import source.tuner.frequency.FrequencyChangeEvent; import source.tuner.frequency.FrequencyChangeEvent;
import source.tuner.frequency.FrequencyChangeListener;
import source.tuner.frequency.FrequencyChangeEvent.Event; import source.tuner.frequency.FrequencyChangeEvent.Event;
import source.tuner.frequency.FrequencyChangeListener;
import controller.ResourceManager; import controller.ResourceManager;
import controller.ThreadPoolManager;
/** /**
* Tuner - provides tuner channel sources, representing a channel frequency * Tuner - provides tuner channel sources, representing a channel frequency
@@ -147,8 +146,8 @@ public abstract class Tuner implements FrequencyChangeBroadcaster,
* *
* @return - source for 48k sample rate * @return - source for 48k sample rate
*/ */
public abstract TunerChannelSource getChannel( ThreadPoolManager threadPoolManager, public abstract TunerChannelSource getChannel( TunerChannel channel )
TunerChannel channel ) throws RejectedExecutionException, SourceException; throws RejectedExecutionException, SourceException;
/** /**
* Releases the tuned channel resources * Releases the tuned channel resources

View File

@@ -97,9 +97,13 @@ public class TunerChannel implements Comparable<TunerChannel>
} }
@Override @Override
public int compareTo( TunerChannel otherLock ) public int compareTo( TunerChannel otherTunerChannel )
{ {
if( mFrequency < otherLock.getFrequency() ) if( this.equals( otherTunerChannel ) )
{
return 0;
}
else if( mFrequency < otherTunerChannel.getFrequency() )
{ {
return -1; return -1;
} }

View File

@@ -14,8 +14,8 @@ public interface TunerChannelProvider
* *
* @return - source for 48k sample rate * @return - source for 48k sample rate
*/ */
public abstract TunerChannelSource getChannel( ThreadPoolManager threadPoolManager, public abstract TunerChannelSource getChannel( TunerChannel channel )
TunerChannel channel ) throws RejectedExecutionException, SourceException; throws RejectedExecutionException, SourceException;
/** /**
* Releases the tuned channel resources * Releases the tuned channel resources

View File

@@ -21,6 +21,7 @@ import java.util.ArrayList;
import java.util.List; import java.util.List;
import java.util.concurrent.LinkedTransferQueue; import java.util.concurrent.LinkedTransferQueue;
import java.util.concurrent.RejectedExecutionException; import java.util.concurrent.RejectedExecutionException;
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 java.util.concurrent.atomic.AtomicBoolean; import java.util.concurrent.atomic.AtomicBoolean;
@@ -37,8 +38,6 @@ 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.FrequencyChangeListener; import source.tuner.frequency.FrequencyChangeListener;
import controller.ThreadPoolManager;
import controller.ThreadPoolManager.ThreadType;
import dsp.filter.FilterFactory; import dsp.filter.FilterFactory;
import dsp.filter.Window.WindowType; import dsp.filter.Window.WindowType;
import dsp.filter.cic.ComplexPrimeCICDecimate; import dsp.filter.cic.ComplexPrimeCICDecimate;
@@ -62,7 +61,6 @@ public class TunerChannelSource extends ComplexSource
private ComplexPrimeCICDecimate mDecimationFilter; private ComplexPrimeCICDecimate mDecimationFilter;
private Listener<ComplexBuffer> mListener; private Listener<ComplexBuffer> mListener;
private FrequencyChangeListener mFrequencyChangeListener; private FrequencyChangeListener mFrequencyChangeListener;
private ThreadPoolManager mThreadPoolManager;
private ScheduledFuture<?> mTaskHandle; private ScheduledFuture<?> mTaskHandle;
private long mTunerFrequency = 0; private long mTunerFrequency = 0;
@@ -71,15 +69,11 @@ public class TunerChannelSource extends ComplexSource
private DecimationProcessor mDecimationProcessor = new DecimationProcessor(); private DecimationProcessor mDecimationProcessor = new DecimationProcessor();
public TunerChannelSource( ThreadPoolManager threadPoolManager, public TunerChannelSource( Tuner tuner, TunerChannel tunerChannel )
Tuner tuner, throws RejectedExecutionException, SourceException
TunerChannel tunerChannel )
throws RejectedExecutionException, SourceException
{ {
super( "Tuner Channel Source" ); super( "Tuner Channel Source" );
mThreadPoolManager = threadPoolManager;
mTuner = tuner; mTuner = tuner;
mTuner.addListener( (FrequencyChangeListener)this ); mTuner.addListener( (FrequencyChangeListener)this );
@@ -95,14 +89,17 @@ public class TunerChannelSource extends ComplexSource
/* 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( frequencyChanged( new FrequencyChangeEvent(
Event.SAMPLE_RATE_CHANGE_NOTIFICATION, mTuner.getSampleRate() ) ); Event.SAMPLE_RATE_CHANGE_NOTIFICATION, mTuner.getSampleRate() ) );
}
/* Schedule the decimation task to run every 20 ms (50 iterations/second) */ public void start( ScheduledExecutorService scheduledExecutorService )
mTaskHandle = mThreadPoolManager.scheduleFixedRate( ThreadType.DECIMATION, {
mDecimationProcessor, 20, TimeUnit.MILLISECONDS ); /* Schedule the decimation task to run every 10 ms (100 iterations/second) */
mTaskHandle = scheduledExecutorService.scheduleAtFixedRate(
mDecimationProcessor, 0, 10, TimeUnit.MILLISECONDS );
/* Finally, register to receive samples from the tuner */ /* Finally, register to receive samples from the tuner */
mTuner.addListener( (Listener<ComplexBuffer>)this ); mTuner.addListener( (Listener<ComplexBuffer>)this );
} }
@Override @Override
public void dispose() public void dispose()
@@ -129,14 +126,15 @@ public class TunerChannelSource extends ComplexSource
{ {
if( mTaskHandle != null ) if( mTaskHandle != null )
{ {
mThreadPoolManager.cancel( mTaskHandle ); mTaskHandle.cancel( true );
mThreadPoolManager = null;
mTaskHandle = null; mTaskHandle = null;
} }
synchronized( mBuffer )
mBuffer.clear(); {
mBuffer = null; mBuffer.clear();
mBuffer = null;
}
mFrequencyChangeListener = null; mFrequencyChangeListener = null;
mListener = null; mListener = null;
@@ -160,7 +158,13 @@ public class TunerChannelSource extends ComplexSource
@Override @Override
public void receive( ComplexBuffer buffer ) public void receive( ComplexBuffer buffer )
{ {
mBuffer.add( buffer ); synchronized( mBuffer )
{
if( mBuffer != null )
{
mBuffer.add( buffer );
}
}
} }
public void setFrequencyChangeListener( FrequencyChangeListener listener ) public void setFrequencyChangeListener( FrequencyChangeListener listener )
@@ -290,7 +294,7 @@ public class TunerChannelSource extends ComplexSource
{ {
if( mBuffer != null ) if( mBuffer != null )
{ {
mBuffer.drainTo( mSampleBuffers, 4 ); mBuffer.drainTo( mSampleBuffers, 8 );
for( Buffer buffer: mSampleBuffers ) for( Buffer buffer: mSampleBuffers )
{ {

View File

@@ -36,6 +36,7 @@ public enum TunerClass
GIGABYTE_GTU7300( TunerType.FITIPOWER_FC0012, "1B80", "D393", "Gigabyte", "GT-U7300" ), GIGABYTE_GTU7300( TunerType.FITIPOWER_FC0012, "1B80", "D393", "Gigabyte", "GT-U7300" ),
GTEK_T803( TunerType.FITIPOWER_FC0012, "1F4D", "B803", "GTek", "T803" ), GTEK_T803( TunerType.FITIPOWER_FC0012, "1F4D", "B803", "GTek", "T803" ),
HACKRF_ONE( TunerType.HACKRF, "1D50", "6089", "Great Scott Gadgets", "HackRF One" ), HACKRF_ONE( TunerType.HACKRF, "1D50", "6089", "Great Scott Gadgets", "HackRF One" ),
RAD1O( TunerType.HACKRF, "1D50", "CC15", "Munich hackerspace", "Rad1o" ),
LIFEVIEW_LV5T_DELUXE( TunerType.FITIPOWER_FC0012, "1F4D", "C803", "Liveview", "LV5T Deluxe" ), LIFEVIEW_LV5T_DELUXE( TunerType.FITIPOWER_FC0012, "1F4D", "C803", "Liveview", "LV5T Deluxe" ),
MYGICA_TD312( TunerType.FITIPOWER_FC0012, "1F4D", "D286", "MyGica", "TD312" ), MYGICA_TD312( TunerType.FITIPOWER_FC0012, "1F4D", "D286", "MyGica", "TD312" ),
PEAK_102569AGPK( TunerType.FITIPOWER_FC0012, "1B80", "D395", "Peak", "102569AGPK" ), PEAK_102569AGPK( TunerType.FITIPOWER_FC0012, "1B80", "D395", "Peak", "102569AGPK" ),
@@ -185,6 +186,10 @@ public enum TunerClass
{ {
retVal = AIRSPY; retVal = AIRSPY;
} }
else if( productID == 52245 ) //CC15
{
retVal = HACKRF_ONE;
}
break; break;
case 8013: //1F4D case 8013: //1F4D
if( productID == 47107 ) //B803 if( productID == 47107 ) //B803

View File

@@ -194,8 +194,8 @@ public abstract class TunerController implements Tunable
* *
* @throws RejectedExecutionException if the decimation processor has an error * @throws RejectedExecutionException if the decimation processor has an error
*/ */
public TunerChannelSource getChannel( ThreadPoolManager threadPoolManager, public TunerChannelSource getChannel( Tuner tuner, TunerChannel channel )
Tuner tuner, TunerChannel channel ) throws RejectedExecutionException throws RejectedExecutionException
{ {
TunerChannelSource source = null; TunerChannelSource source = null;
@@ -210,8 +210,7 @@ public abstract class TunerController implements Tunable
updateLOFrequency(); updateLOFrequency();
} }
source = new TunerChannelSource( threadPoolManager, source = new TunerChannelSource( tuner, channel );
tuner, channel );
} }
catch( SourceException se ) catch( SourceException se )
{ {
@@ -246,10 +245,6 @@ public abstract class TunerController implements Tunable
{ {
mTunedChannels.remove( tunerChannelSource.getTunerChannel() ); mTunedChannels.remove( tunerChannelSource.getTunerChannel() );
} }
else
{
mLog.error( "Request to release channel that is not in current tuned channels set" );
}
} }
/** /**

View File

@@ -100,8 +100,7 @@ public class TunerManager
try try
{ {
retVal = tuner.getChannel( retVal = tuner.getChannel( tunerChannel );
mResourceManager.getThreadPoolManager(), tunerChannel );
} }
catch ( RejectedExecutionException ree ) catch ( RejectedExecutionException ree )
{ {
@@ -219,6 +218,7 @@ public class TunerManager
case FUNCUBE_DONGLE_PRO_PLUS: case FUNCUBE_DONGLE_PRO_PLUS:
return initFuncubeProPlusTuner( device, descriptor ); return initFuncubeProPlusTuner( device, descriptor );
case HACKRF_ONE: case HACKRF_ONE:
case RAD1O:
return initHackRFTuner( device, descriptor ); return initHackRFTuner( device, descriptor );
case COMPRO_VIDEOMATE_U620F: case COMPRO_VIDEOMATE_U620F:
case COMPRO_VIDEOMATE_U650F: case COMPRO_VIDEOMATE_U650F:
@@ -545,16 +545,14 @@ public class TunerManager
} }
} }
} }
else
{
config = configs.get( 0 );
}
//If we're still null at this point, create a default config //If we're still null at this point, create a default config
if( config == null ) if( config == null )
{ {
String name = "Default-" + tuner.getUniqueID();
config = mResourceManager.getSettingsManager() config = mResourceManager.getSettingsManager()
.addNewTunerConfiguration( tuner.getTunerType(), "Default" ); .addNewTunerConfiguration( tuner.getTunerType(), name );
} }
//Persist the config as the selected tuner configuration. The method //Persist the config as the selected tuner configuration. The method

View File

@@ -128,8 +128,8 @@ public class AirspyTuner extends Tuner
} }
@Override @Override
public TunerChannelSource getChannel( ThreadPoolManager threadPoolManager, public TunerChannelSource getChannel( TunerChannel channel )
TunerChannel channel ) throws RejectedExecutionException, SourceException throws RejectedExecutionException, SourceException
{ {
//TODO: this channel has a decimated sample rate of: //TODO: this channel has a decimated sample rate of:
// 10.0 MSps = 10,000,000 / 208 = 48076.923 // 10.0 MSps = 10,000,000 / 208 = 48076.923
@@ -138,7 +138,7 @@ public class AirspyTuner extends Tuner
//Consider implementing a fractional resampler to get a correct 48 kHz //Consider implementing a fractional resampler to get a correct 48 kHz
//output sample rate //output sample rate
return mController.getChannel( threadPoolManager, this, channel ); return mController.getChannel( this, channel );
} }
@Override @Override

View File

@@ -262,7 +262,30 @@ public class AirspyTunerController extends TunerController
} }
} }
@Override public static String getTransferStatus( int status )
{
switch( status )
{
case 0:
return "TRANSFER COMPLETED (0)";
case 1:
return "TRANSFER ERROR (1)";
case 2:
return "TRANSFER TIMED OUT (2)";
case 3:
return "TRANSFER CANCELLED (3)";
case 4:
return "TRANSFER STALL (4)";
case 5:
return "TRANSFER NO DEVICE (5)";
case 6:
return "TRANSFER OVERFLOW (6)";
default:
return "UNKNOWN TRANSFER STATUS (" + status + ")";
}
}
@Override
public void apply( TunerConfiguration config ) throws SourceException public void apply( TunerConfiguration config ) throws SourceException
{ {
if( config instanceof AirspyTunerConfiguration ) if( config instanceof AirspyTunerConfiguration )
@@ -1192,18 +1215,21 @@ public class AirspyTunerController extends TunerController
*/ */
public void addListener( Listener<ComplexBuffer> listener ) public void addListener( Listener<ComplexBuffer> listener )
{ {
mComplexBufferBroadcaster.addListener( listener ); synchronized( mComplexBufferBroadcaster )
{
mComplexBufferBroadcaster.addListener( listener );
if( mBufferProcessor == null || !mBufferProcessor.isRunning() ) if( mBufferProcessor == null || !mBufferProcessor.isRunning() )
{ {
mBufferProcessor = new BufferProcessor(); mBufferProcessor = new BufferProcessor();
Thread thread = new Thread( mBufferProcessor ); Thread thread = new Thread( mBufferProcessor );
thread.setDaemon( true ); thread.setDaemon( true );
thread.setName( "Airspy Buffer Processor" ); thread.setName( "Airspy Buffer Processor" );
thread.start(); thread.start();
} }
}
} }
/** /**
@@ -1212,12 +1238,15 @@ public class AirspyTunerController extends TunerController
*/ */
public void removeListener( Listener<ComplexBuffer> listener ) public void removeListener( Listener<ComplexBuffer> listener )
{ {
mComplexBufferBroadcaster.removeListener( listener ); synchronized( mComplexBufferBroadcaster )
{
mComplexBufferBroadcaster.removeListener( listener );
if( !mComplexBufferBroadcaster.hasListeners() ) if( !mComplexBufferBroadcaster.hasListeners() )
{ {
mBufferProcessor.stop(); mBufferProcessor.stop();
} }
}
} }
/** /**
@@ -1227,8 +1256,11 @@ public class AirspyTunerController extends TunerController
public class BufferProcessor implements Runnable, TransferCallback public class BufferProcessor implements Runnable, TransferCallback
{ {
private ScheduledFuture<?> mSampleDispatcherTask; private ScheduledFuture<?> mSampleDispatcherTask;
private CopyOnWriteArrayList<Transfer> mTransfers; private LinkedTransferQueue<Transfer> mAvailableTransfers;
private LinkedTransferQueue<Transfer> mTransfersInProgress = new LinkedTransferQueue<>();
private AtomicBoolean mRunning = new AtomicBoolean(); private AtomicBoolean mRunning = new AtomicBoolean();
private ByteBuffer mLibUsbHandlerStatus;
private boolean mCancel = false;
@Override @Override
public void run() public void run()
@@ -1237,33 +1269,64 @@ public class AirspyTunerController extends TunerController
{ {
prepareTransfers(); prepareTransfers();
for( Transfer transfer: mTransfers )
{
int result = LibUsb.submitTransfer( transfer );
if( result != LibUsb.SUCCESS )
{
mLog.error( "error submitting transfer [" +
LibUsb.errorName( result ) + "]" );
break;
}
}
mSampleDispatcherTask = mThreadPoolManager.scheduleFixedRate( mSampleDispatcherTask = mThreadPoolManager.scheduleFixedRate(
ThreadType.SOURCE_SAMPLE_PROCESSING, new BufferDispatcher(), ThreadType.SOURCE_SAMPLE_PROCESSING, new BufferDispatcher(),
20, TimeUnit.MILLISECONDS ); 20, TimeUnit.MILLISECONDS );
mLibUsbHandlerStatus = ByteBuffer.allocateDirect( 4 );
List<Transfer> transfers = new ArrayList<>();
mCancel = false;
while( mRunning.get() ) while( mRunning.get() )
{ {
ByteBuffer completed = ByteBuffer.allocateDirect( 4 ); mAvailableTransfers.drainTo( transfers );
for( Transfer transfer: transfers )
{
int result = LibUsb.submitTransfer( transfer );
if( result == LibUsb.SUCCESS )
{
mTransfersInProgress.add( transfer );
}
else
{
mLog.error( "error submitting transfer [" +
LibUsb.errorName( result ) + "]" );
}
}
int result = LibUsb.handleEventsTimeoutCompleted( int result = LibUsb.handleEventsTimeoutCompleted(
null, USB_TIMEOUT_MS, completed.asIntBuffer() ); null, USB_TIMEOUT_MS, mLibUsbHandlerStatus.asIntBuffer() );
if( result != LibUsb.SUCCESS ) if( result != LibUsb.SUCCESS )
{ {
mLog.error( "error handling events for libusb" ); mLog.error( "error handling events for libusb" );
} }
transfers.clear();
mLibUsbHandlerStatus.rewind();
}
if( mCancel )
{
for( Transfer transfer: mTransfersInProgress )
{
LibUsb.cancelTransfer( transfer );
}
int result = LibUsb.handleEventsTimeoutCompleted( null,
USB_TIMEOUT_MS, mLibUsbHandlerStatus.asIntBuffer() );
if( result != LibUsb.SUCCESS )
{
mLog.error( "error handling events for libusb" );
}
mLibUsbHandlerStatus.rewind();
} }
} }
} }
@@ -1273,10 +1336,10 @@ public class AirspyTunerController extends TunerController
*/ */
public void stop() public void stop()
{ {
mCancel = true;
if( mRunning.compareAndSet( true, false ) ) if( mRunning.compareAndSet( true, false ) )
{ {
cancel();
if( mSampleDispatcherTask != null ) if( mSampleDispatcherTask != null )
{ {
mSampleDispatcherTask.cancel( true ); mSampleDispatcherTask.cancel( true );
@@ -1296,97 +1359,68 @@ public class AirspyTunerController extends TunerController
@Override @Override
public void processTransfer( Transfer transfer ) public void processTransfer( Transfer transfer )
{ {
if( transfer.status() == LibUsb.TRANSFER_COMPLETED ) mTransfersInProgress.remove( transfer );
{
ByteBuffer buffer = transfer.buffer();
byte[] data = new byte[ transfer.actualLength() ];
buffer.get( data );
buffer.rewind();
mFilledBuffers.add( data );
if( !isRunning() )
{
LibUsb.cancelTransfer( transfer );
}
}
switch( transfer.status() ) switch( transfer.status() )
{ {
case LibUsb.TRANSFER_COMPLETED: case LibUsb.TRANSFER_COMPLETED:
/* resubmit the transfer */ case LibUsb.TRANSFER_STALL:
int result = LibUsb.submitTransfer( transfer ); if( transfer.actualLength() > 0 )
if( result != LibUsb.SUCCESS )
{ {
mLog.error( "couldn't resubmit buffer transfer to tuner" ); ByteBuffer buffer = transfer.buffer();
LibUsb.freeTransfer( transfer );
mTransfers.remove( transfer ); byte[] data = new byte[ transfer.actualLength() ];
buffer.get( data );
buffer.rewind();
if( isRunning() )
{
mFilledBuffers.add( data );
}
} }
break; break;
case LibUsb.TRANSFER_CANCELLED: case LibUsb.TRANSFER_CANCELLED:
/* free the transfer and remove it */
LibUsb.freeTransfer( transfer );
mTransfers.remove( transfer );
break; break;
default: default:
/* unexpected error */ /* unexpected error */
mLog.error( "transfer error" ); mLog.error( "transfer error [" +
getTransferStatus( transfer.status() ) +
"] transferred actual: " + transfer.actualLength() );
} }
mAvailableTransfers.add( transfer );
} }
private void cancel()
{
for( Transfer transfer: mTransfers )
{
LibUsb.cancelTransfer( transfer );
}
ByteBuffer completed = ByteBuffer.allocateDirect( 4 );
int result = LibUsb.handleEventsTimeoutCompleted(
null, USB_TIMEOUT_MS, completed.asIntBuffer() );
if( result != LibUsb.SUCCESS )
{
mLog.error( "error handling usb events during cancel [" +
LibUsb.errorName( result ) + "]" );
}
}
/** /**
* Prepares (allocates) a set of transfer buffers for use in * Prepares (allocates) a set of transfer buffers for use in
* transferring data from the tuner via the bulk interface * transferring data from the tuner via the bulk interface
*/ */
private void prepareTransfers() throws LibUsbException private void prepareTransfers() throws LibUsbException
{ {
mTransfers = new CopyOnWriteArrayList<Transfer>(); if( mAvailableTransfers == null )
for( int x = 0; x < TRANSFER_BUFFER_POOL_SIZE; x++ )
{ {
Transfer transfer = LibUsb.allocTransfer(); mAvailableTransfers = new LinkedTransferQueue<>();
if( transfer == null ) for( int x = 0; x < TRANSFER_BUFFER_POOL_SIZE; x++ )
{ {
throw new LibUsbException( "couldn't allocate transfer", Transfer transfer = LibUsb.allocTransfer();
LibUsb.ERROR_NO_MEM );
}
final ByteBuffer buffer = if( transfer == null )
ByteBuffer.allocateDirect( mBufferSize ); {
throw new LibUsbException( "couldn't allocate transfer",
LibUsb.ERROR_NO_MEM );
}
LibUsb.fillBulkTransfer( transfer, final ByteBuffer buffer =
mDeviceHandle, ByteBuffer.allocateDirect( mBufferSize );
USB_ENDPOINT,
buffer,
BufferProcessor.this,
"Buffer #" + x,
USB_TIMEOUT_MS );
mTransfers.add( transfer ); LibUsb.fillBulkTransfer( transfer, mDeviceHandle, USB_ENDPOINT,
buffer, BufferProcessor.this, "Buffer", USB_TIMEOUT_MS );
mAvailableTransfers.add( transfer );
}
} }
} }
} }

View File

@@ -99,8 +99,8 @@ public class B100Tuner extends Tuner
} }
@Override @Override
public TunerChannelSource getChannel( ThreadPoolManager threadPoolManager, public TunerChannelSource getChannel( TunerChannel channel )
TunerChannel channel ) throws RejectedExecutionException, SourceException throws RejectedExecutionException, SourceException
{ {
//TODO: pass this call to the tuner controller //TODO: pass this call to the tuner controller
return null; return null;

View File

@@ -112,11 +112,10 @@ public class FCDTuner extends MixerTuner
} }
@Override @Override
public TunerChannelSource getChannel( ThreadPoolManager threadPoolManager, public TunerChannelSource getChannel( TunerChannel tunerChannel )
TunerChannel tunerChannel ) throws RejectedExecutionException, throws RejectedExecutionException, SourceException
SourceException
{ {
return mController.getChannel( threadPoolManager, this, tunerChannel ); return mController.getChannel( this, tunerChannel );
} }
/** /**

View File

@@ -106,10 +106,10 @@ public class HackRFTuner extends Tuner
} }
@Override @Override
public TunerChannelSource getChannel( ThreadPoolManager threadPoolManager, public TunerChannelSource getChannel( TunerChannel channel )
TunerChannel channel ) throws RejectedExecutionException, SourceException throws RejectedExecutionException, SourceException
{ {
return mController.getChannel( threadPoolManager, this, channel ); return mController.getChannel( this, channel );
} }
@Override @Override

View File

@@ -56,6 +56,7 @@ import java.nio.ByteBuffer;
import java.nio.ByteOrder; import java.nio.ByteOrder;
import java.util.ArrayList; import java.util.ArrayList;
import java.util.Arrays; import java.util.Arrays;
import java.util.List;
import java.util.concurrent.CopyOnWriteArrayList; import java.util.concurrent.CopyOnWriteArrayList;
import java.util.concurrent.LinkedTransferQueue; import java.util.concurrent.LinkedTransferQueue;
import java.util.concurrent.ScheduledFuture; import java.util.concurrent.ScheduledFuture;
@@ -91,7 +92,7 @@ public class HackRFTunerController extends TunerController
private final static Logger mLog = private final static Logger mLog =
LoggerFactory.getLogger( HackRFTunerController.class ); LoggerFactory.getLogger( HackRFTunerController.class );
public final static long TIMEOUT_US = 1000000l; //uSeconds public final static long USB_TIMEOUT_MS = 1000000l; //uSeconds
public static final byte USB_ENDPOINT = (byte)0x81; public static final byte USB_ENDPOINT = (byte)0x81;
public static final byte USB_INTERFACE = (byte)0x0; public static final byte USB_INTERFACE = (byte)0x0;
@@ -394,7 +395,7 @@ public class HackRFTunerController extends TunerController
(short)value, (short)value,
(short)index, (short)index,
buffer, buffer,
TIMEOUT_US ); USB_TIMEOUT_MS );
if( transferred < 0 ) if( transferred < 0 )
{ {
@@ -467,7 +468,7 @@ public class HackRFTunerController extends TunerController
(short)value, (short)value,
(short)index, (short)index,
buffer, buffer,
TIMEOUT_US ); USB_TIMEOUT_MS );
if( transferred < 0 ) if( transferred < 0 )
{ {
@@ -886,18 +887,21 @@ public class HackRFTunerController extends TunerController
*/ */
public void addListener( Listener<ComplexBuffer> listener ) public void addListener( Listener<ComplexBuffer> listener )
{ {
mComplexBufferBroadcaster.addListener( listener ); synchronized( mComplexBufferBroadcaster )
{
mComplexBufferBroadcaster.addListener( listener );
if( mBufferProcessor == null || !mBufferProcessor.isRunning() ) if( mBufferProcessor == null || !mBufferProcessor.isRunning() )
{ {
mBufferProcessor = new BufferProcessor(); mBufferProcessor = new BufferProcessor();
Thread thread = new Thread( mBufferProcessor ); Thread thread = new Thread( mBufferProcessor );
thread.setDaemon( true ); thread.setDaemon( true );
thread.setName( "HackRF Sample Processor" ); thread.setName( "HackRF Sample Processor" );
thread.start(); thread.start();
} }
}
} }
/** /**
@@ -906,12 +910,15 @@ public class HackRFTunerController extends TunerController
*/ */
public void removeListener( Listener<ComplexBuffer> listener ) public void removeListener( Listener<ComplexBuffer> listener )
{ {
mComplexBufferBroadcaster.removeListener( listener ); synchronized( mComplexBufferBroadcaster )
{
mComplexBufferBroadcaster.removeListener( listener );
if( !mComplexBufferBroadcaster.hasListeners() ) if( !mComplexBufferBroadcaster.hasListeners() )
{ {
mBufferProcessor.stop(); mBufferProcessor.stop();
} }
}
} }
/** /**
@@ -921,45 +928,77 @@ public class HackRFTunerController extends TunerController
public class BufferProcessor implements Runnable, TransferCallback public class BufferProcessor implements Runnable, TransferCallback
{ {
private ScheduledFuture<?> mSampleDispatcherTask; private ScheduledFuture<?> mSampleDispatcherTask;
private CopyOnWriteArrayList<Transfer> mTransfers; private LinkedTransferQueue<Transfer> mAvailableTransfers;
private LinkedTransferQueue<Transfer> mTransfersInProgress = new LinkedTransferQueue<>();
private AtomicBoolean mRunning = new AtomicBoolean(); private AtomicBoolean mRunning = new AtomicBoolean();
private ByteBuffer mLibUsbHandlerStatus;
private boolean mCancel = false;
@Override @Override
public void run() public void run()
{ {
if( mRunning.compareAndSet( false, true ) ) if( mRunning.compareAndSet( false, true ) )
{ {
mLog.info( "starting HackRF sample fetch thread" );
prepareTransfers(); prepareTransfers();
for( Transfer transfer: mTransfers )
{
int result = LibUsb.submitTransfer( transfer );
if( result != LibUsb.SUCCESS )
{
mLog.error( "error submitting transfer [" +
LibUsb.errorName( result ) + "]" );
break;
}
}
mSampleDispatcherTask = mThreadPoolManager.scheduleFixedRate( mSampleDispatcherTask = mThreadPoolManager.scheduleFixedRate(
ThreadType.SOURCE_SAMPLE_PROCESSING, new BufferDispatcher(), ThreadType.SOURCE_SAMPLE_PROCESSING, new BufferDispatcher(),
20, TimeUnit.MILLISECONDS ); 20, TimeUnit.MILLISECONDS );
mLibUsbHandlerStatus = ByteBuffer.allocateDirect( 4 );
List<Transfer> transfers = new ArrayList<>();
mCancel = false;
while( mRunning.get() ) while( mRunning.get() )
{ {
ByteBuffer completed = ByteBuffer.allocateDirect( 4 ); mAvailableTransfers.drainTo( transfers );
for( Transfer transfer: transfers )
{
int result = LibUsb.submitTransfer( transfer );
if( result == LibUsb.SUCCESS )
{
mTransfersInProgress.add( transfer );
}
else
{
mLog.error( "error submitting transfer [" +
LibUsb.errorName( result ) + "]" );
}
}
int result = LibUsb.handleEventsTimeoutCompleted( int result = LibUsb.handleEventsTimeoutCompleted(
null, TIMEOUT_US, completed.asIntBuffer() ); null, USB_TIMEOUT_MS, mLibUsbHandlerStatus.asIntBuffer() );
if( result != LibUsb.SUCCESS ) if( result != LibUsb.SUCCESS )
{ {
mLog.error( "error handling events for libusb" ); mLog.error( "error handling events for libusb" );
} }
transfers.clear();
mLibUsbHandlerStatus.rewind();
}
if( mCancel )
{
for( Transfer transfer: mTransfersInProgress )
{
LibUsb.cancelTransfer( transfer );
}
int result = LibUsb.handleEventsTimeoutCompleted(
null, USB_TIMEOUT_MS, mLibUsbHandlerStatus.asIntBuffer() );
if( result != LibUsb.SUCCESS )
{
mLog.error( "error cancelling transfers in progress" );
}
mLibUsbHandlerStatus.rewind();
} }
} }
} }
@@ -969,12 +1008,10 @@ public class HackRFTunerController extends TunerController
*/ */
public void stop() public void stop()
{ {
mCancel = true;
if( mRunning.compareAndSet( true, false ) ) if( mRunning.compareAndSet( true, false ) )
{ {
mLog.info( "stopping HackRF sample fetch thread" );
cancel();
if( mSampleDispatcherTask != null ) if( mSampleDispatcherTask != null )
{ {
mSampleDispatcherTask.cancel( true ); mSampleDispatcherTask.cancel( true );
@@ -994,41 +1031,29 @@ public class HackRFTunerController extends TunerController
@Override @Override
public void processTransfer( Transfer transfer ) public void processTransfer( Transfer transfer )
{ {
if( transfer.status() == LibUsb.TRANSFER_COMPLETED ) mTransfersInProgress.remove( transfer );
{
ByteBuffer buffer = transfer.buffer();
byte[] data = new byte[ transfer.actualLength() ];
buffer.get( data );
buffer.rewind();
mFilledBuffers.add( data );
if( !isRunning() )
{
LibUsb.cancelTransfer( transfer );
}
}
switch( transfer.status() ) switch( transfer.status() )
{ {
case LibUsb.TRANSFER_COMPLETED: case LibUsb.TRANSFER_COMPLETED:
/* resubmit the transfer */ case LibUsb.TRANSFER_STALL:
int result = LibUsb.submitTransfer( transfer ); if( transfer.actualLength() > 0 )
if( result != LibUsb.SUCCESS )
{ {
mLog.error( "couldn't resubmit buffer transfer to tuner" ); ByteBuffer buffer = transfer.buffer();
LibUsb.freeTransfer( transfer );
mTransfers.remove( transfer ); byte[] data = new byte[ transfer.actualLength() ];
buffer.get( data );
buffer.rewind();
if( isRunning() )
{
mFilledBuffers.add( data );
}
} }
break; break;
case LibUsb.TRANSFER_CANCELLED: case LibUsb.TRANSFER_CANCELLED:
/* free the transfer and remove it */
LibUsb.freeTransfer( transfer );
mTransfers.remove( transfer );
break; break;
default: default:
/* unexpected error */ /* unexpected error */
@@ -1036,57 +1061,38 @@ public class HackRFTunerController extends TunerController
getTransferStatus( transfer.status() ) + getTransferStatus( transfer.status() ) +
"] transferred actual: " + transfer.actualLength() ); "] transferred actual: " + transfer.actualLength() );
} }
mAvailableTransfers.add( transfer );
} }
private void cancel()
{
for( Transfer transfer: mTransfers )
{
LibUsb.cancelTransfer( transfer );
}
ByteBuffer completed = ByteBuffer.allocateDirect( 4 );
int result = LibUsb.handleEventsTimeoutCompleted(
null, TIMEOUT_US, completed.asIntBuffer() );
if( result != LibUsb.SUCCESS )
{
mLog.error( "error handling usb events during cancel [" +
LibUsb.errorName( result ) + "]" );
}
}
/** /**
* Prepares (allocates) a set of transfer buffers for use in * Prepares (allocates) a set of transfer buffers for use in
* transferring data from the tuner via the bulk interface * transferring data from the tuner via the bulk interface
*/ */
private void prepareTransfers() throws LibUsbException private void prepareTransfers() throws LibUsbException
{ {
mTransfers = new CopyOnWriteArrayList<Transfer>(); if( mAvailableTransfers == null )
for( int x = 0; x < TRANSFER_BUFFER_POOL_SIZE; x++ )
{ {
Transfer transfer = LibUsb.allocTransfer(); mAvailableTransfers = new LinkedTransferQueue<>();
if( transfer == null ) for( int x = 0; x < TRANSFER_BUFFER_POOL_SIZE; x++ )
{ {
throw new LibUsbException( "couldn't allocate transfer", Transfer transfer = LibUsb.allocTransfer();
LibUsb.ERROR_NO_MEM );
}
final ByteBuffer buffer = if( transfer == null )
ByteBuffer.allocateDirect( mBufferSize ); {
throw new LibUsbException( "couldn't allocate transfer",
LibUsb.ERROR_NO_MEM );
}
LibUsb.fillBulkTransfer( transfer, final ByteBuffer buffer =
mDeviceHandle, ByteBuffer.allocateDirect( mBufferSize );
USB_ENDPOINT,
buffer,
BufferProcessor.this,
"Buffer #" + x,
TIMEOUT_US );
mTransfers.add( transfer ); LibUsb.fillBulkTransfer( transfer, mDeviceHandle,USB_ENDPOINT,
buffer, BufferProcessor.this, "Buffer", USB_TIMEOUT_MS );
mAvailableTransfers.add( transfer );
}
} }
} }
} }

View File

@@ -37,7 +37,6 @@ import source.tuner.TunerType;
import source.tuner.frequency.FrequencyChangeListener; import source.tuner.frequency.FrequencyChangeListener;
import source.tuner.rtl.RTL2832TunerController.SampleRate; import source.tuner.rtl.RTL2832TunerController.SampleRate;
import controller.ResourceManager; import controller.ResourceManager;
import controller.ThreadPoolManager;
public class RTL2832Tuner extends Tuner public class RTL2832Tuner extends Tuner
{ {
@@ -148,16 +147,14 @@ public class RTL2832Tuner extends Tuner
} }
@Override @Override
public TunerChannelSource getChannel( ThreadPoolManager threadPoolManager, public TunerChannelSource getChannel( TunerChannel channel )
TunerChannel channel ) throws RejectedExecutionException, SourceException
throws RejectedExecutionException,
SourceException
{ {
TunerChannelSource source = null; TunerChannelSource source = null;
try try
{ {
source = mController.getChannel( threadPoolManager, this, channel ); source = mController.getChannel( this, channel );
} }
catch( Exception e ) catch( Exception e )
{ {

View File

@@ -27,7 +27,7 @@ import java.nio.charset.Charset;
import java.text.DecimalFormat; import java.text.DecimalFormat;
import java.util.ArrayList; import java.util.ArrayList;
import java.util.Arrays; import java.util.Arrays;
import java.util.concurrent.CopyOnWriteArrayList; import java.util.List;
import java.util.concurrent.LinkedTransferQueue; import java.util.concurrent.LinkedTransferQueue;
import java.util.concurrent.ScheduledFuture; import java.util.concurrent.ScheduledFuture;
import java.util.concurrent.TimeUnit; import java.util.concurrent.TimeUnit;
@@ -55,8 +55,6 @@ import sample.complex.ComplexBuffer;
import source.SourceException; import source.SourceException;
import source.tuner.TunerController; import source.tuner.TunerController;
import source.tuner.TunerType; import source.tuner.TunerType;
import source.tuner.frequency.FrequencyChangeEvent;
import source.tuner.frequency.FrequencyChangeEvent.Event;
import buffer.FloatAveragingBuffer; import buffer.FloatAveragingBuffer;
import controller.ResourceManager; import controller.ResourceManager;
import controller.ThreadPoolManager; import controller.ThreadPoolManager;
@@ -73,7 +71,7 @@ public abstract class RTL2832TunerController extends TunerController
public final static int TWO_TO_22_POWER = 4194304; public final static int TWO_TO_22_POWER = 4194304;
public final static byte USB_INTERFACE = (byte)0x0; public final static byte USB_INTERFACE = (byte)0x0;
public final static byte BULK_ENDPOINT_IN = (byte)0x81; public final static byte USB_BULK_ENDPOINT = (byte)0x81;
public final static byte CONTROL_ENDPOINT_IN = public final static byte CONTROL_ENDPOINT_IN =
(byte)( LibUsb.ENDPOINT_IN | LibUsb.REQUEST_TYPE_VENDOR ); (byte)( LibUsb.ENDPOINT_IN | LibUsb.REQUEST_TYPE_VENDOR );
public final static byte CONTROL_ENDPOINT_OUT = public final static byte CONTROL_ENDPOINT_OUT =
@@ -1342,21 +1340,24 @@ public abstract class RTL2832TunerController extends TunerController
*/ */
public void addListener( Listener<ComplexBuffer> listener ) public void addListener( Listener<ComplexBuffer> listener )
{ {
mComplexBufferBroadcaster.addListener( listener ); synchronized( mComplexBufferBroadcaster )
{
mComplexBufferBroadcaster.addListener( listener );
if( mBufferProcessor == null ) if( mBufferProcessor == null )
{ {
mBufferProcessor = new BufferProcessor( mThreadPoolManager ); mBufferProcessor = new BufferProcessor( mThreadPoolManager );
} }
if( !mBufferProcessor.isRunning() ) if( !mBufferProcessor.isRunning() )
{ {
Thread thread = new Thread( mBufferProcessor ); Thread thread = new Thread( mBufferProcessor );
thread.setDaemon( true ); thread.setDaemon( true );
thread.setName( "RTL2832 Sample Processor" ); thread.setName( "RTL2832 Sample Processor" );
thread.start(); thread.start();
} }
}
} }
/** /**
@@ -1365,12 +1366,15 @@ public abstract class RTL2832TunerController extends TunerController
*/ */
public void removeListener( Listener<ComplexBuffer> listener ) public void removeListener( Listener<ComplexBuffer> listener )
{ {
mComplexBufferBroadcaster.removeListener( listener ); synchronized( mComplexBufferBroadcaster )
{
mComplexBufferBroadcaster.removeListener( listener );
if( !mComplexBufferBroadcaster.hasListeners() ) if( !mComplexBufferBroadcaster.hasListeners() )
{ {
mBufferProcessor.stop(); mBufferProcessor.stop();
} }
}
} }
/** /**
@@ -1380,9 +1384,12 @@ public abstract class RTL2832TunerController extends TunerController
public class BufferProcessor implements Runnable, TransferCallback public class BufferProcessor implements Runnable, TransferCallback
{ {
private ScheduledFuture<?> mSampleDispatcherTask; private ScheduledFuture<?> mSampleDispatcherTask;
private CopyOnWriteArrayList<Transfer> mTransfers; private LinkedTransferQueue<Transfer> mAvailableTransfers;
private LinkedTransferQueue<Transfer> mTransfersInProgress = new LinkedTransferQueue<>();
private AtomicBoolean mRunning = new AtomicBoolean(); private AtomicBoolean mRunning = new AtomicBoolean();
private ThreadPoolManager mThreadPoolManager; private ThreadPoolManager mThreadPoolManager;
private ByteBuffer mLibUsbHandlerStatus;
private boolean mCancel = false;
public BufferProcessor( ThreadPoolManager manager ) public BufferProcessor( ThreadPoolManager manager )
{ {
@@ -1409,18 +1416,6 @@ public abstract class RTL2832TunerController extends TunerController
prepareTransfers(); prepareTransfers();
for( Transfer transfer: mTransfers )
{
int result = LibUsb.submitTransfer( transfer );
if( result != LibUsb.SUCCESS )
{
mLog.error( "error submitting transfer [" +
LibUsb.errorName( result ) + "]" );
break;
}
}
try try
{ {
mSampleDispatcherTask = mThreadPoolManager.scheduleFixedRate( mSampleDispatcherTask = mThreadPoolManager.scheduleFixedRate(
@@ -1432,19 +1427,65 @@ public abstract class RTL2832TunerController extends TunerController
mLog.error( "NPE! = tpm null: " + ( mThreadPoolManager == null ), npe ); mLog.error( "NPE! = tpm null: " + ( mThreadPoolManager == null ), npe );
} }
mLibUsbHandlerStatus = ByteBuffer.allocateDirect( 4 );
while( mRunning.get() ) List<Transfer> transfers = new ArrayList<>();
mCancel = false;
while( mRunning.get() )
{ {
ByteBuffer completed = ByteBuffer.allocateDirect( 4 ); mAvailableTransfers.drainTo( transfers );
for( Transfer transfer: transfers )
{
int result = LibUsb.submitTransfer( transfer );
if( result == LibUsb.SUCCESS )
{
mTransfersInProgress.add( transfer );
}
else
{
mLog.error( "Error submitting transfer [" +
LibUsb.errorName( result ) + "]" );
}
}
int result = LibUsb.handleEventsTimeoutCompleted( int result = LibUsb.handleEventsTimeoutCompleted(
null, TIMEOUT_US, completed.asIntBuffer() ); null, TIMEOUT_US, mLibUsbHandlerStatus.asIntBuffer() );
if( result != LibUsb.SUCCESS ) if( result != LibUsb.SUCCESS )
{ {
mLog.error( "error handling events for libusb" ); mLog.error( "error handling events for libusb" );
} }
transfers.clear();
mLibUsbHandlerStatus.rewind();
} }
if( mCancel )
{
for( Transfer transfer: mTransfersInProgress )
{
LibUsb.cancelTransfer( transfer );
}
int result = LibUsb.handleEventsTimeoutCompleted(
null, TIMEOUT_US, mLibUsbHandlerStatus.asIntBuffer() );
if( result != LibUsb.SUCCESS )
{
mLog.error( "error handling events for libusb" );
}
mLibUsbHandlerStatus.rewind();
}
}
else
{
mLog.error( "Couldn't start buffer processor - already running!" );
} }
} }
@@ -1453,10 +1494,10 @@ public abstract class RTL2832TunerController extends TunerController
*/ */
public void stop() public void stop()
{ {
mCancel = true;
if( mRunning.compareAndSet( true, false ) ) if( mRunning.compareAndSet( true, false ) )
{ {
cancel();
if( mSampleDispatcherTask != null ) if( mSampleDispatcherTask != null )
{ {
mThreadPoolManager.cancel( mSampleDispatcherTask ); mThreadPoolManager.cancel( mSampleDispatcherTask );
@@ -1477,43 +1518,29 @@ public abstract class RTL2832TunerController extends TunerController
@Override @Override
public void processTransfer( Transfer transfer ) public void processTransfer( Transfer transfer )
{ {
if( transfer.status() == LibUsb.TRANSFER_COMPLETED ) mTransfersInProgress.remove( transfer );
{
ByteBuffer buffer = transfer.buffer();
byte[] data = new byte[ transfer.actualLength() ];
buffer.get( data );
buffer.rewind();
mFilledBuffers.add( data );
mSampleCounter.addAndGet( transfer.actualLength() );
if( !isRunning() )
{
LibUsb.cancelTransfer( transfer );
}
}
switch( transfer.status() ) switch( transfer.status() )
{ {
case LibUsb.TRANSFER_COMPLETED: case LibUsb.TRANSFER_COMPLETED:
/* resubmit the transfer */ case LibUsb.TRANSFER_STALL:
int result = LibUsb.submitTransfer( transfer ); if( transfer.actualLength() > 0 )
if( result != LibUsb.SUCCESS )
{ {
mLog.error( "couldn't resubmit buffer transfer to tuner" ); ByteBuffer buffer = transfer.buffer();
LibUsb.freeTransfer( transfer );
mTransfers.remove( transfer ); byte[] data = new byte[ transfer.actualLength() ];
buffer.get( data );
buffer.rewind();
if( isRunning() )
{
mFilledBuffers.add( data );
}
} }
break; break;
case LibUsb.TRANSFER_CANCELLED: case LibUsb.TRANSFER_CANCELLED:
/* free the transfer and remove it */
LibUsb.freeTransfer( transfer );
mTransfers.remove( transfer );
break; break;
default: default:
/* unexpected error */ /* unexpected error */
@@ -1521,57 +1548,38 @@ public abstract class RTL2832TunerController extends TunerController
getTransferStatus( transfer.status() ) + getTransferStatus( transfer.status() ) +
"] transferred actual: " + transfer.actualLength() ); "] transferred actual: " + transfer.actualLength() );
} }
mAvailableTransfers.add( transfer );
} }
private void cancel()
{
for( Transfer transfer: mTransfers )
{
LibUsb.cancelTransfer( transfer );
}
ByteBuffer completed = ByteBuffer.allocateDirect( 4 );
int result = LibUsb.handleEventsTimeoutCompleted(
null, TIMEOUT_US, completed.asIntBuffer() );
if( result != LibUsb.SUCCESS )
{
mLog.error( "error handling usb events during cancel [" +
LibUsb.errorName( result ) + "]" );
}
}
/** /**
* Prepares (allocates) a set of transfer buffers for use in * Prepares (allocates) a set of transfer buffers for use in
* transferring data from the tuner via the bulk interface * transferring data from the tuner via the bulk interface
*/ */
private void prepareTransfers() throws LibUsbException private void prepareTransfers() throws LibUsbException
{ {
mTransfers = new CopyOnWriteArrayList<Transfer>(); if( mAvailableTransfers == null )
for( int x = 0; x < TRANSFER_BUFFER_POOL_SIZE; x++ )
{ {
Transfer transfer = LibUsb.allocTransfer(); mAvailableTransfers = new LinkedTransferQueue<>();
if( transfer == null ) for( int x = 0; x < TRANSFER_BUFFER_POOL_SIZE; x++ )
{ {
throw new LibUsbException( "couldn't allocate transfer", Transfer transfer = LibUsb.allocTransfer();
LibUsb.ERROR_NO_MEM );
}
final ByteBuffer buffer = if( transfer == null )
ByteBuffer.allocateDirect( mBufferSize ); {
throw new LibUsbException( "couldn't allocate transfer",
LibUsb.ERROR_NO_MEM );
}
LibUsb.fillBulkTransfer( transfer, final ByteBuffer buffer =
mDeviceHandle, ByteBuffer.allocateDirect( mBufferSize );
BULK_ENDPOINT_IN,
buffer,
BufferProcessor.this,
"Buffer #" + x,
TIMEOUT_US );
mTransfers.add( transfer ); LibUsb.fillBulkTransfer( transfer, mDeviceHandle, USB_BULK_ENDPOINT,
buffer, BufferProcessor.this, "Buffer", TIMEOUT_US );
mAvailableTransfers.add( transfer );
}
} }
} }
} }

View File

@@ -34,7 +34,7 @@ public class R820TTunerConfiguration extends TunerConfiguration
private R820TLNAGain mLNAGain = R820TLNAGain.GAIN_222; private R820TLNAGain mLNAGain = R820TLNAGain.GAIN_222;
private R820TVGAGain mVGAGain = R820TVGAGain.GAIN_210; private R820TVGAGain mVGAGain = R820TVGAGain.GAIN_210;
private double mFrequencyCorrection = 0.0d; private double mFrequencyCorrection = 0.0d;
private SampleRate mSampleRate = SampleRate.RATE_1_920MHZ; private SampleRate mSampleRate = SampleRate.RATE_2_400MHZ;
/** /**
* Default constructor for JAXB * Default constructor for JAXB

View File

@@ -20,6 +20,7 @@ package spectrum;
import java.util.Arrays; import java.util.Arrays;
import java.util.Iterator; import java.util.Iterator;
import java.util.concurrent.ArrayBlockingQueue; import java.util.concurrent.ArrayBlockingQueue;
import java.util.concurrent.BlockingQueue;
import java.util.concurrent.CopyOnWriteArrayList; import java.util.concurrent.CopyOnWriteArrayList;
import java.util.concurrent.Executors; import java.util.concurrent.Executors;
import java.util.concurrent.ScheduledExecutorService; import java.util.concurrent.ScheduledExecutorService;
@@ -56,8 +57,12 @@ public class DFTProcessor implements Listener<ComplexBuffer>,
private CopyOnWriteArrayList<DFTResultsConverter> mListeners = private CopyOnWriteArrayList<DFTResultsConverter> mListeners =
new CopyOnWriteArrayList<DFTResultsConverter>(); new CopyOnWriteArrayList<DFTResultsConverter>();
private ArrayBlockingQueue<ComplexBuffer> mQueue = //Fixed size sample queue. We set the size at 12 based on the highest
new ArrayBlockingQueue<ComplexBuffer>( 200 ); //sample rate tuner (10 MHz) producing 153 frames per second and the lowest
//DFT processing frame rate of 12 frames per second. This means that the
//maximum size needed is 153 / 12 = 11 frames per DFT processing iteration
//so we set it at 12.
private BlockingQueue<ComplexBuffer> mQueue = new ArrayBlockingQueue<>( 12 );
private ScheduledExecutorService mScheduler = Executors private ScheduledExecutorService mScheduler = Executors
.newScheduledThreadPool( 1, new NamingThreadFactory( "spectrum dft" ) ); .newScheduledThreadPool( 1, new NamingThreadFactory( "spectrum dft" ) );
@@ -246,15 +251,7 @@ public class DFTProcessor implements Listener<ComplexBuffer>,
@Override @Override
public void receive( ComplexBuffer sampleBuffer ) public void receive( ComplexBuffer sampleBuffer )
{ {
if( !mQueue.offer( sampleBuffer ) ) mQueue.offer( sampleBuffer );
{
mLog.error( "DFTProcessor - [" + mSampleType.toString()
+ "] queue is full, purging queue, "
+ "samples[" + sampleBuffer + "]" );
mQueue.clear();
mQueue.offer( sampleBuffer );
}
} }
private void getNextBuffer() private void getNextBuffer()