Compare commits

...

19 Commits

Author SHA1 Message Date
Denny
a8837c8e2a Updates version to 0.5.0-alpha2 2020-06-13 09:35:11 -04:00
Denny Sheirer
7395f02fb8 #864 Updates radio reference service library to resolve issue where the library wasn't parsing the system information object correctly and was assuming a singular bandplan object versus an array of bandplans. (#865)
Updating the service library prompted some minor changes with the way that county info objects are handled for each site.

Co-authored-by: Denny <denny@denny-desktop>
2020-06-13 08:16:37 -04:00
Denny Sheirer
706ada2940 #862 Updates JavaFX libraries to use version 13.0.2 (#863)
Co-authored-by: Denny <denny@denny-desktop>
2020-06-12 05:39:01 -04:00
Denny Sheirer
d957846ab0 #837 Sorted list ArrayIndexOutOfBoundsException during alias save. (#858)
Co-authored-by: Denny <denny@denny-desktop>
2020-06-11 04:59:15 -04:00
Denny Sheirer
bbe90ca74b #856 Adds support for resetting alias color selection in the alias bulk editor. (#857)
Co-authored-by: Denny <denny@denny-desktop>
2020-06-11 04:26:40 -04:00
Denny Sheirer
96611fa2c2 #854 Resolves issue with radio reference system view where site list is getting squeezed by height requirements in the site editor components. (#855)
Co-authored-by: Denny <denny@denny-desktop>
2020-06-10 16:11:17 -04:00
Denny Sheirer
c94edf05ef #851 Resolves issue with residual alias list names after playlist change. (#852)
Co-authored-by: Denny <denny@denny-desktop>
2020-06-10 05:48:45 -04:00
Denny Sheirer
74adb6f30c #849 Resolves issue with frequency table column not updating after saving new channel configuration. (#850)
Co-authored-by: Denny <denny@denny-desktop>
2020-06-10 05:34:21 -04:00
Denny Sheirer
421abe5768 #724 Resolves issue with usb transfer buffer(s) memory leak. (#847)
Co-authored-by: Denny <denny@denny-desktop>
2020-06-09 04:33:33 -04:00
Denny Sheirer
1d003c561e #845 File Not Found when setting initial directory on JMBE library select button. (#846)
Co-authored-by: Denny <denny@denny-desktop>
2020-06-08 04:47:19 -04:00
Denny Sheirer
b22177ee17 #829 Integrates JMBE Creator with new user preferences editor for auto-downloading and compiling the JMBE library from within sdrtrunk. (#844)
Co-authored-by: Denny <denny@denny-desktop>
2020-06-07 08:04:57 -04:00
Denny Sheirer
a1e8c761ef #438 Adds feature for duplicate call audio detection and suppression for playback, recording and streaming. (#835)
Co-authored-by: Denny <denny@denny-desktop>
2020-05-27 06:08:53 -04:00
Denny Sheirer
ad194d1bab #818 Resolves P25 duplicate traffic channel issue where duplicate traffic channels were being allocated for the same frequency. (#834)
Co-authored-by: Denny <denny@denny-desktop>
2020-05-25 06:29:56 -04:00
Denny Sheirer
2bb5082943 #831 Resolves issue with playlist editor and channel map editor. (#832)
Co-authored-by: Denny <denny@denny-desktop>
2020-05-24 08:11:39 -04:00
Denny Sheirer
2c3648f8c2 #817 Resolves issue with jittery audio playback and recording for MPT1327 channels. (#830)
Co-authored-by: Denny <denny@denny-desktop>
2020-05-24 07:58:59 -04:00
Andrzej Pańkowski
3d816c3570 MPT1327: Fix decoding of calls to past talkgroups (#824)
* MPT1327: Fix decoding of calls to past talkgroups

* Fixed updating event in case of teardown. Other improvements.
2020-05-21 05:22:12 -04:00
Andrzej Pańkowski
e1194d26bd MPT1327: Fix for call timeout setting not being honored (#823)
* MPT1327: Fix for call timeout setting not being honored
2020-05-21 05:14:50 -04:00
Denny Sheirer
8167e9e9f2 #812 Alias Bulk Editor feature (#821)
Co-authored-by: Denny <denny@denny-desktop>
2020-05-18 05:11:48 -04:00
Denny Sheirer
3b357c4d40 Update README.md 2020-05-15 04:39:14 -04:00
72 changed files with 4167 additions and 425 deletions

View File

@@ -37,7 +37,7 @@ Once unzipped, open a command prompt to where you unzipped the release. Change
If you're using sdrtrunk with a P25 trunked radio system, the [JMBE](https://github.com/DSheirer/sdrtrunk/wiki/JMBE) wiki page contains instructions for downloading the JMBE audio library source code and compiling the JMBE library. Once you have compiled the library, launch the sdrtrunk application. From the menu bar, choose **View >> Preferences**. In the **JMBE Audio Codec** section, update the path to where your compiled JMBE library is located. Any channels that are started after you set the path will be able to produce P25 audio.
## Minimum System Requirements
* **Operating System:** Windows (32 or 64-bit), Linux (32 or 64-bit) or Mac/Linux (64-bit, 10.14 or higher)
* **Operating System:** Windows (~~32 or~~ 64-bit), Linux (~~32 or~~ 64-bit) or Mac/Linux (64-bit, 10.14 or higher)
* **CPU:** 4-core
* **RAM:** 8GB or more (preferred). Depending on usage, 4GB may be sufficient.

View File

@@ -33,7 +33,7 @@ repositories {
maven { url "https://jitpack.io" }
}
version = '0.5.0-alpha1'
version = '0.5.0-alpha2'
sourceCompatibility = '13'
sourceSets {
@@ -51,7 +51,6 @@ dependencies {
implementation 'com.github.DSheirer.jmbe:jmbe-api:1.0.0'
//mavenCentral/jcenter imports
implementation 'org.apache.commons:commons-math3:3.6.1'
implementation 'ch.qos.logback:logback-core:1.2.3'
implementation 'ch.qos.logback:logback-classic:1.2.3'
implementation 'com.fasterxml.jackson.dataformat:jackson-dataformat-xml:2.9.8'
@@ -60,38 +59,43 @@ dependencies {
implementation 'com.github.jiconfont:jiconfont-javafx:1.0.0'
implementation 'com.github.jiconfont:jiconfont-swing:1.0.1'
implementation 'com.github.wendykierp:JTransforms:3.1'
implementation 'com.google.code.gson:gson:2.8.6'
implementation 'com.google.guava:guava:27.0.1-jre'
implementation 'com.jidesoft:jide-oss:3.6.18'
implementation 'com.miglayout:miglayout-swing:5.2'
implementation 'com.mpatric:mp3agic:0.9.1'
implementation 'eu.hansolo:charts:1.0.5'
implementation 'io.github.dsheirer:radio-reference-api:15.1.4'
implementation 'io.github.dsheirer:radio-reference-api:15.1.6'
implementation 'javax.usb:usb-api:1.0.2'
implementation 'net.coderazzi:tablefilter-swing:5.4.0'
implementation 'org.apache.commons:commons-compress:1.20'
implementation 'commons-io:commons-io:2.7'
implementation 'org.apache.commons:commons-lang3:3.8.1'
implementation 'org.apache.commons:commons-io:1.3.2'
implementation 'org.apache.commons:commons-math3:3.6.1'
implementation 'org.apache.mina:mina-core:2.1.3'
implementation 'org.apache.mina:mina-http:2.1.3'
implementation 'org.controlsfx:controlsfx:11.0.1'
implementation 'org.rauschig:jarchivelib:1.0.0'
implementation 'org.slf4j:slf4j-api:1.7.25'
implementation 'org.usb4java:libusb4java:1.3.0'
implementation 'org.usb4java:usb4java:1.3.0'
implementation 'org.usb4java:usb4java-javax:1.3.0'
implementation 'pl.edu.icm:JLargeArrays:1.6'
//The must remain as compile options to ensure they are included in the build to produce multi-OS images
compile "org.openjfx:javafx-base:13:win"
compile "org.openjfx:javafx-base:13:linux"
compile "org.openjfx:javafx-base:13:mac"
compile "org.openjfx:javafx-controls:13.0.1:win"
compile "org.openjfx:javafx-controls:13.0.1:linux"
compile "org.openjfx:javafx-controls:13.0.1:mac"
compile "org.openjfx:javafx-graphics:13.0.1:win"
compile "org.openjfx:javafx-graphics:13.0.1:linux"
compile "org.openjfx:javafx-graphics:13.0.1:mac"
compile "org.openjfx:javafx-swing:13.0.1:win"
compile "org.openjfx:javafx-swing:13.0.1:linux"
compile "org.openjfx:javafx-swing:13.0.1:mac"
//These must remain as compile options to ensure they are included in the build to produce multi-OS images
compile "org.openjfx:javafx-base:13.0.2:win"
compile "org.openjfx:javafx-base:13.0.2:linux"
compile "org.openjfx:javafx-base:13.0.2:mac"
compile "org.openjfx:javafx-controls:13.0.2:win"
compile "org.openjfx:javafx-controls:13.0.2:linux"
compile "org.openjfx:javafx-controls:13.0.2:mac"
compile "org.openjfx:javafx-graphics:13.0.2:win"
compile "org.openjfx:javafx-graphics:13.0.2:linux"
compile "org.openjfx:javafx-graphics:13.0.2:mac"
compile "org.openjfx:javafx-swing:13.0.2:win"
compile "org.openjfx:javafx-swing:13.0.2:linux"
compile "org.openjfx:javafx-swing:13.0.2:mac"
}
application {
@@ -174,7 +178,8 @@ runtime {
}
options = ['--strip-debug', '--compress', '2', '--no-header-files', '--no-man-pages']
modules = ['java.desktop', 'java.naming', 'jdk.unsupported', 'jdk.unsupported.desktop', 'java.net.http', 'java.sql']
modules = ['java.desktop', 'java.naming', 'jdk.unsupported', 'jdk.unsupported.desktop', 'java.net.http',
'java.sql', 'jdk.crypto.ec']
imageZip = hasTargetJdk ? file("$buildDir/image/sdr-trunk.zip") : file("$buildDir/image/sdr-trunk-" + version + ".zip")
}

View File

@@ -86,6 +86,8 @@ public class AliasModel
{
removeAlias(alias);
}
mAliasListNames.clear();
}
/**

View File

@@ -36,8 +36,9 @@ import org.slf4j.LoggerFactory;
public abstract class AbstractAudioModule extends Module implements IAudioSegmentProvider, IdentifierUpdateListener
{
private final static Logger mLog = LoggerFactory.getLogger(AbstractAudioModule.class);
private static final int MAX_SEGMENT_AUDIO_SAMPLE_LENGTH = 8000 * 60 * 1; //8 kHz - 1 minute
private static final int DEFAULT_SEGMENT_AUDIO_SAMPLE_LENGTH = 60 * 8000; // 1 minute @ 8kHz
private final int mMaxSegmentAudioSampleLength;
private Listener<AudioSegment> mAudioSegmentListener;
protected MutableIdentifierCollection mIdentifierCollection = new MutableIdentifierCollection();
private Broadcaster<IdentifierUpdateNotification> mIdentifierUpdateNotificationBroadcaster = new Broadcaster<>();
@@ -46,13 +47,25 @@ public abstract class AbstractAudioModule extends Module implements IAudioSegmen
private int mAudioSampleCount = 0;
private boolean mRecordAudioOverride;
/**
* Constructs an abstract audio module
*
* @param aliasList for aliasing identifiers
* @param maxSegmentAudioSampleLength in milliseconds
*/
public AbstractAudioModule(AliasList aliasList, long maxSegmentAudioSampleLength)
{
mAliasList = aliasList;
mMaxSegmentAudioSampleLength = (int)(maxSegmentAudioSampleLength * 8); //Convert milliseconds to samples
mIdentifierUpdateNotificationBroadcaster.addListener(mIdentifierCollection);
}
/**
* Constructs an abstract audio module
*/
public AbstractAudioModule(AliasList aliasList)
{
mAliasList = aliasList;
mIdentifierUpdateNotificationBroadcaster.addListener(mIdentifierCollection);
this(aliasList, DEFAULT_SEGMENT_AUDIO_SAMPLE_LENGTH);
}
protected abstract int getTimeslot();
@@ -68,6 +81,7 @@ public abstract class AbstractAudioModule extends Module implements IAudioSegmen
{
mAudioSegment.completeProperty().set(true);
mIdentifierUpdateNotificationBroadcaster.removeListener(mAudioSegment);
mAudioSegment.decrementConsumerCount();
mAudioSegment = null;
}
}
@@ -90,6 +104,7 @@ public abstract class AbstractAudioModule extends Module implements IAudioSegmen
if(mAudioSegment == null)
{
mAudioSegment = new AudioSegment(mAliasList, getTimeslot());
mAudioSegment.incrementConsumerCount();
mAudioSegment.addIdentifiers(mIdentifierCollection.getIdentifiers());
mIdentifierUpdateNotificationBroadcaster.addListener(mAudioSegment);
@@ -117,7 +132,7 @@ public abstract class AbstractAudioModule extends Module implements IAudioSegmen
//If the current segment exceeds the max samples length, close it so that a new segment gets generated
//and then link the segments together
if(mAudioSampleCount >= MAX_SEGMENT_AUDIO_SAMPLE_LENGTH)
if(mAudioSampleCount >= mMaxSegmentAudioSampleLength)
{
AudioSegment previous = getAudioSegment();
closeAudioSegment();

View File

@@ -77,6 +77,17 @@ public class AudioModule extends AbstractAudioModule implements ISquelchStateLis
private SquelchStateListener mSquelchStateListener = new SquelchStateListener();
private SquelchState mSquelchState = SquelchState.SQUELCH;
/**
* Creates an Audio Module.
*
* @param aliasList for aliasing identifiers
* @param maxAudioSegmentLength in milliseconds
*/
public AudioModule(AliasList aliasList, long maxAudioSegmentLength)
{
super(aliasList, maxAudioSegmentLength);
}
/**
* Creates an Audio Module.
*/
@@ -116,17 +127,17 @@ public class AudioModule extends AbstractAudioModule implements ISquelchStateLis
}
@Override
public void receive(ReusableFloatBuffer reusableFloatBuffer)
public void receive(ReusableFloatBuffer audioBuffer)
{
if(mSquelchState == SquelchState.UNSQUELCH)
{
ReusableFloatBuffer buffer = mHighPassFilter.filter(reusableFloatBuffer);
addAudio(buffer.getSamples());
buffer.decrementUserCount();
ReusableFloatBuffer highPassFilteredAudio = mHighPassFilter.filter(audioBuffer);
addAudio(highPassFilteredAudio.getSamplesCopy());
highPassFilteredAudio.decrementUserCount();
}
else
{
reusableFloatBuffer.decrementUserCount();
audioBuffer.decrementUserCount();
}
}

View File

@@ -65,6 +65,7 @@ public class AudioSegment implements Listener<IdentifierUpdateNotification>
{
private final static Logger mLog = LoggerFactory.getLogger(AudioSegment.class);
private BooleanProperty mComplete = new SimpleBooleanProperty(false);
private BooleanProperty mDuplicate = new SimpleBooleanProperty(false);
private BooleanProperty mRecordAudio = new SimpleBooleanProperty(false);
private IntegerProperty mMonitorPriority = new SimpleIntegerProperty(Priority.DEFAULT_PRIORITY);
private ObservableSet<BroadcastChannel> mBroadcastChannels = FXCollections.observableSet(new HashSet<>());
@@ -74,6 +75,7 @@ public class AudioSegment implements Listener<IdentifierUpdateNotification>
private AtomicInteger mConsumerCount = new AtomicInteger();
private AliasList mAliasList;
private long mStartTimestamp = System.currentTimeMillis();
private long mSampleCount = 0;
private boolean mDisposing = false;
private AudioSegment mLinkedAudioSegment;
private int mTimeslot;
@@ -107,6 +109,24 @@ public class AudioSegment implements Listener<IdentifierUpdateNotification>
return mStartTimestamp;
}
/**
* End timestamp as calculated from start timestamp and current sample count
*/
public long getEndTimestamp()
{
return mStartTimestamp + getDuration();
}
/**
* Current duration of this audio segment. Note: this is a dynamic value until the complete property is set to true.
*
* @return duration in milliseconds
*/
public long getDuration()
{
return (mSampleCount / 8); //8 kHz audio generates 8 samples per millisecond
}
/**
* The complete property is used by the audio segment producer to signal that the segment is complete and no
* additional audio or identifiers will be added to the segment.
@@ -118,6 +138,15 @@ public class AudioSegment implements Listener<IdentifierUpdateNotification>
return mComplete;
}
/**
* Duplicate call audio property. This flag is set to true whenever a duplicate call detection function detects
* an audio segment is a duplicate.
*/
public BooleanProperty duplicateProperty()
{
return mDuplicate;
}
/**
* An observable list of broadcast channels for this audio segment. Broadcast channels are added to this segment
* across the life-cycle of the segment. As each new alias identifier is added to the segment, any broadcast
@@ -347,6 +376,7 @@ public class AudioSegment implements Listener<IdentifierUpdateNotification>
}
mAudioBuffers.add(audioBuffer);
mSampleCount += audioBuffer.length;
}
/**
@@ -396,6 +426,23 @@ public class AudioSegment implements Listener<IdentifierUpdateNotification>
}
}
/**
* Indicates if this audio segment has been flagged as a duplicate audio call
*/
public boolean isDuplicate()
{
return mDuplicate.get();
}
/**
* Sets the duplicate audio call flag for this audio segment
* @param duplicate true if this is a duplicate audio segment
*/
public void setDuplicate(boolean duplicate)
{
mDuplicate.set(duplicate);
}
/**
* Processes identifier update notifications and updates audio segment properties using the associated aliases
* from the alias list.

View File

@@ -0,0 +1,310 @@
/*
* *****************************************************************************
* Copyright (C) 2014-2020 Dennis Sheirer
*
* This program is free software: you can redistribute it and/or modify
* it under the terms of the GNU General Public License as published by
* the Free Software Foundation, either version 3 of the License, or
* (at your option) any later version.
*
* This program is distributed in the hope that it will be useful,
* but WITHOUT ANY WARRANTY; without even the implied warranty of
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
* GNU General Public License for more details.
*
* You should have received a copy of the GNU General Public License
* along with this program. If not, see <http://www.gnu.org/licenses/>
* ****************************************************************************
*/
package io.github.dsheirer.audio;
import io.github.dsheirer.identifier.Form;
import io.github.dsheirer.identifier.Identifier;
import io.github.dsheirer.identifier.IdentifierClass;
import io.github.dsheirer.identifier.Role;
import io.github.dsheirer.identifier.configuration.SystemConfigurationIdentifier;
import io.github.dsheirer.identifier.patch.PatchGroupIdentifier;
import io.github.dsheirer.identifier.radio.RadioIdentifier;
import io.github.dsheirer.identifier.talkgroup.TalkgroupIdentifier;
import io.github.dsheirer.preference.UserPreferences;
import io.github.dsheirer.preference.duplicate.DuplicateCallDetectionPreference;
import io.github.dsheirer.sample.Listener;
import io.github.dsheirer.util.ThreadPool;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import java.util.ArrayList;
import java.util.HashMap;
import java.util.List;
import java.util.Map;
import java.util.concurrent.LinkedTransferQueue;
import java.util.concurrent.ScheduledFuture;
import java.util.concurrent.TimeUnit;
import java.util.concurrent.atomic.AtomicBoolean;
/**
* Detects duplicate calls that occur within the same system. This detector is thread safe for the receive() method.
*
* Note: system in this context refers to the system name value that is used in channel configurations. All decoder
* channels must share the same system name for call duplication detection.
*/
public class DuplicateCallDetector implements Listener<AudioSegment>
{
private final static Logger mLog = LoggerFactory.getLogger(DuplicateCallDetector.class);
private DuplicateCallDetectionPreference mDuplicateCallDetectionPreference;
private Map<String,SystemDuplicateCallDetector> mDetectorMap = new HashMap();
public DuplicateCallDetector(UserPreferences userPreferences)
{
mDuplicateCallDetectionPreference = userPreferences.getDuplicateCallDetectionPreference();
}
@Override
public void receive(AudioSegment audioSegment)
{
if(mDuplicateCallDetectionPreference.isDuplicateCallDetectionEnabled())
{
Identifier identifier = audioSegment.getIdentifierCollection()
.getIdentifier(IdentifierClass.CONFIGURATION, Form.SYSTEM, Role.ANY);
if(identifier instanceof SystemConfigurationIdentifier)
{
String system = ((SystemConfigurationIdentifier)identifier).getValue();
synchronized(mDetectorMap)
{
SystemDuplicateCallDetector detector = mDetectorMap.get(system);
if(detector == null)
{
detector = new SystemDuplicateCallDetector();
mDetectorMap.put(system, detector);
}
detector.add(audioSegment);
}
}
}
}
public class SystemDuplicateCallDetector
{
private LinkedTransferQueue<AudioSegment> mAudioSegmentQueue = new LinkedTransferQueue<>();
private List<AudioSegment> mAudioSegments = new ArrayList<>();
private AtomicBoolean mMonitoring = new AtomicBoolean();
private ScheduledFuture<?> mProcessorFuture;
public SystemDuplicateCallDetector()
{
}
public void add(AudioSegment audioSegment)
{
//Block on audio segment queue so that we don't interfere with monitoring shutdown
synchronized(mAudioSegmentQueue)
{
mAudioSegmentQueue.add(audioSegment);
startMonitoring();
}
}
private void startMonitoring()
{
if(mMonitoring.compareAndSet(false, true))
{
mProcessorFuture = ThreadPool.SCHEDULED.scheduleAtFixedRate(() -> process(),
0, 25, TimeUnit.MILLISECONDS);
}
}
private void stopMonitoring()
{
if(mMonitoring.compareAndSet(true, false))
{
if(mProcessorFuture != null)
{
mProcessorFuture.cancel(true);
}
}
}
/**
* Checks if either the TO or FROM integer identifiers for the audio segments are the same
* @param segment1 to compare
* @param segment2 to compare
* @return true if integer identifiers are the same
*/
private boolean isDuplicate(AudioSegment segment1, AudioSegment segment2)
{
if(mDuplicateCallDetectionPreference.isDuplicateCallDetectionByTalkgroupEnabled())
{
//Step 1 check for duplicate TO values
List<Identifier> to1 = segment1.getIdentifierCollection().getIdentifiers(Role.TO);
List<Identifier> to2 = segment2.getIdentifierCollection().getIdentifiers(Role.TO);
if(isDuplicate(to1, to2))
{
return true;
}
}
if(mDuplicateCallDetectionPreference.isDuplicateCallDetectionByRadioEnabled())
{
//Step 2 check for duplicate FROM values
List<Identifier> from1 = segment1.getIdentifierCollection().getIdentifiers(Role.FROM);
List<Identifier> from2 = segment2.getIdentifierCollection().getIdentifiers(Role.FROM);
return isDuplicate(from1, from2);
}
return false;
}
/**
* Checks both lists of identifiers to determine if there are talkgroups or radio identifiers that are
* the same in both lists. Note: talkgroup check compares both talkgroups and patch groups.
*
* @param identifiers1
* @param identifiers2
* @return
*/
private boolean isDuplicate(List<Identifier> identifiers1, List<Identifier> identifiers2)
{
for(Identifier identifier1: identifiers1)
{
if(identifier1 instanceof TalkgroupIdentifier)
{
int talkgroup1 = ((TalkgroupIdentifier)identifier1).getValue();
for(Identifier identifier2: identifiers2)
{
if(identifier2 instanceof TalkgroupIdentifier &&
((TalkgroupIdentifier)identifier2).getValue() == talkgroup1)
{
return true;
}
else if(identifier2 instanceof PatchGroupIdentifier &&
((PatchGroupIdentifier)identifier2).getValue().getPatchGroup().getValue() == talkgroup1)
{
return true;
}
}
}
else if(identifier1 instanceof PatchGroupIdentifier)
{
int talkgroup1 = ((PatchGroupIdentifier)identifier1).getValue().getPatchGroup().getValue();
for(Identifier identifier2: identifiers2)
{
if(identifier2 instanceof TalkgroupIdentifier &&
((TalkgroupIdentifier)identifier2).getValue() == talkgroup1)
{
return true;
}
else if(identifier2 instanceof PatchGroupIdentifier &&
((PatchGroupIdentifier)identifier2).getValue().getPatchGroup().getValue() == talkgroup1)
{
return true;
}
}
}
else if(identifier1 instanceof RadioIdentifier)
{
int radio1 = ((RadioIdentifier)identifier1).getValue();
for(Identifier identifier2: identifiers2)
{
if(identifier2 instanceof RadioIdentifier && ((RadioIdentifier)identifier2).getValue() == radio1)
{
return true;
}
}
}
}
return false;
}
/**
* Processes audio segments to detect duplicates
*/
private void process()
{
//Transfer in newly arrived audio segments
mAudioSegmentQueue.drainTo(mAudioSegments);
//Remove any completed audio segments.
mAudioSegments.removeIf(audioSegment -> {
boolean complete = audioSegment.completeProperty().get();
if(complete)
{
audioSegment.decrementConsumerCount();
}
return complete;
});
//Only check for duplicates if there is more than one call
if(mAudioSegments.size() > 1)
{
List<AudioSegment> duplicates = new ArrayList<>();
int currentIndex = 0;
while(currentIndex < mAudioSegments.size() - 1)
{
AudioSegment current = mAudioSegments.get(currentIndex);
if(!current.isDuplicate())
{
int checkIndex = currentIndex + 1;
while(checkIndex < mAudioSegments.size())
{
AudioSegment toCheck = mAudioSegments.get(checkIndex);
if(!toCheck.isDuplicate())
{
if(isDuplicate(current, toCheck))
{
toCheck.setDuplicate(true);
toCheck.decrementConsumerCount();
duplicates.add(toCheck);
}
}
checkIndex++;
}
}
currentIndex++;
}
mAudioSegments.removeAll(duplicates);
}
//Finally, if the audio segment queue is empty, shutdown montitoring until a new segment arrives
if(mAudioSegments.isEmpty())
{
//Block on the audio segment queue so that we can shutdown before any new segments are added, and
//allow the add(segment) to restart monitoring as soon as needed.
synchronized(mAudioSegmentQueue)
{
if(mAudioSegmentQueue.isEmpty())
{
try
{
stopMonitoring();
}
catch(Exception e)
{
mLog.error("Unexpected error during duplicate audio segment monitoring shutdown", e);
//Do nothing, we got interrupted
}
}
}
}
}
}
}

View File

@@ -127,7 +127,12 @@ public class AudioStreamingManager implements Listener<AudioSegment>
{
audioSegment = it.next();
if(audioSegment.completeProperty().get())
if(audioSegment.isDuplicate() && mUserPreferences.getDuplicateCallDetectionPreference().isDuplicateStreamingSuppressionEnabled())
{
it.remove();
audioSegment.decrementConsumerCount();
}
else if(audioSegment.completeProperty().get())
{
it.remove();

View File

@@ -381,8 +381,9 @@ public abstract class AudioOutput implements LineListener, Listener<IdentifierUp
try
{
//Remove an assigned audio segment that's subsequently flagged as do not monitor before playback
if(mNextAudioSegment.isDoNotMonitor())
//Remove an assigned segment that's subsequently flagged as duplicate or do not monitor before playback
if(mNextAudioSegment.isDoNotMonitor() || (mNextAudioSegment.isDuplicate() &&
mUserPreferences.getDuplicateCallDetectionPreference().isDuplicatePlaybackSuppressionEnabled()))
{
mNextAudioSegment = null;
@@ -456,9 +457,11 @@ public abstract class AudioOutput implements LineListener, Listener<IdentifierUp
if(mCurrentAudioSegment != null)
{
//Check for completed audio segment or a segment flagged as Do Not Monitor
if(mCurrentAudioSegment.isDoNotMonitor() || (mCurrentAudioSegment.completeProperty().get() &&
mCurrentBufferIndex >= mCurrentAudioSegment.getAudioBufferCount()))
//Check for completed audio segment or a segment flagged as duplicate or Do Not Monitor
if(mCurrentAudioSegment.isDoNotMonitor() ||
(mCurrentAudioSegment.isDuplicate() &&
mUserPreferences.getDuplicateCallDetectionPreference().isDuplicatePlaybackSuppressionEnabled()) ||
(mCurrentAudioSegment.completeProperty().get() && mCurrentBufferIndex >= mCurrentAudioSegment.getAudioBufferCount()))
{
if(mCurrentAudioSegment.isDoNotMonitor())
{
@@ -490,7 +493,8 @@ public abstract class AudioOutput implements LineListener, Listener<IdentifierUp
//segment is a linked segment, ignore it so that we can close out the current segment.
while(mCurrentAudioSegment != null && (mNextAudioSegment == null || mNextAudioSegment.isLinked()) &&
mCurrentBufferIndex < mCurrentAudioSegment.getAudioBufferCount() &&
!mCurrentAudioSegment.isDoNotMonitor())
!mCurrentAudioSegment.isDoNotMonitor() && !(mCurrentAudioSegment.isDuplicate() &&
mUserPreferences.getDuplicateCallDetectionPreference().isDuplicatePlaybackSuppressionEnabled()))
{
float[] audioBuffer = mCurrentAudioSegment.getAudioBuffers().get(mCurrentBufferIndex++);

View File

@@ -27,20 +27,15 @@ import io.github.dsheirer.audio.IAudioController;
import io.github.dsheirer.eventbus.MyEventBus;
import io.github.dsheirer.preference.PreferenceType;
import io.github.dsheirer.preference.UserPreferences;
import io.github.dsheirer.properties.SystemProperties;
import io.github.dsheirer.sample.Broadcaster;
import io.github.dsheirer.sample.Listener;
import io.github.dsheirer.source.mixer.MixerChannel;
import io.github.dsheirer.source.mixer.MixerChannelConfiguration;
import io.github.dsheirer.source.mixer.MixerManager;
import io.github.dsheirer.util.ThreadPool;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import javax.sound.sampled.AudioSystem;
import javax.sound.sampled.Mixer;
import java.util.ArrayList;
import java.util.Collections;
import java.util.Comparator;
import java.util.Iterator;
import java.util.List;
@@ -123,7 +118,12 @@ public class AudioPlaybackManager implements Listener<AudioSegment>, IAudioContr
while(newSegment != null)
{
if(newSegment.hasAudio())
if(newSegment.isDuplicate() &&
mUserPreferences.getDuplicateCallDetectionPreference().isDuplicatePlaybackSuppressionEnabled())
{
newSegment.decrementConsumerCount();
}
else if(newSegment.hasAudio())
{
mAudioSegments.add(newSegment);
}
@@ -146,7 +146,13 @@ public class AudioPlaybackManager implements Listener<AudioSegment>, IAudioContr
{
audioSegment = it.next();
if(audioSegment.hasAudio())
if(audioSegment.isDuplicate() &&
mUserPreferences.getDuplicateCallDetectionPreference().isDuplicatePlaybackSuppressionEnabled())
{
it.remove();
audioSegment.decrementConsumerCount();
}
else if(audioSegment.hasAudio())
{
//Queue it up for replay
it.remove();
@@ -174,7 +180,8 @@ public class AudioPlaybackManager implements Listener<AudioSegment>, IAudioContr
{
audioSegment = it.next();
if(audioSegment.isDoNotMonitor())
if(audioSegment.isDoNotMonitor() || (audioSegment.isDuplicate() &&
mUserPreferences.getDuplicateCallDetectionPreference().isDuplicatePlaybackSuppressionEnabled()))
{
it.remove();
audioSegment.decrementConsumerCount();
@@ -235,7 +242,8 @@ public class AudioPlaybackManager implements Listener<AudioSegment>, IAudioContr
{
audioSegment = it.next();
if(audioSegment.completeProperty().get())
if(audioSegment.completeProperty().get() || (audioSegment.isDuplicate() &&
mUserPreferences.getDuplicateCallDetectionPreference().isDuplicatePlaybackSuppressionEnabled()))
{
it.remove();
audioSegment.decrementConsumerCount();

View File

@@ -30,15 +30,28 @@ import io.github.dsheirer.controller.channel.Channel.ChannelType;
public class ChangeChannelTimeoutEvent extends DecoderStateEvent
{
private ChannelType mChannelType;
private long mCallTimeout;
private long mCallTimeoutMilliseconds;
/**
* Constructs an instance
* @param source of the event
* @param channelType for the event
* @param timeout in milliseconds
* @param timeslot of the event
*/
public ChangeChannelTimeoutEvent(Object source, ChannelType channelType, long timeout, int timeslot)
{
super(source, Event.CHANGE_CALL_TIMEOUT, State.IDLE, timeslot);
mChannelType = channelType;
mCallTimeout = timeout;
mCallTimeoutMilliseconds = timeout;
}
/**
* Constructs an instance
* @param source of the event
* @param channelType for the event
* @param timeout value in milliseconds
*/
public ChangeChannelTimeoutEvent(Object source, ChannelType channelType, long timeout)
{
this(source, channelType, timeout, 0);
@@ -49,8 +62,11 @@ public class ChangeChannelTimeoutEvent extends DecoderStateEvent
return mChannelType;
}
public long getCallTimeout()
/**
* Call timeout value in milliseconds
*/
public long getCallTimeoutMilliseconds()
{
return mCallTimeout;
return mCallTimeoutMilliseconds;
}
}

View File

@@ -115,14 +115,14 @@ public class MultiChannelState extends AbstractChannelState implements IDecoderS
stateMachine.setChannelType(mChannel.getChannelType());
stateMachine.setIdentifierUpdateListener(mutableIdentifierCollection);
stateMachine.setEndTimeoutBuffer(RESET_TIMEOUT_DELAY);
stateMachine.setEndTimeoutBufferMilliseconds(RESET_TIMEOUT_DELAY);
if(channel.getChannelType() == ChannelType.STANDARD)
{
stateMachine.setFadeTimeoutBuffer(FADE_TIMEOUT_DELAY);
stateMachine.setFadeTimeoutBufferMilliseconds(FADE_TIMEOUT_DELAY);
}
else
{
stateMachine.setFadeTimeoutBuffer(DecodeConfiguration.DEFAULT_CALL_TIMEOUT_DELAY_SECONDS * 1000);
stateMachine.setFadeTimeoutBufferMilliseconds(DecodeConfiguration.DEFAULT_CALL_TIMEOUT_DELAY_SECONDS * 1000);
}
}
@@ -529,7 +529,7 @@ public class MultiChannelState extends AbstractChannelState implements IDecoderS
if(event instanceof ChangeChannelTimeoutEvent)
{
ChangeChannelTimeoutEvent timeout = (ChangeChannelTimeoutEvent)event;
mStateMachineMap.get(event.getTimeslot()).setFadeTimeoutBuffer(timeout.getCallTimeout());
mStateMachineMap.get(event.getTimeslot()).setFadeTimeoutBufferMilliseconds(timeout.getCallTimeoutMilliseconds());
}
break;
case CONTINUATION:

View File

@@ -40,6 +40,7 @@ import io.github.dsheirer.identifier.configuration.SiteConfigurationIdentifier;
import io.github.dsheirer.identifier.configuration.SystemConfigurationIdentifier;
import io.github.dsheirer.identifier.decoder.ChannelStateIdentifier;
import io.github.dsheirer.module.decode.config.DecodeConfiguration;
import io.github.dsheirer.module.decode.config.WithCallTimeout;
import io.github.dsheirer.module.decode.event.IDecodeEvent;
import io.github.dsheirer.sample.Listener;
import io.github.dsheirer.source.ISourceEventListener;
@@ -108,14 +109,19 @@ public class SingleChannelState extends AbstractChannelState implements IDecoder
mStateMachine.addListener(mSquelchController);
mStateMachine.setChannelType(mChannel.getChannelType());
mStateMachine.setIdentifierUpdateListener(mIdentifierCollection);
mStateMachine.setEndTimeoutBuffer(RESET_TIMEOUT_DELAY);
mStateMachine.setEndTimeoutBufferMilliseconds(RESET_TIMEOUT_DELAY);
if(channel.getChannelType() == ChannelType.STANDARD)
{
mStateMachine.setFadeTimeoutBuffer(FADE_TIMEOUT_DELAY);
mStateMachine.setFadeTimeoutBufferMilliseconds(FADE_TIMEOUT_DELAY);
}
else
{
mStateMachine.setFadeTimeoutBuffer(DecodeConfiguration.DEFAULT_CALL_TIMEOUT_DELAY_SECONDS * 1000);
final DecodeConfiguration decodeConfig = channel.getDecodeConfiguration();
final int fadeTimeoutSeconds = decodeConfig instanceof WithCallTimeout
? ((WithCallTimeout) decodeConfig).getCallTimeoutSeconds()
: DecodeConfiguration.DEFAULT_CALL_TIMEOUT_DELAY_SECONDS;
mStateMachine.setFadeTimeoutBufferMilliseconds(fadeTimeoutSeconds * 1000);
}
}
@@ -406,7 +412,7 @@ public class SingleChannelState extends AbstractChannelState implements IDecoder
if(event instanceof ChangeChannelTimeoutEvent)
{
ChangeChannelTimeoutEvent timeout = (ChangeChannelTimeoutEvent)event;
mStateMachine.setFadeTimeoutBuffer(timeout.getCallTimeout());
mStateMachine.setFadeTimeoutBufferMilliseconds(timeout.getCallTimeoutMilliseconds());
}
case CONTINUATION:
case DECODE:

View File

@@ -42,9 +42,9 @@ public class StateMachine
protected State mState = State.IDLE;
protected long mFadeTimeout;
protected long mFadeTimeoutBuffer = 0;
protected long mFadeTimeoutBufferMilliseconds = 0;
protected long mEndTimeout;
protected long mEndTimeoutBuffer = 0;
protected long mEndTimeoutBufferMilliseconds = 0;
private int mTimeslot;
private EnumSet<State> mActiveStates;
private Channel.ChannelType mChannelType = Channel.ChannelType.STANDARD;
@@ -63,26 +63,42 @@ public class StateMachine
mActiveStates = activeStates;
}
/**
* Adds a state change listener
* @param listener to receive state change events
*/
public void addListener(IStateMachineListener listener)
{
mStateMachineListeners.add(listener);
}
/**
* Removes the state change listener
*/
public void removeListener(IStateMachineListener listener)
{
mStateMachineListeners.remove(listener);
}
/**
* Sets the listener to receive identifier update notifications (ie state identifier updates)
*/
public void setIdentifierUpdateListener(Listener<IdentifierUpdateNotification> listener)
{
mIdentifierUpdateListener = listener;
}
/**
* Sets the channel type for this state machine
*/
public void setChannelType(Channel.ChannelType channelType)
{
mChannelType = channelType;
}
/**
* Checks the state and transitions to FADE or TEARDOWN if timers have expired
*/
public void checkState()
{
if(mActiveStates.contains(mState) && mFadeTimeout <= System.currentTimeMillis())
@@ -95,11 +111,17 @@ public class StateMachine
}
}
/**
* Current state of the state machine
*/
public State getState()
{
return mState;
}
/**
* Sets the state
*/
public void setState(State state)
{
if(state == mState)
@@ -173,6 +195,9 @@ public class StateMachine
}
}
/**
* Broadcasts a channel state identifier update
*/
private void broadcast(ChannelStateIdentifier channelStateIdentifier)
{
if(mIdentifierUpdateListener != null)
@@ -182,43 +207,70 @@ public class StateMachine
}
}
/**
* Updates the fade timeout to current time plus the fade timeout buffer value.
*/
private void updateFadeTimeout()
{
mFadeTimeout = System.currentTimeMillis() + mFadeTimeoutBuffer;
mFadeTimeout = System.currentTimeMillis() + mFadeTimeoutBufferMilliseconds;
}
/**
* Manually sets the fade timeout
*/
public void setFadeTimeout(long timeout)
{
mFadeTimeout = timeout;
}
/**
* Fade timeout value
*/
public long getFadeTimeout()
{
return mFadeTimeout;
}
public void setFadeTimeoutBuffer(long buffer)
/**
* Sets the fade timeout buffer value. This value determines the maximum length of time the state machine
* can stay in any state (without an update) before automatically transitioning to FADE state.
* @param buffer in milliseconds
*/
public void setFadeTimeoutBufferMilliseconds(long buffer)
{
mFadeTimeoutBuffer = buffer;
mFadeTimeoutBufferMilliseconds = buffer;
updateFadeTimeout();
}
/**
* Updates the end timeout value to current system time plus the end timeout buffer value.
*/
private void updateEndTimeout()
{
mEndTimeout = System.currentTimeMillis() + mEndTimeoutBuffer;
mEndTimeout = System.currentTimeMillis() + mEndTimeoutBufferMilliseconds;
}
public void setEndTimeoutBuffer(long buffer)
/**
* Sets the end timeout buffer value. This value is the maximum length of time that the channel can
* stay in the FADE state (without an update) before transitioning to a TEARDOWN state.
*/
public void setEndTimeoutBufferMilliseconds(long buffer)
{
mEndTimeoutBuffer = buffer;
mEndTimeoutBufferMilliseconds = buffer;
updateEndTimeout();
}
/**
* Sets the end timeout value
*/
public void setEndTimeout(long endTimeout)
{
mEndTimeout = endTimeout;
}
/**
* Current end timeout value
*/
public long getEndTimeout()
{
return mEndTimeout;

View File

@@ -39,6 +39,7 @@ import io.github.dsheirer.source.SourceType;
import io.github.dsheirer.source.config.SourceConfigFactory;
import io.github.dsheirer.source.config.SourceConfigRecording;
import io.github.dsheirer.source.config.SourceConfigTuner;
import io.github.dsheirer.source.config.SourceConfigTunerMultipleFrequency;
import io.github.dsheirer.source.config.SourceConfiguration;
import io.github.dsheirer.source.tuner.channel.TunerChannel;
import javafx.beans.Observable;
@@ -48,10 +49,13 @@ import javafx.beans.property.SimpleBooleanProperty;
import javafx.beans.property.SimpleIntegerProperty;
import javafx.beans.property.SimpleStringProperty;
import javafx.beans.property.StringProperty;
import javafx.collections.FXCollections;
import javafx.collections.ObservableList;
import javafx.util.Callback;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import java.beans.Transient;
import java.util.Objects;
@JacksonXmlRootElement(localName = "channel")
@@ -78,6 +82,7 @@ public class Channel extends Configuration implements Listener<SourceEvent>
private StringProperty mSystem = new SimpleStringProperty();
private StringProperty mSite = new SimpleStringProperty();
private StringProperty mName = new SimpleStringProperty();
private ObservableList<Long> mFrequencyList;
private BooleanProperty mProcessing = new SimpleBooleanProperty();
private BooleanProperty mAutoStart = new SimpleBooleanProperty();
@@ -178,6 +183,23 @@ public class Channel extends Configuration implements Listener<SourceEvent>
return channel;
}
/**
* Updates the frequencies property as the source configuration changes or is initialized.
*/
private void updateFrequencies()
{
getFrequencyList().clear();
if(mSourceConfiguration instanceof SourceConfigTuner)
{
getFrequencyList().add(((SourceConfigTuner)mSourceConfiguration).getFrequency());
}
else if(mSourceConfiguration instanceof SourceConfigTunerMultipleFrequency)
{
getFrequencyList().addAll(((SourceConfigTunerMultipleFrequency)mSourceConfiguration).getFrequencies());
}
}
/**
* Alias list name property
*/
@@ -243,6 +265,23 @@ public class Channel extends Configuration implements Listener<SourceEvent>
return mChannelID;
}
/**
* List of frequencies extracted from the source configuration. This method only exists to support JavaFX
* monitoring of frequency list changes. Use the source configuration to manage frequencies.
*/
@JsonIgnore
@Transient
public ObservableList<Long> getFrequencyList()
{
if(mFrequencyList == null)
{
mFrequencyList = FXCollections.observableArrayList();
updateFrequencies();
}
return mFrequencyList;
}
/**
* Indicates if this channel has been selected for prioritized audio output
* and display of processing chain products
@@ -529,6 +568,8 @@ public class Channel extends Configuration implements Listener<SourceEvent>
mSourceConfiguration = config;
}
updateFrequencies();
//Clear the tune channel object so that it can be recreated if the
//source configuration changes
mTunerChannel = null;
@@ -676,6 +717,7 @@ public class Channel extends Configuration implements Listener<SourceEvent>
public static Callback<Channel,Observable[]> extractor()
{
return (Channel c) -> new Observable[] {c.processingProperty(), c.nameProperty(), c.aliasListNameProperty(),
c.autoStartOrderProperty(), c.autoStartProperty(), c.siteProperty(), c.systemProperty()};
c.autoStartOrderProperty(), c.autoStartProperty(), c.siteProperty(), c.systemProperty(),
c.getFrequencyList()};
}
}

View File

@@ -49,6 +49,8 @@ import io.github.dsheirer.source.Source;
import io.github.dsheirer.source.SourceEvent;
import io.github.dsheirer.source.SourceException;
import io.github.dsheirer.source.SourceManager;
import io.github.dsheirer.source.config.SourceConfigTuner;
import io.github.dsheirer.source.config.SourceConfigTunerMultipleFrequency;
import javafx.application.Platform;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
@@ -180,7 +182,23 @@ public class ChannelProcessingManager implements Listener<ChannelEvent>
}
catch(ChannelException ce)
{
mLog.error("Error starting requested channel [" + channel.getName() + "] - " + ce.getMessage());
if(channel.getSourceConfiguration() instanceof SourceConfigTuner)
{
long frequency = ((SourceConfigTuner)channel.getSourceConfiguration()).getFrequency();
mLog.error("Error starting requested channel [" + channel.getName() + ":" + frequency +
"] - " + ce.getMessage());
}
else if(channel.getSourceConfiguration() instanceof SourceConfigTunerMultipleFrequency)
{
List<Long> frequencies = ((SourceConfigTunerMultipleFrequency)channel
.getSourceConfiguration()).getFrequencies();
mLog.error("Error starting requested channel [" + channel.getName() + ":" + frequencies +
"] - " + ce.getMessage());
}
else
{
mLog.error("Error starting requested channel [" + channel.getName() + "] - " + ce.getMessage());
}
}
}
break;

View File

@@ -38,6 +38,8 @@ import io.github.dsheirer.gui.preference.PreferenceEditorType;
import io.github.dsheirer.gui.preference.UserPreferencesEditor;
import io.github.dsheirer.gui.preference.ViewUserPreferenceEditorRequest;
import io.github.dsheirer.icon.IconModel;
import io.github.dsheirer.jmbe.JmbeEditor;
import io.github.dsheirer.jmbe.JmbeEditorRequest;
import io.github.dsheirer.module.log.EventLogManager;
import io.github.dsheirer.playlist.PlaylistManager;
import io.github.dsheirer.preference.UserPreferences;
@@ -68,12 +70,14 @@ public class JavaFxWindowManager extends Application
public static final String USER_PREFERENCES_EDITOR = "preferences";
public static final String STAGE_MONITOR_KEY_CHANNEL_MAP_EDITOR = "channel.map";
public static final String STAGE_MONITOR_KEY_ICON_MANAGER_EDITOR = "icon.manager";
public static final String STAGE_MONITOR_KEY_JMBE_EDITOR = "jmbe.editor";
public static final String STAGE_MONITOR_KEY_PLAYLIST_EDITOR = "playlist";
public static final String STAGE_MONITOR_KEY_USER_PREFERENCES_EDITOR = "user.preferences";
private JFXPanel mJFXPanel;
private ChannelMapEditor mChannelMapEditor;
private IconManager mIconManager;
private JmbeEditor mJmbeEditor;
private PlaylistEditor mPlaylistEditor;
private PlaylistManager mPlaylistManager;
private UserPreferences mUserPreferences;
@@ -81,6 +85,7 @@ public class JavaFxWindowManager extends Application
private Stage mChannelMapStage;
private Stage mIconManagerStage;
private Stage mJmbeEditorStage;
private Stage mPlaylistStage;
private Stage mUserPreferencesStage;
@@ -205,6 +210,56 @@ public class JavaFxWindowManager extends Application
return mIconManager;
}
/**
* Processes a JMBE s editor request
*/
@Subscribe
public void process(final JmbeEditorRequest request)
{
if(request.isCloseEditorRequest())
{
execute(() -> {
getJmbeEditorStage().hide();
mJmbeEditorStage = null;
mJmbeEditor = null;
});
}
else
{
execute(() -> {
getJmbeEditorStage().show();
getJmbeEditorStage().requestFocus();
getJmbeEditorStage().toFront();
getJmbeEditor().process(request);
});
}
}
public Stage getJmbeEditorStage()
{
if(mJmbeEditorStage == null)
{
createJFXPanel();
Scene scene = new Scene(getJmbeEditor(), 650, 650);
mJmbeEditorStage = new Stage();
mJmbeEditorStage.setTitle("sdrtrunk - JMBE Library Updater");
mJmbeEditorStage.setScene(scene);
mUserPreferences.getJavaFxPreferences().monitor(mJmbeEditorStage, STAGE_MONITOR_KEY_JMBE_EDITOR);
}
return mJmbeEditorStage;
}
public JmbeEditor getJmbeEditor()
{
if(mJmbeEditor == null)
{
mJmbeEditor = new JmbeEditor(mUserPreferences);
}
return mJmbeEditor;
}
/**
* Lazy construct and access the playlist editor
*/

View File

@@ -21,6 +21,7 @@ package io.github.dsheirer.gui;
import com.jidesoft.plaf.LookAndFeelFactory;
import com.jidesoft.swing.JideSplitPane;
import io.github.dsheirer.alias.AliasModel;
import io.github.dsheirer.audio.DuplicateCallDetector;
import io.github.dsheirer.audio.broadcast.AudioStreamingManager;
import io.github.dsheirer.audio.broadcast.BroadcastFormat;
import io.github.dsheirer.audio.broadcast.BroadcastStatusPanel;
@@ -180,6 +181,9 @@ public class SDRTrunk implements Listener<TunerEvent>
mUserPreferences);
mAudioStreamingManager.start();
DuplicateCallDetector duplicateCallDetector = new DuplicateCallDetector(mUserPreferences);
mPlaylistManager.getChannelProcessingManager().addAudioSegmentListener(duplicateCallDetector);
mPlaylistManager.getChannelProcessingManager().addAudioSegmentListener(audioPlaybackManager);
mPlaylistManager.getChannelProcessingManager().addAudioSegmentListener(mAudioRecordingManager);
mPlaylistManager.getChannelProcessingManager().addAudioSegmentListener(mAudioStreamingManager);

View File

@@ -243,7 +243,7 @@ public class PlaylistEditor extends BorderPane
{
if(mChannelEditor == null)
{
mChannelEditor = new ChannelEditor(mPlaylistManager);
mChannelEditor = new ChannelEditor(mPlaylistManager, mUserPreferences);
}
return mChannelEditor;

View File

@@ -0,0 +1,374 @@
/*
* *****************************************************************************
* Copyright (C) 2014-2020 Dennis Sheirer
*
* This program is free software: you can redistribute it and/or modify
* it under the terms of the GNU General Public License as published by
* the Free Software Foundation, either version 3 of the License, or
* (at your option) any later version.
*
* This program is distributed in the hope that it will be useful,
* but WITHOUT ANY WARRANTY; without even the implied warranty of
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
* GNU General Public License for more details.
*
* You should have received a copy of the GNU General Public License
* along with this program. If not, see <http://www.gnu.org/licenses/>
* ****************************************************************************
*/
package io.github.dsheirer.gui.playlist.alias;
import com.google.common.collect.Ordering;
import io.github.dsheirer.alias.Alias;
import io.github.dsheirer.gui.playlist.Editor;
import io.github.dsheirer.icon.Icon;
import io.github.dsheirer.playlist.PlaylistManager;
import javafx.collections.transformation.SortedList;
import javafx.event.ActionEvent;
import javafx.event.EventHandler;
import javafx.geometry.HPos;
import javafx.geometry.Insets;
import javafx.scene.control.Button;
import javafx.scene.control.ColorPicker;
import javafx.scene.control.ComboBox;
import javafx.scene.control.Label;
import javafx.scene.control.ListCell;
import javafx.scene.control.ListView;
import javafx.scene.control.Separator;
import javafx.scene.image.ImageView;
import javafx.scene.layout.ColumnConstraints;
import javafx.scene.layout.GridPane;
import javafx.scene.paint.Color;
import javafx.util.Callback;
import org.controlsfx.control.ToggleSwitch;
import java.util.List;
/**
* Editor for multiple selected aliases providing limited options for changing attributes of multiple aliases
*/
public class AliasBulkEditor extends Editor<List<Alias>>
{
private PlaylistManager mPlaylistManager;
private Label mEditingLabel;
private ColorPicker mColorPicker;
private Button mApplyColorButton;
private Button mResetColorButton;
private ComboBox<Icon> mIconNodeComboBox;
private Button mApplyIconButton;
private ToggleSwitch mMonitorAudioToggleSwitch;
private ComboBox<Integer> mMonitorPriorityComboBox;
private Button mApplyMonitorButton;
/**
* Constructs an instance
* @param playlistManager for accessing icon manager
*/
public AliasBulkEditor(PlaylistManager playlistManager)
{
mPlaylistManager = playlistManager;
GridPane gridPane = new GridPane();
gridPane.setPadding(new Insets(10,10,10,10));
gridPane.setHgap(10);
gridPane.setVgap(10);
int row = 0;
Label editorLabel = new Label("Multiple Alias Editor");
GridPane.setConstraints(editorLabel, 0, row, 3, 1);
gridPane.getChildren().add(editorLabel);
GridPane.setConstraints(getEditingLabel(), 3, row, 3, 1);
GridPane.setHalignment(getEditingLabel(), HPos.RIGHT);
gridPane.getChildren().add(getEditingLabel());
Separator separator = new Separator();
separator.setMaxWidth(Double.MAX_VALUE);
GridPane.setConstraints(separator, 0, ++row, 6, 1);
gridPane.getChildren().add(separator);
Label colorLabel = new Label("Color");
GridPane.setHalignment(colorLabel, HPos.RIGHT);
GridPane.setConstraints(colorLabel, 0, ++row);
gridPane.getChildren().add(colorLabel);
GridPane.setConstraints(getColorPicker(), 1, row, 3, 1);
gridPane.getChildren().add(getColorPicker());
GridPane.setConstraints(getApplyColorButton(), 4, row);
gridPane.getChildren().add(getApplyColorButton());
GridPane.setConstraints(getResetColorButton(), 5, row);
gridPane.getChildren().add(getResetColorButton());
Label iconLabel = new Label("Icon");
GridPane.setHalignment(iconLabel, HPos.RIGHT);
GridPane.setConstraints(iconLabel, 0, ++row);
gridPane.getChildren().add(iconLabel);
GridPane.setConstraints(getIconNodeComboBox(), 1, row, 3, 1);
gridPane.getChildren().add(getIconNodeComboBox());
GridPane.setConstraints(getApplyIconButton(), 4, row);
gridPane.getChildren().add(getApplyIconButton());
Label listenLabel = new Label("Listen");
GridPane.setHalignment(listenLabel, HPos.RIGHT);
GridPane.setConstraints(listenLabel, 0, ++row);
gridPane.getChildren().add(listenLabel);
GridPane.setConstraints(getMonitorAudioToggleSwitch(), 1, row);
gridPane.getChildren().add(getMonitorAudioToggleSwitch());
Label priorityLabel = new Label("Priority");
GridPane.setHalignment(priorityLabel, HPos.RIGHT);
GridPane.setConstraints(priorityLabel, 2, row);
gridPane.getChildren().add(priorityLabel);
GridPane.setConstraints(getMonitorPriorityComboBox(), 3, row);
gridPane.getChildren().add(getMonitorPriorityComboBox());
GridPane.setConstraints(getApplyMonitorButton(), 4, row);
gridPane.getChildren().add(getApplyMonitorButton());
getChildren().add(gridPane);
}
@Override
public List<Alias> getItem()
{
return super.getItem();
}
@Override
public void setItem(List<Alias> item)
{
super.setItem(item);
getEditingLabel().setText("Editing " + item.size() + " Aliases");
}
@Override
public void save()
{
//no-op
}
@Override
public void dispose()
{
//no-op
}
private Label getEditingLabel()
{
if(mEditingLabel== null)
{
mEditingLabel = new Label("Editing 0 Aliases");
}
return mEditingLabel;
}
private ColorPicker getColorPicker()
{
if(mColorPicker == null)
{
mColorPicker = new ColorPicker(Color.BLACK);
mColorPicker.setEditable(true);
mColorPicker.setStyle("-fx-color-rect-width: 60px; -fx-color-label-visible: false;");
}
return mColorPicker;
}
private Button getApplyColorButton()
{
if(mApplyColorButton == null)
{
mApplyColorButton = new Button("Apply");
mApplyColorButton.setOnAction(event -> {
int colorValue = ColorUtil.toInteger(getColorPicker().getValue());
for(Alias alias: getItem())
{
alias.setColor(colorValue);
}
});
}
return mApplyColorButton;
}
private Button getResetColorButton()
{
if(mResetColorButton == null)
{
mResetColorButton = new Button("Reset Color");
mResetColorButton.setOnAction(event -> {
for(Alias alias: getItem())
{
alias.setColor(0);
}
});
}
return mResetColorButton;
}
private ComboBox<Icon> getIconNodeComboBox()
{
if(mIconNodeComboBox == null)
{
mIconNodeComboBox = new ComboBox<>();
mIconNodeComboBox.setMaxWidth(Double.MAX_VALUE);
mIconNodeComboBox.setItems(new SortedList(mPlaylistManager.getIconModel().iconsProperty(), Ordering.natural()));
mIconNodeComboBox.setCellFactory(new IconCellFactory());
mIconNodeComboBox.getSelectionModel().selectedItemProperty()
.addListener((observable, oldValue, newValue) ->
{
getApplyIconButton().setDisable(newValue == null);
});
}
return mIconNodeComboBox;
}
private Button getApplyIconButton()
{
if(mApplyIconButton == null)
{
mApplyIconButton = new Button("Apply");
mApplyIconButton.setDisable(true);
mApplyIconButton.setOnAction(new EventHandler<ActionEvent>()
{
@Override
public void handle(ActionEvent event)
{
Icon icon = getIconNodeComboBox().getSelectionModel().getSelectedItem();
if(icon != null)
{
for(Alias alias: getItem())
{
alias.setIconName(icon.getName());
}
}
}
});
}
return mApplyIconButton;
}
private ToggleSwitch getMonitorAudioToggleSwitch()
{
if(mMonitorAudioToggleSwitch == null)
{
mMonitorAudioToggleSwitch = new ToggleSwitch();
mMonitorAudioToggleSwitch.setSelected(true);
mMonitorAudioToggleSwitch.selectedProperty()
.addListener((observable, oldValue, newValue) -> modifiedProperty().set(true));
}
return mMonitorAudioToggleSwitch;
}
private ComboBox<Integer> getMonitorPriorityComboBox()
{
if(mMonitorPriorityComboBox == null)
{
mMonitorPriorityComboBox = new ComboBox<>();
mMonitorPriorityComboBox.getItems().add(null);
for(int x = io.github.dsheirer.alias.id.priority.Priority.MIN_PRIORITY;
x < io.github.dsheirer.alias.id.priority.Priority.MAX_PRIORITY; x++)
{
mMonitorPriorityComboBox.getItems().add(x);
}
mMonitorPriorityComboBox.disableProperty().bind(getMonitorAudioToggleSwitch().selectedProperty().not());
mMonitorPriorityComboBox.getSelectionModel().selectedItemProperty()
.addListener((observable, oldValue, newValue) -> modifiedProperty().set(true));
}
return mMonitorPriorityComboBox;
}
private Button getApplyMonitorButton()
{
if(mApplyMonitorButton == null)
{
mApplyMonitorButton = new Button("Apply");
mApplyMonitorButton.setOnAction(new EventHandler<ActionEvent>()
{
@Override
public void handle(ActionEvent event)
{
boolean canMonitor = getMonitorAudioToggleSwitch().isSelected();
Integer priority = getMonitorPriorityComboBox().getSelectionModel().getSelectedItem();
if(canMonitor)
{
if(priority == null)
{
priority = io.github.dsheirer.alias.id.priority.Priority.DEFAULT_PRIORITY;
}
}
else
{
priority = io.github.dsheirer.alias.id.priority.Priority.DO_NOT_MONITOR;
}
for(Alias alias: getItem())
{
alias.setCallPriority(priority);
}
}
});
}
return mApplyMonitorButton;
}
/**
* Cell factory for combo box for dislaying icon name and graphic
*/
public class IconCellFactory implements Callback<ListView<Icon>, ListCell<Icon>>
{
@Override
public ListCell<Icon> call(ListView<Icon> param)
{
Label iconLabel = new Label();
Label textLabel = new Label();
GridPane gridPane = new GridPane();
gridPane.setHgap(5);
GridPane.setHalignment(iconLabel, HPos.RIGHT);
gridPane.getColumnConstraints().add(new ColumnConstraints(50));
gridPane.add(iconLabel, 0, 0);
gridPane.add(textLabel,1,0);
ListCell<Icon> cell = new ListCell<>()
{
@Override
protected void updateItem(Icon item, boolean empty)
{
super.updateItem(item, empty);
if(empty)
{
setText(null);
setGraphic(null);
}
else
{
textLabel.setText(item.getName());
iconLabel.setGraphic(new ImageView(item.getFxImage()));
setGraphic(gridPane);
}
}
};
return cell;
}
}
}

View File

@@ -27,12 +27,14 @@ import io.github.dsheirer.alias.AliasFactory;
import io.github.dsheirer.alias.AliasList;
import io.github.dsheirer.alias.AliasModel;
import io.github.dsheirer.gui.control.MaxLengthUnaryOperator;
import io.github.dsheirer.gui.playlist.Editor;
import io.github.dsheirer.icon.Icon;
import io.github.dsheirer.playlist.PlaylistManager;
import io.github.dsheirer.preference.UserPreferences;
import javafx.application.Platform;
import javafx.beans.property.ReadOnlyObjectWrapper;
import javafx.beans.value.ObservableValue;
import javafx.collections.ListChangeListener;
import javafx.collections.transformation.FilteredList;
import javafx.collections.transformation.SortedList;
import javafx.geometry.Insets;
@@ -85,6 +87,8 @@ public class AliasConfigurationEditor extends SplitPane
private PlaylistManager mPlaylistManager;
private UserPreferences mUserPreferences;
private AliasItemEditor mAliasItemEditor;
private AliasBulkEditor mAliasBulkEditor;
private Editor mCurrentEditor;
private TableView<Alias> mAliasTableView;
private Label mPlaceholderLabel;
private Button mNewAliasButton;
@@ -114,6 +118,7 @@ public class AliasConfigurationEditor extends SplitPane
topBox.getChildren().addAll(leftBox, getButtonBox());
setOrientation(Orientation.VERTICAL);
mCurrentEditor = getAliasItemEditor();
getItems().addAll(topBox, getAliasItemEditor());
}
@@ -141,7 +146,20 @@ public class AliasConfigurationEditor extends SplitPane
}
}
private void setAlias(Alias alias)
/**
* Sets the editor as the bottom alias editor, either single alias or bulk alias editor.
*/
private void setEditor(Editor editor)
{
if(editor != mCurrentEditor)
{
getItems().remove(mCurrentEditor);
mCurrentEditor = editor;
getItems().add(mCurrentEditor);
}
}
private void setAliases(List<Alias> aliases)
{
//Prompt the user to save if the contents of the current channel editor have been modified
if(getAliasItemEditor().modifiedProperty().get())
@@ -168,18 +186,27 @@ public class AliasConfigurationEditor extends SplitPane
}
}
getCloneAliasButton().setDisable(alias == null || getAliasTableView().getSelectionModel().getSelectedItems().size() > 1);
getDeleteAliasButton().setDisable(alias == null);
getMoveToAliasButton().setDisable(alias == null);
if(getAliasTableView().getSelectionModel().getSelectedItems().size() <= 1)
if(aliases.size() <= 1)
{
getAliasItemEditor().setItem(alias);
setEditor(getAliasItemEditor());
if(aliases.size() == 1)
{
getAliasItemEditor().setItem(aliases.get(0));
}
else
{
getAliasItemEditor().setItem(null);
}
}
else
{
getAliasItemEditor().setItem(null);
setEditor(getAliasBulkEditor());
getAliasBulkEditor().setItem(aliases);
}
getCloneAliasButton().setDisable(aliases.isEmpty() || aliases.size() > 1);
getDeleteAliasButton().setDisable(aliases.isEmpty());
getMoveToAliasButton().setDisable(aliases.isEmpty());
}
private AliasItemEditor getAliasItemEditor()
@@ -192,6 +219,16 @@ public class AliasConfigurationEditor extends SplitPane
return mAliasItemEditor;
}
private AliasBulkEditor getAliasBulkEditor()
{
if(mAliasBulkEditor == null)
{
mAliasBulkEditor = new AliasBulkEditor(mPlaylistManager);
}
return mAliasBulkEditor;
}
private HBox getSearchAndListSelectionBox()
{
if(mSearchAndListSelectionBox == null)
@@ -384,10 +421,9 @@ public class AliasConfigurationEditor extends SplitPane
mAliasTableView.setPlaceholder(getPlaceholderLabel());
mAliasTableView.setItems(getAliasSortedList());
mAliasTableView.getSelectionModel().setSelectionMode(SelectionMode.MULTIPLE);
mAliasTableView.getSelectionModel().selectedItemProperty()
.addListener((observable, oldValue, newValue) -> {
setAlias(newValue);
});
mAliasTableView.getSelectionModel().getSelectedItems().addListener((ListChangeListener<Alias>)c -> {
setAliases(mAliasTableView.getSelectionModel().getSelectedItems());
});
}
return mAliasTableView;

View File

@@ -330,17 +330,14 @@ public class AliasItemEditor extends Editor<Alias>
if(alias != null)
{
alias.setName(getNameField().getText());
alias.setGroup(getGroupField().getText());
alias.setRecordable(getRecordAudioToggleSwitch().isSelected());
alias.setColor(ColorUtil.toInteger(getColorPicker().getValue()));
boolean canMonitor = getMonitorAudioToggleSwitch().isSelected();
Integer priority = getMonitorPriorityComboBox().getSelectionModel().getSelectedItem();
Icon icon = getIconNodeComboBox().getSelectionModel().getSelectedItem();
alias.setIconName(icon != null ? icon.getName() : null);
boolean canMonitor = getMonitorAudioToggleSwitch().isSelected();
Integer priority = getMonitorPriorityComboBox().getSelectionModel().getSelectedItem();
if(canMonitor)
{
if(priority == null)
@@ -377,6 +374,27 @@ public class AliasItemEditor extends Editor<Alias>
{
alias.addAliasAction(aliasAction);
}
//Hack: because we're using a sorted list for the alias editor, sometimes setting
//name and or group can cause list errors. So, we wrap each of these in a try/catch
//block to mitigate the error and effect the changes.
try
{
alias.setName(getNameField().getText());
}
catch(Exception e)
{
mLog.error("Error while updating alias name.", e);
}
try
{
alias.setGroup(getGroupField().getText());
}
catch(Exception e)
{
mLog.error("Error while updating alias group value", e);
}
}
AliasList aliasList = mPlaylistManager.getAliasModel().getAliasList(alias.getAliasListName());

View File

@@ -30,6 +30,7 @@ import io.github.dsheirer.module.decode.config.AuxDecodeConfiguration;
import io.github.dsheirer.module.decode.config.DecodeConfiguration;
import io.github.dsheirer.module.log.config.EventLogConfiguration;
import io.github.dsheirer.playlist.PlaylistManager;
import io.github.dsheirer.preference.UserPreferences;
import io.github.dsheirer.record.RecorderType;
import io.github.dsheirer.record.config.RecordConfiguration;
import io.github.dsheirer.source.config.SourceConfiguration;
@@ -60,9 +61,9 @@ public class AMConfigurationEditor extends ChannelConfigurationEditor
* Constructs an instance
* @param playlistManager
*/
public AMConfigurationEditor(PlaylistManager playlistManager)
public AMConfigurationEditor(PlaylistManager playlistManager, UserPreferences userPreferences)
{
super(playlistManager);
super(playlistManager, userPreferences);
getTitledPanesBox().getChildren().add(getSourcePane());
getTitledPanesBox().getChildren().add(getDecoderPane());
getTitledPanesBox().getChildren().add(getRecordPane());

View File

@@ -25,13 +25,17 @@ package io.github.dsheirer.gui.playlist.channel;
import io.github.dsheirer.alias.AliasModel;
import io.github.dsheirer.controller.channel.Channel;
import io.github.dsheirer.controller.channel.ChannelException;
import io.github.dsheirer.eventbus.MyEventBus;
import io.github.dsheirer.gui.control.MaxLengthUnaryOperator;
import io.github.dsheirer.gui.playlist.Editor;
import io.github.dsheirer.gui.preference.PreferenceEditorType;
import io.github.dsheirer.gui.preference.ViewUserPreferenceEditorRequest;
import io.github.dsheirer.module.decode.DecoderType;
import io.github.dsheirer.module.decode.config.AuxDecodeConfiguration;
import io.github.dsheirer.module.decode.config.DecodeConfiguration;
import io.github.dsheirer.module.log.config.EventLogConfiguration;
import io.github.dsheirer.playlist.PlaylistManager;
import io.github.dsheirer.preference.UserPreferences;
import io.github.dsheirer.record.config.RecordConfiguration;
import io.github.dsheirer.source.config.SourceConfigTuner;
import io.github.dsheirer.source.config.SourceConfiguration;
@@ -48,6 +52,7 @@ import javafx.geometry.Insets;
import javafx.scene.control.Alert;
import javafx.scene.control.Button;
import javafx.scene.control.ButtonType;
import javafx.scene.control.CheckBox;
import javafx.scene.control.ComboBox;
import javafx.scene.control.Label;
import javafx.scene.control.ScrollPane;
@@ -78,6 +83,7 @@ public abstract class ChannelConfigurationEditor extends Editor<Channel>
private final static Logger mLog = LoggerFactory.getLogger(ChannelConfigurationEditor.class);
private PlaylistManager mPlaylistManager;
private UserPreferences mUserPreferences;
protected EditorModificationListener mEditorModificationListener = new EditorModificationListener();
private Button mPlayButton;
private TextField mSystemField;
@@ -97,9 +103,10 @@ public abstract class ChannelConfigurationEditor extends Editor<Channel>
private IconNode mStopGraphicNode;
private ChannelProcessingMonitor mChannelProcessingMonitor = new ChannelProcessingMonitor();
public ChannelConfigurationEditor(PlaylistManager playlistManager)
public ChannelConfigurationEditor(PlaylistManager playlistManager, UserPreferences userPreferences)
{
mPlaylistManager = playlistManager;
mUserPreferences = userPreferences;
setMaxWidth(Double.MAX_VALUE);
@@ -317,6 +324,45 @@ public abstract class ChannelConfigurationEditor extends Editor<Channel>
});
}
if(requiresJmbeLibrarySetup() &&
mUserPreferences.getJmbeLibraryPreference().getAlertIfMissingLibraryRequired() &&
!getItem().processingProperty().get())
{
String content = "The decoder for this channel configuration requires the (optional) JMBE " +
"library to produce audio and the JMBE library is not currently setup. Do you want to " +
"setup the JMBE library?";
Alert alert = new Alert(Alert.AlertType.CONFIRMATION, content, ButtonType.YES, ButtonType.NO);
alert.setTitle("JMBE Library");
alert.setHeaderText("Setup JMBE Library?");
Label label = new Label(content);
label.setMaxWidth(Double.MAX_VALUE);
label.setMaxHeight(Double.MAX_VALUE);
label.getStyleClass().add("content");
label.setWrapText(true);
CheckBox checkBox = new CheckBox("Don't Show This Again");
checkBox.setOnAction(event1 -> {
boolean dontShowAgain = checkBox.isSelected();
mUserPreferences.getJmbeLibraryPreference().setAlertIfMissingLibraryRequired(!dontShowAgain);
});
VBox contentBox = new VBox();
contentBox.setPrefWidth(360);
contentBox.setSpacing(10);
contentBox.getChildren().addAll(label, checkBox);
alert.getDialogPane().setContent(contentBox);
alert.initOwner(getPlayButton().getScene().getWindow());
Optional<ButtonType> optionalButtonType = alert.showAndWait();
if(optionalButtonType.isPresent() && optionalButtonType.get() == ButtonType.YES)
{
MyEventBus.getEventBus().post(new ViewUserPreferenceEditorRequest(PreferenceEditorType.JMBE_LIBRARY));
return;
}
}
if(!getItem().processingProperty().get())
{
ThreadPool.SCHEDULED.execute(() -> {
@@ -366,6 +412,17 @@ public abstract class ChannelConfigurationEditor extends Editor<Channel>
return mPlayButton;
}
/**
* Indicates if the decoder type for the channel configuration requires the JMBE library and if the
* application is not currently setup for the JMBE library.
*/
private boolean requiresJmbeLibrarySetup()
{
return getItem() != null &&
getItem().getDecodeConfiguration().getDecoderType().providesMBEAudioFrames() &&
!mUserPreferences.getJmbeLibraryPreference().hasJmbeLibraryPath();
}
/**
* Toggles the text and graphic of the channel play button to reflect the playing state of the channel
*/

View File

@@ -24,6 +24,7 @@ package io.github.dsheirer.gui.playlist.channel;
import io.github.dsheirer.module.decode.DecoderType;
import io.github.dsheirer.playlist.PlaylistManager;
import io.github.dsheirer.preference.UserPreferences;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
@@ -44,26 +45,27 @@ public class ChannelConfigurationEditorFactory
* @param playlistManager for the editor
* @return constructed editor
*/
public static ChannelConfigurationEditor getEditor(DecoderType decoderType, PlaylistManager playlistManager)
public static ChannelConfigurationEditor getEditor(DecoderType decoderType, PlaylistManager playlistManager,
UserPreferences userPreferences)
{
switch(decoderType)
{
case AM:
return new AMConfigurationEditor(playlistManager);
return new AMConfigurationEditor(playlistManager, userPreferences);
case NBFM:
return new NBFMConfigurationEditor(playlistManager);
return new NBFMConfigurationEditor(playlistManager, userPreferences);
case LTR_NET:
return new LTRNetConfigurationEditor(playlistManager);
return new LTRNetConfigurationEditor(playlistManager, userPreferences);
case LTR:
return new LTRConfigurationEditor(playlistManager);
return new LTRConfigurationEditor(playlistManager, userPreferences);
case MPT1327:
return new MPT1327ConfigurationEditor(playlistManager);
return new MPT1327ConfigurationEditor(playlistManager, userPreferences);
case PASSPORT:
return new PassportConfigurationEditor(playlistManager);
return new PassportConfigurationEditor(playlistManager, userPreferences);
case P25_PHASE1:
return new P25P1ConfigurationEditor(playlistManager);
return new P25P1ConfigurationEditor(playlistManager, userPreferences);
case P25_PHASE2:
return new P25P2ConfigurationEditor(playlistManager);
return new P25P2ConfigurationEditor(playlistManager, userPreferences);
default:
if(decoderType != null && !mLoggedUnrecognizedTypes.contains(decoderType))
{

View File

@@ -27,11 +27,11 @@ import io.github.dsheirer.controller.channel.Channel;
import io.github.dsheirer.module.decode.DecoderFactory;
import io.github.dsheirer.module.decode.DecoderType;
import io.github.dsheirer.playlist.PlaylistManager;
import io.github.dsheirer.source.config.SourceConfigTuner;
import io.github.dsheirer.source.config.SourceConfigTunerMultipleFrequency;
import io.github.dsheirer.preference.UserPreferences;
import javafx.application.Platform;
import javafx.beans.property.SimpleStringProperty;
import javafx.beans.value.ObservableValue;
import javafx.collections.ObservableList;
import javafx.collections.transformation.FilteredList;
import javafx.collections.transformation.SortedList;
import javafx.geometry.Insets;
@@ -79,6 +79,7 @@ public class ChannelEditor extends SplitPane
{
private final static Logger mLog = LoggerFactory.getLogger(ChannelEditor.class);
private PlaylistManager mPlaylistManager;
private UserPreferences mUserPreferences;
private TableView<Channel> mChannelTableView;
private Label mPlaceholderLabel;
private MenuButton mNewButton;
@@ -101,10 +102,11 @@ public class ChannelEditor extends SplitPane
* Constructs an instance
* @param playlistManager containing playlists and channel configurations
*/
public ChannelEditor(PlaylistManager playlistManager)
public ChannelEditor(PlaylistManager playlistManager, UserPreferences userPreferences)
{
mPlaylistManager = playlistManager;
mUnknownConfigurationEditor = new UnknownConfigurationEditor(mPlaylistManager);
mUserPreferences = userPreferences;
mUnknownConfigurationEditor = new UnknownConfigurationEditor(mPlaylistManager, userPreferences);
HBox channelsBox = new HBox();
channelsBox.setSpacing(10.0);
@@ -200,7 +202,8 @@ public class ChannelEditor extends SplitPane
if(editor == null)
{
editor = ChannelConfigurationEditorFactory.getEditor(channelDecoderType, mPlaylistManager);
editor = ChannelConfigurationEditorFactory.getEditor(channelDecoderType, mPlaylistManager,
mUserPreferences);
if(editor != null)
{
@@ -638,6 +641,7 @@ public class ChannelEditor extends SplitPane
}
}
/**
* Channel tuner channel source frequencies value factory
*/
@@ -649,26 +653,17 @@ public class ChannelEditor extends SplitPane
@Override
public ObservableValue<String> call(TableColumn.CellDataFeatures<Channel, String> param)
{
Channel channel = param.getValue();
ObservableList<Long> frequencies = param.getValue().getFrequencyList();
if(channel != null)
if(frequencies != null)
{
if(channel.getSourceConfiguration() instanceof SourceConfigTuner)
List<String> freqsMHz = new ArrayList<>();
for(Long frequency: frequencies)
{
long frequency = ((SourceConfigTuner)channel.getSourceConfiguration()).getFrequency();
mFrequency.set(String.valueOf(frequency / 1E6));
freqsMHz.add(String.valueOf(frequency / 1E6));
}
else if(channel.getSourceConfiguration() instanceof SourceConfigTunerMultipleFrequency)
{
List<Long> frequencies = ((SourceConfigTunerMultipleFrequency)channel.getSourceConfiguration()).getFrequencies();
List<String> formatted = new ArrayList<>();
for(Long frequency: frequencies)
{
formatted.add(String.valueOf(frequency / 1E6));
}
mFrequency.set(Joiner.on(",").join(formatted));
}
mFrequency.set(Joiner.on(", ").join(freqsMHz));
}
else
{

View File

@@ -35,6 +35,7 @@ import io.github.dsheirer.module.decode.ltrstandard.DecodeConfigLTRStandard;
import io.github.dsheirer.module.log.EventLogType;
import io.github.dsheirer.module.log.config.EventLogConfiguration;
import io.github.dsheirer.playlist.PlaylistManager;
import io.github.dsheirer.preference.UserPreferences;
import io.github.dsheirer.record.RecorderType;
import io.github.dsheirer.record.config.RecordConfiguration;
import io.github.dsheirer.source.config.SourceConfiguration;
@@ -74,10 +75,11 @@ public class LTRConfigurationEditor extends ChannelConfigurationEditor
/**
* Constructs an instance
* @param playlistManager
* @param userPreferences
*/
public LTRConfigurationEditor(PlaylistManager playlistManager)
public LTRConfigurationEditor(PlaylistManager playlistManager, UserPreferences userPreferences)
{
super(playlistManager);
super(playlistManager, userPreferences);
getTitledPanesBox().getChildren().add(getSourcePane());
getTitledPanesBox().getChildren().add(getDecoderPane());
getTitledPanesBox().getChildren().add(getAuxDecoderPane());

View File

@@ -35,6 +35,7 @@ import io.github.dsheirer.module.decode.ltrnet.DecodeConfigLTRNet;
import io.github.dsheirer.module.log.EventLogType;
import io.github.dsheirer.module.log.config.EventLogConfiguration;
import io.github.dsheirer.playlist.PlaylistManager;
import io.github.dsheirer.preference.UserPreferences;
import io.github.dsheirer.record.RecorderType;
import io.github.dsheirer.record.config.RecordConfiguration;
import io.github.dsheirer.source.config.SourceConfiguration;
@@ -74,10 +75,11 @@ public class LTRNetConfigurationEditor extends ChannelConfigurationEditor
/**
* Constructs an instance
* @param playlistManager
* @param userPreferences
*/
public LTRNetConfigurationEditor(PlaylistManager playlistManager)
public LTRNetConfigurationEditor(PlaylistManager playlistManager, UserPreferences userPreferences)
{
super(playlistManager);
super(playlistManager, userPreferences);
getTitledPanesBox().getChildren().add(getSourcePane());
getTitledPanesBox().getChildren().add(getDecoderPane());
getTitledPanesBox().getChildren().add(getAuxDecoderPane());

View File

@@ -36,6 +36,7 @@ import io.github.dsheirer.module.decode.mpt1327.DecodeConfigMPT1327;
import io.github.dsheirer.module.log.EventLogType;
import io.github.dsheirer.module.log.config.EventLogConfiguration;
import io.github.dsheirer.playlist.PlaylistManager;
import io.github.dsheirer.preference.UserPreferences;
import io.github.dsheirer.record.RecorderType;
import io.github.dsheirer.record.config.RecordConfiguration;
import io.github.dsheirer.source.config.SourceConfiguration;
@@ -60,6 +61,9 @@ import org.slf4j.LoggerFactory;
import java.util.ArrayList;
import java.util.List;
import static io.github.dsheirer.module.decode.config.DecodeConfiguration.CALL_TIMEOUT_MAXIMUM;
import static io.github.dsheirer.module.decode.config.DecodeConfiguration.CALL_TIMEOUT_MINIMUM;
/**
* MPT-1327 channel configuration editor
*/
@@ -81,10 +85,11 @@ public class MPT1327ConfigurationEditor extends ChannelConfigurationEditor
/**
* Constructs an instance
* @param playlistManager
* @param userPreferences
*/
public MPT1327ConfigurationEditor(PlaylistManager playlistManager)
public MPT1327ConfigurationEditor(PlaylistManager playlistManager, UserPreferences userPreferences)
{
super(playlistManager);
super(playlistManager, userPreferences);
getTitledPanesBox().getChildren().add(getSourcePane());
getTitledPanesBox().getChildren().add(getDecoderPane());
getTitledPanesBox().getChildren().add(getEventLogPane());
@@ -244,7 +249,7 @@ public class MPT1327ConfigurationEditor extends ChannelConfigurationEditor
mChannelMapComboBox.setMaxWidth(Double.MAX_VALUE);
mChannelMapComboBox.setDisable(true);
mChannelMapComboBox.setTooltip(new Tooltip("Select a channel map to use for this system"));
mChannelMapComboBox.getItems().addAll(getPlaylistManager().getChannelMapModel().getChannelMaps());
mChannelMapComboBox.setItems(getPlaylistManager().getChannelMapModel().getChannelMaps());
mChannelMapComboBox.setDisable(true);
mChannelMapComboBox.getSelectionModel().selectedItemProperty()
.addListener((observable, oldValue, newValue) -> modifiedProperty().set(true));
@@ -301,11 +306,11 @@ public class MPT1327ConfigurationEditor extends ChannelConfigurationEditor
{
if(mCallTimeoutSpinner == null)
{
mCallTimeoutSpinner = new Spinner();
mCallTimeoutSpinner = new Spinner<>();
mCallTimeoutSpinner.setDisable(true);
mCallTimeoutSpinner.setTooltip(new Tooltip("Maximum call limit in seconds"));
mCallTimeoutSpinner.getStyleClass().add(Spinner.STYLE_CLASS_SPLIT_ARROWS_HORIZONTAL);
SpinnerValueFactory<Integer> svf = new SpinnerValueFactory.IntegerSpinnerValueFactory(1, 120);
var svf = new SpinnerValueFactory.IntegerSpinnerValueFactory(CALL_TIMEOUT_MINIMUM, CALL_TIMEOUT_MAXIMUM);
mCallTimeoutSpinner.setValueFactory(svf);
mCallTimeoutSpinner.getValueFactory().valueProperty()
.addListener((observable, oldValue, newValue) -> modifiedProperty().set(true));
@@ -340,7 +345,7 @@ public class MPT1327ConfigurationEditor extends ChannelConfigurationEditor
}
}
int callTimeout = decodeConfigMPT1327.getCallTimeout();
int callTimeout = decodeConfigMPT1327.getCallTimeoutSeconds();
getCallTimeoutSpinner().getValueFactory().setValue(callTimeout);
int channelPoolSize = decodeConfigMPT1327.getTrafficChannelPoolSize();
@@ -367,7 +372,7 @@ public class MPT1327ConfigurationEditor extends ChannelConfigurationEditor
config = new DecodeConfigMPT1327();
}
config.setCallTimeout(getCallTimeoutSpinner().getValue());
config.setCallTimeoutSeconds(getCallTimeoutSpinner().getValue());
config.setTrafficChannelPoolSize(getTrafficChannelPoolSizeSpinner().getValue());
ChannelMap selected = getChannelMapComboBox().getSelectionModel().getSelectedItem();

View File

@@ -33,6 +33,7 @@ import io.github.dsheirer.module.decode.nbfm.DecodeConfigNBFM;
import io.github.dsheirer.module.log.EventLogType;
import io.github.dsheirer.module.log.config.EventLogConfiguration;
import io.github.dsheirer.playlist.PlaylistManager;
import io.github.dsheirer.preference.UserPreferences;
import io.github.dsheirer.record.RecorderType;
import io.github.dsheirer.record.config.RecordConfiguration;
import io.github.dsheirer.source.config.SourceConfiguration;
@@ -73,10 +74,11 @@ public class NBFMConfigurationEditor extends ChannelConfigurationEditor
/**
* Constructs an instance
* @param playlistManager
* @param userPreferences
*/
public NBFMConfigurationEditor(PlaylistManager playlistManager)
public NBFMConfigurationEditor(PlaylistManager playlistManager, UserPreferences userPreferences)
{
super(playlistManager);
super(playlistManager, userPreferences);
getTitledPanesBox().getChildren().add(getSourcePane());
getTitledPanesBox().getChildren().add(getDecoderPane());
getTitledPanesBox().getChildren().add(getAuxDecoderPane());

View File

@@ -34,6 +34,7 @@ import io.github.dsheirer.module.decode.p25.phase1.P25P1Decoder;
import io.github.dsheirer.module.log.EventLogType;
import io.github.dsheirer.module.log.config.EventLogConfiguration;
import io.github.dsheirer.playlist.PlaylistManager;
import io.github.dsheirer.preference.UserPreferences;
import io.github.dsheirer.record.RecorderType;
import io.github.dsheirer.record.config.RecordConfiguration;
import io.github.dsheirer.source.config.SourceConfiguration;
@@ -80,10 +81,11 @@ public class P25P1ConfigurationEditor extends ChannelConfigurationEditor
/**
* Constructs an instance
* @param playlistManager
* @param userPreferences
*/
public P25P1ConfigurationEditor(PlaylistManager playlistManager)
public P25P1ConfigurationEditor(PlaylistManager playlistManager, UserPreferences userPreferences)
{
super(playlistManager);
super(playlistManager, userPreferences);
getTitledPanesBox().getChildren().add(getSourcePane());
getTitledPanesBox().getChildren().add(getDecoderPane());
getTitledPanesBox().getChildren().add(getEventLogPane());

View File

@@ -35,6 +35,7 @@ import io.github.dsheirer.module.decode.p25.phase2.enumeration.ScrambleParameter
import io.github.dsheirer.module.log.EventLogType;
import io.github.dsheirer.module.log.config.EventLogConfiguration;
import io.github.dsheirer.playlist.PlaylistManager;
import io.github.dsheirer.preference.UserPreferences;
import io.github.dsheirer.record.RecorderType;
import io.github.dsheirer.record.config.RecordConfiguration;
import io.github.dsheirer.source.config.SourceConfiguration;
@@ -70,10 +71,11 @@ public class P25P2ConfigurationEditor extends ChannelConfigurationEditor
/**
* Constructs an instance
* @param playlistManager
* @param userPreferences
*/
public P25P2ConfigurationEditor(PlaylistManager playlistManager)
public P25P2ConfigurationEditor(PlaylistManager playlistManager, UserPreferences userPreferences)
{
super(playlistManager);
super(playlistManager, userPreferences);
getTitledPanesBox().getChildren().add(getSourcePane());
getTitledPanesBox().getChildren().add(getDecoderPane());
getTitledPanesBox().getChildren().add(getEventLogPane());

View File

@@ -34,6 +34,7 @@ import io.github.dsheirer.module.decode.passport.DecodeConfigPassport;
import io.github.dsheirer.module.log.EventLogType;
import io.github.dsheirer.module.log.config.EventLogConfiguration;
import io.github.dsheirer.playlist.PlaylistManager;
import io.github.dsheirer.preference.UserPreferences;
import io.github.dsheirer.record.RecorderType;
import io.github.dsheirer.record.config.RecordConfiguration;
import io.github.dsheirer.source.config.SourceConfiguration;
@@ -66,10 +67,11 @@ public class PassportConfigurationEditor extends ChannelConfigurationEditor
/**
* Constructs an instance
* @param playlistManager
* @param userPreferences
*/
public PassportConfigurationEditor(PlaylistManager playlistManager)
public PassportConfigurationEditor(PlaylistManager playlistManager, UserPreferences userPreferences)
{
super(playlistManager);
super(playlistManager, userPreferences);
getTitledPanesBox().getChildren().add(getSourcePane());
getTitledPanesBox().getChildren().add(getDecoderPane());
getTitledPanesBox().getChildren().add(getAuxDecoderPane());

View File

@@ -27,6 +27,7 @@ import io.github.dsheirer.module.decode.config.AuxDecodeConfiguration;
import io.github.dsheirer.module.decode.config.DecodeConfiguration;
import io.github.dsheirer.module.log.config.EventLogConfiguration;
import io.github.dsheirer.playlist.PlaylistManager;
import io.github.dsheirer.preference.UserPreferences;
import io.github.dsheirer.record.config.RecordConfiguration;
import io.github.dsheirer.source.config.SourceConfiguration;
import javafx.scene.control.TitledPane;
@@ -45,9 +46,9 @@ public class UnknownConfigurationEditor extends ChannelConfigurationEditor
* Constructs an instance
* @param aliasModel
*/
public UnknownConfigurationEditor(PlaylistManager playlistManager)
public UnknownConfigurationEditor(PlaylistManager playlistManager, UserPreferences userPreferences)
{
super(playlistManager);
super(playlistManager, userPreferences);
getTitledPanesBox().getChildren().add(getDecoderPane());
}

View File

@@ -0,0 +1,131 @@
/*
* *****************************************************************************
* Copyright (C) 2014-2020 Dennis Sheirer
*
* This program is free software: you can redistribute it and/or modify
* it under the terms of the GNU General Public License as published by
* the Free Software Foundation, either version 3 of the License, or
* (at your option) any later version.
*
* This program is distributed in the hope that it will be useful,
* but WITHOUT ANY WARRANTY; without even the implied warranty of
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
* GNU General Public License for more details.
*
* You should have received a copy of the GNU General Public License
* along with this program. If not, see <http://www.gnu.org/licenses/>
* ****************************************************************************
*/
package io.github.dsheirer.gui.playlist.radioreference;
import io.github.dsheirer.rrapi.type.CountyInfo;
import io.github.dsheirer.rrapi.type.Site;
/**
* Wrapper class to join a Site and a corresponding County Info
*/
public class EnrichedSite
{
private Site mSite;
private CountyInfo mCountyInfo;
/**
* Constructs an instance
* @param site object
* @param countyInfo optional
*/
public EnrichedSite(Site site, CountyInfo countyInfo)
{
mSite = site;
mCountyInfo = countyInfo;
}
/**
* Site instance
* @return site or null
*/
public Site getSite()
{
return mSite;
}
/**
* Sets the site instance
* @param site or null
*/
public void setSite(Site site)
{
mSite = site;
}
/**
* County information
* @return county info or null
*/
public CountyInfo getCountyInfo()
{
return mCountyInfo;
}
/**
* Sets the county info
* @param countyInfo or null
*/
public void setCountyInfo(CountyInfo countyInfo)
{
mCountyInfo = countyInfo;
}
/**
* Optional site number
*/
public Integer getSiteNumber()
{
if(mSite != null)
{
return mSite.getSiteNumber();
}
return null;
}
/**
* Optional site RFSS value
*/
public Integer getRfss()
{
if(mSite != null)
{
return mSite.getRfss();
}
return null;
}
/**
* Optional county name
*/
public String getCountyName()
{
if(mCountyInfo != null)
{
return mCountyInfo.getName();
}
return null;
}
/**
* Optional description of the site
*/
public String getDescription()
{
if(mSite != null)
{
return mSite.getDescription();
}
return null;
}
}

View File

@@ -204,7 +204,7 @@ public class RadioReferenceDecoder
{
List<Tag> tags = new ArrayList<>();
if(talkgroup.getTags() != null)
if(talkgroup != null && talkgroup.getTags() != null)
{
for(Tag tag: talkgroup.getTags())
{
@@ -220,7 +220,12 @@ public class RadioReferenceDecoder
*/
public Type getType(System system)
{
return mTypeMap.get(system.getTypeId());
if(system != null)
{
return mTypeMap.get(system.getTypeId());
}
return null;
}
/**
@@ -228,7 +233,12 @@ public class RadioReferenceDecoder
*/
public Flavor getFlavor(System system)
{
return mFlavorMap.get(system.getFlavorId());
if(system != null)
{
return mFlavorMap.get(system.getFlavorId());
}
return null;
}
/**
@@ -236,7 +246,12 @@ public class RadioReferenceDecoder
*/
public Voice getVoice(System system)
{
return mVoiceMap.get(system.getVoiceId());
if(system != null)
{
return mVoiceMap.get(system.getVoiceId());
}
return null;
}
/**
@@ -340,51 +355,54 @@ public class RadioReferenceDecoder
Flavor flavor = getFlavor(system);
Voice voice = getVoice(system);
switch(type.getName())
if(type != null && flavor != null && voice != null)
{
case "LTR":
if(flavor.getName().contentEquals("Net"))
{
return DecoderType.LTR_NET;
}
else if(flavor.getName().contentEquals("Passport"))
{
return DecoderType.PASSPORT;
}
else
{
return DecoderType.LTR;
}
case "MPT-1327":
return DecoderType.MPT1327;
case "Project 25":
if(flavor.getName().contentEquals("Phase II"))
{
return DecoderType.P25_PHASE2;
}
else if(flavor.getName().contentEquals("Phase I"))
{
return DecoderType.P25_PHASE1;
}
break;
case "Motorola":
if(voice.getName().contentEquals("Analog and APCO-25 Common Air Interface") ||
voice.getName().contentEquals("APCO-25 Common Air Interface Exclusive"))
{
return DecoderType.P25_PHASE1;
}
break;
case "DMR":
case "NXDN":
switch(type.getName())
{
case "LTR":
if(flavor.getName().contentEquals("Net"))
{
return DecoderType.LTR_NET;
}
else if(flavor.getName().contentEquals("Passport"))
{
return DecoderType.PASSPORT;
}
else
{
return DecoderType.LTR;
}
case "MPT-1327":
return DecoderType.MPT1327;
case "Project 25":
if(flavor.getName().contentEquals("Phase II"))
{
return DecoderType.P25_PHASE2;
}
else if(flavor.getName().contentEquals("Phase I"))
{
return DecoderType.P25_PHASE1;
}
break;
case "Motorola":
if(voice.getName().contentEquals("Analog and APCO-25 Common Air Interface") ||
voice.getName().contentEquals("APCO-25 Common Air Interface Exclusive"))
{
return DecoderType.P25_PHASE1;
}
break;
case "DMR":
case "NXDN":
case "EDACS":
case "TETRA":
case "Midland CMS":
case "OpenSky":
case "iDEN":
case "SmarTrunk":
case "Other":
default:
case "EDACS":
case "TETRA":
case "Midland CMS":
case "OpenSky":
case "iDEN":
case "SmarTrunk":
case "Other":
default:
}
}
return null;

View File

@@ -30,10 +30,13 @@ public class RadioReferenceUnavailableAlert extends Alert
{
public RadioReferenceUnavailableAlert(Node ownerNode)
{
super(AlertType.INFORMATION, "Check network connection or see details in sdrtrunk log.", ButtonType.OK);
super(AlertType.INFORMATION, "This may be caused by a network outage or there was an error " +
"accessing the service. If this problem occurs each time you attempt to access a " +
"specific system or agency, please notify the developer. Details for this issue " +
"are located in the sdrtrunk application log.", ButtonType.OK);
setTitle("Service Unavailable");
setHeaderText("Radio Reference Is Currently Unavailable");
setHeaderText("Radio Reference Error or Service Is Currently Unavailable");
initOwner(ownerNode.getScene().getWindow());
}
}

View File

@@ -108,7 +108,7 @@ public class SiteEditor extends GridPane
private CheckBox mGoToChannelEditorCheckBox;
private Label mProtocolNotSupportedLabel;
private RadioReferenceDecoder mRadioReferenceDecoder;
private Site mCurrentSite;
private EnrichedSite mCurrentSite;
private System mCurrentSystem;
private SystemInformation mCurrentSystemInformation;
private ComboBox mAliasListNameComboBox;
@@ -290,7 +290,7 @@ public class SiteEditor extends GridPane
}
}
public void setSite(Site site, System system, SystemInformation systemInformation, RadioReferenceDecoder decoder)
public void setSite(EnrichedSite site, System system, SystemInformation systemInformation, RadioReferenceDecoder decoder)
{
mCurrentSite = site;
mCurrentSystem = system;
@@ -299,7 +299,7 @@ public class SiteEditor extends GridPane
getSiteFrequencyTableView().getItems().clear();
boolean disable = site == null || site.getSiteFrequencies().isEmpty();
boolean disable = site == null || site.getSite().getSiteFrequencies().isEmpty();
boolean supported = decoder.hasSupportedProtocol(system);
getFrequenciesSegmentedButton().setDisable(disable || !supported);
@@ -318,7 +318,7 @@ public class SiteEditor extends GridPane
if(site != null)
{
List<SiteFrequency> siteFrequencies = site.getSiteFrequencies();
List<SiteFrequency> siteFrequencies = site.getSite().getSiteFrequencies();
for(SiteFrequency siteFrequency: siteFrequencies)
{
if(siteFrequency.getFrequency() > 0.01)
@@ -340,7 +340,7 @@ public class SiteEditor extends GridPane
getCreateChannelConfigurationButton().setVisible(true);
getGoToChannelEditorCheckBox().setVisible(true);
getSystemTextField().setText(system.getName());
getSiteTextField().setText(site.getCountyName());
getSiteTextField().setText(mCurrentSite.getCountyName());
getNameTextField().setText("Control");
if(!siteFrequencies.isEmpty())
@@ -456,7 +456,8 @@ public class SiteEditor extends GridPane
Channel channel = getChannelTemplate();
channel.setName("LCN " + siteFrequency.getLogicalChannelNumber());
DecoderType decoderType = mRadioReferenceDecoder.getDecoderType(mCurrentSystem);
channel.setDecodeConfiguration(getDecodeConfiguration(decoderType, mCurrentSite, mCurrentSystemInformation));
channel.setDecodeConfiguration(getDecodeConfiguration(decoderType, mCurrentSite.getSite(),
mCurrentSystemInformation));
SourceConfigTuner sourceConfigTuner = new SourceConfigTuner();
sourceConfigTuner.setFrequency(getFrequency(siteFrequency));
channel.setSourceConfiguration(sourceConfigTuner);
@@ -511,7 +512,7 @@ public class SiteEditor extends GridPane
decoderType = DecoderType.P25_PHASE1;
}
channel.setDecodeConfiguration(getDecodeConfiguration(decoderType, mCurrentSite, mCurrentSystemInformation));
channel.setDecodeConfiguration(getDecodeConfiguration(decoderType, mCurrentSite.getSite(), mCurrentSystemInformation));
SourceConfigTuner sourceConfigTuner = new SourceConfigTuner();
sourceConfigTuner.setFrequency((long)(control.getFrequency() * 1E6));
@@ -569,7 +570,8 @@ public class SiteEditor extends GridPane
decoderType = DecoderType.P25_PHASE1;
}
channel.setDecodeConfiguration(getDecodeConfiguration(decoderType, mCurrentSite, mCurrentSystemInformation));
channel.setDecodeConfiguration(getDecodeConfiguration(decoderType, mCurrentSite.getSite(),
mCurrentSystemInformation));
if(siteFrequencies.size() == 1)
{
@@ -605,7 +607,8 @@ public class SiteEditor extends GridPane
Channel channel = getChannelTemplate();
channel.setName("LCN " + siteFrequency.getLogicalChannelNumber());
DecoderType decoderType = mRadioReferenceDecoder.getDecoderType(mCurrentSystem);
channel.setDecodeConfiguration(getDecodeConfiguration(decoderType, mCurrentSite, mCurrentSystemInformation));
channel.setDecodeConfiguration(getDecodeConfiguration(decoderType, mCurrentSite.getSite(),
mCurrentSystemInformation));
SourceConfigTuner sourceConfigTuner = new SourceConfigTuner();
sourceConfigTuner.setFrequency(getFrequency(siteFrequency));
channel.setSourceConfiguration(sourceConfigTuner);
@@ -659,7 +662,8 @@ public class SiteEditor extends GridPane
Channel channel = getChannelTemplate();
DecoderType decoderType = mRadioReferenceDecoder.getDecoderType(mCurrentSystem);
channel.setDecodeConfiguration(getDecodeConfiguration(decoderType, mCurrentSite, mCurrentSystemInformation));
channel.setDecodeConfiguration(getDecodeConfiguration(decoderType, mCurrentSite.getSite(),
mCurrentSystemInformation));
if(siteFrequencies.size() == 1)
{
@@ -695,7 +699,8 @@ public class SiteEditor extends GridPane
Channel channel = getChannelTemplate();
channel.setName("LCN " + siteFrequency.getLogicalChannelNumber());
DecoderType decoderType = mRadioReferenceDecoder.getDecoderType(mCurrentSystem);
channel.setDecodeConfiguration(getDecodeConfiguration(decoderType, mCurrentSite, mCurrentSystemInformation));
channel.setDecodeConfiguration(getDecodeConfiguration(decoderType, mCurrentSite.getSite(),
mCurrentSystemInformation));
SourceConfigTuner sourceConfigTuner = new SourceConfigTuner();
sourceConfigTuner.setFrequency(getFrequency(siteFrequency));
@@ -946,7 +951,6 @@ public class SiteEditor extends GridPane
{
mSiteFrequencyTableView = new TableView<>();
mSiteFrequencyTableView.getSelectionModel().setSelectionMode(SelectionMode.MULTIPLE);
mSiteFrequencyTableView.setMaxHeight(Double.MAX_VALUE);
mSiteFrequencyTableView.setPlaceholder(new Label("Please select a site to view frequencies"));
mTypeColumn = new TableColumn("Type");

View File

@@ -54,6 +54,7 @@ import jiconfont.javafx.IconNode;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import java.util.ArrayList;
import java.util.Collections;
import java.util.Comparator;
import java.util.List;
@@ -281,13 +282,14 @@ public class SystemEditor extends VBox
List<Site> sites = mRadioReference.getService().getSites(system.getSystemId());
//The service api doesn't provide the county name, so we run a separate query to update each value
List<EnrichedSite> enrichedSites = new ArrayList<>();
for(Site site: sites)
{
CountyInfo countyInfo = mRadioReference.getService().getCountyInfo(site.getCountyId());
site.setCountyName(countyInfo.getName());
enrichedSites.add(new EnrichedSite(site, countyInfo));
}
Platform.runLater(() -> getSystemSiteSelectionEditor().setSystem(system, sites, mRadioReferenceDecoder,
Platform.runLater(() -> getSystemSiteSelectionEditor().setSystem(system, enrichedSites, mRadioReferenceDecoder,
systemInformation));
//Query and load the talkgroup view second

View File

@@ -25,7 +25,6 @@ package io.github.dsheirer.gui.playlist.radioreference;
import io.github.dsheirer.playlist.PlaylistManager;
import io.github.dsheirer.preference.UserPreferences;
import io.github.dsheirer.rrapi.type.Flavor;
import io.github.dsheirer.rrapi.type.Site;
import io.github.dsheirer.rrapi.type.System;
import io.github.dsheirer.rrapi.type.SystemInformation;
import io.github.dsheirer.rrapi.type.Type;
@@ -60,7 +59,7 @@ public class SystemSiteSelectionEditor extends GridPane
private Label mProtocolLabel;
private Label mFlavorLabel;
private Label mVoiceLabel;
private TableView<Site> mSiteTableView;
private TableView<EnrichedSite> mSiteTableView;
private SiteEditor mSiteEditor;
private RadioReferenceDecoder mRadioReferenceDecoder;
private System mCurrentSystem;
@@ -113,7 +112,8 @@ public class SystemSiteSelectionEditor extends GridPane
* Updates this view with the specified system
* @param system to view
*/
public void setSystem(System system, List<Site> sites, RadioReferenceDecoder decoder, SystemInformation systemInformation)
public void setSystem(System system, List<EnrichedSite> sites, RadioReferenceDecoder decoder,
SystemInformation systemInformation)
{
mCurrentSystem = system;
mCurrentSystemInformation = systemInformation;
@@ -205,13 +205,12 @@ public class SystemSiteSelectionEditor extends GridPane
return mPlaceholderLabel;
}
private TableView<Site> getSiteTableView()
private TableView<EnrichedSite> getSiteTableView()
{
if(mSiteTableView == null)
{
mSiteTableView = new TableView<>();
mSiteTableView.setPlaceholder(getPlaceholderLabel());
mSiteTableView.setPrefHeight(125);
TableColumn numberColumn = new TableColumn();
numberColumn.setText("Site");
@@ -236,8 +235,11 @@ public class SystemSiteSelectionEditor extends GridPane
mSiteTableView.getColumns().addAll(numberColumn, rfssColumn, countyColumn, descriptionColumn);
mSiteTableView.getSelectionModel().setSelectionMode(SelectionMode.SINGLE);
mSiteTableView.getSelectionModel().selectedItemProperty()
.addListener((observable, oldValue, selected) -> getSiteEditor()
.setSite(selected, mCurrentSystem, mCurrentSystemInformation, mRadioReferenceDecoder));
.addListener((observable, oldValue, selected) ->
{
getSiteEditor().setSite(selected, mCurrentSystem, mCurrentSystemInformation,
mRadioReferenceDecoder);
});
}
return mSiteTableView;

View File

@@ -21,6 +21,7 @@ package io.github.dsheirer.gui.preference;
import io.github.dsheirer.gui.preference.decoder.JmbeLibraryPreferenceEditor;
import io.github.dsheirer.gui.preference.directory.DirectoryPreferenceEditor;
import io.github.dsheirer.gui.preference.duplicate.DuplicateCallPreferenceEditor;
import io.github.dsheirer.gui.preference.playback.PlaybackPreferenceEditor;
import io.github.dsheirer.gui.preference.record.RecordPreferenceEditor;
import io.github.dsheirer.gui.preference.tuner.ChannelMultipleFrequencyPreferenceEditor;
@@ -37,6 +38,8 @@ public class PreferenceEditorFactory
{
switch(preferenceEditorType)
{
case AUDIO_DUPLICATE_CALL_DETECTION:
return new DuplicateCallPreferenceEditor(userPreferences);
case AUDIO_OUTPUT:
return new PlaybackPreferenceEditor(userPreferences);
case AUDIO_RECORD:

View File

@@ -29,6 +29,7 @@ public enum PreferenceEditorType
JMBE_LIBRARY("JMBE Audio Library"),
AUDIO_RECORD("Record"),
AUDIO_OUTPUT("Output/Tones"),
AUDIO_DUPLICATE_CALL_DETECTION("Duplicate Calls"),
SOURCE_CHANNEL_MULTIPLE_FREQUENCY("Channel - Multiple Frequency"),
SOURCE_TUNER_CHANNELIZER("Tuner Channelizer"),
TALKGROUP_FORMAT("Talkgroup & Radio ID"),

View File

@@ -167,6 +167,7 @@ public class UserPreferencesEditor extends BorderPane
TreeItem<String> treeRoot = new TreeItem<>("Root node");
TreeItem<String> audioItem = new TreeItem<>("Audio");
audioItem.getChildren().add(new TreeItem(PreferenceEditorType.AUDIO_DUPLICATE_CALL_DETECTION));
audioItem.getChildren().add(new TreeItem(PreferenceEditorType.AUDIO_OUTPUT));
audioItem.getChildren().add(new TreeItem(PreferenceEditorType.AUDIO_RECORD));
treeRoot.getChildren().add(audioItem);

View File

@@ -22,50 +22,99 @@ package io.github.dsheirer.gui.preference.decoder;
import com.google.common.eventbus.Subscribe;
import io.github.dsheirer.eventbus.MyEventBus;
import io.github.dsheirer.jmbe.JmbeCreator;
import io.github.dsheirer.jmbe.JmbeEditorRequest;
import io.github.dsheirer.jmbe.github.GitHub;
import io.github.dsheirer.jmbe.github.Release;
import io.github.dsheirer.jmbe.github.Version;
import io.github.dsheirer.preference.PreferenceType;
import io.github.dsheirer.preference.UserPreferences;
import io.github.dsheirer.preference.decoder.JmbeLibraryPreference;
import io.github.dsheirer.util.ThreadPool;
import javafx.application.Platform;
import javafx.event.ActionEvent;
import javafx.event.EventHandler;
import javafx.geometry.HPos;
import javafx.geometry.Insets;
import javafx.scene.control.Alert;
import javafx.scene.control.Button;
import javafx.scene.control.ButtonType;
import javafx.scene.control.CheckBox;
import javafx.scene.control.Label;
import javafx.scene.layout.GridPane;
import javafx.scene.layout.HBox;
import javafx.scene.layout.Priority;
import javafx.scene.layout.Region;
import javafx.scene.layout.VBox;
import javafx.stage.FileChooser;
import javafx.stage.Stage;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import java.io.File;
import java.nio.file.Files;
import java.nio.file.Path;
/**
* Preference settings for decoders
*/
public class JmbeLibraryPreferenceEditor extends HBox
public class JmbeLibraryPreferenceEditor extends VBox
{
private final static Logger mLog = LoggerFactory.getLogger(JmbeLibraryPreferenceEditor.class);
private static final String PATH_NOT_SET = "(not set)";
private JmbeLibraryPreference mJmbeLibraryPreference;
private static final String CREATE_LIBRARY = "Create Library";
private static final String CHECK_FOR_UPDATE = "Check For Library Update";
private static final String LIBRARY_NOT_SETUP = "None. Click Select or Create Library button to setup.";
private UserPreferences mUserPreferences;
private GridPane mEditorPane;
private Label mJmbeLibraryLabel;
private Button mPathToJmbeLibraryButton;
private Button mPathToJmbeLibraryResetButton;
private Label mJmbeVersionLabel;
private Label mPathToJmbeLibraryLabel;
private Button mSelectButton;
private Button mResetButton;
private Button mCreateButton;
private HBox mButtonsBox;
private CheckBox mAlertUserWhenMissingCheckBox;
public JmbeLibraryPreferenceEditor(UserPreferences userPreferences)
{
mJmbeLibraryPreference = userPreferences.getJmbeLibraryPreference();
mUserPreferences = userPreferences;
//Register to receive directory preference update notifications so we can update the path labels
MyEventBus.getEventBus().register(this);
HBox.setHgrow(getEditorPane(), Priority.ALWAYS);
getChildren().add(getEditorPane());
setPadding(new Insets(10,10,10,10));
setSpacing(10);
getChildren().addAll(getEditorPane(), getButtonsBox(), getAlertUserWhenMissingCheckBox());
}
private CheckBox getAlertUserWhenMissingCheckBox()
{
if(mAlertUserWhenMissingCheckBox == null)
{
mAlertUserWhenMissingCheckBox = new CheckBox("Alert when decoder requires missing JMBE library");
mAlertUserWhenMissingCheckBox.setSelected(mUserPreferences.getJmbeLibraryPreference()
.getAlertIfMissingLibraryRequired());
mAlertUserWhenMissingCheckBox.setOnAction(event -> {
boolean alert = mAlertUserWhenMissingCheckBox.isSelected();
mUserPreferences.getJmbeLibraryPreference().setAlertIfMissingLibraryRequired(alert);
});
}
return mAlertUserWhenMissingCheckBox;
}
private HBox getButtonsBox()
{
if(mButtonsBox == null)
{
mButtonsBox = new HBox();
mButtonsBox.setSpacing(10);
mButtonsBox.getChildren().addAll(getCreateButton(), getSelectButton(), getResetButton());
}
return mButtonsBox;
}
private GridPane getEditorPane()
@@ -73,85 +122,213 @@ public class JmbeLibraryPreferenceEditor extends HBox
if(mEditorPane == null)
{
mEditorPane = new GridPane();
mEditorPane.setPadding(new Insets(10, 10, 10, 10));
mEditorPane.setVgap(10);
mEditorPane.setHgap(10);
int row = 0;
GridPane.setMargin(getJmbeLibraryLabel(), new Insets(0, 10, 0, 0));
mEditorPane.add(getJmbeLibraryLabel(), 0, row);
mEditorPane.add(getJmbeLibraryLabel(), 0, row, 2, 1);
Label versionLabel = new Label("Current Version:");
GridPane.setHalignment(versionLabel, HPos.RIGHT);
mEditorPane.add(versionLabel, 0, ++row);
mEditorPane.add(getJmbeVersionLabel(), 1, row);
Label fileLabel = new Label("File:");
GridPane.setHalignment(fileLabel, HPos.RIGHT);
mEditorPane.add(fileLabel, 0, ++row);
GridPane.setMargin(getPathToJmbeLibraryLabel(), new Insets(0, 10, 0, 0));
mEditorPane.add(getPathToJmbeLibraryLabel(), 1, row);
GridPane.setMargin(getPathToJmbeLibraryButton(), new Insets(2, 10, 2, 0));
mEditorPane.add(getPathToJmbeLibraryButton(), 2, row);
GridPane.setMargin(getPathToJmbeLibraryResetButton(), new Insets(2, 0, 2, 0));
mEditorPane.add(getPathToJmbeLibraryResetButton(), 3, row++);
}
return mEditorPane;
}
private Button getCreateButton()
{
if(mCreateButton == null)
{
mCreateButton = new Button();
mCreateButton.setText(mUserPreferences.getJmbeLibraryPreference()
.getPathJmbeLibrary() != null ? CHECK_FOR_UPDATE : CREATE_LIBRARY);
mCreateButton.setOnAction(event -> checkForUpdatedLibrary());
}
return mCreateButton;
}
/**
* Checks for availability of an updated JMBE library
*/
private void checkForUpdatedLibrary()
{
ThreadPool.SCHEDULED.execute(() -> {
try
{
Version current = mUserPreferences.getJmbeLibraryPreference().getCurrentVersion();
final Release release = GitHub.getLatestRelease(JmbeCreator.GITHUB_JMBE_RELEASES_URL);
mLog.info("Checking for JMBE Library Updates ...");
mLog.info("Current: " + (current != null ? current.toString() : "empty"));
final boolean canUpdate = (release != null) && ((current == null) ||
(release.getVersion().compareTo(current) > 0));
if(release != null)
{
mLog.info("Available: " + release.getVersion().toString());
}
if(canUpdate)
{
mLog.info("JMBE Library update is available");
}
else
{
mLog.info("No JMBE library update is available at this time");
}
Platform.runLater(new Runnable()
{
@Override
public void run()
{
try
{
if(canUpdate)
{
String content = "JMBE library version " + release.getVersion().toString() +
" is available. Would you like to download the latest source code and " +
"create an updated JMBE library?";
Alert alert = new Alert(Alert.AlertType.INFORMATION, content, ButtonType.YES, ButtonType.NO);
alert.setResizable(true);
//Workaroud for dialog sizing issue on Windows
alert.getDialogPane().setMinHeight(Region.USE_PREF_SIZE);
alert.setTitle("JMBE Library Update Check");
alert.setHeaderText("Update is available");
alert.initOwner(getCreateButton().getScene().getWindow());
alert.showAndWait().ifPresent(buttonType -> {
if(buttonType == ButtonType.YES)
{
mLog.info("Posting JMBE editor request to event bus");
MyEventBus.getEventBus().post(new JmbeEditorRequest(release));
}
});
}
else
{
Alert alert = new Alert(Alert.AlertType.INFORMATION,
"No JMBE library update is available.", ButtonType.OK);
alert.setTitle("JMBE Library Update Check");
if(release != null)
{
alert.setHeaderText("No update available");
}
else
{
alert.setHeaderText("Unable to determine latest release version. Check network connection");
}
alert.initOwner(getCreateButton().getScene().getWindow());
alert.showAndWait();
}
}
catch(Throwable t)
{
mLog.error("Error during JavaFX portion of create library");
}
}
});
}
catch(Throwable t)
{
mLog.error("Error during create library", t);
}
});
}
private Label getJmbeLibraryLabel()
{
if(mJmbeLibraryLabel == null)
{
mJmbeLibraryLabel = new Label("JMBE Audio Library:");
mJmbeLibraryLabel = new Label("JMBE Audio Library");
}
return mJmbeLibraryLabel;
}
private Button getPathToJmbeLibraryButton()
private Label getJmbeVersionLabel()
{
if(mPathToJmbeLibraryButton == null)
if(mJmbeVersionLabel == null)
{
mPathToJmbeLibraryButton = new Button("Change...");
mPathToJmbeLibraryButton.setOnAction(new EventHandler<ActionEvent>()
mJmbeVersionLabel = new Label();
Version version = mUserPreferences.getJmbeLibraryPreference().getCurrentVersion();
if(version != null)
{
mJmbeVersionLabel.setText(version.toString());
}
else
{
mJmbeVersionLabel.setText(LIBRARY_NOT_SETUP);
}
}
return mJmbeVersionLabel;
}
private Button getSelectButton()
{
if(mSelectButton == null)
{
mSelectButton = new Button("Select...");
mSelectButton.setOnAction(new EventHandler<ActionEvent>()
{
@Override
public void handle(ActionEvent event)
{
FileChooser fileChooser = new FileChooser();
fileChooser.setTitle("Select JMBE Audio Library Location");
Stage stage = (Stage)getPathToJmbeLibraryButton().getScene().getWindow();
Path path = mUserPreferences.getDirectoryPreference().getDefaultJmbeDirectory();
if(Files.exists(path))
{
fileChooser.setInitialDirectory(path.toFile());
}
Stage stage = (Stage)getSelectButton().getScene().getWindow();
File selected = fileChooser.showOpenDialog(stage);
if(selected != null)
{
mJmbeLibraryPreference.setPathJmbeLibrary(selected.toPath());
mUserPreferences.getJmbeLibraryPreference().setPathJmbeLibrary(selected.toPath());
}
}
});
}
return mPathToJmbeLibraryButton;
return mSelectButton;
}
private Button getPathToJmbeLibraryResetButton()
private Button getResetButton()
{
if(mPathToJmbeLibraryResetButton == null)
if(mResetButton == null)
{
mPathToJmbeLibraryResetButton = new Button("Reset");
mPathToJmbeLibraryResetButton.setOnAction(new EventHandler<ActionEvent>()
{
@Override
public void handle(ActionEvent event)
{
mJmbeLibraryPreference.resetPathJmbeLibrary();
}
});
mResetButton = new Button("Reset");
mResetButton.setOnAction(event -> mUserPreferences.getJmbeLibraryPreference().resetPathJmbeLibrary());
}
return mPathToJmbeLibraryResetButton;
return mResetButton;
}
private Label getPathToJmbeLibraryLabel()
{
if(mPathToJmbeLibraryLabel == null)
{
Path path = mJmbeLibraryPreference.getPathJmbeLibrary();
Path path = mUserPreferences.getJmbeLibraryPreference().getPathJmbeLibrary();
mPathToJmbeLibraryLabel = new Label(path != null ? path.toString() : PATH_NOT_SET);
}
@@ -163,8 +340,14 @@ public class JmbeLibraryPreferenceEditor extends HBox
{
if(preferenceType != null && preferenceType == PreferenceType.JMBE_LIBRARY)
{
Path path = mJmbeLibraryPreference.getPathJmbeLibrary();
Path path = mUserPreferences.getJmbeLibraryPreference().getPathJmbeLibrary();
getPathToJmbeLibraryLabel().setText(path != null ? path.toString() : PATH_NOT_SET);
Version version = mUserPreferences.getJmbeLibraryPreference().getCurrentVersion();
getJmbeVersionLabel().setText(version != null ? version.toString() : LIBRARY_NOT_SETUP);
getCreateButton().setText(mUserPreferences.getJmbeLibraryPreference()
.getPathJmbeLibrary() != null ? CHECK_FOR_UPDATE : CREATE_LIBRARY);
getAlertUserWhenMissingCheckBox().setSelected(mUserPreferences.getJmbeLibraryPreference()
.getAlertIfMissingLibraryRequired());
}
}
}

View File

@@ -68,6 +68,11 @@ public class DirectoryPreferenceEditor extends HBox
private Button mResetEventLogsButton;
private Label mEventLogsPathLabel;
private Label mJmbeLabel;
private Button mChangeJmbeButton;
private Button mResetJmbeButton;
private Label mJmbePathLabel;
private Label mPlaylistLabel;
private Button mChangePlaylistButton;
private Button mResetPlaylistButton;
@@ -157,6 +162,19 @@ public class DirectoryPreferenceEditor extends HBox
mEditorPane.add(getResetEventLogsButton(), 3, row++);
GridPane.setMargin(getJmbeLabel(), new Insets(0, 10, 0, 0));
mEditorPane.add(getJmbeLabel(), 0, row);
GridPane.setMargin(getJmbePathLabel(), new Insets(0, 10, 0, 0));
mEditorPane.add(getJmbePathLabel(), 1, row);
GridPane.setMargin(getChangeJmbeButton(), new Insets(2, 10, 2, 0));
mEditorPane.add(getChangeJmbeButton(), 2, row);
GridPane.setMargin(getResetJmbeButton(), new Insets(2, 0, 2, 0));
mEditorPane.add(getResetJmbeButton(), 3, row++);
GridPane.setMargin(getPlaylistLabel(), new Insets(0, 10, 0, 0));
mEditorPane.add(getPlaylistLabel(), 0, row);
@@ -407,6 +425,71 @@ public class DirectoryPreferenceEditor extends HBox
return mEventLogsPathLabel;
}
private Label getJmbeLabel()
{
if(mJmbeLabel == null)
{
mJmbeLabel = new Label("JMBE Libraries");
}
return mJmbeLabel;
}
private Button getChangeJmbeButton()
{
if(mChangeJmbeButton == null)
{
mChangeJmbeButton = new Button("Change...");
mChangeJmbeButton.setOnAction(new EventHandler<ActionEvent>()
{
@Override
public void handle(ActionEvent event)
{
DirectoryChooser directoryChooser = new DirectoryChooser();
directoryChooser.setTitle("Select JMBE Folder");
directoryChooser.setInitialDirectory(mDirectoryPreference.getDirectoryJmbe().toFile());
Stage stage = (Stage)getChangeJmbeButton().getScene().getWindow();
File selected = directoryChooser.showDialog(stage);
if(selected != null)
{
mDirectoryPreference.setDirectoryJmbe(selected.toPath());
}
}
});
}
return mChangeJmbeButton;
}
private Button getResetJmbeButton()
{
if(mResetJmbeButton == null)
{
mResetJmbeButton = new Button("Reset");
mResetJmbeButton.setOnAction(new EventHandler<ActionEvent>()
{
@Override
public void handle(ActionEvent event)
{
mDirectoryPreference.resetDirectoryJmbe();
}
});
}
return mResetJmbeButton;
}
private Label getJmbePathLabel()
{
if(mJmbePathLabel == null)
{
mJmbePathLabel = new Label(mDirectoryPreference.getDirectoryJmbe().toString());
}
return mJmbePathLabel;
}
private Label getPlaylistLabel()
{
if(mPlaylistLabel == null)

View File

@@ -0,0 +1,215 @@
/*
* *****************************************************************************
* Copyright (C) 2014-2020 Dennis Sheirer
*
* This program is free software: you can redistribute it and/or modify
* it under the terms of the GNU General Public License as published by
* the Free Software Foundation, either version 3 of the License, or
* (at your option) any later version.
*
* This program is distributed in the hope that it will be useful,
* but WITHOUT ANY WARRANTY; without even the implied warranty of
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
* GNU General Public License for more details.
*
* You should have received a copy of the GNU General Public License
* along with this program. If not, see <http://www.gnu.org/licenses/>
* ****************************************************************************
*/
package io.github.dsheirer.gui.preference.duplicate;
import io.github.dsheirer.preference.UserPreferences;
import io.github.dsheirer.preference.duplicate.DuplicateCallDetectionPreference;
import javafx.beans.binding.Bindings;
import javafx.geometry.Insets;
import javafx.scene.control.Label;
import javafx.scene.control.Separator;
import javafx.scene.layout.GridPane;
import javafx.scene.layout.HBox;
import javafx.scene.layout.Priority;
import org.controlsfx.control.ToggleSwitch;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
/**
* Preference settings for duplicate call audio handling
*/
public class DuplicateCallPreferenceEditor extends HBox
{
private final static Logger mLog = LoggerFactory.getLogger(DuplicateCallPreferenceEditor.class);
private DuplicateCallDetectionPreference mPreference;
private GridPane mEditorPane;
private ToggleSwitch mDetectDuplicateTalkgroups;
private ToggleSwitch mDetectDuplicateRadios;
private ToggleSwitch mSuppressDuplicateListening;
private ToggleSwitch mSuppressDuplicateRecording;
private ToggleSwitch mSuppressDuplicateStreaming;
/**
* Constructs an instance
*/
public DuplicateCallPreferenceEditor(UserPreferences userPreferences)
{
mPreference = userPreferences.getDuplicateCallDetectionPreference();
HBox.setHgrow(getEditorPane(), Priority.ALWAYS);
getChildren().add(getEditorPane());
}
private GridPane getEditorPane()
{
if(mEditorPane == null)
{
int row = 0;
mEditorPane = new GridPane();
mEditorPane.setPadding(new Insets(10, 10, 10, 10));
mEditorPane.setHgap(10);
mEditorPane.setVgap(10);
Label detectionLabel = new Label("Duplicate Call Detection. Detect duplicate" +
" calls across channels that share a common System name in each channel configuration.");
detectionLabel.setWrapText(true);
GridPane.setConstraints(detectionLabel, 0, row, 2, 1);
mEditorPane.getChildren().add(detectionLabel);
GridPane.setConstraints(getDetectDuplicateTalkgroups(), 0, ++row);
mEditorPane.getChildren().add(getDetectDuplicateTalkgroups());
Label talkgroupLabel = new Label("Talkgroup. Detect duplicate calls by matching talkgroup or " +
"patchgroup values");
talkgroupLabel.setWrapText(true);
GridPane.setConstraints(talkgroupLabel, 1, row);
mEditorPane.getChildren().add(talkgroupLabel);
GridPane.setConstraints(getDetectDuplicateRadios(), 0, ++row);
mEditorPane.getChildren().add(getDetectDuplicateRadios());
Label radioLabel = new Label("Radio ID. Detect duplicate calls by matching radio identifiers.");
radioLabel.setWrapText(true);
GridPane.setConstraints(radioLabel, 1, row);
mEditorPane.getChildren().add(radioLabel);
Label warningLabel = new Label("Note: be careful when enabling duplicate call detection by Radio ID " +
"because this can produce unintended side-effects. For example, if you have two talkgroups with " +
"talkgroup 1 set to record and talkgroup 2 set to stream and dispatch radio ID 1234 makes a " +
"simultaneous call to both talkgroup 1 and talkgroup 2, there is no way to control which call audio " +
"(talkgroup 1 or 2) gets flagged as the duplicate call and therefore either the audio for talkgroup 1 " +
"doesn't record, or the audio for talkgroup 2 doesn't stream.");
warningLabel.setWrapText(true);
GridPane.setConstraints(warningLabel, 0, ++row, 2, 1);
mEditorPane.getChildren().add(warningLabel);
Separator separator = new Separator();
GridPane.setHgrow(separator, Priority.ALWAYS);
GridPane.setConstraints(separator, 0, ++row, 2, 1);
mEditorPane.getChildren().add(separator);
Label suppressionLabel = new Label("Duplicate Call Suppression. When duplicate call audio is detected, " +
"suppress the audio during:");
suppressionLabel.setWrapText(true);
GridPane.setConstraints(suppressionLabel, 0, ++row, 2, 1);
mEditorPane.getChildren().add(suppressionLabel);
GridPane.setConstraints(getSuppressDuplicateListening(), 0, ++row);
mEditorPane.getChildren().add(getSuppressDuplicateListening());
Label listeningLabel = new Label("Listening");
listeningLabel.setWrapText(true);
GridPane.setConstraints(listeningLabel, 1, row);
mEditorPane.getChildren().add(listeningLabel);
GridPane.setConstraints(getSuppressDuplicateRecording(), 0, ++row);
mEditorPane.getChildren().add(getSuppressDuplicateRecording());
Label recordingLabel = new Label("Recording");
recordingLabel.setWrapText(true);
GridPane.setConstraints(recordingLabel, 1, row);
mEditorPane.getChildren().add(recordingLabel);
GridPane.setConstraints(getSuppressDuplicateStreaming(), 0, ++row);
mEditorPane.getChildren().add(getSuppressDuplicateStreaming());
Label streamingLabel = new Label("Streaming");
streamingLabel.setWrapText(true);
GridPane.setConstraints(streamingLabel, 1, row);
mEditorPane.getChildren().add(streamingLabel);
}
return mEditorPane;
}
private ToggleSwitch getDetectDuplicateTalkgroups()
{
if(mDetectDuplicateTalkgroups == null)
{
mDetectDuplicateTalkgroups = new ToggleSwitch();
mDetectDuplicateTalkgroups.setSelected(mPreference.isDuplicateCallDetectionByTalkgroupEnabled());
mDetectDuplicateTalkgroups.selectedProperty()
.addListener((observable, oldValue, newValue) -> mPreference.setDuplicateCallDetectionByTalkgroupEnabled(newValue));
}
return mDetectDuplicateTalkgroups;
}
private ToggleSwitch getDetectDuplicateRadios()
{
if(mDetectDuplicateRadios == null)
{
mDetectDuplicateRadios = new ToggleSwitch();
mDetectDuplicateRadios.setSelected(mPreference.isDuplicateCallDetectionByRadioEnabled());
mDetectDuplicateRadios.selectedProperty()
.addListener((observable, oldValue, newValue) -> mPreference.setDuplicateCallDetectionByRadioEnabled(newValue));
}
return mDetectDuplicateRadios;
}
private ToggleSwitch getSuppressDuplicateListening()
{
if(mSuppressDuplicateListening == null)
{
mSuppressDuplicateListening = new ToggleSwitch();
mSuppressDuplicateListening.disableProperty()
.bind(Bindings.and(getDetectDuplicateTalkgroups().selectedProperty().not(),
getDetectDuplicateRadios().selectedProperty().not()));
mSuppressDuplicateListening.setSelected(mPreference.isDuplicatePlaybackSuppressionEnabled());
mSuppressDuplicateListening.selectedProperty()
.addListener((observable, oldValue, newValue) -> mPreference.setDuplicatePlaybackSuppressionEnabled(newValue));
}
return mSuppressDuplicateListening;
}
private ToggleSwitch getSuppressDuplicateRecording()
{
if(mSuppressDuplicateRecording == null)
{
mSuppressDuplicateRecording = new ToggleSwitch();
mSuppressDuplicateRecording.disableProperty()
.bind(Bindings.and(getDetectDuplicateTalkgroups().selectedProperty().not(),
getDetectDuplicateRadios().selectedProperty().not()));
mSuppressDuplicateRecording.setSelected(mPreference.isDuplicateRecordingSuppressionEnabled());
mSuppressDuplicateRecording.selectedProperty()
.addListener((observable, oldValue, newValue) -> mPreference.setDuplicateRecordingSuppressionEnabled(newValue));
}
return mSuppressDuplicateRecording;
}
private ToggleSwitch getSuppressDuplicateStreaming()
{
if(mSuppressDuplicateStreaming == null)
{
mSuppressDuplicateStreaming = new ToggleSwitch();
mSuppressDuplicateStreaming.disableProperty()
.bind(Bindings.and(getDetectDuplicateTalkgroups().selectedProperty().not(),
getDetectDuplicateRadios().selectedProperty().not()));
mSuppressDuplicateStreaming.setSelected(mPreference.isDuplicateStreamingSuppressionEnabled());
mSuppressDuplicateStreaming.selectedProperty()
.addListener((observable, oldValue, newValue) -> mPreference.setDuplicateStreamingSuppressionEnabled(newValue));
}
return mSuppressDuplicateStreaming;
}
}

View File

@@ -0,0 +1,323 @@
/*
* *****************************************************************************
* Copyright (C) 2014-2020 Dennis Sheirer
*
* This program is free software: you can redistribute it and/or modify
* it under the terms of the GNU General Public License as published by
* the Free Software Foundation, either version 3 of the License, or
* (at your option) any later version.
*
* This program is distributed in the hope that it will be useful,
* but WITHOUT ANY WARRANTY; without even the implied warranty of
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
* GNU General Public License for more details.
*
* You should have received a copy of the GNU General Public License
* along with this program. If not, see <http://www.gnu.org/licenses/>
* ****************************************************************************
*/
package io.github.dsheirer.jmbe;
import io.github.dsheirer.jmbe.github.Asset;
import io.github.dsheirer.jmbe.github.GitHub;
import io.github.dsheirer.jmbe.github.Release;
import io.github.dsheirer.util.FileUtil;
import io.github.dsheirer.util.OSType;
import io.github.dsheirer.util.ThreadPool;
import javafx.application.Platform;
import javafx.beans.property.BooleanProperty;
import javafx.beans.property.SimpleBooleanProperty;
import javafx.beans.property.SimpleStringProperty;
import javafx.beans.property.StringProperty;
import org.apache.commons.io.FileUtils;
import org.rauschig.jarchivelib.ArchiveFormat;
import org.rauschig.jarchivelib.Archiver;
import org.rauschig.jarchivelib.ArchiverFactory;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import java.io.BufferedReader;
import java.io.IOException;
import java.io.InputStreamReader;
import java.nio.file.Files;
import java.nio.file.Path;
import java.nio.file.Paths;
/**
* Creates the JMBE library by downloading the JMBE Creator CLI application from GitHub and executing it.
*/
public class JmbeCreator
{
private final static Logger mLog = LoggerFactory.getLogger(JmbeCreator.class);
public static final String GITHUB_JMBE_RELEASES_URL = "https://api.github.com/repos/dsheirer/jmbe/releases";
public static final String CREATOR_SCRIPT_LINUX = "creator";
public static final String CREATOR_SCRIPT_WINDOWS = "creator.bat";
private StringProperty mConsoleOutput = new SimpleStringProperty();
private BooleanProperty mCompleteProperty = new SimpleBooleanProperty();
private boolean mHasErrors = false;
private Release mRelease;
private Path mLibraryPath;
private StringBuilder mConsoleStringBuilder = new StringBuilder();
/**
* Creates an instance.
* @param release for the version to create
* @param library path where the library should be created
*/
public JmbeCreator(Release release, Path library)
{
mRelease = release;
mLibraryPath = library;
}
/**
* Path to where the library should be created
*/
public Path getLibraryPath()
{
return mLibraryPath;
}
/**
* Property that indicates if the creation process has finished.
*/
public BooleanProperty completeProperty()
{
return mCompleteProperty;
}
/**
* Console output from the build process.
*/
public StringProperty consoleOutputProperty()
{
return mConsoleOutput;
}
/**
* Prints (adds) the message text to the console property
* @param message to add
*/
private void printToConsole(String message)
{
mConsoleStringBuilder.append(message).append("\n");
final String console = mConsoleStringBuilder.toString();
Platform.runLater(() -> consoleOutputProperty().setValue(console));
}
/**
* Indicates if the build process completed successfully (false) or there were errors (true).
*/
public boolean hasErrors()
{
return mHasErrors;
}
/**
* Executes the build process that downloads the JMBE creator utility, unzips it, and commands the utility
* to create the library at the specified library path.
*/
public void execute()
{
final Asset asset = getJMBECreatorAsset(mRelease);
if(asset != null)
{
ThreadPool.SCHEDULED.execute(() -> {
Path tempDirectory = null;
try
{
tempDirectory = Files.createTempDirectory("sdrtrunk-jmbe-creator");
printToConsole("Created: Temp Directory [" + tempDirectory.toString() + "]");
printToConsole("Downloading: JMBE Creator [" + asset.toString() + "]");
printToConsole("Please wait ...");
Path creator = GitHub.downloadArtifact(asset.getDownloadUrl(), tempDirectory);
if(creator != null)
{
printToConsole("Downloaded: JMBE Creator [" + creator.toString() + "]");
Path unzipped = creator.getParent();
Archiver archiver = ArchiverFactory.createArchiver(ArchiveFormat.ZIP);
archiver.extract(creator.toFile(), unzipped.toFile());
printToConsole("Unzipped: [" + unzipped.toString() + "]");
Path script = null;
OSType osType = OSType.getCurrentOSType();
if(osType.isLinux())
{
script = FileUtil.findFile(unzipped, CREATOR_SCRIPT_LINUX);
}
else if(osType.isWindows())
{
script = FileUtil.findFile(unzipped, CREATOR_SCRIPT_WINDOWS);
}
if(script != null)
{
ProcessBuilder processBuilder = new ProcessBuilder();
processBuilder.command(script.toString(), mLibraryPath.toString());
try
{
Process process = processBuilder.start();
BufferedReader reader = new BufferedReader(new InputStreamReader(process.getInputStream()));
String line;
while((line = reader.readLine()) != null)
{
printToConsole(line);
}
int exitCode = process.waitFor();
if(exitCode == 0)
{
printToConsole("Library Created Successfully!");
Platform.runLater(() -> completeProperty().set(true));
}
else
{
terminateWithErrors("Failed: Exit Code [" + exitCode + "]");
mLog.error("Script failed with exit code: " + exitCode);
}
}
catch(InterruptedException ie)
{
terminateWithErrors("Failed: Script Process was interrupted");
mLog.error("Interrupted", ie);
}
}
else
{
terminateWithErrors("Failed: Unable to find JMBE creator launch script for this OS");
mLog.error("Script was null. Unable to find JMBE creator launch script");
}
}
}
catch(Throwable t)
{
terminateWithErrors("Failed: Unknown Error - " + t.getLocalizedMessage());
mLog.error("Failed to create the JMBE library", t);
}
finally
{
if(tempDirectory != null)
{
try
{
FileUtils.deleteDirectory(tempDirectory.toFile());
printToConsole("Deleted: Temporary Directory [" + tempDirectory.toString() + "]");
}
catch(IOException ioe)
{
printToConsole("Failed: Deleting Temporary Directory [" + tempDirectory.toString() + "]");
mLog.error("Error deleting temporary directory [" + tempDirectory.toString() + "]");
}
}
}
});
}
else
{
terminateWithErrors("Failed: Unable to identify correct JMBE creator utility from GitHub " +
"repository for this computer's operating system and architecture");
mLog.error("Unable to create JMBE library. Can't find JMBE Creator for this OS and architecture");
}
}
/**
* Terminates the execution and updates flags to indicate error state
* @param message to display as console output
*/
private void terminateWithErrors(String message)
{
printToConsole(message);
printToConsole("Please follow the instructions for manually creating and installing the JMBE library");
Platform.runLater(() -> {
mHasErrors = true;
mCompleteProperty.set(true);
});
}
/**
* Attempts to find the correct JMBE creator for this operating system and architecture for the specified release
* @param release to find
* @return JMBE creator asset or null.
*/
public static Asset getJMBECreatorAsset(Release release)
{
OSType osType = OSType.getCurrentOSType();
if(release != null)
{
for(Asset asset: release.getAssets())
{
if(isCorrectAsset(asset, osType))
{
return asset;
}
}
}
return null;
}
/**
* Indicates if the asset is correct for the host operating system and architecture
* @param asset to check
* @param osType for the current host (OS & architecture)
* @return true if the asset is correct for this host.
*/
public static boolean isCorrectAsset(Asset asset, OSType osType)
{
if(isJMBECreator(asset))
{
String name = asset.getName();
switch(osType)
{
case LINUX_32:
return name.contains("linux") && name.contains("_32");
case LINUX_64:
return name.contains("linux") && name.contains("_64");
case OSX_64:
return name.contains("osx") && name.contains("_64");
case WINDOWS_32:
return name.contains("windows") && name.contains("_32");
case WINDOWS_64:
return name.contains("windows") && name.contains("_64");
case UNKNOWN:
case OSX_32:
default:
return false;
}
}
return false;
}
/**
* Indicates if the GitHub asset has a non-null asset name and is a JMBE Creator asset
*/
private static boolean isJMBECreator(Asset asset)
{
return asset.getName() != null && asset.getName().startsWith("jmbe-creator");
}
public static void main(String[] args)
{
Release release = GitHub.getLatestRelease(GITHUB_JMBE_RELEASES_URL);
Path library = Paths.get("/home/denny/temp/jmbe.jar");
JmbeCreator jmbeCreator = new JmbeCreator(release, library);
jmbeCreator.execute();
while(true);
}
}

View File

@@ -0,0 +1,327 @@
/*
* *****************************************************************************
* Copyright (C) 2014-2020 Dennis Sheirer
*
* This program is free software: you can redistribute it and/or modify
* it under the terms of the GNU General Public License as published by
* the Free Software Foundation, either version 3 of the License, or
* (at your option) any later version.
*
* This program is distributed in the hope that it will be useful,
* but WITHOUT ANY WARRANTY; without even the implied warranty of
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
* GNU General Public License for more details.
*
* You should have received a copy of the GNU General Public License
* along with this program. If not, see <http://www.gnu.org/licenses/>
* ****************************************************************************
*/
package io.github.dsheirer.jmbe;
import io.github.dsheirer.eventbus.MyEventBus;
import io.github.dsheirer.jmbe.github.Release;
import io.github.dsheirer.jmbe.github.Version;
import io.github.dsheirer.preference.UserPreferences;
import javafx.geometry.HPos;
import javafx.geometry.Insets;
import javafx.scene.control.Alert;
import javafx.scene.control.Button;
import javafx.scene.control.ButtonType;
import javafx.scene.control.Label;
import javafx.scene.control.Separator;
import javafx.scene.control.TextArea;
import javafx.scene.layout.GridPane;
import javafx.scene.layout.Priority;
import javafx.scene.layout.Region;
import javafx.scene.layout.VBox;
import javafx.stage.DirectoryChooser;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import java.io.File;
import java.nio.file.Path;
/**
* Editor pane for JMBE Library creator
*/
public class JmbeEditor extends VBox
{
private final static Logger mLog = LoggerFactory.getLogger(JmbeEditor.class);
private UserPreferences mUserPreferences;
private Release mCurrentRelease;
private Label mCurrentVersionLabel;
private Label mUpdatedVersionLabel;
private Label mLibraryPathLabel;
private Button mChangeDirectoryButton;
private GridPane mLabelGridPane;
private Path mLibraryDirectoryPath;
private Button mCreateButton;
private TextArea mConsoleTextArea;
/**
* Constructs an instance
* @param userPreferences
*/
public JmbeEditor(UserPreferences userPreferences)
{
mUserPreferences = userPreferences;
setPadding(new Insets(10,10,10,10));
setSpacing(10);
Separator separator = new Separator();
separator.setMaxWidth(Double.MAX_VALUE);
VBox.setVgrow(getConsoleTextArea(), Priority.ALWAYS);
getChildren().addAll(getLabelGridPane(), separator, getCreateButton(), getConsoleTextArea());
}
/**
* Processes a request to configure the editor to build the release version specified in the request.
* @param request to build a specific release version
*/
public void process(JmbeEditorRequest request)
{
if(request != null)
{
setCurrentRelease(request.getCurrentRelease());
}
}
private TextArea getConsoleTextArea()
{
if(mConsoleTextArea == null)
{
mConsoleTextArea = new TextArea();
mConsoleTextArea.setMaxHeight(Double.MAX_VALUE);
mConsoleTextArea.setMaxWidth(Double.MAX_VALUE);
}
return mConsoleTextArea;
}
private Button getCreateButton()
{
if(mCreateButton == null)
{
mCreateButton = new Button("Create Library");
mCreateButton.setOnAction(event -> {
String noticeText = "The JMBE library is available as source code for educational purposes " +
"only. It is a written description of how certain voice encoding/decoding algorithms " +
"could be implemented. Executable objects compiled or derived from this package may " +
"be covered by one or more patents. Users are strongly advised to check for any " +
"patent restrictions or licensing requirements before compiling or using this " +
"source code.\n\nClicking the YES button indicates that you are requesting to download " +
"the JMBE source code and compile the JMBE library.";
Alert alert = new Alert(Alert.AlertType.WARNING, noticeText, ButtonType.YES, ButtonType.NO);
alert.setResizable(true);
//Workaroud for dialog sizing issue on Windows
alert.getDialogPane().setMinHeight(Region.USE_PREF_SIZE);
alert.setTitle("Download and Compile JMBE Library?");
alert.setHeaderText("Do you want to continue?");
alert.initOwner(getCreateButton().getScene().getWindow());
alert.showAndWait().ifPresent(buttonType -> {
if(buttonType == ButtonType.YES)
{
Path library = getLibraryDirectoryPath().resolve(getJarName(mCurrentRelease.getVersion()));
getCreateButton().setDisable(true);
createJmbeLibrary(library);
}
});
});
}
return mCreateButton;
}
/**
* Downloads the JMBE creator and creates the library using the specified path argument.
* @param library path
*/
private void createJmbeLibrary(Path library)
{
JmbeCreator jmbeCreator = new JmbeCreator(mCurrentRelease, library);
getConsoleTextArea().textProperty().bind(jmbeCreator.consoleOutputProperty());
jmbeCreator.completeProperty().addListener((observable, oldValue, newValue) -> {
getCreateButton().setDisable(false);
boolean failed = jmbeCreator.hasErrors();
String content = null;
if(failed)
{
content = "JMBE library creation attempt failed. Please update the library manually by " +
"downloading the JMBE creator application.";
}
else
{
content = "JMBE library successfully created/updated";
mUserPreferences.getJmbeLibraryPreference().setPathJmbeLibrary(jmbeCreator.getLibraryPath());
getCurrentVersionLabel().setText(mCurrentRelease.getVersion().toString());
}
Alert alert = new Alert(Alert.AlertType.INFORMATION, content, ButtonType.OK);
alert.setTitle("JMBE Library Creator");
alert.setHeaderText((failed ? "Attempt failed." : "Success!") + " Please click OK to close this window");
alert.initOwner(getCreateButton().getScene().getWindow());
alert.showAndWait().ifPresent(buttonType -> {
jmbeCreator.completeProperty().unbind();
MyEventBus.getEventBus().post(new JmbeEditorRequest(true));
});
});
jmbeCreator.execute();
}
/**
* Creates a jar name for the specified release version
* @param version of the GitHub JMBE release
* @return JMBE library jar name
*/
public static String getJarName(Version version)
{
String name = version.toString();
name = name.replace("v", "");
return "jmbe-" + name + ".jar";
}
private GridPane getLabelGridPane()
{
if(mLabelGridPane == null)
{
mLabelGridPane = new GridPane();
mLabelGridPane.setHgap(10);
mLabelGridPane.setVgap(10);
int row = 0;
Label topLabel = new Label("Create or Update JMBE Library");
mLabelGridPane.add(topLabel, 0, row, 2, 1);
Label currentLabel = new Label("Current Version:");
GridPane.setHalignment(currentLabel, HPos.RIGHT);
mLabelGridPane.add(currentLabel, 0, ++row);
mLabelGridPane.add(getCurrentVersionLabel(), 1, row);
Label updateLabel = new Label("Updating To Version:");
GridPane.setHalignment(updateLabel, HPos.RIGHT);
mLabelGridPane.add(updateLabel, 0, ++row);
mLabelGridPane.add(getUpdatedVersionLabel(), 1, row);
Label pathLabel = new Label("Store Library At:");
GridPane.setHalignment(pathLabel, HPos.RIGHT);
mLabelGridPane.add(pathLabel, 0, ++row);
mLabelGridPane.add(getLibraryPathLabel(), 1, row);
mLabelGridPane.add(getChangeDirectoryButton(), 1, ++row);
}
return mLabelGridPane;
}
private Path getLibraryDirectoryPath()
{
if(mLibraryDirectoryPath == null)
{
mLibraryDirectoryPath = mUserPreferences.getDirectoryPreference().getDefaultJmbeDirectory();
}
return mLibraryDirectoryPath;
}
private Button getChangeDirectoryButton()
{
if(mChangeDirectoryButton == null)
{
mChangeDirectoryButton = new Button("Change Directory ...");
mChangeDirectoryButton.setOnAction(event -> {
DirectoryChooser chooser = new DirectoryChooser();
chooser.setInitialDirectory(getLibraryDirectoryPath().toFile());
chooser.setTitle("JMBE Library Directory");
File newDirectory = chooser.showDialog(getChangeDirectoryButton().getScene().getWindow());
setLibraryDirectoryPath(newDirectory.toPath());
});
}
return mChangeDirectoryButton;
}
/**
* Sets the library path and updates the display label
*/
private void setLibraryDirectoryPath(Path path)
{
if(path != null)
{
mLibraryDirectoryPath = path;
getLibraryPathLabel().setText(mLibraryDirectoryPath.toString());
}
}
private Label getLibraryPathLabel()
{
if(mLibraryPathLabel == null)
{
mLibraryPathLabel = new Label();
if(getLibraryDirectoryPath() != null)
{
mLibraryPathLabel.setText(getLibraryDirectoryPath().toString());
}
}
return mLibraryPathLabel;
}
private Label getCurrentVersionLabel()
{
if(mCurrentVersionLabel == null)
{
mCurrentVersionLabel = new Label("(empty)");
Version currentVersion = mUserPreferences.getJmbeLibraryPreference().getCurrentVersion();
if(currentVersion != null)
{
mCurrentVersionLabel.setText(currentVersion.toString());
}
}
return mCurrentVersionLabel;
}
private Label getUpdatedVersionLabel()
{
if(mUpdatedVersionLabel == null)
{
mUpdatedVersionLabel = new Label("(empty)");
}
return mUpdatedVersionLabel;
}
/**
* Current release version available from GitHub.
* @return current release or null if the release hasn't been set.
*/
private Release getCurrentRelease()
{
return mCurrentRelease;
}
/**
* Sets the current release version available from GitHub.
* @param release version
*/
public void setCurrentRelease(Release release)
{
mCurrentRelease = release;
if(release != null)
{
Version updateVersion = release.getVersion();
if(updateVersion != null)
{
getUpdatedVersionLabel().setText(updateVersion.toString());
}
}
}
}

View File

@@ -0,0 +1,65 @@
/*
* *****************************************************************************
* Copyright (C) 2014-2020 Dennis Sheirer
*
* This program is free software: you can redistribute it and/or modify
* it under the terms of the GNU General Public License as published by
* the Free Software Foundation, either version 3 of the License, or
* (at your option) any later version.
*
* This program is distributed in the hope that it will be useful,
* but WITHOUT ANY WARRANTY; without even the implied warranty of
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
* GNU General Public License for more details.
*
* You should have received a copy of the GNU General Public License
* along with this program. If not, see <http://www.gnu.org/licenses/>
* ****************************************************************************
*/
package io.github.dsheirer.jmbe;
import io.github.dsheirer.jmbe.github.Release;
/**
* Request to create a JMBE library from the specified GitHub release version
*/
public class JmbeEditorRequest
{
private Release mCurrentRelease;
private boolean mCloseEditor = false;
/**
* Constructs an instance to request an edit of the specified release
* @param release available on GitHub
*/
public JmbeEditorRequest(Release release)
{
mCurrentRelease = release;
}
/**
* Constructs a request to close and dispose of the editor
* @param close
*/
public JmbeEditorRequest(boolean close)
{
mCloseEditor = close;
}
/**
* Indicates if this is a close (and dispose) editor request
*/
public boolean isCloseEditorRequest()
{
return mCloseEditor;
}
/**
* Release version requested
*/
public Release getCurrentRelease()
{
return mCurrentRelease;
}
}

View File

@@ -0,0 +1,75 @@
/*
* *****************************************************************************
* Copyright (C) 2014-2020 Dennis Sheirer
*
* This program is free software: you can redistribute it and/or modify
* it under the terms of the GNU General Public License as published by
* the Free Software Foundation, either version 3 of the License, or
* (at your option) any later version.
*
* This program is distributed in the hope that it will be useful,
* but WITHOUT ANY WARRANTY; without even the implied warranty of
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
* GNU General Public License for more details.
*
* You should have received a copy of the GNU General Public License
* along with this program. If not, see <http://www.gnu.org/licenses/>
* ****************************************************************************
*/
package io.github.dsheirer.jmbe.github;
import com.google.gson.JsonElement;
import com.google.gson.JsonObject;
/**
* GitHub Asset
*/
public class Asset
{
private JsonObject mJsonObject;
public Asset(JsonObject jsonObject)
{
mJsonObject = jsonObject;
}
public String getName()
{
return getTag("name");
}
/**
* URL to download this asset
*/
public String getDownloadUrl()
{
return getTag("browser_download_url");
}
@Override
public String toString()
{
return getName();
}
/**
* Access the string contents of a json tag
* @param tagName to access
* @return string value or null
*/
private String getTag(String tagName)
{
if(mJsonObject != null)
{
JsonElement element = mJsonObject.get(tagName);
if(!element.isJsonNull())
{
return element.toString().replace("\"", "");
}
}
return null;
}
}

View File

@@ -0,0 +1,175 @@
/*
* ******************************************************************************
* Copyright (C) 2015-2020 Dennis Sheirer
*
* This program is free software: you can redistribute it and/or modify
* it under the terms of the GNU General Public License as published by
* the Free Software Foundation, either version 3 of the License, or
* (at your option) any later version.
*
* This program is distributed in the hope that it will be useful,
* but WITHOUT ANY WARRANTY; without even the implied warranty of
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
* GNU General Public License for more details.
*
* You should have received a copy of the GNU General Public License
* along with this program. If not, see <http://www.gnu.org/licenses/>
* *****************************************************************************
*/
package io.github.dsheirer.jmbe.github;
import com.google.gson.JsonArray;
import com.google.gson.JsonElement;
import com.google.gson.JsonObject;
import com.google.gson.JsonParser;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import java.io.IOException;
import java.net.URI;
import java.net.http.HttpClient;
import java.net.http.HttpRequest;
import java.net.http.HttpResponse;
import java.nio.file.Path;
import java.nio.file.StandardOpenOption;
/**
* Utilities for accessing GitHub repositories and releases
*/
public class GitHub
{
private final static Logger mLog = LoggerFactory.getLogger(GitHub.class);
private GitHub()
{
//There's nothing to construct in this class
}
/**
* Download an artifact to the specified directory
* @param url of the artifact to download
* @param directory to store the downloaded file
* @return path to the downloaded artifact or null
*/
public static Path downloadArtifact(String url, Path directory)
{
HttpClient client = HttpClient.newBuilder()
.followRedirects(HttpClient.Redirect.ALWAYS)
.build();
HttpRequest request = HttpRequest.newBuilder().uri(URI.create(url)).build();
try
{
HttpResponse<Path> response = client.send(request, HttpResponse.BodyHandlers.ofFileDownload(directory,
StandardOpenOption.CREATE, StandardOpenOption.WRITE));
if(response.statusCode() != 200)
{
mLog.error("HTTP Download Status Code: " + response.statusCode());
}
return response.body();
}
catch(Exception e)
{
mLog.error("Error downloading source code from GitHub", e);
}
return null;
}
/**
* Obtain the latest release object from the repository.
* @param repositoryURL for the GitHub api
* @return the latest release or null
*/
public static Release getLatestRelease(String repositoryURL)
{
HttpClient httpClient = HttpClient.newHttpClient();
HttpRequest request = HttpRequest.newBuilder().uri(URI.create(repositoryURL)).build();
try
{
HttpResponse<String> response = httpClient.send(request, HttpResponse.BodyHandlers.ofString());
if(response.statusCode() == 200)
{
return parseResponse(response.body());
}
else
{
mLog.error("Error while fetching latest releases - HTTP:" + response.statusCode());
}
}
catch(IOException | InterruptedException e)
{
mLog.error("Error while detecting the current release version of JMBE library", e);
}
return null;
}
/**
* Parses a json string containing an array or GitHub release objects and returns the latest release.
* @param json string returned from URL call
* @return latest release version
*/
private static Release parseResponse(String json)
{
Release release = null;
if(json != null)
{
JsonElement element = JsonParser.parseString(json);
if(element.isJsonArray())
{
JsonArray array = element.getAsJsonArray();
for(JsonElement child: array)
{
if(child.isJsonObject())
{
JsonObject releaseObject = child.getAsJsonObject();
Version version = getVersion(releaseObject);
if(version != null)
{
if(release == null || release.getVersion().compareTo(version) < 0)
{
release = new Release(version, releaseObject);
}
}
}
}
}
}
return release;
}
/**
* Extracts a version object from the tag_name.
* @param jsonObject containing a tag_name formatted as: vAAAAA.BBBBB.CCCCx where:
* A = major, 0-99999
* B = minor, 0-99999
* C = release, 0-99999
* x = (optional)patch, a-z
*
* @return version instance or null
*/
private static Version getVersion(JsonObject jsonObject)
{
JsonElement tagName = jsonObject.get("tag_name");
if(tagName != null)
{
return Version.fromString(tagName.toString());
}
return null;
}
}

View File

@@ -0,0 +1,159 @@
/*
* *****************************************************************************
* Copyright (C) 2014-2020 Dennis Sheirer
*
* This program is free software: you can redistribute it and/or modify
* it under the terms of the GNU General Public License as published by
* the Free Software Foundation, either version 3 of the License, or
* (at your option) any later version.
*
* This program is distributed in the hope that it will be useful,
* but WITHOUT ANY WARRANTY; without even the implied warranty of
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
* GNU General Public License for more details.
*
* You should have received a copy of the GNU General Public License
* along with this program. If not, see <http://www.gnu.org/licenses/>
* ****************************************************************************
*/
package io.github.dsheirer.jmbe.github;
import com.google.gson.JsonArray;
import com.google.gson.JsonElement;
import com.google.gson.JsonObject;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import java.util.ArrayList;
import java.util.List;
/**
* GitHub release
*/
public class Release
{
private final static Logger mLog = LoggerFactory.getLogger(Release.class);
private Version mVersion;
private JsonObject mJsonObject;
/**
* Constructs an instance
* @param version of the release
* @param jsonObject that represents the release
*/
public Release(Version version, JsonObject jsonObject)
{
mVersion = version;
mJsonObject = jsonObject;
}
/**
* Version stamp for the release
*/
public Version getVersion()
{
return mVersion;
}
/**
* JsonObject for the release
*/
public JsonObject getJsonObject()
{
return mJsonObject;
}
/**
* Name of the release
*/
public String getName()
{
return getTag("name");
}
/**
* Assets URL
*/
public String getAssetsUrl()
{
return getTag("assets_url");
}
public List<Asset> getAssets()
{
List<Asset> assets = new ArrayList<>();
JsonArray array = mJsonObject.getAsJsonArray("assets");
if(!array.isJsonNull())
{
for(JsonElement element: array)
{
assets.add(new Asset(element.getAsJsonObject()));
}
}
return assets;
}
/**
* HTML URL
*/
public String getHtmlUrl()
{
return getTag("html_url");
}
/**
* Zip File Download URL
*/
public String getDownloadUrl()
{
return getTag("zipball_url");
}
/**
* Access the string contents of a json tag
* @param tagName to access
* @return string value or null
*/
private String getTag(String tagName)
{
if(mJsonObject != null)
{
JsonElement element = mJsonObject.get(tagName);
if(!element.isJsonNull())
{
return element.toString().replace("\"", "");
}
}
return null;
}
/**
* Indicates if the release has a download URL
*/
public boolean hasDownloadUrl()
{
return getDownloadUrl() != null;
}
public String toString()
{
StringBuilder sb = new StringBuilder();
sb.append("\nName:").append(getName());
sb.append("\nVersion: ").append(getVersion().toString());
sb.append("\nHTML URL:").append(getHtmlUrl());
sb.append("\nDownload URL:").append(getDownloadUrl());
for(Asset asset: getAssets())
{
sb.append("\nAsset: " + asset.toString());
}
return sb.toString();
}
}

View File

@@ -0,0 +1,226 @@
/*
* *****************************************************************************
* Copyright (C) 2014-2020 Dennis Sheirer
*
* This program is free software: you can redistribute it and/or modify
* it under the terms of the GNU General Public License as published by
* the Free Software Foundation, either version 3 of the License, or
* (at your option) any later version.
*
* This program is distributed in the hope that it will be useful,
* but WITHOUT ANY WARRANTY; without even the implied warranty of
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
* GNU General Public License for more details.
*
* You should have received a copy of the GNU General Public License
* along with this program. If not, see <http://www.gnu.org/licenses/>
* ****************************************************************************
*/
package io.github.dsheirer.jmbe.github;
import java.util.regex.Matcher;
import java.util.regex.Pattern;
/**
* Version representation and parsing class
*/
public class Version implements Comparable<Version>
{
public final static Pattern VERSION_PATTERN = Pattern.compile("v?(\\d{1,5}).(\\d{1,5}).(\\d{1,5})(\\w*)");
private Integer mMajor;
private Integer mMinor;
private Integer mRelease;
private Character mPatch;
/**
* Constructs an instance
* @param major version
* @param minor version
* @param release version
* @param patch version (optional)
*/
public Version(int major, int minor, int release, Character patch)
{
mMajor = major;
mMinor = minor;
mRelease = release;
mPatch = patch;
}
/**
* Parses the argument into a version instance.
* @param version string (e.g. v1.0.6a)
* @return
*/
public static Version fromString(String version)
{
if(version != null)
{
Matcher m = VERSION_PATTERN.matcher(version.replace("\"", ""));
if(m.matches())
{
int major = 0;
int minor = 0;
int release = 0;
Character patch = null;
try
{
major = Integer.parseInt(m.group(1));
}
catch(Exception e)
{
//Do nothing, we couldn't parse the value
}
try
{
minor = Integer.parseInt(m.group(2));
}
catch(Exception e)
{
//Do nothing, we couldn't parse the value
}
try
{
release = Integer.parseInt(m.group(3));
}
catch(Exception e)
{
//Do nothing, we couldn't parse the value
}
String rawPatch = m.group(4);
if(rawPatch != null && rawPatch.length() >= 1)
{
patch = rawPatch.charAt(0);
}
return new Version(major, minor, release, patch);
}
}
return null;
}
public Integer getMajor()
{
return mMajor;
}
public boolean hasMajor()
{
return mMajor != null;
}
public Integer getMinor()
{
return mMinor;
}
public boolean hasMinor()
{
return mMinor != null;
}
public Integer getRelease()
{
return mRelease;
}
public boolean hasRelease()
{
return mRelease != null;
}
public char getPatch()
{
return mPatch;
}
public boolean hasPatch()
{
return mPatch != null;
}
public String toString()
{
StringBuilder sb = new StringBuilder();
sb.append(hasMajor() ? getMajor() : "x");
sb.append(".").append(hasMinor() ? getMinor() : "x");
sb.append(".").append(hasRelease() ? getRelease() : "x");
if(hasPatch())
{
sb.append(mPatch);
}
return sb.toString();
}
@Override
public int compareTo(Version other)
{
if(hasMajor() && other.hasMajor())
{
if(getMajor() != other.getMajor())
{
return Integer.compare(getMajor(), other.getMajor());
}
else
{
if(hasMinor() && other.hasMinor())
{
if(getMinor() != other.getMinor())
{
return Integer.compare(getMinor(), other.getMinor());
}
else
{
if(hasRelease() && other.hasRelease())
{
if(getRelease() != other.getRelease())
{
return Integer.compare(getRelease(), other.getRelease());
}
else
{
if(hasPatch() && other.hasPatch())
{
if(getPatch() != other.getPatch())
{
return Character.compare(getPatch(), other.getPatch());
}
else
{
return 0;
}
}
else
{
return hasPatch() ? 1 : -1;
}
}
}
else
{
return hasRelease() ? -1 : 1;
}
}
}
else
{
return hasMinor() ? -1 : 1;
}
}
}
else
{
return hasMajor() ? -1 : 1;
}
}
}

View File

@@ -226,24 +226,30 @@ public class DecoderFactory
ChannelMap channelMap = channelMapModel.getChannelMap(mptConfig.getChannelMapName());
Sync sync = mptConfig.getSync();
modules.add(new MPT1327Decoder(sync));
modules.add(new AudioModule(aliasList));
final int callTimeoutMilliseconds = mptConfig.getCallTimeoutSeconds() * 1000;
// Set max segment audio sample length slightly above call timeout to
// not create a new segment if the processing chain finishes a bit after
// actual call timeout.
int maxAudioSegmentLengthMillis = (callTimeoutMilliseconds + 5000);
modules.add(new AudioModule(aliasList, maxAudioSegmentLengthMillis));
SourceType sourceType = channel.getSourceConfiguration().getSourceType();
if(sourceType == SourceType.TUNER || sourceType == SourceType.TUNER_MULTIPLE_FREQUENCIES)
{
modules.add(new FMDemodulatorModule(FM_CHANNEL_BANDWIDTH, DEMODULATED_AUDIO_SAMPLE_RATE));
}
long timeout = mptConfig.getCallTimeout() * 1000;
if(channelType == ChannelType.STANDARD)
{
MPT1327TrafficChannelManager trafficChannelManager = new MPT1327TrafficChannelManager(channel, channelMap);
modules.add(trafficChannelManager);
modules.add(new MPT1327DecoderState(trafficChannelManager, channelType, timeout));
modules.add(new MPT1327DecoderState(trafficChannelManager, channelType, callTimeoutMilliseconds));
}
else
{
modules.add(new MPT1327DecoderState(channelType, timeout));
modules.add(new MPT1327DecoderState(channelType, callTimeoutMilliseconds));
}
//Add a channel rotation monitor when we have multiple control channel frequencies specified
@@ -479,7 +485,7 @@ public class DecoderFactory
case MPT1327:
DecodeConfigMPT1327 originalMPT = (DecodeConfigMPT1327)config;
DecodeConfigMPT1327 copyMPT = new DecodeConfigMPT1327();
copyMPT.setCallTimeout(originalMPT.getCallTimeout());
copyMPT.setCallTimeoutSeconds(originalMPT.getCallTimeoutSeconds());
copyMPT.setChannelMapName(originalMPT.getChannelMapName());
copyMPT.setSync(originalMPT.getSync());
copyMPT.setTrafficChannelPoolSize(originalMPT.getTrafficChannelPoolSize());

View File

@@ -55,7 +55,7 @@ public abstract class DecodeConfiguration extends Configuration
{
public static final int DEFAULT_CALL_TIMEOUT_DELAY_SECONDS = 45;
public static final int CALL_TIMEOUT_MINIMUM = 1;
public static final int CALL_TIMEOUT_MAXIMUM = 300; //5 minutes
public static final int CALL_TIMEOUT_MAXIMUM = 180;
public static final int TRAFFIC_CHANNEL_LIMIT_DEFAULT = 3;
public static final int TRAFFIC_CHANNEL_LIMIT_MINIMUM = 0;

View File

@@ -0,0 +1,34 @@
/*
*
* * ******************************************************************************
* * Copyright (C) 2014-2020 Dennis Sheirer
* *
* * This program is free software: you can redistribute it and/or modify
* * it under the terms of the GNU General Public License as published by
* * the Free Software Foundation, either version 3 of the License, or
* * (at your option) any later version.
* *
* * This program is distributed in the hope that it will be useful,
* * but WITHOUT ANY WARRANTY; without even the implied warranty of
* * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
* * GNU General Public License for more details.
* *
* * You should have received a copy of the GNU General Public License
* * along with this program. If not, see <http://www.gnu.org/licenses/>
* * *****************************************************************************
*
*
*/
package io.github.dsheirer.module.decode.config;
/**
* Trait of decoder configurations that support providing information about call timeout.
*/
public interface WithCallTimeout
{
/**
* @return call timeout in seconds
*/
int getCallTimeoutSeconds();
}

View File

@@ -25,14 +25,15 @@ import com.fasterxml.jackson.annotation.JsonIgnore;
import com.fasterxml.jackson.dataformat.xml.annotation.JacksonXmlProperty;
import io.github.dsheirer.module.decode.DecoderType;
import io.github.dsheirer.module.decode.config.DecodeConfiguration;
import io.github.dsheirer.module.decode.config.WithCallTimeout;
import io.github.dsheirer.source.tuner.channel.ChannelSpecification;
public class DecodeConfigMPT1327 extends DecodeConfiguration
public class DecodeConfigMPT1327 extends DecodeConfiguration implements WithCallTimeout
{
private String mChannelMapName;
private Sync mSync = Sync.NORMAL;
private int mCallTimeout = DEFAULT_CALL_TIMEOUT_DELAY_SECONDS;
private int mCallTimeoutSeconds = DEFAULT_CALL_TIMEOUT_DELAY_SECONDS;
private int mTrafficChannelPoolSize = TRAFFIC_CHANNEL_LIMIT_DEFAULT;
public DecodeConfigMPT1327()
@@ -68,25 +69,25 @@ public class DecodeConfigMPT1327 extends DecodeConfiguration
}
@JacksonXmlProperty(isAttribute = true, localName = "call_timeout")
public int getCallTimeout()
public int getCallTimeoutSeconds()
{
return mCallTimeout;
return mCallTimeoutSeconds;
}
/**
* Sets the call timeout value in seconds ( 10 - 600 );
* Sets the call timeout value in seconds ( 1 - 180 );
*
* @param timeout
*/
public void setCallTimeout(int timeout)
public void setCallTimeoutSeconds(int timeout)
{
if(CALL_TIMEOUT_MINIMUM <= timeout && timeout <= CALL_TIMEOUT_MAXIMUM)
{
mCallTimeout = timeout;
mCallTimeoutSeconds = timeout;
}
else
{
mCallTimeout = DEFAULT_CALL_TIMEOUT_DELAY_SECONDS;
mCallTimeoutSeconds = DEFAULT_CALL_TIMEOUT_DELAY_SECONDS;
}
}

View File

@@ -42,7 +42,6 @@ import org.slf4j.LoggerFactory;
import java.util.ArrayList;
import java.util.Collections;
import java.util.HashMap;
import java.util.Iterator;
import java.util.List;
import java.util.TreeSet;
@@ -59,26 +58,30 @@ public class MPT1327DecoderState extends DecoderState
private ChannelType mChannelType;
private long mFrequency = 0;
private long mCallTimeout;
private long mCallTimeoutMilliseconds;
private MPT1327TrafficChannelManager mMPT1327TrafficChannelManager;
/**
* Constructs an MPT-1327 decoder state with an optional traffic channel manager for allocating traffic channels
* when go to channel (GTC) messages are detected.
* when go to channel (GTC) messages are detected. This constructor assumes that channel type is STANDARD.
*
* @param trafficChannelManager for managing MPT1327 Traffic Channels
* @param callTimeoutMilliseconds for when traffic channels should automatically be stopped.
*/
public MPT1327DecoderState(MPT1327TrafficChannelManager trafficChannelManager, ChannelType channelType, long callTimeout)
public MPT1327DecoderState(MPT1327TrafficChannelManager trafficChannelManager, ChannelType channelType,
long callTimeoutMilliseconds)
{
mMPT1327TrafficChannelManager = trafficChannelManager;
mChannelType = channelType;
mCallTimeout = callTimeout;
mCallTimeoutMilliseconds = callTimeoutMilliseconds;
}
/**
* Constructs an MPT1327 decoder state that does not allocate traffic channels
*/
public MPT1327DecoderState(ChannelType channelType, long callTimeout)
public MPT1327DecoderState(ChannelType channelType, long callTimeoutMilliseconds)
{
this(null, channelType, callTimeout);
this(null, channelType, callTimeoutMilliseconds);
}
@Override
@@ -250,7 +253,7 @@ public class MPT1327DecoderState extends DecoderState
{
//When we receive a MAINT message and we're configured as a standard channel, apply the call
// timeout specified by the user. Otherwise we'll be using the shorter default call timeout
broadcast(new ChangeChannelTimeoutEvent(this, mChannelType, mCallTimeout));
broadcast(new ChangeChannelTimeoutEvent(this, mChannelType, mCallTimeoutMilliseconds));
MutableIdentifierCollection ic = new MutableIdentifierCollection(getIdentifierCollection().getIdentifiers());
ic.remove(IdentifierClass.USER);

View File

@@ -325,23 +325,14 @@ public class MPT1327TrafficChannelManager extends Module implements IDecodeEvent
switch(channelEvent.getEvent())
{
case NOTIFICATION_PROCESSING_STOP:
MPT1327Channel toRemove = null;
for(Map.Entry<MPT1327Channel,Channel> entry : mAllocatedTrafficChannelMap.entrySet())
{
if(entry.getValue() == channel)
{
toRemove = entry.getKey();
continue;
}
}
final var toRemove = mptChannelForChannel(channel);
if(toRemove != null)
{
mAllocatedTrafficChannelMap.remove(toRemove);
mAvailableTrafficChannelQueue.add(channel);
MPT1327ChannelGrantEvent event = mChannelGrantEventMap.get(toRemove);
final var event = mChannelGrantEventMap.remove(toRemove);
if(event != null)
{
@@ -351,23 +342,14 @@ public class MPT1327TrafficChannelManager extends Module implements IDecodeEvent
}
break;
case NOTIFICATION_PROCESSING_START_REJECTED:
MPT1327Channel rejected = null;
for(Map.Entry<MPT1327Channel,Channel> entry : mAllocatedTrafficChannelMap.entrySet())
{
if(entry.getValue() == channel)
{
rejected = entry.getKey();
continue;
}
}
final var rejected = mptChannelForChannel(channel);
if(rejected != null)
{
mAllocatedTrafficChannelMap.remove(rejected);
mAvailableTrafficChannelQueue.add(channel);
MPT1327ChannelGrantEvent event = mChannelGrantEventMap.get(rejected);
final var event = mChannelGrantEventMap.remove(rejected);
if(event != null)
{
@@ -386,5 +368,17 @@ public class MPT1327TrafficChannelManager extends Module implements IDecodeEvent
}
}
}
private MPT1327Channel mptChannelForChannel(final Channel channel)
{
for (final var entry : mAllocatedTrafficChannelMap.entrySet())
{
if (entry.getValue() == channel)
{
return entry.getKey();
}
}
return null;
}
}
}

View File

@@ -67,7 +67,6 @@ import java.util.Objects;
import java.util.Queue;
import java.util.concurrent.ConcurrentHashMap;
import java.util.concurrent.ConcurrentLinkedQueue;
import java.util.function.Consumer;
/**
* Monitors channel grant and channel grant update messages to allocate traffic channels to capture
@@ -98,10 +97,9 @@ public class P25TrafficChannelManager extends Module implements IDecodeEventProv
private Queue<Channel> mAvailablePhase2TrafficChannelQueue = new ConcurrentLinkedQueue<>();
private List<Channel> mManagedPhase2TrafficChannels;
private Map<APCO25Channel,Channel> mAllocatedTrafficChannelMap = new ConcurrentHashMap<>();
private Map<APCO25Channel,P25ChannelGrantEvent> mPhase1ChannelGrantEventMap = new ConcurrentHashMap<>();
private Map<APCO25Channel,P25ChannelGrantEvent> mPhase2TS0ChannelGrantEventMap = new ConcurrentHashMap<>();
private Map<APCO25Channel,P25ChannelGrantEvent> mPhase2TS1ChannelGrantEventMap = new ConcurrentHashMap<>();
private Map<Long,Channel> mAllocatedTrafficChannelMap = new ConcurrentHashMap<>();
private Map<Long,P25ChannelGrantEvent> mTS0ChannelGrantEventMap = new ConcurrentHashMap<>();
private Map<Long,P25ChannelGrantEvent> mTS1ChannelGrantEventMap = new ConcurrentHashMap<>();
private Listener<ChannelEvent> mChannelEventListener;
private Listener<IDecodeEvent> mDecodeEventListener;
@@ -114,10 +112,7 @@ public class P25TrafficChannelManager extends Module implements IDecodeEventProv
private boolean mIgnoreDataCalls;
/**
* Monitors call events and allocates traffic decoder channels in response
* to traffic channel allocation call events. Manages a pool of reusable
* traffic channel allocations.
*
* Constructs an instance.
* @param parentChannel that owns this traffic channel manager
*/
public P25TrafficChannelManager(Channel parentChannel)
@@ -273,8 +268,9 @@ public class P25TrafficChannelManager extends Module implements IDecodeEventProv
private void processPhase1ChannelGrant(APCO25Channel apco25Channel, ServiceOptions serviceOptions,
IdentifierCollection identifierCollection, Opcode opcode, long timestamp)
{
long frequency = apco25Channel.getDownlinkFrequency();
P25ChannelGrantEvent event = mPhase1ChannelGrantEventMap.get(apco25Channel);
P25ChannelGrantEvent event = mTS0ChannelGrantEventMap.get(frequency);
if(event != null && isSameCall(identifierCollection, event.getIdentifierCollection()))
{
@@ -294,7 +290,7 @@ public class P25TrafficChannelManager extends Module implements IDecodeEventProv
.identifiers(identifierCollection)
.build();
mPhase1ChannelGrantEventMap.put(apco25Channel, continuationGrantEvent);
mTS0ChannelGrantEventMap.put(frequency, continuationGrantEvent);
broadcast(continuationGrantEvent);
}
}
@@ -305,7 +301,7 @@ public class P25TrafficChannelManager extends Module implements IDecodeEventProv
//Even though we have an event, the initial channel grant may have been rejected. Check to see if there
//is a traffic channel allocated. If not, allocate one and update the event description.
if(!mAllocatedTrafficChannelMap.containsKey(apco25Channel) && !(mIgnoreDataCalls && opcode.isDataChannelGrant()))
if(!mAllocatedTrafficChannelMap.containsKey(frequency) && !(mIgnoreDataCalls && opcode.isDataChannelGrant()))
{
Channel trafficChannel = mAvailablePhase1TrafficChannelQueue.poll();
@@ -316,9 +312,9 @@ public class P25TrafficChannelManager extends Module implements IDecodeEventProv
event.setChannelDescriptor(apco25Channel);
broadcast(event);
SourceConfigTuner sourceConfig = new SourceConfigTuner();
sourceConfig.setFrequency(apco25Channel.getDownlinkFrequency());
sourceConfig.setFrequency(frequency);
trafficChannel.setSourceConfiguration(sourceConfig);
mAllocatedTrafficChannelMap.put(apco25Channel, trafficChannel);
mAllocatedTrafficChannelMap.put(frequency, trafficChannel);
broadcast(new ChannelGrantEvent(trafficChannel, Event.REQUEST_ENABLE, apco25Channel, identifierCollection));
}
}
@@ -335,8 +331,7 @@ public class P25TrafficChannelManager extends Module implements IDecodeEventProv
.identifiers(identifierCollection)
.build();
mPhase1ChannelGrantEventMap.put(apco25Channel, channelGrantEvent);
mTS0ChannelGrantEventMap.put(frequency, channelGrantEvent);
broadcast(channelGrantEvent);
return;
}
@@ -348,11 +343,10 @@ public class P25TrafficChannelManager extends Module implements IDecodeEventProv
.identifiers(identifierCollection)
.build();
mPhase1ChannelGrantEventMap.put(apco25Channel, channelGrantEvent);
mTS0ChannelGrantEventMap.put(frequency, channelGrantEvent);
//Allocate a traffic channel for the downlink frequency if one isn't already allocated
//NOTE: we could also allocate a traffic channel for the uplink frequency here, in the future
if(!mAllocatedTrafficChannelMap.containsKey(apco25Channel))
if(!mAllocatedTrafficChannelMap.containsKey(frequency))
{
Channel trafficChannel = mAvailablePhase1TrafficChannelQueue.poll();
@@ -364,9 +358,9 @@ public class P25TrafficChannelManager extends Module implements IDecodeEventProv
}
SourceConfigTuner sourceConfig = new SourceConfigTuner();
sourceConfig.setFrequency(apco25Channel.getDownlinkFrequency());
sourceConfig.setFrequency(frequency);
trafficChannel.setSourceConfiguration(sourceConfig);
mAllocatedTrafficChannelMap.put(apco25Channel, trafficChannel);
mAllocatedTrafficChannelMap.put(frequency, trafficChannel);
broadcast(new ChannelGrantEvent(trafficChannel, Event.REQUEST_ENABLE, apco25Channel, identifierCollection));
}
@@ -392,16 +386,17 @@ public class P25TrafficChannelManager extends Module implements IDecodeEventProv
}
int timeslot = apco25Channel.getTimeslot();
long frequency = apco25Channel.getDownlinkFrequency();
P25ChannelGrantEvent event = null;
if(timeslot == 0)
{
event = mPhase2TS0ChannelGrantEventMap.get(apco25Channel);
event = mTS0ChannelGrantEventMap.get(frequency);
}
else if(timeslot == 1)
{
event = mPhase2TS1ChannelGrantEventMap.get(apco25Channel);
event = mTS1ChannelGrantEventMap.get(frequency);
}
else
{
@@ -431,11 +426,11 @@ public class P25TrafficChannelManager extends Module implements IDecodeEventProv
if(timeslot == 0)
{
mPhase2TS0ChannelGrantEventMap.put(apco25Channel, continuationGrantEvent);
mTS0ChannelGrantEventMap.put(frequency, continuationGrantEvent);
}
else
{
mPhase2TS1ChannelGrantEventMap.put(apco25Channel, continuationGrantEvent);
mTS1ChannelGrantEventMap.put(frequency, continuationGrantEvent);
}
broadcast(continuationGrantEvent);
@@ -448,8 +443,7 @@ public class P25TrafficChannelManager extends Module implements IDecodeEventProv
//Even though we have an event, the initial channel grant may have been rejected. Check to see if there
//is a traffic channel allocated. If not, allocate one and update the event description.
if(!mAllocatedTrafficChannelMap.containsKey(apco25Channel) &&
!(mIgnoreDataCalls && opcode.isDataChannelGrant()))
if(!mAllocatedTrafficChannelMap.containsKey(frequency) && !(mIgnoreDataCalls && opcode.isDataChannelGrant()))
{
Channel trafficChannel = mAvailablePhase2TrafficChannelQueue.poll();
@@ -460,9 +454,9 @@ public class P25TrafficChannelManager extends Module implements IDecodeEventProv
event.setChannelDescriptor(apco25Channel);
broadcast(event);
SourceConfigTuner sourceConfig = new SourceConfigTuner();
sourceConfig.setFrequency(apco25Channel.getDownlinkFrequency());
sourceConfig.setFrequency(frequency);
trafficChannel.setSourceConfiguration(sourceConfig);
mAllocatedTrafficChannelMap.put(apco25Channel, trafficChannel);
mAllocatedTrafficChannelMap.put(frequency, trafficChannel);
//If we have valid scramble/randomizer parameters, set them in the decode config
if(mPhase2ScrambleParameters != null)
@@ -487,7 +481,7 @@ public class P25TrafficChannelManager extends Module implements IDecodeEventProv
.identifiers(identifierCollection)
.build();
mPhase1ChannelGrantEventMap.put(apco25Channel, channelGrantEvent);
mTS0ChannelGrantEventMap.put(frequency, channelGrantEvent);
broadcast(channelGrantEvent);
return;
@@ -502,16 +496,15 @@ public class P25TrafficChannelManager extends Module implements IDecodeEventProv
if(timeslot == 0)
{
mPhase2TS0ChannelGrantEventMap.put(apco25Channel, channelGrantEvent);
mTS0ChannelGrantEventMap.put(frequency, channelGrantEvent);
}
else
{
mPhase2TS1ChannelGrantEventMap.put(apco25Channel, channelGrantEvent);
mTS1ChannelGrantEventMap.put(frequency, channelGrantEvent);
}
//Allocate a traffic channel for the downlink frequency if one isn't already allocated
//NOTE: we could also allocate a traffic channel for the uplink frequency here, in the future
if(!mAllocatedTrafficChannelMap.containsKey(apco25Channel))
if(!mAllocatedTrafficChannelMap.containsKey(apco25Channel.getDownlinkFrequency()))
{
Channel trafficChannel = mAvailablePhase2TrafficChannelQueue.poll();
@@ -523,9 +516,9 @@ public class P25TrafficChannelManager extends Module implements IDecodeEventProv
}
SourceConfigTuner sourceConfig = new SourceConfigTuner();
sourceConfig.setFrequency(apco25Channel.getDownlinkFrequency());
sourceConfig.setFrequency(frequency);
trafficChannel.setSourceConfiguration(sourceConfig);
mAllocatedTrafficChannelMap.put(apco25Channel, trafficChannel);
mAllocatedTrafficChannelMap.put(frequency, trafficChannel);
broadcast(new ChannelGrantEvent(trafficChannel, Event.REQUEST_ENABLE, apco25Channel, identifierCollection));
}
@@ -660,6 +653,11 @@ public class P25TrafficChannelManager extends Module implements IDecodeEventProv
{
broadcast(new ChannelEvent(trafficChannel, Event.REQUEST_DISABLE));
}
for(Channel trafficChannel : mAvailablePhase2TrafficChannelQueue)
{
broadcast(new ChannelEvent(trafficChannel, Event.REQUEST_DISABLE));
}
}
/**
@@ -701,6 +699,9 @@ public class P25TrafficChannelManager extends Module implements IDecodeEventProv
mLog.info("Stopping traffic channel: " + channel);
broadcast(new ChannelEvent(channel, Event.REQUEST_DISABLE));
}
mTS0ChannelGrantEventMap.clear();
mTS1ChannelGrantEventMap.clear();
}
/**
@@ -715,21 +716,16 @@ public class P25TrafficChannelManager extends Module implements IDecodeEventProv
{
if(mMessageListener == null)
{
mMessageListener = new Listener<IMessage>()
{
@Override
public void receive(IMessage message)
mMessageListener = message -> {
if(mPhase2ScrambleParameters == null && message.isValid())
{
if(mPhase2ScrambleParameters == null && message.isValid())
if(message instanceof NetworkStatusBroadcast)
{
if(message instanceof NetworkStatusBroadcast)
{
mPhase2ScrambleParameters = ((NetworkStatusBroadcast)message).getScrambleParameters();
}
else if(message instanceof AMBTCNetworkStatusBroadcast)
{
mPhase2ScrambleParameters = ((AMBTCNetworkStatusBroadcast)message).getScrambleParameters();
}
mPhase2ScrambleParameters = ((NetworkStatusBroadcast)message).getScrambleParameters();
}
else if(message instanceof AMBTCNetworkStatusBroadcast)
{
mPhase2ScrambleParameters = ((AMBTCNetworkStatusBroadcast)message).getScrambleParameters();
}
}
};
@@ -770,24 +766,22 @@ public class P25TrafficChannelManager extends Module implements IDecodeEventProv
public class TrafficChannelTeardownMonitor implements Listener<ChannelEvent>
{
/**
* Removes any call events that are associated with the specified channel.
* @param channel that was removed
* Removes an allocated traffic channel and adds it to the available channel queue
* @param channel to reset
* @param frequency of the channel
* @param isPhase1 true or false if it is a phase 2 channel
*/
private void cleanupCallEvents(Channel channel)
private void resetTrafficChannel(Channel channel, long frequency, boolean isPhase1)
{
if(channel.getSourceConfiguration() instanceof SourceConfigTuner)
{
long frequency = ((SourceConfigTuner)channel.getSourceConfiguration()).getFrequency();
mAllocatedTrafficChannelMap.remove(frequency);
if(channel.getDecodeConfiguration().getDecoderType() == DecoderType.P25_PHASE1)
{
mPhase1ChannelGrantEventMap.entrySet().removeIf(entry -> entry.getKey().getDownlinkFrequency() == frequency);
}
else
{
mPhase2TS0ChannelGrantEventMap.entrySet().removeIf(entry -> entry.getKey().getDownlinkFrequency() == frequency);
mPhase2TS1ChannelGrantEventMap.entrySet().removeIf(entry -> entry.getKey().getDownlinkFrequency() == frequency);
}
if(isPhase1)
{
mAvailablePhase1TrafficChannelQueue.add(channel);
}
else
{
mAvailablePhase2TrafficChannelQueue.add(channel);
}
}
@@ -810,48 +804,43 @@ public class P25TrafficChannelManager extends Module implements IDecodeEventProv
.filter(entry -> entry.getValue() == channel)
.map(Map.Entry::getKey)
.findFirst()
.ifPresent(toRemove -> {
mAllocatedTrafficChannelMap.remove(toRemove);
if (isPhase1) {
mAvailablePhase1TrafficChannelQueue.add(channel);
} else {
mAvailablePhase2TrafficChannelQueue.add(channel);
}
.ifPresent(frequencyToRemove -> {
resetTrafficChannel(channel, frequencyToRemove, isPhase1);
mTS0ChannelGrantEventMap.remove(frequencyToRemove);
mTS1ChannelGrantEventMap.remove(frequencyToRemove);
});
cleanupCallEvents(channel);
break;
case NOTIFICATION_PROCESSING_START_REJECTED:
mAllocatedTrafficChannelMap.entrySet().stream()
.filter(entry -> entry.getValue() == channel)
.map(Map.Entry::getKey)
.findFirst()
.ifPresent(rejected -> {
mAllocatedTrafficChannelMap.remove(rejected);
.ifPresent(rejectedFrequency -> {
resetTrafficChannel(channel, rejectedFrequency, isPhase1);
if (isPhase1) {
mAvailablePhase1TrafficChannelQueue.add(channel);
} else {
mAvailablePhase2TrafficChannelQueue.add(channel);
P25ChannelGrantEvent event = mTS0ChannelGrantEventMap.remove(rejectedFrequency);
if(event == null)
{
event = mTS1ChannelGrantEventMap.remove(rejectedFrequency);
}
P25ChannelGrantEvent event = mPhase1ChannelGrantEventMap.get(rejected);
if (event != null) {
if (event != null)
{
event.setEventDescription(event.getEventDescription() + " - Rejected");
if (channelEvent.getDescription() != null) {
if (channelEvent.getDescription() != null)
{
event.setDetails(channelEvent.getDescription() + " - " + event.getDetails());
} else {
}
else
{
event.setDetails(CHANNEL_START_REJECTED + " - " + event.getDetails());
}
broadcast(event);
}
});
cleanupCallEvents(channel);
break;
}
}

View File

@@ -32,7 +32,6 @@ public class DecodeConfigP25Phase1 extends DecodeConfiguration
{
private P25P1Decoder.Modulation mModulation = P25P1Decoder.Modulation.C4FM;
private int mCallTimeout = 1;
private int mTrafficChannelPoolSize = TRAFFIC_CHANNEL_LIMIT_DEFAULT;
private boolean mIgnoreDataCalls = false;
@@ -68,28 +67,6 @@ public class DecodeConfigP25Phase1 extends DecodeConfiguration
mIgnoreDataCalls = ignore;
}
/**
* Note: this field is now deprecated.
*
* @return
*/
@JsonIgnore
@Deprecated
public int getCallTimeout()
{
return mCallTimeout;
}
/**
* Sets the call timeout value in seconds ( 10 - 600 );
*
* @param timeout
*/
@Deprecated
public void setCallTimeout(int timeout)
{
}
@JacksonXmlProperty(isAttribute = true, localName = "traffic_channel_pool_size")
public int getTrafficChannelPoolSize()

View File

@@ -32,6 +32,7 @@ import io.github.dsheirer.controller.channel.Channel.ChannelType;
import io.github.dsheirer.controller.channel.ChannelEvent;
import io.github.dsheirer.controller.channel.ChannelModel;
import io.github.dsheirer.controller.channel.ChannelProcessingManager;
import io.github.dsheirer.controller.channel.map.ChannelMap;
import io.github.dsheirer.controller.channel.map.ChannelMapModel;
import io.github.dsheirer.icon.IconModel;
import io.github.dsheirer.module.log.EventLogManager;
@@ -113,10 +114,7 @@ public class PlaylistManager implements Listener<ChannelEvent>
mAliasModel.aliasList().addListener((ListChangeListener<Alias>)c -> schedulePlaylistSave());
mChannelMapModel.addListener(t -> {
//Save the playlist for all channel map events
schedulePlaylistSave();
});
mChannelMapModel.getChannelMaps().addListener((ListChangeListener<ChannelMap>)c -> schedulePlaylistSave());
mBroadcastModel.addListener(broadcastEvent -> {
switch(broadcastEvent.getEvent())

View File

@@ -26,6 +26,7 @@ public enum PreferenceType
{
DECODE_EVENT,
DIRECTORY,
DUPLICATE_CALL_DETECTION,
JMBE_LIBRARY,
MULTI_FREQUENCY,
PLAYLIST,

View File

@@ -22,6 +22,7 @@ package io.github.dsheirer.preference;
import io.github.dsheirer.eventbus.MyEventBus;
import io.github.dsheirer.preference.decoder.JmbeLibraryPreference;
import io.github.dsheirer.preference.directory.DirectoryPreference;
import io.github.dsheirer.preference.duplicate.DuplicateCallDetectionPreference;
import io.github.dsheirer.preference.event.DecodeEventPreference;
import io.github.dsheirer.preference.identifier.TalkgroupFormatPreference;
import io.github.dsheirer.preference.javafx.JavaFxPreferences;
@@ -52,16 +53,18 @@ import io.github.dsheirer.sample.Listener;
*/
public class UserPreferences implements Listener<PreferenceType>
{
private JmbeLibraryPreference mJmbeLibraryPreference;
private ChannelMultiFrequencyPreference mChannelMultiFrequencyPreference;
private DecodeEventPreference mDecodeEventPreference;
private DirectoryPreference mDirectoryPreference;
private ChannelMultiFrequencyPreference mChannelMultiFrequencyPreference;
private DuplicateCallDetectionPreference mDuplicateCallDetectionPreference;
private JmbeLibraryPreference mJmbeLibraryPreference;
private PlaybackPreference mPlaybackPreference;
private PlaylistPreference mPlaylistPreference;
private RadioReferencePreference mRadioReferencePreference;
private RecordPreference mRecordPreference;
private TalkgroupFormatPreference mTalkgroupFormatPreference;
private TunerPreference mTunerPreference;
private SwingPreference mSwingPreference = new SwingPreference();
private JavaFxPreferences mJavaFxPreferences = new JavaFxPreferences();
@@ -170,15 +173,24 @@ public class UserPreferences implements Listener<PreferenceType>
return mSwingPreference;
}
/**
* Duplicate call detection preferences
*/
public DuplicateCallDetectionPreference getDuplicateCallDetectionPreference()
{
return mDuplicateCallDetectionPreference;
}
/**
* Loads the managed preferences
*/
private void loadPreferenceTypes()
{
mDecodeEventPreference = new DecodeEventPreference(this::receive);
mJmbeLibraryPreference = new JmbeLibraryPreference(this::receive);
mDirectoryPreference = new DirectoryPreference(this::receive);
mChannelMultiFrequencyPreference = new ChannelMultiFrequencyPreference(this::receive);
mDecodeEventPreference = new DecodeEventPreference(this::receive);
mDirectoryPreference = new DirectoryPreference(this::receive);
mDuplicateCallDetectionPreference = new DuplicateCallDetectionPreference(this::receive);
mJmbeLibraryPreference = new JmbeLibraryPreference(this::receive);
mPlaybackPreference = new PlaybackPreference(this::receive);
mPlaylistPreference = new PlaylistPreference(this::receive, mDirectoryPreference);
mRadioReferencePreference = new RadioReferencePreference(this::receive);

View File

@@ -22,6 +22,7 @@
package io.github.dsheirer.preference.decoder;
import io.github.dsheirer.jmbe.github.Version;
import io.github.dsheirer.preference.Preference;
import io.github.dsheirer.preference.PreferenceType;
import io.github.dsheirer.sample.Listener;
@@ -32,6 +33,8 @@ import java.nio.file.Files;
import java.nio.file.Path;
import java.nio.file.Paths;
import java.util.prefs.Preferences;
import java.util.regex.Matcher;
import java.util.regex.Pattern;
/**
* Decoder preferences
@@ -42,7 +45,10 @@ public class JmbeLibraryPreference extends Preference
private Preferences mPreferences = Preferences.userNodeForPackage(JmbeLibraryPreference.class);
private static final String PREFERENCE_KEY_PATH_JMBE_LIBRARY = "path.jmbe.library.1.0.0";
private static final String PREFERENCE_KEY_PATH_ALERT_LIBRARY_REQUIRED = "alert.jmbe.required";
private final Pattern VERSION_PATTERN = Pattern.compile(".*jmbe-(\\d{1,5}.\\d{1,5}.\\d{1,5}\\w*)\\.jar");
private Path mPathJmbeLibrary;
private Boolean mAlertIfMissingLibraryRequired;
/**
* Constructs this preference with an update listener
@@ -60,6 +66,23 @@ public class JmbeLibraryPreference extends Preference
return PreferenceType.JMBE_LIBRARY;
}
public Version getCurrentVersion()
{
Path path = getPathJmbeLibrary();
if(path != null)
{
Matcher m = VERSION_PATTERN.matcher(path.toString());
if(m.matches())
{
return Version.fromString(m.group(1));
}
}
return null;
}
/*
* Path to the JMBE audio codec library
*/
@@ -73,6 +96,14 @@ public class JmbeLibraryPreference extends Preference
return mPathJmbeLibrary;
}
/**
* Indicates if a path to a JMBE library is setup (ie non-null).
*/
public boolean hasJmbeLibraryPath()
{
return getPathJmbeLibrary() != null;
}
/**
* Sets the path to the JMBE library
*/
@@ -93,6 +124,30 @@ public class JmbeLibraryPreference extends Preference
notifyPreferenceUpdated();
}
/**
* Indicates if the user should be alerted when the JMBE library is not setup, but required for decoding.
*/
public boolean getAlertIfMissingLibraryRequired()
{
if(mAlertIfMissingLibraryRequired == null)
{
mAlertIfMissingLibraryRequired = mPreferences.getBoolean(PREFERENCE_KEY_PATH_ALERT_LIBRARY_REQUIRED, true);
}
return mAlertIfMissingLibraryRequired;
}
/**
* Sets the value for alerting when the library is required for decoding but the library is not currently setup.
* @param alert true if the user should be alerted when the library is not setup, but required for decoding.
*/
public void setAlertIfMissingLibraryRequired(boolean alert)
{
mAlertIfMissingLibraryRequired = alert;
mPreferences.putBoolean(PREFERENCE_KEY_PATH_ALERT_LIBRARY_REQUIRED, mAlertIfMissingLibraryRequired);
notifyPreferenceUpdated();
}
/**
* Returns the path stored in preferences and referenced by the key argument if it exists, otherwise returns the
* default path.

View File

@@ -42,6 +42,7 @@ public class DirectoryPreference extends Preference
private static final String DIRECTORY_APPLICATION_ROOT = "SDRTrunk";
private static final String DIRECTORY_APPLICATION_LOG = "logs";
private static final String DIRECTORY_EVENT_LOG = "event_logs";
private static final String DIRECTORY_JMBE = "jmbe";
private static final String DIRECTORY_PLAYLIST = "playlist";
private static final String DIRECTORY_RECORDING = "recordings";
private static final String DIRECTORY_SCREEN_CAPTURE = "screen_captures";
@@ -50,6 +51,7 @@ public class DirectoryPreference extends Preference
private static final String PREFERENCE_KEY_DIRECTORY_APPLICATION_ROOT = "directory.application.root";
private static final String PREFERENCE_KEY_DIRECTORY_APPLICATION_LOGS = "directory.application.logs";
private static final String PREFERENCE_KEY_DIRECTORY_EVENT_LOGS = "directory.event.logs";
private static final String PREFERENCE_KEY_DIRECTORY_JMBE = "directory.jmbe";
private static final String PREFERENCE_KEY_DIRECTORY_PLAYLIST = "directory.playlist";
private static final String PREFERENCE_KEY_DIRECTORY_RECORDING = "directory.recording";
private static final String PREFERENCE_KEY_DIRECTORY_SCREEN_CAPTURE = "directory.screen.capture";
@@ -58,6 +60,7 @@ public class DirectoryPreference extends Preference
private Path mDirectoryApplicationRoot;
private Path mDirectoryApplicationLogs;
private Path mDirectoryEventLogs;
private Path mDirectoryJmbe;
private Path mDirectoryPlaylist;
private Path mDirectoryRecording;
private Path mDirectoryScreenCapture;
@@ -115,6 +118,7 @@ public class DirectoryPreference extends Preference
{
mDirectoryApplicationLogs = null;
mDirectoryEventLogs = null;
mDirectoryJmbe = null;
mDirectoryPlaylist = null;
mDirectoryRecording = null;
mDirectoryScreenCapture = null;
@@ -204,6 +208,40 @@ public class DirectoryPreference extends Preference
notifyPreferenceUpdated();
}
/**
* Path to the folder for storing the JMBE audio library
*/
public Path getDirectoryJmbe()
{
if(mDirectoryJmbe == null)
{
mDirectoryJmbe = getPath(PREFERENCE_KEY_DIRECTORY_JMBE, getDefaultJmbeDirectory());
createDirectory(mDirectoryJmbe);
}
return mDirectoryJmbe;
}
/**
* Sets the path to the JMBE folder
*/
public void setDirectoryJmbe(Path path)
{
mDirectoryJmbe = path;
mPreferences.put(PREFERENCE_KEY_DIRECTORY_JMBE, path.toString());
notifyPreferenceUpdated();
}
/**
* Removes a stored playlist directory preference so that the default path can be used again
*/
public void resetDirectoryJmbe()
{
mPreferences.remove(PREFERENCE_KEY_DIRECTORY_JMBE);
mDirectoryJmbe = null;
notifyPreferenceUpdated();
}
/**
* Path to the folder for storing playlist
*/
@@ -364,6 +402,14 @@ public class DirectoryPreference extends Preference
return getDirectoryApplicationRoot().resolve(DIRECTORY_EVENT_LOG);
}
/**
* Default JMBE directory
*/
public Path getDefaultJmbeDirectory()
{
return getDirectoryApplicationRoot().resolve(DIRECTORY_JMBE);
}
/**
* Default playlist directory
*/

View File

@@ -0,0 +1,187 @@
/*
* *****************************************************************************
* Copyright (C) 2014-2020 Dennis Sheirer
*
* This program is free software: you can redistribute it and/or modify
* it under the terms of the GNU General Public License as published by
* the Free Software Foundation, either version 3 of the License, or
* (at your option) any later version.
*
* This program is distributed in the hope that it will be useful,
* but WITHOUT ANY WARRANTY; without even the implied warranty of
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
* GNU General Public License for more details.
*
* You should have received a copy of the GNU General Public License
* along with this program. If not, see <http://www.gnu.org/licenses/>
* ****************************************************************************
*/
package io.github.dsheirer.preference.duplicate;
import io.github.dsheirer.preference.Preference;
import io.github.dsheirer.preference.PreferenceType;
import io.github.dsheirer.sample.Listener;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import java.util.prefs.Preferences;
/**
* User preferences for duplicate call detection
*/
public class DuplicateCallDetectionPreference extends Preference
{
private static final String PREFERENCE_KEY_DETECT_DUPLICATE_TALKGROUP = "duplicate.call.detect.talkgroup";
private static final String PREFERENCE_KEY_DETECT_DUPLICATE_RADIO = "duplicate.call.detect.radio";
private static final String PREFERENCE_KEY_SUPPRESS_DUPLICATE_PLAYBACK = "suppress.duplicate.audio.playback";
private static final String PREFERENCE_KEY_SUPPRESS_DUPLICATE_RECORDING = "suppress.duplicate.audio.recording";
private static final String PREFERENCE_KEY_SUPPRESS_DUPLICATE_STREAMING = "suppress.duplicate.audio.streaming";
private final static Logger mLog = LoggerFactory.getLogger(DuplicateCallDetectionPreference.class);
private Preferences mPreferences = Preferences.userNodeForPackage(DuplicateCallDetectionPreference.class);
private Boolean mDuplicateCallDetectionByTalkgroupEnabled;
private Boolean mDuplicateCallDetectionByRadioEnabled;
private Boolean mDuplicatePlaybackSuppressionEnabled;
private Boolean mDuplicateRecordingSuppressionEnabled;
private Boolean mDuplicateStreamingSuppressionEnabled;
/**
* Constructs an instance
* @param updateListener to receive notifications that a preference has been updated
*/
public DuplicateCallDetectionPreference(Listener<PreferenceType> updateListener)
{
super(updateListener);
}
@Override
public PreferenceType getPreferenceType()
{
return PreferenceType.DUPLICATE_CALL_DETECTION;
}
/**
* Indicates if duplicate call detection is enabled for either talkgroups or radio identifiers.
*/
public boolean isDuplicateCallDetectionEnabled()
{
return isDuplicateCallDetectionByTalkgroupEnabled() || isDuplicateCallDetectionByRadioEnabled();
}
/**
* Preference for detecting duplicate calls by talkgroup or patchgroup
*/
public boolean isDuplicateCallDetectionByTalkgroupEnabled()
{
if(mDuplicateCallDetectionByTalkgroupEnabled == null)
{
mDuplicateCallDetectionByTalkgroupEnabled =
mPreferences.getBoolean(PREFERENCE_KEY_DETECT_DUPLICATE_TALKGROUP, true);
}
return mDuplicateCallDetectionByTalkgroupEnabled;
}
/**
* Sets the preference for detecting duplicate calls by talkgroup
*/
public void setDuplicateCallDetectionByTalkgroupEnabled(boolean enabled)
{
mPreferences.putBoolean(PREFERENCE_KEY_DETECT_DUPLICATE_TALKGROUP, enabled);
mDuplicateCallDetectionByTalkgroupEnabled = enabled;
notifyPreferenceUpdated();
}
/**
* Preference for detecting duplicate calls by radio identifier
*/
public boolean isDuplicateCallDetectionByRadioEnabled()
{
if(mDuplicateCallDetectionByRadioEnabled == null)
{
mDuplicateCallDetectionByRadioEnabled = mPreferences.getBoolean(PREFERENCE_KEY_DETECT_DUPLICATE_RADIO, false);
}
return mDuplicateCallDetectionByRadioEnabled;
}
/**
* Sets the preference for detecting duplicate calls by radio identifier
*/
public void setDuplicateCallDetectionByRadioEnabled(boolean enabled)
{
mPreferences.putBoolean(PREFERENCE_KEY_DETECT_DUPLICATE_RADIO, enabled);
mDuplicateCallDetectionByRadioEnabled = enabled;
notifyPreferenceUpdated();
}
/**
* Preference for suppressig playback of duplicate calls.
*/
public boolean isDuplicatePlaybackSuppressionEnabled()
{
if(mDuplicatePlaybackSuppressionEnabled == null)
{
mDuplicatePlaybackSuppressionEnabled = mPreferences.getBoolean(PREFERENCE_KEY_SUPPRESS_DUPLICATE_PLAYBACK, true);
}
return mDuplicatePlaybackSuppressionEnabled;
}
/**
* Sets the preference for suppressing playback of duplicate calls.
*/
public void setDuplicatePlaybackSuppressionEnabled(boolean enabled)
{
mPreferences.putBoolean(PREFERENCE_KEY_SUPPRESS_DUPLICATE_PLAYBACK, enabled);
mDuplicatePlaybackSuppressionEnabled = enabled;
notifyPreferenceUpdated();
}
/**
* Preference for suppressing recording of duplicate calls.
*/
public boolean isDuplicateRecordingSuppressionEnabled()
{
if(mDuplicateRecordingSuppressionEnabled == null)
{
mDuplicateRecordingSuppressionEnabled = mPreferences.getBoolean(PREFERENCE_KEY_SUPPRESS_DUPLICATE_RECORDING, true);
}
return mDuplicateRecordingSuppressionEnabled;
}
/**
* Sets the preference for suppressing recording of duplicate calls.
*/
public void setDuplicateRecordingSuppressionEnabled(boolean enabled)
{
mPreferences.putBoolean(PREFERENCE_KEY_SUPPRESS_DUPLICATE_RECORDING, enabled);
mDuplicateRecordingSuppressionEnabled = enabled;
notifyPreferenceUpdated();
}
/**
* Preference for suppressing streaming of duplicate calls
*/
public boolean isDuplicateStreamingSuppressionEnabled()
{
if(mDuplicateStreamingSuppressionEnabled == null)
{
mDuplicateStreamingSuppressionEnabled = mPreferences.getBoolean(PREFERENCE_KEY_SUPPRESS_DUPLICATE_STREAMING, true);
}
return mDuplicateStreamingSuppressionEnabled;
}
/**
* Sets the preference for suppressing streaming of duplicate calls
*/
public void setDuplicateStreamingSuppressionEnabled(boolean enabled)
{
mPreferences.putBoolean(PREFERENCE_KEY_SUPPRESS_DUPLICATE_STREAMING, enabled);
mDuplicateStreamingSuppressionEnabled = enabled;
notifyPreferenceUpdated();;
}
}

View File

@@ -139,21 +139,28 @@ public class AudioRecordingManager implements Listener<AudioSegment>
while(audioSegment != null)
{
Path path = getAudioRecordingPath(audioSegment.getIdentifierCollection(), recordFormat);
try
if(audioSegment.isDuplicate() && mUserPreferences.getDuplicateCallDetectionPreference().isDuplicateRecordingSuppressionEnabled())
{
AudioSegmentRecorder.record(audioSegment, path, recordFormat);
audioSegment.decrementConsumerCount();
}
catch(IOException ioe)
else
{
mLog.error("Error recording audio segment to [" + path.toString() + "]");
Path path = getAudioRecordingPath(audioSegment.getIdentifierCollection(), recordFormat);
try
{
AudioSegmentRecorder.record(audioSegment, path, recordFormat);
}
catch(IOException ioe)
{
mLog.error("Error recording audio segment to [" + path.toString() + "]");
}
audioSegment.decrementConsumerCount();
//Grab the next one to record
audioSegment = mCompletedAudioSegmentQueue.poll();
}
audioSegment.decrementConsumerCount();
//Grab the next one to record
audioSegment = mCompletedAudioSegmentQueue.poll();
}
}

View File

@@ -162,7 +162,7 @@ public class USBTransferProcessor implements TransferCallback
{
if(mBufferDispatcherFuture != null)
{
mBufferDispatcherFuture.cancel(true);
mBufferDispatcherFuture.cancel(false);
mBufferDispatcherFuture = null;
}
@@ -174,7 +174,7 @@ public class USBTransferProcessor implements TransferCallback
//Await completion of all in-progress transfers
int waitCycleCount = 0;
while(!mInProgressTransfers.isEmpty() && waitCycleCount < 10)
while(!mInProgressTransfers.isEmpty() && waitCycleCount < 30)
{
waitCycleCount++;

View File

@@ -0,0 +1,76 @@
/*
* *****************************************************************************
* Copyright (C) 2014-2020 Dennis Sheirer
*
* This program is free software: you can redistribute it and/or modify
* it under the terms of the GNU General Public License as published by
* the Free Software Foundation, either version 3 of the License, or
* (at your option) any later version.
*
* This program is distributed in the hope that it will be useful,
* but WITHOUT ANY WARRANTY; without even the implied warranty of
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
* GNU General Public License for more details.
*
* You should have received a copy of the GNU General Public License
* along with this program. If not, see <http://www.gnu.org/licenses/>
* ****************************************************************************
*/
package io.github.dsheirer.util;
import java.io.IOException;
import java.nio.file.DirectoryStream;
import java.nio.file.Files;
import java.nio.file.Path;
import java.util.Iterator;
/**
* File Utilities
*/
public class FileUtil
{
/**
* Recursively searches for the named file starting in the specified directory.
* @param directory to search for the file
* @return path to the file or null if the file can't be found
* @throws IOException if there is a file system error or if the file is not found
*/
public static Path findFile(Path directory, String name) throws IOException
{
if(Files.isDirectory(directory))
{
DirectoryStream<Path> stream = Files.newDirectoryStream(directory);
Iterator<Path> it = stream.iterator();
Path path = null;
while(it.hasNext())
{
path = it.next();
if(Files.isDirectory(path))
{
Path candidate = findFile(path, name);
if(candidate != null)
{
return candidate;
}
}
else
{
if(path.endsWith(name))
{
return path;
}
}
}
}
else
{
throw new IOException("A directory required for this method [" +
(directory != null ? directory.toString() : "null") + "]");
}
return null;
}
}

View File

@@ -0,0 +1,101 @@
/*
* *****************************************************************************
* Copyright (C) 2014-2020 Dennis Sheirer
*
* This program is free software: you can redistribute it and/or modify
* it under the terms of the GNU General Public License as published by
* the Free Software Foundation, either version 3 of the License, or
* (at your option) any later version.
*
* This program is distributed in the hope that it will be useful,
* but WITHOUT ANY WARRANTY; without even the implied warranty of
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
* GNU General Public License for more details.
*
* You should have received a copy of the GNU General Public License
* along with this program. If not, see <http://www.gnu.org/licenses/>
* ****************************************************************************
*/
package io.github.dsheirer.util;
import java.util.EnumSet;
import java.util.Locale;
public enum OSType
{
LINUX_32,
LINUX_64,
OSX_32,
OSX_64,
WINDOWS_32,
WINDOWS_64,
UNKNOWN;
public static EnumSet<OSType> WINDOWS_TYPES = EnumSet.of(WINDOWS_32, WINDOWS_64);
public static EnumSet<OSType> LINUX_TYPES = EnumSet.of(LINUX_32, LINUX_64, OSX_32, OSX_64);
/**
* Indicates if this enumeration entry is a Windows type
*/
public boolean isWindows()
{
return WINDOWS_TYPES.contains(this);
}
/**
* Indicates if this enumeration entry is a Linux type
*/
public boolean isLinux()
{
return LINUX_TYPES.contains(this);
}
/**
* Detects the current host operating system and architecture
*/
public static OSType getCurrentOSType()
{
String os = System.getProperty("os.name", "unknown").toLowerCase(Locale.ENGLISH);
String arch = System.getProperty("os.arch");
if(os.contains("win"))
{
if(arch.contains("64"))
{
return OSType.WINDOWS_64;
}
else
{
return OSType.WINDOWS_32;
}
}
if(os.contains("mac") || os.contains("darwin"))
{
if(arch.contains("64"))
{
return OSType.OSX_64;
}
else
{
return OSType.OSX_32;
}
}
if(os.contains("nux") || os.contains("nix") || os.contains("aix"))
{
if(arch.contains("64"))
{
return OSType.LINUX_64;
}
else
{
return OSType.LINUX_32;
}
}
return OSType.UNKNOWN;
}
}

View File

@@ -0,0 +1,158 @@
package io.github.dsheirer.util;
import org.rauschig.jarchivelib.ArchiveFormat;
import org.rauschig.jarchivelib.Archiver;
import org.rauschig.jarchivelib.ArchiverFactory;
import java.io.BufferedInputStream;
import java.io.File;
import java.io.FileInputStream;
import java.io.FileNotFoundException;
import java.io.FileOutputStream;
import java.io.IOException;
import java.nio.file.Path;
import java.util.ArrayList;
import java.util.List;
import java.util.zip.ZipEntry;
import java.util.zip.ZipOutputStream;
/**
* This utility compresses a list of files to standard ZIP format file.
* It is able to compress all sub files and sub directories, recursively.
*
* @author www.codejava.net
*/
public class ZipUtility
{
/**
* A constants for buffer size used to read/write data
*/
private static final int BUFFER_SIZE = 4096;
/**
* Compresses a list of files to a destination zip file
*
* @param listFiles A collection of files and directories
* @param destZipFile The path of the destination zip file
* @throws FileNotFoundException
* @throws IOException
*/
public void zip(List<File> listFiles, String destZipFile) throws FileNotFoundException,
IOException
{
ZipOutputStream zos = new ZipOutputStream(new FileOutputStream(destZipFile));
for(File file : listFiles)
{
if(file.isDirectory())
{
zipDirectory(file, file.getName(), zos);
}
else
{
zipFile(file, zos);
}
}
zos.flush();
zos.close();
}
/**
* Compresses files represented in an array of paths
*
* @param files a String array containing file paths
* @param destZipFile The path of the destination zip file
* @throws FileNotFoundException
* @throws IOException
*/
public void zip(String[] files, String destZipFile) throws FileNotFoundException, IOException
{
List<File> listFiles = new ArrayList<File>();
for(int i = 0; i < files.length; i++)
{
listFiles.add(new File(files[i]));
}
zip(listFiles, destZipFile);
}
/**
* Adds a directory to the current zip output stream
*
* @param folder the directory to be added
* @param parentFolder the path of parent directory
* @param zos the current zip output stream
* @throws FileNotFoundException
* @throws IOException
*/
private void zipDirectory(File folder, String parentFolder, ZipOutputStream zos) throws
FileNotFoundException, IOException
{
if(folder == null || folder.listFiles() == null)
{
return;
}
for(File file : folder.listFiles())
{
if(file.exists())
{
if(file.isDirectory())
{
zipDirectory(file, parentFolder + "/" + file.getName(), zos);
continue;
}
zos.putNextEntry(new ZipEntry(parentFolder + "/" + file.getName()));
BufferedInputStream bis = new BufferedInputStream(new FileInputStream(file));
long bytesRead = 0;
byte[] bytesIn = new byte[BUFFER_SIZE];
int read = 0;
while((read = bis.read(bytesIn)) != -1)
{
zos.write(bytesIn, 0, read);
bytesRead += read;
}
zos.closeEntry();
}
}
}
/**
* Adds a file to the current zip output stream
*
* @param file the file to be added
* @param zos the current zip output stream
* @throws FileNotFoundException
* @throws IOException
*/
private void zipFile(File file, ZipOutputStream zos) throws FileNotFoundException, IOException
{
zos.putNextEntry(new ZipEntry(file.getName()));
BufferedInputStream bis = new BufferedInputStream(new FileInputStream(file));
long bytesRead = 0;
byte[] bytesIn = new byte[BUFFER_SIZE];
int read = 0;
while((read = bis.read(bytesIn)) != -1)
{
zos.write(bytesIn, 0, read);
bytesRead += read;
}
zos.closeEntry();
}
/**
* Unzips the specified file into the directory where it's located
*
* @param zipFile to unzip
* @return path to where the file was unzipped.
* @throws IOException if there are errors unzipping
*/
public static Path unzip(Path zipFile) throws IOException
{
Path parent = zipFile.getParent();
Archiver archiver = ArchiverFactory.createArchiver(ArchiveFormat.ZIP);
archiver.extract(zipFile.toFile(), parent.toFile());
return parent;
}
}