diff --git a/src/main/java/io/github/dsheirer/audio/AbstractAudioModule.java b/src/main/java/io/github/dsheirer/audio/AbstractAudioModule.java index 376f76c7..f4bc74c7 100644 --- a/src/main/java/io/github/dsheirer/audio/AbstractAudioModule.java +++ b/src/main/java/io/github/dsheirer/audio/AbstractAudioModule.java @@ -81,6 +81,7 @@ public abstract class AbstractAudioModule extends Module implements IAudioSegmen { mAudioSegment.completeProperty().set(true); mIdentifierUpdateNotificationBroadcaster.removeListener(mAudioSegment); + mAudioSegment.decrementConsumerCount(); mAudioSegment = null; } } @@ -103,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); diff --git a/src/main/java/io/github/dsheirer/audio/AudioSegment.java b/src/main/java/io/github/dsheirer/audio/AudioSegment.java index 5fd20ef7..1f40ecfa 100644 --- a/src/main/java/io/github/dsheirer/audio/AudioSegment.java +++ b/src/main/java/io/github/dsheirer/audio/AudioSegment.java @@ -65,6 +65,7 @@ public class AudioSegment implements Listener { 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 mBroadcastChannels = FXCollections.observableSet(new HashSet<>()); @@ -74,6 +75,7 @@ public class AudioSegment implements Listener 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 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 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 } mAudioBuffers.add(audioBuffer); + mSampleCount += audioBuffer.length; } /** @@ -396,6 +426,23 @@ public class AudioSegment implements Listener } } + /** + * 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. diff --git a/src/main/java/io/github/dsheirer/audio/DuplicateCallDetector.java b/src/main/java/io/github/dsheirer/audio/DuplicateCallDetector.java new file mode 100644 index 00000000..5f838502 --- /dev/null +++ b/src/main/java/io/github/dsheirer/audio/DuplicateCallDetector.java @@ -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 + * **************************************************************************** + */ + +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 +{ + private final static Logger mLog = LoggerFactory.getLogger(DuplicateCallDetector.class); + private DuplicateCallDetectionPreference mDuplicateCallDetectionPreference; + private Map 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 mAudioSegmentQueue = new LinkedTransferQueue<>(); + private List 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 to1 = segment1.getIdentifierCollection().getIdentifiers(Role.TO); + List to2 = segment2.getIdentifierCollection().getIdentifiers(Role.TO); + + if(isDuplicate(to1, to2)) + { + return true; + } + } + + if(mDuplicateCallDetectionPreference.isDuplicateCallDetectionByRadioEnabled()) + { + //Step 2 check for duplicate FROM values + List from1 = segment1.getIdentifierCollection().getIdentifiers(Role.FROM); + List 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 identifiers1, List 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 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 + } + } + } + } + } + } +} diff --git a/src/main/java/io/github/dsheirer/audio/broadcast/AudioStreamingManager.java b/src/main/java/io/github/dsheirer/audio/broadcast/AudioStreamingManager.java index dbd78444..352860ac 100644 --- a/src/main/java/io/github/dsheirer/audio/broadcast/AudioStreamingManager.java +++ b/src/main/java/io/github/dsheirer/audio/broadcast/AudioStreamingManager.java @@ -127,7 +127,12 @@ public class AudioStreamingManager implements Listener { audioSegment = it.next(); - if(audioSegment.completeProperty().get()) + if(audioSegment.isDuplicate() && mUserPreferences.getDuplicateCallDetectionPreference().isDuplicateStreamingSuppressionEnabled()) + { + it.remove(); + audioSegment.decrementConsumerCount(); + } + else if(audioSegment.completeProperty().get()) { it.remove(); diff --git a/src/main/java/io/github/dsheirer/audio/playback/AudioOutput.java b/src/main/java/io/github/dsheirer/audio/playback/AudioOutput.java index b3aea3c6..42d84de5 100644 --- a/src/main/java/io/github/dsheirer/audio/playback/AudioOutput.java +++ b/src/main/java/io/github/dsheirer/audio/playback/AudioOutput.java @@ -381,8 +381,9 @@ public abstract class AudioOutput implements LineListener, Listener= 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, 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, 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, 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, IAudioContr { audioSegment = it.next(); - if(audioSegment.completeProperty().get()) + if(audioSegment.completeProperty().get() || (audioSegment.isDuplicate() && + mUserPreferences.getDuplicateCallDetectionPreference().isDuplicatePlaybackSuppressionEnabled())) { it.remove(); audioSegment.decrementConsumerCount(); diff --git a/src/main/java/io/github/dsheirer/controller/channel/ChannelProcessingManager.java b/src/main/java/io/github/dsheirer/controller/channel/ChannelProcessingManager.java index e5a61f98..02ff6c73 100644 --- a/src/main/java/io/github/dsheirer/controller/channel/ChannelProcessingManager.java +++ b/src/main/java/io/github/dsheirer/controller/channel/ChannelProcessingManager.java @@ -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 } 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 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; diff --git a/src/main/java/io/github/dsheirer/gui/SDRTrunk.java b/src/main/java/io/github/dsheirer/gui/SDRTrunk.java index 5e3e6e6b..030d0d04 100644 --- a/src/main/java/io/github/dsheirer/gui/SDRTrunk.java +++ b/src/main/java/io/github/dsheirer/gui/SDRTrunk.java @@ -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 mUserPreferences); mAudioStreamingManager.start(); + DuplicateCallDetector duplicateCallDetector = new DuplicateCallDetector(mUserPreferences); + + mPlaylistManager.getChannelProcessingManager().addAudioSegmentListener(duplicateCallDetector); mPlaylistManager.getChannelProcessingManager().addAudioSegmentListener(audioPlaybackManager); mPlaylistManager.getChannelProcessingManager().addAudioSegmentListener(mAudioRecordingManager); mPlaylistManager.getChannelProcessingManager().addAudioSegmentListener(mAudioStreamingManager); diff --git a/src/main/java/io/github/dsheirer/gui/preference/PreferenceEditorFactory.java b/src/main/java/io/github/dsheirer/gui/preference/PreferenceEditorFactory.java index ac978a8c..074c2a4f 100644 --- a/src/main/java/io/github/dsheirer/gui/preference/PreferenceEditorFactory.java +++ b/src/main/java/io/github/dsheirer/gui/preference/PreferenceEditorFactory.java @@ -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: diff --git a/src/main/java/io/github/dsheirer/gui/preference/PreferenceEditorType.java b/src/main/java/io/github/dsheirer/gui/preference/PreferenceEditorType.java index cb6f348e..4b5eda57 100644 --- a/src/main/java/io/github/dsheirer/gui/preference/PreferenceEditorType.java +++ b/src/main/java/io/github/dsheirer/gui/preference/PreferenceEditorType.java @@ -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"), diff --git a/src/main/java/io/github/dsheirer/gui/preference/UserPreferencesEditor.java b/src/main/java/io/github/dsheirer/gui/preference/UserPreferencesEditor.java index 00f4562b..07f72310 100644 --- a/src/main/java/io/github/dsheirer/gui/preference/UserPreferencesEditor.java +++ b/src/main/java/io/github/dsheirer/gui/preference/UserPreferencesEditor.java @@ -167,6 +167,7 @@ public class UserPreferencesEditor extends BorderPane TreeItem treeRoot = new TreeItem<>("Root node"); TreeItem 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); diff --git a/src/main/java/io/github/dsheirer/gui/preference/duplicate/DuplicateCallPreferenceEditor.java b/src/main/java/io/github/dsheirer/gui/preference/duplicate/DuplicateCallPreferenceEditor.java new file mode 100644 index 00000000..f04fe190 --- /dev/null +++ b/src/main/java/io/github/dsheirer/gui/preference/duplicate/DuplicateCallPreferenceEditor.java @@ -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 + * **************************************************************************** + */ + +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; + } +} diff --git a/src/main/java/io/github/dsheirer/preference/PreferenceType.java b/src/main/java/io/github/dsheirer/preference/PreferenceType.java index 5e3d57cb..0e00518f 100644 --- a/src/main/java/io/github/dsheirer/preference/PreferenceType.java +++ b/src/main/java/io/github/dsheirer/preference/PreferenceType.java @@ -26,6 +26,7 @@ public enum PreferenceType { DECODE_EVENT, DIRECTORY, + DUPLICATE_CALL_DETECTION, JMBE_LIBRARY, MULTI_FREQUENCY, PLAYLIST, diff --git a/src/main/java/io/github/dsheirer/preference/UserPreferences.java b/src/main/java/io/github/dsheirer/preference/UserPreferences.java index 61ab1529..0181d3bf 100644 --- a/src/main/java/io/github/dsheirer/preference/UserPreferences.java +++ b/src/main/java/io/github/dsheirer/preference/UserPreferences.java @@ -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 { - 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 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); diff --git a/src/main/java/io/github/dsheirer/preference/duplicate/DuplicateCallDetectionPreference.java b/src/main/java/io/github/dsheirer/preference/duplicate/DuplicateCallDetectionPreference.java new file mode 100644 index 00000000..74749fe3 --- /dev/null +++ b/src/main/java/io/github/dsheirer/preference/duplicate/DuplicateCallDetectionPreference.java @@ -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 + * **************************************************************************** + */ + +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 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();; + } +} diff --git a/src/main/java/io/github/dsheirer/record/AudioRecordingManager.java b/src/main/java/io/github/dsheirer/record/AudioRecordingManager.java index 9a0d623b..b4b882fd 100644 --- a/src/main/java/io/github/dsheirer/record/AudioRecordingManager.java +++ b/src/main/java/io/github/dsheirer/record/AudioRecordingManager.java @@ -139,21 +139,28 @@ public class AudioRecordingManager implements Listener 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(); } }