Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

1772 Patch Group Streaming Preference (Patch Group vs Individual Talkgroups) #1773

Merged
merged 1 commit into from
Dec 17, 2023
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
19 changes: 9 additions & 10 deletions src/main/java/io/github/dsheirer/audio/DuplicateCallDetector.java
Original file line number Diff line number Diff line change
@@ -1,6 +1,6 @@
/*
* *****************************************************************************
* Copyright (C) 2014-2020 Dennis Sheirer
* Copyright (C) 2014-2023 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
Expand Down Expand Up @@ -28,12 +28,9 @@
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.preference.duplicate.CallManagementPreference;
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;
Expand All @@ -42,6 +39,8 @@
import java.util.concurrent.ScheduledFuture;
import java.util.concurrent.TimeUnit;
import java.util.concurrent.atomic.AtomicBoolean;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;

/**
* Detects duplicate calls that occur within the same system. This detector is thread safe for the receive() method.
Expand All @@ -52,18 +51,18 @@
public class DuplicateCallDetector implements Listener<AudioSegment>
{
private final static Logger mLog = LoggerFactory.getLogger(DuplicateCallDetector.class);
private DuplicateCallDetectionPreference mDuplicateCallDetectionPreference;
private CallManagementPreference mCallManagementPreference;
private Map<String,SystemDuplicateCallDetector> mDetectorMap = new HashMap();

public DuplicateCallDetector(UserPreferences userPreferences)
{
mDuplicateCallDetectionPreference = userPreferences.getDuplicateCallDetectionPreference();
mCallManagementPreference = userPreferences.getDuplicateCallDetectionPreference();
}

@Override
public void receive(AudioSegment audioSegment)
{
if(mDuplicateCallDetectionPreference.isDuplicateCallDetectionEnabled())
if(mCallManagementPreference.isDuplicateCallDetectionEnabled())
{
Identifier identifier = audioSegment.getIdentifierCollection()
.getIdentifier(IdentifierClass.CONFIGURATION, Form.SYSTEM, Role.ANY);
Expand Down Expand Up @@ -137,7 +136,7 @@ private void stopMonitoring()
*/
private boolean isDuplicate(AudioSegment segment1, AudioSegment segment2)
{
if(mDuplicateCallDetectionPreference.isDuplicateCallDetectionByTalkgroupEnabled())
if(mCallManagementPreference.isDuplicateCallDetectionByTalkgroupEnabled())
{
//Step 1 check for duplicate TO values
List<Identifier> to1 = segment1.getIdentifierCollection().getIdentifiers(Role.TO);
Expand All @@ -149,7 +148,7 @@ private boolean isDuplicate(AudioSegment segment1, AudioSegment segment2)
}
}

