mirror of
https://github.com/DSheirer/sdrtrunk.git
synced 2026-11-14 09:37:36 -05:00
#438 Adds feature for duplicate call audio detection and suppression for playback, recording and streaming. (#835)
Co-authored-by: Denny <denny@denny-desktop>
This commit is contained in:
@@ -81,6 +81,7 @@ public abstract class AbstractAudioModule extends Module implements IAudioSegmen
|
|||||||
{
|
{
|
||||||
mAudioSegment.completeProperty().set(true);
|
mAudioSegment.completeProperty().set(true);
|
||||||
mIdentifierUpdateNotificationBroadcaster.removeListener(mAudioSegment);
|
mIdentifierUpdateNotificationBroadcaster.removeListener(mAudioSegment);
|
||||||
|
mAudioSegment.decrementConsumerCount();
|
||||||
mAudioSegment = null;
|
mAudioSegment = null;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -103,6 +104,7 @@ public abstract class AbstractAudioModule extends Module implements IAudioSegmen
|
|||||||
if(mAudioSegment == null)
|
if(mAudioSegment == null)
|
||||||
{
|
{
|
||||||
mAudioSegment = new AudioSegment(mAliasList, getTimeslot());
|
mAudioSegment = new AudioSegment(mAliasList, getTimeslot());
|
||||||
|
mAudioSegment.incrementConsumerCount();
|
||||||
mAudioSegment.addIdentifiers(mIdentifierCollection.getIdentifiers());
|
mAudioSegment.addIdentifiers(mIdentifierCollection.getIdentifiers());
|
||||||
mIdentifierUpdateNotificationBroadcaster.addListener(mAudioSegment);
|
mIdentifierUpdateNotificationBroadcaster.addListener(mAudioSegment);
|
||||||
|
|
||||||
|
|||||||
@@ -65,6 +65,7 @@ public class AudioSegment implements Listener<IdentifierUpdateNotification>
|
|||||||
{
|
{
|
||||||
private final static Logger mLog = LoggerFactory.getLogger(AudioSegment.class);
|
private final static Logger mLog = LoggerFactory.getLogger(AudioSegment.class);
|
||||||
private BooleanProperty mComplete = new SimpleBooleanProperty(false);
|
private BooleanProperty mComplete = new SimpleBooleanProperty(false);
|
||||||
|
private BooleanProperty mDuplicate = new SimpleBooleanProperty(false);
|
||||||
private BooleanProperty mRecordAudio = new SimpleBooleanProperty(false);
|
private BooleanProperty mRecordAudio = new SimpleBooleanProperty(false);
|
||||||
private IntegerProperty mMonitorPriority = new SimpleIntegerProperty(Priority.DEFAULT_PRIORITY);
|
private IntegerProperty mMonitorPriority = new SimpleIntegerProperty(Priority.DEFAULT_PRIORITY);
|
||||||
private ObservableSet<BroadcastChannel> mBroadcastChannels = FXCollections.observableSet(new HashSet<>());
|
private ObservableSet<BroadcastChannel> mBroadcastChannels = FXCollections.observableSet(new HashSet<>());
|
||||||
@@ -74,6 +75,7 @@ public class AudioSegment implements Listener<IdentifierUpdateNotification>
|
|||||||
private AtomicInteger mConsumerCount = new AtomicInteger();
|
private AtomicInteger mConsumerCount = new AtomicInteger();
|
||||||
private AliasList mAliasList;
|
private AliasList mAliasList;
|
||||||
private long mStartTimestamp = System.currentTimeMillis();
|
private long mStartTimestamp = System.currentTimeMillis();
|
||||||
|
private long mSampleCount = 0;
|
||||||
private boolean mDisposing = false;
|
private boolean mDisposing = false;
|
||||||
private AudioSegment mLinkedAudioSegment;
|
private AudioSegment mLinkedAudioSegment;
|
||||||
private int mTimeslot;
|
private int mTimeslot;
|
||||||
@@ -107,6 +109,24 @@ public class AudioSegment implements Listener<IdentifierUpdateNotification>
|
|||||||
return mStartTimestamp;
|
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
|
* 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.
|
* additional audio or identifiers will be added to the segment.
|
||||||
@@ -118,6 +138,15 @@ public class AudioSegment implements Listener<IdentifierUpdateNotification>
|
|||||||
return mComplete;
|
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
|
* 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
|
* 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);
|
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
|
* Processes identifier update notifications and updates audio segment properties using the associated aliases
|
||||||
* from the alias list.
|
* from the alias list.
|
||||||
|
|||||||
@@ -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
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -127,7 +127,12 @@ public class AudioStreamingManager implements Listener<AudioSegment>
|
|||||||
{
|
{
|
||||||
audioSegment = it.next();
|
audioSegment = it.next();
|
||||||
|
|
||||||
if(audioSegment.completeProperty().get())
|
if(audioSegment.isDuplicate() && mUserPreferences.getDuplicateCallDetectionPreference().isDuplicateStreamingSuppressionEnabled())
|
||||||
|
{
|
||||||
|
it.remove();
|
||||||
|
audioSegment.decrementConsumerCount();
|
||||||
|
}
|
||||||
|
else if(audioSegment.completeProperty().get())
|
||||||
{
|
{
|
||||||
it.remove();
|
it.remove();
|
||||||
|
|
||||||
|
|||||||
@@ -381,8 +381,9 @@ public abstract class AudioOutput implements LineListener, Listener<IdentifierUp
|
|||||||
|
|
||||||
try
|
try
|
||||||
{
|
{
|
||||||
//Remove an assigned audio segment that's subsequently flagged as do not monitor before playback
|
//Remove an assigned segment that's subsequently flagged as duplicate or do not monitor before playback
|
||||||
if(mNextAudioSegment.isDoNotMonitor())
|
if(mNextAudioSegment.isDoNotMonitor() || (mNextAudioSegment.isDuplicate() &&
|
||||||
|
mUserPreferences.getDuplicateCallDetectionPreference().isDuplicatePlaybackSuppressionEnabled()))
|
||||||
{
|
{
|
||||||
mNextAudioSegment = null;
|
mNextAudioSegment = null;
|
||||||
|
|
||||||
@@ -456,9 +457,11 @@ public abstract class AudioOutput implements LineListener, Listener<IdentifierUp
|
|||||||
|
|
||||||
if(mCurrentAudioSegment != null)
|
if(mCurrentAudioSegment != null)
|
||||||
{
|
{
|
||||||
//Check for completed audio segment or a segment flagged as Do Not Monitor
|
//Check for completed audio segment or a segment flagged as duplicate or Do Not Monitor
|
||||||
if(mCurrentAudioSegment.isDoNotMonitor() || (mCurrentAudioSegment.completeProperty().get() &&
|
if(mCurrentAudioSegment.isDoNotMonitor() ||
|
||||||
mCurrentBufferIndex >= mCurrentAudioSegment.getAudioBufferCount()))
|
(mCurrentAudioSegment.isDuplicate() &&
|
||||||
|
mUserPreferences.getDuplicateCallDetectionPreference().isDuplicatePlaybackSuppressionEnabled()) ||
|
||||||
|
(mCurrentAudioSegment.completeProperty().get() && mCurrentBufferIndex >= mCurrentAudioSegment.getAudioBufferCount()))
|
||||||
{
|
{
|
||||||
if(mCurrentAudioSegment.isDoNotMonitor())
|
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.
|
//segment is a linked segment, ignore it so that we can close out the current segment.
|
||||||
while(mCurrentAudioSegment != null && (mNextAudioSegment == null || mNextAudioSegment.isLinked()) &&
|
while(mCurrentAudioSegment != null && (mNextAudioSegment == null || mNextAudioSegment.isLinked()) &&
|
||||||
mCurrentBufferIndex < mCurrentAudioSegment.getAudioBufferCount() &&
|
mCurrentBufferIndex < mCurrentAudioSegment.getAudioBufferCount() &&
|
||||||
!mCurrentAudioSegment.isDoNotMonitor())
|
!mCurrentAudioSegment.isDoNotMonitor() && !(mCurrentAudioSegment.isDuplicate() &&
|
||||||
|
mUserPreferences.getDuplicateCallDetectionPreference().isDuplicatePlaybackSuppressionEnabled()))
|
||||||
{
|
{
|
||||||
float[] audioBuffer = mCurrentAudioSegment.getAudioBuffers().get(mCurrentBufferIndex++);
|
float[] audioBuffer = mCurrentAudioSegment.getAudioBuffers().get(mCurrentBufferIndex++);
|
||||||
|
|
||||||
|
|||||||
@@ -27,20 +27,15 @@ import io.github.dsheirer.audio.IAudioController;
|
|||||||
import io.github.dsheirer.eventbus.MyEventBus;
|
import io.github.dsheirer.eventbus.MyEventBus;
|
||||||
import io.github.dsheirer.preference.PreferenceType;
|
import io.github.dsheirer.preference.PreferenceType;
|
||||||
import io.github.dsheirer.preference.UserPreferences;
|
import io.github.dsheirer.preference.UserPreferences;
|
||||||
import io.github.dsheirer.properties.SystemProperties;
|
|
||||||
import io.github.dsheirer.sample.Broadcaster;
|
import io.github.dsheirer.sample.Broadcaster;
|
||||||
import io.github.dsheirer.sample.Listener;
|
import io.github.dsheirer.sample.Listener;
|
||||||
import io.github.dsheirer.source.mixer.MixerChannel;
|
import io.github.dsheirer.source.mixer.MixerChannel;
|
||||||
import io.github.dsheirer.source.mixer.MixerChannelConfiguration;
|
import io.github.dsheirer.source.mixer.MixerChannelConfiguration;
|
||||||
import io.github.dsheirer.source.mixer.MixerManager;
|
|
||||||
import io.github.dsheirer.util.ThreadPool;
|
import io.github.dsheirer.util.ThreadPool;
|
||||||
import org.slf4j.Logger;
|
import org.slf4j.Logger;
|
||||||
import org.slf4j.LoggerFactory;
|
import org.slf4j.LoggerFactory;
|
||||||
|
|
||||||
import javax.sound.sampled.AudioSystem;
|
|
||||||
import javax.sound.sampled.Mixer;
|
|
||||||
import java.util.ArrayList;
|
import java.util.ArrayList;
|
||||||
import java.util.Collections;
|
|
||||||
import java.util.Comparator;
|
import java.util.Comparator;
|
||||||
import java.util.Iterator;
|
import java.util.Iterator;
|
||||||
import java.util.List;
|
import java.util.List;
|
||||||
@@ -123,7 +118,12 @@ public class AudioPlaybackManager implements Listener<AudioSegment>, IAudioContr
|
|||||||
|
|
||||||
while(newSegment != null)
|
while(newSegment != null)
|
||||||
{
|
{
|
||||||
if(newSegment.hasAudio())
|
if(newSegment.isDuplicate() &&
|
||||||
|
mUserPreferences.getDuplicateCallDetectionPreference().isDuplicatePlaybackSuppressionEnabled())
|
||||||
|
{
|
||||||
|
newSegment.decrementConsumerCount();
|
||||||
|
}
|
||||||
|
else if(newSegment.hasAudio())
|
||||||
{
|
{
|
||||||
mAudioSegments.add(newSegment);
|
mAudioSegments.add(newSegment);
|
||||||
}
|
}
|
||||||
@@ -146,7 +146,13 @@ public class AudioPlaybackManager implements Listener<AudioSegment>, IAudioContr
|
|||||||
{
|
{
|
||||||
audioSegment = it.next();
|
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
|
//Queue it up for replay
|
||||||
it.remove();
|
it.remove();
|
||||||
@@ -174,7 +180,8 @@ public class AudioPlaybackManager implements Listener<AudioSegment>, IAudioContr
|
|||||||
{
|
{
|
||||||
audioSegment = it.next();
|
audioSegment = it.next();
|
||||||
|
|
||||||
if(audioSegment.isDoNotMonitor())
|
if(audioSegment.isDoNotMonitor() || (audioSegment.isDuplicate() &&
|
||||||
|
mUserPreferences.getDuplicateCallDetectionPreference().isDuplicatePlaybackSuppressionEnabled()))
|
||||||
{
|
{
|
||||||
it.remove();
|
it.remove();
|
||||||
audioSegment.decrementConsumerCount();
|
audioSegment.decrementConsumerCount();
|
||||||
@@ -235,7 +242,8 @@ public class AudioPlaybackManager implements Listener<AudioSegment>, IAudioContr
|
|||||||
{
|
{
|
||||||
audioSegment = it.next();
|
audioSegment = it.next();
|
||||||
|
|
||||||
if(audioSegment.completeProperty().get())
|
if(audioSegment.completeProperty().get() || (audioSegment.isDuplicate() &&
|
||||||
|
mUserPreferences.getDuplicateCallDetectionPreference().isDuplicatePlaybackSuppressionEnabled()))
|
||||||
{
|
{
|
||||||
it.remove();
|
it.remove();
|
||||||
audioSegment.decrementConsumerCount();
|
audioSegment.decrementConsumerCount();
|
||||||
|
|||||||
@@ -49,6 +49,8 @@ import io.github.dsheirer.source.Source;
|
|||||||
import io.github.dsheirer.source.SourceEvent;
|
import io.github.dsheirer.source.SourceEvent;
|
||||||
import io.github.dsheirer.source.SourceException;
|
import io.github.dsheirer.source.SourceException;
|
||||||
import io.github.dsheirer.source.SourceManager;
|
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 javafx.application.Platform;
|
||||||
import org.slf4j.Logger;
|
import org.slf4j.Logger;
|
||||||
import org.slf4j.LoggerFactory;
|
import org.slf4j.LoggerFactory;
|
||||||
@@ -180,7 +182,23 @@ public class ChannelProcessingManager implements Listener<ChannelEvent>
|
|||||||
}
|
}
|
||||||
catch(ChannelException ce)
|
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;
|
break;
|
||||||
|
|||||||
@@ -21,6 +21,7 @@ package io.github.dsheirer.gui;
|
|||||||
import com.jidesoft.plaf.LookAndFeelFactory;
|
import com.jidesoft.plaf.LookAndFeelFactory;
|
||||||
import com.jidesoft.swing.JideSplitPane;
|
import com.jidesoft.swing.JideSplitPane;
|
||||||
import io.github.dsheirer.alias.AliasModel;
|
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.AudioStreamingManager;
|
||||||
import io.github.dsheirer.audio.broadcast.BroadcastFormat;
|
import io.github.dsheirer.audio.broadcast.BroadcastFormat;
|
||||||
import io.github.dsheirer.audio.broadcast.BroadcastStatusPanel;
|
import io.github.dsheirer.audio.broadcast.BroadcastStatusPanel;
|
||||||
@@ -180,6 +181,9 @@ public class SDRTrunk implements Listener<TunerEvent>
|
|||||||
mUserPreferences);
|
mUserPreferences);
|
||||||
mAudioStreamingManager.start();
|
mAudioStreamingManager.start();
|
||||||
|
|
||||||
|
DuplicateCallDetector duplicateCallDetector = new DuplicateCallDetector(mUserPreferences);
|
||||||
|
|
||||||
|
mPlaylistManager.getChannelProcessingManager().addAudioSegmentListener(duplicateCallDetector);
|
||||||
mPlaylistManager.getChannelProcessingManager().addAudioSegmentListener(audioPlaybackManager);
|
mPlaylistManager.getChannelProcessingManager().addAudioSegmentListener(audioPlaybackManager);
|
||||||
mPlaylistManager.getChannelProcessingManager().addAudioSegmentListener(mAudioRecordingManager);
|
mPlaylistManager.getChannelProcessingManager().addAudioSegmentListener(mAudioRecordingManager);
|
||||||
mPlaylistManager.getChannelProcessingManager().addAudioSegmentListener(mAudioStreamingManager);
|
mPlaylistManager.getChannelProcessingManager().addAudioSegmentListener(mAudioStreamingManager);
|
||||||
|
|||||||
@@ -21,6 +21,7 @@ package io.github.dsheirer.gui.preference;
|
|||||||
|
|
||||||
import io.github.dsheirer.gui.preference.decoder.JmbeLibraryPreferenceEditor;
|
import io.github.dsheirer.gui.preference.decoder.JmbeLibraryPreferenceEditor;
|
||||||
import io.github.dsheirer.gui.preference.directory.DirectoryPreferenceEditor;
|
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.playback.PlaybackPreferenceEditor;
|
||||||
import io.github.dsheirer.gui.preference.record.RecordPreferenceEditor;
|
import io.github.dsheirer.gui.preference.record.RecordPreferenceEditor;
|
||||||
import io.github.dsheirer.gui.preference.tuner.ChannelMultipleFrequencyPreferenceEditor;
|
import io.github.dsheirer.gui.preference.tuner.ChannelMultipleFrequencyPreferenceEditor;
|
||||||
@@ -37,6 +38,8 @@ public class PreferenceEditorFactory
|
|||||||
{
|
{
|
||||||
switch(preferenceEditorType)
|
switch(preferenceEditorType)
|
||||||
{
|
{
|
||||||
|
case AUDIO_DUPLICATE_CALL_DETECTION:
|
||||||
|
return new DuplicateCallPreferenceEditor(userPreferences);
|
||||||
case AUDIO_OUTPUT:
|
case AUDIO_OUTPUT:
|
||||||
return new PlaybackPreferenceEditor(userPreferences);
|
return new PlaybackPreferenceEditor(userPreferences);
|
||||||
case AUDIO_RECORD:
|
case AUDIO_RECORD:
|
||||||
|
|||||||
@@ -29,6 +29,7 @@ public enum PreferenceEditorType
|
|||||||
JMBE_LIBRARY("JMBE Audio Library"),
|
JMBE_LIBRARY("JMBE Audio Library"),
|
||||||
AUDIO_RECORD("Record"),
|
AUDIO_RECORD("Record"),
|
||||||
AUDIO_OUTPUT("Output/Tones"),
|
AUDIO_OUTPUT("Output/Tones"),
|
||||||
|
AUDIO_DUPLICATE_CALL_DETECTION("Duplicate Calls"),
|
||||||
SOURCE_CHANNEL_MULTIPLE_FREQUENCY("Channel - Multiple Frequency"),
|
SOURCE_CHANNEL_MULTIPLE_FREQUENCY("Channel - Multiple Frequency"),
|
||||||
SOURCE_TUNER_CHANNELIZER("Tuner Channelizer"),
|
SOURCE_TUNER_CHANNELIZER("Tuner Channelizer"),
|
||||||
TALKGROUP_FORMAT("Talkgroup & Radio ID"),
|
TALKGROUP_FORMAT("Talkgroup & Radio ID"),
|
||||||
|
|||||||
@@ -167,6 +167,7 @@ public class UserPreferencesEditor extends BorderPane
|
|||||||
TreeItem<String> treeRoot = new TreeItem<>("Root node");
|
TreeItem<String> treeRoot = new TreeItem<>("Root node");
|
||||||
|
|
||||||
TreeItem<String> audioItem = new TreeItem<>("Audio");
|
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_OUTPUT));
|
||||||
audioItem.getChildren().add(new TreeItem(PreferenceEditorType.AUDIO_RECORD));
|
audioItem.getChildren().add(new TreeItem(PreferenceEditorType.AUDIO_RECORD));
|
||||||
treeRoot.getChildren().add(audioItem);
|
treeRoot.getChildren().add(audioItem);
|
||||||
|
|||||||
@@ -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;
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -26,6 +26,7 @@ public enum PreferenceType
|
|||||||
{
|
{
|
||||||
DECODE_EVENT,
|
DECODE_EVENT,
|
||||||
DIRECTORY,
|
DIRECTORY,
|
||||||
|
DUPLICATE_CALL_DETECTION,
|
||||||
JMBE_LIBRARY,
|
JMBE_LIBRARY,
|
||||||
MULTI_FREQUENCY,
|
MULTI_FREQUENCY,
|
||||||
PLAYLIST,
|
PLAYLIST,
|
||||||
|
|||||||
@@ -22,6 +22,7 @@ package io.github.dsheirer.preference;
|
|||||||
import io.github.dsheirer.eventbus.MyEventBus;
|
import io.github.dsheirer.eventbus.MyEventBus;
|
||||||
import io.github.dsheirer.preference.decoder.JmbeLibraryPreference;
|
import io.github.dsheirer.preference.decoder.JmbeLibraryPreference;
|
||||||
import io.github.dsheirer.preference.directory.DirectoryPreference;
|
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.event.DecodeEventPreference;
|
||||||
import io.github.dsheirer.preference.identifier.TalkgroupFormatPreference;
|
import io.github.dsheirer.preference.identifier.TalkgroupFormatPreference;
|
||||||
import io.github.dsheirer.preference.javafx.JavaFxPreferences;
|
import io.github.dsheirer.preference.javafx.JavaFxPreferences;
|
||||||
@@ -52,16 +53,18 @@ import io.github.dsheirer.sample.Listener;
|
|||||||
*/
|
*/
|
||||||
public class UserPreferences implements Listener<PreferenceType>
|
public class UserPreferences implements Listener<PreferenceType>
|
||||||
{
|
{
|
||||||
private JmbeLibraryPreference mJmbeLibraryPreference;
|
private ChannelMultiFrequencyPreference mChannelMultiFrequencyPreference;
|
||||||
private DecodeEventPreference mDecodeEventPreference;
|
private DecodeEventPreference mDecodeEventPreference;
|
||||||
private DirectoryPreference mDirectoryPreference;
|
private DirectoryPreference mDirectoryPreference;
|
||||||
private ChannelMultiFrequencyPreference mChannelMultiFrequencyPreference;
|
private DuplicateCallDetectionPreference mDuplicateCallDetectionPreference;
|
||||||
|
private JmbeLibraryPreference mJmbeLibraryPreference;
|
||||||
private PlaybackPreference mPlaybackPreference;
|
private PlaybackPreference mPlaybackPreference;
|
||||||
private PlaylistPreference mPlaylistPreference;
|
private PlaylistPreference mPlaylistPreference;
|
||||||
private RadioReferencePreference mRadioReferencePreference;
|
private RadioReferencePreference mRadioReferencePreference;
|
||||||
private RecordPreference mRecordPreference;
|
private RecordPreference mRecordPreference;
|
||||||
private TalkgroupFormatPreference mTalkgroupFormatPreference;
|
private TalkgroupFormatPreference mTalkgroupFormatPreference;
|
||||||
private TunerPreference mTunerPreference;
|
private TunerPreference mTunerPreference;
|
||||||
|
|
||||||
private SwingPreference mSwingPreference = new SwingPreference();
|
private SwingPreference mSwingPreference = new SwingPreference();
|
||||||
private JavaFxPreferences mJavaFxPreferences = new JavaFxPreferences();
|
private JavaFxPreferences mJavaFxPreferences = new JavaFxPreferences();
|
||||||
|
|
||||||
@@ -170,15 +173,24 @@ public class UserPreferences implements Listener<PreferenceType>
|
|||||||
return mSwingPreference;
|
return mSwingPreference;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Duplicate call detection preferences
|
||||||
|
*/
|
||||||
|
public DuplicateCallDetectionPreference getDuplicateCallDetectionPreference()
|
||||||
|
{
|
||||||
|
return mDuplicateCallDetectionPreference;
|
||||||
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Loads the managed preferences
|
* Loads the managed preferences
|
||||||
*/
|
*/
|
||||||
private void loadPreferenceTypes()
|
private void loadPreferenceTypes()
|
||||||
{
|
{
|
||||||
mDecodeEventPreference = new DecodeEventPreference(this::receive);
|
|
||||||
mJmbeLibraryPreference = new JmbeLibraryPreference(this::receive);
|
|
||||||
mDirectoryPreference = new DirectoryPreference(this::receive);
|
|
||||||
mChannelMultiFrequencyPreference = new ChannelMultiFrequencyPreference(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);
|
mPlaybackPreference = new PlaybackPreference(this::receive);
|
||||||
mPlaylistPreference = new PlaylistPreference(this::receive, mDirectoryPreference);
|
mPlaylistPreference = new PlaylistPreference(this::receive, mDirectoryPreference);
|
||||||
mRadioReferencePreference = new RadioReferencePreference(this::receive);
|
mRadioReferencePreference = new RadioReferencePreference(this::receive);
|
||||||
|
|||||||
@@ -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();;
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -139,21 +139,28 @@ public class AudioRecordingManager implements Listener<AudioSegment>
|
|||||||
|
|
||||||
while(audioSegment != null)
|
while(audioSegment != null)
|
||||||
{
|
{
|
||||||
Path path = getAudioRecordingPath(audioSegment.getIdentifierCollection(), recordFormat);
|
if(audioSegment.isDuplicate() && mUserPreferences.getDuplicateCallDetectionPreference().isDuplicateRecordingSuppressionEnabled())
|
||||||
|
|
||||||
try
|
|
||||||
{
|
{
|
||||||
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();
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
Reference in New Issue
Block a user