if(mDuplicateCallDetectionPreference.isDuplicateCallDetectionByRadioEnabled())
if(mCallManagementPreference.isDuplicateCallDetectionByRadioEnabled())
{
//Step 2 check for duplicate FROM values
List<Identifier> from1 = segment1.getIdentifierCollection().getIdentifiers(Role.FROM);
Expand Down
Original file line number Diff line number Diff line change
@@ -1,6 +1,6 @@
/*
* *****************************************************************************
* Copyright (C) 2014-2022 Dennis Sheirer
* Copyright (C) 2014-2023 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
Expand All @@ -19,25 +19,34 @@

package io.github.dsheirer.audio.broadcast;

import io.github.dsheirer.alias.Alias;
import io.github.dsheirer.alias.AliasList;
import io.github.dsheirer.alias.id.broadcast.BroadcastChannel;
import io.github.dsheirer.audio.AudioSegment;
import io.github.dsheirer.identifier.Identifier;
import io.github.dsheirer.identifier.IdentifierCollection;
import io.github.dsheirer.identifier.MutableIdentifierCollection;
import io.github.dsheirer.identifier.Role;
import io.github.dsheirer.identifier.patch.PatchGroup;
import io.github.dsheirer.identifier.patch.PatchGroupIdentifier;
import io.github.dsheirer.preference.UserPreferences;
import io.github.dsheirer.record.AudioSegmentRecorder;
import io.github.dsheirer.record.RecordFormat;
import io.github.dsheirer.sample.Listener;
import io.github.dsheirer.util.ThreadPool;
import io.github.dsheirer.util.TimeStamp;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;

import java.io.IOException;
import java.nio.file.Path;
import java.util.ArrayList;
import java.util.HashSet;
import java.util.Iterator;
import java.util.List;
import java.util.Set;
import java.util.concurrent.LinkedTransferQueue;
import java.util.concurrent.ScheduledFuture;
import java.util.concurrent.TimeUnit;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;

/**
* Audio streaming manager monitors audio segments through completion and creates temporary streaming recordings on
Expand Down Expand Up @@ -138,29 +147,60 @@ else if(audioSegment.completeProperty().get())

if(mAudioRecordingListener != null && audioSegment.hasBroadcastChannels())
{
Path path = getTemporaryRecordingPath();
long length = 0;
IdentifierCollection identifiers =
new IdentifierCollection(audioSegment.getIdentifierCollection().getIdentifiers());

for(float[] audioBuffer: audioSegment.getAudioBuffers())
if(identifiers.getToIdentifier() instanceof PatchGroupIdentifier patchGroupIdentifier)
{
length += audioBuffer.length;
}
if(mUserPreferences.getDuplicateCallDetectionPreference()
.getPatchGroupStreamingOption() == PatchGroupStreamingOption.TALKGROUPS)
{
//Decompose the patch group into the individual (patched) talkgroups and process the audio
//segment for each patched talkgroup.
PatchGroup patchGroup = patchGroupIdentifier.getValue();

length /= 8; //Sample rate is 8000 samples per second, or 8 samples per millisecond.
List<Identifier> ids = new ArrayList<>();
ids.addAll(patchGroup.getPatchedTalkgroupIdentifiers());
ids.addAll(patchGroup.getPatchedRadioIdentifiers());

try
{
AudioSegmentRecorder.record(audioSegment, path, RecordFormat.MP3, mUserPreferences);
IdentifierCollection identifierCollectionCopy =
new IdentifierCollection(audioSegment.getIdentifierCollection().getIdentifiers());
//If there are no patched radios/talkgroups, override user preference and stream as a patch group
if(ids.isEmpty() || audioSegment.getAliasList() == null)
{
processAudioSegment(audioSegment, identifiers, audioSegment.getBroadcastChannels());
}
else
{
AliasList aliasList = audioSegment.getAliasList();

for(Identifier identifier: ids)
{
List<Alias> aliases = aliasList.getAliases(identifier);
Set<BroadcastChannel> broadcastChannels = new HashSet<>();
for(Alias alias: aliases)
{
broadcastChannels.addAll(alias.getBroadcastChannels());
}

AudioRecording audioRecording = new AudioRecording(path, audioSegment.getBroadcastChannels(),
identifierCollectionCopy, audioSegment.getStartTimestamp(), length);
mAudioRecordingListener.receive(audioRecording);
if(!broadcastChannels.isEmpty())
{
MutableIdentifierCollection decomposedIdentifiers =
new MutableIdentifierCollection(identifiers.getIdentifiers());
//Remove patch group TO identifier & replace with the patched talkgroup/radio
decomposedIdentifiers.remove(Role.TO);
decomposedIdentifiers.update(identifier);
processAudioSegment(audioSegment, decomposedIdentifiers, broadcastChannels);
}
}
}
}
else
{
processAudioSegment(audioSegment, identifiers, audioSegment.getBroadcastChannels());
}
}
catch(IOException ioe)
else
{
mLog.error("Error recording temporary stream MP3");
processAudioSegment(audioSegment, identifiers, audioSegment.getBroadcastChannels());
}
}

Expand All @@ -169,6 +209,40 @@ else if(audioSegment.completeProperty().get())
}
}

/**
* Processes an audio segment for streaming by creating a temporary MP3 recording and submitting the recording
* to the specific broadcast channel(s).
* @param audioSegment to process for streaming
* @param identifierCollection to use for the streamed audio recording.
* @param broadcastChannels to receive the audio recording
*/
private void processAudioSegment(AudioSegment audioSegment, IdentifierCollection identifierCollection,
Set<BroadcastChannel> broadcastChannels)
{
Path path = getTemporaryRecordingPath();
long length = 0;

for(float[] audioBuffer: audioSegment.getAudioBuffers())
{
length += audioBuffer.length;
}

length /= 8; //Sample rate is 8000 samples per second, or 8 samples per millisecond.

try
{
AudioSegmentRecorder.record(audioSegment, path, RecordFormat.MP3, mUserPreferences, identifierCollection);

AudioRecording audioRecording = new AudioRecording(path, broadcastChannels, identifierCollection,
audioSegment.getStartTimestamp(), length);
mAudioRecordingListener.receive(audioRecording);
}
catch(IOException ioe)
{
mLog.error("Error recording temporary stream MP3");
}
}

/**
* Creates a temporary streaming recording file path
*/
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,48 @@
/*
* *****************************************************************************
* Copyright (C) 2014-2023 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.broadcast;

/**
* Options for streaming management of patch groups. These options are used when streaming an audio call that is
* tagged to a patch group. The audio will either be streamed once and identified as the patch group, or it will
* be streamed multiple times, once for each individual patched talkgroup.
*/
public enum PatchGroupStreamingOption
{
PATCH_GROUP("Patch Group"),
TALKGROUPS("Individual Talkgroups");

private String mLabel;

/**
* Constructs an instance
* @param label to display
*/
PatchGroupStreamingOption(String label)
{
mLabel = label;
}

@Override
public String toString()
{
return mLabel;
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -125,7 +125,7 @@ public static void convert(MBECallSequence callSequence, Path outputPath)

try
{
AudioSegmentRecorder.recordWAVE(audioSegment, outputPath);
AudioSegmentRecorder.recordWAVE(audioSegment, outputPath, audioSegment.getIdentifierCollection());
}
catch(IOException ioe)
{
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -21,9 +21,9 @@

import io.github.dsheirer.gui.preference.application.ApplicationPreferenceEditor;
import io.github.dsheirer.gui.preference.calibration.VectorCalibrationPreferenceEditor;
import io.github.dsheirer.gui.preference.call.CallManagementPreferenceEditor;
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.mp3.MP3PreferenceEditor;
import io.github.dsheirer.gui.preference.playback.PlaybackPreferenceEditor;
import io.github.dsheirer.gui.preference.record.RecordPreferenceEditor;
Expand All @@ -42,8 +42,8 @@ public static Node getEditor(PreferenceEditorType preferenceEditorType, UserPref
{
case APPLICATION:
return new ApplicationPreferenceEditor(userPreferences);
case AUDIO_DUPLICATE_CALL_DETECTION:
return new DuplicateCallPreferenceEditor(userPreferences);
case AUDIO_CALL_MANAGEMENT:
return new CallManagementPreferenceEditor(userPreferences);
case AUDIO_MP3:
return new MP3PreferenceEditor(userPreferences);
case AUDIO_OUTPUT:
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -31,7 +31,7 @@ public enum PreferenceEditorType
AUDIO_MP3("MP3"),
AUDIO_RECORD("Record"),
AUDIO_OUTPUT("Output/Tones"),
AUDIO_DUPLICATE_CALL_DETECTION("Duplicate Calls"),
AUDIO_CALL_MANAGEMENT("Call Management"),
SOURCE_TUNERS("Tuners"),
TALKGROUP_FORMAT("Talkgroup & Radio ID"),
VECTOR_CALIBRATION("Vector Calibration"),
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -171,7 +171,7 @@ private TreeView getEditorSelectionTreeView()
applicationItem.setExpanded(true);

TreeItem<String> audioItem = new TreeItem<>("Audio");
audioItem.getChildren().add(new TreeItem(PreferenceEditorType.AUDIO_DUPLICATE_CALL_DETECTION));
audioItem.getChildren().add(new TreeItem(PreferenceEditorType.AUDIO_CALL_MANAGEMENT));
audioItem.getChildren().add(new TreeItem(PreferenceEditorType.AUDIO_MP3));
audioItem.getChildren().add(new TreeItem(PreferenceEditorType.AUDIO_OUTPUT));
audioItem.getChildren().add(new TreeItem(PreferenceEditorType.AUDIO_RECORD));
Expand Down
Loading