diff --git a/src/main/java/io/github/dsheirer/audio/DuplicateCallDetector.java b/src/main/java/io/github/dsheirer/audio/DuplicateCallDetector.java index 95e5ccb88..e4bdc9d89 100644 --- a/src/main/java/io/github/dsheirer/audio/DuplicateCallDetector.java +++ b/src/main/java/io/github/dsheirer/audio/DuplicateCallDetector.java @@ -62,7 +62,7 @@ public class DuplicateCallDetector implements Listener */ public DuplicateCallDetector(UserPreferences userPreferences) { - this(userPreferences.getDuplicateCallDetectionPreference()); + this(userPreferences.getCallManagementPreference()); } /** 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 ea6d4d15b..62dc73f07 100644 --- a/src/main/java/io/github/dsheirer/audio/broadcast/AudioStreamingManager.java +++ b/src/main/java/io/github/dsheirer/audio/broadcast/AudioStreamingManager.java @@ -1,6 +1,6 @@ /* * ***************************************************************************** - * Copyright (C) 2014-2023 Dennis Sheirer + * Copyright (C) 2014-2024 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 @@ -136,7 +136,7 @@ private void processAudioSegments() { audioSegment = it.next(); - if(audioSegment.isDuplicate() && mUserPreferences.getDuplicateCallDetectionPreference().isDuplicateStreamingSuppressionEnabled()) + if(audioSegment.isDuplicate() && mUserPreferences.getCallManagementPreference().isDuplicateStreamingSuppressionEnabled()) { it.remove(); audioSegment.decrementConsumerCount(); @@ -152,7 +152,7 @@ else if(audioSegment.completeProperty().get()) if(identifiers.getToIdentifier() instanceof PatchGroupIdentifier patchGroupIdentifier) { - if(mUserPreferences.getDuplicateCallDetectionPreference() + if(mUserPreferences.getCallManagementPreference() .getPatchGroupStreamingOption() == PatchGroupStreamingOption.TALKGROUPS) { //Decompose the patch group into the individual (patched) talkgroups and process the audio 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 c3555e9f6..65d43ae82 100644 --- a/src/main/java/io/github/dsheirer/audio/playback/AudioOutput.java +++ b/src/main/java/io/github/dsheirer/audio/playback/AudioOutput.java @@ -110,7 +110,7 @@ public AudioOutput(Mixer mixer, MixerChannel mixerChannel, AudioFormat audioForm mScheduledExecutorService = Executors.newSingleThreadScheduledExecutor(new NamingThreadFactory( "sdrtrunk audio output " + mixerChannel.name())); mUserPreferences = userPreferences; - mDropDuplicates = mUserPreferences.getDuplicateCallDetectionPreference().isDuplicatePlaybackSuppressionEnabled(); + mDropDuplicates = mUserPreferences.getCallManagementPreference().isDuplicatePlaybackSuppressionEnabled(); mAudioFormat = audioFormat; mLineInfo = lineInfo; mRequestedBufferSize = requestedBufferSize; @@ -238,7 +238,7 @@ public void preferenceUpdated(PreferenceType preferenceType) } else if(preferenceType == PreferenceType.DUPLICATE_CALL_DETECTION) { - mDropDuplicates = mUserPreferences.getDuplicateCallDetectionPreference().isDuplicatePlaybackSuppressionEnabled(); + mDropDuplicates = mUserPreferences.getCallManagementPreference().isDuplicatePlaybackSuppressionEnabled(); } } diff --git a/src/main/java/io/github/dsheirer/audio/playback/AudioPlaybackManager.java b/src/main/java/io/github/dsheirer/audio/playback/AudioPlaybackManager.java index a09ad65dd..626226445 100644 --- a/src/main/java/io/github/dsheirer/audio/playback/AudioPlaybackManager.java +++ b/src/main/java/io/github/dsheirer/audio/playback/AudioPlaybackManager.java @@ -1,6 +1,6 @@ /* * ***************************************************************************** - * Copyright (C) 2014-2023 Dennis Sheirer + * Copyright (C) 2014-2024 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 @@ -120,7 +120,7 @@ private void processAudioSegments() while(newSegment != null) { if(newSegment.isDuplicate() && - mUserPreferences.getDuplicateCallDetectionPreference().isDuplicatePlaybackSuppressionEnabled()) + mUserPreferences.getCallManagementPreference().isDuplicatePlaybackSuppressionEnabled()) { newSegment.decrementConsumerCount(); } @@ -148,7 +148,7 @@ else if(newSegment.hasAudio()) audioSegment = it.next(); if(audioSegment.isDuplicate() && - mUserPreferences.getDuplicateCallDetectionPreference().isDuplicatePlaybackSuppressionEnabled()) + mUserPreferences.getCallManagementPreference().isDuplicatePlaybackSuppressionEnabled()) { it.remove(); audioSegment.decrementConsumerCount(); @@ -182,7 +182,7 @@ else if(audioSegment.completeProperty().get()) audioSegment = it.next(); if(audioSegment.isDoNotMonitor() || (audioSegment.isDuplicate() && - mUserPreferences.getDuplicateCallDetectionPreference().isDuplicatePlaybackSuppressionEnabled())) + mUserPreferences.getCallManagementPreference().isDuplicatePlaybackSuppressionEnabled())) { it.remove(); audioSegment.decrementConsumerCount(); @@ -228,7 +228,7 @@ else if(audioSegment.isLinked()) audioSegment = it.next(); if(audioSegment.completeProperty().get() || (audioSegment.isDuplicate() && - mUserPreferences.getDuplicateCallDetectionPreference().isDuplicatePlaybackSuppressionEnabled())) + mUserPreferences.getCallManagementPreference().isDuplicatePlaybackSuppressionEnabled())) { it.remove(); audioSegment.decrementConsumerCount(); diff --git a/src/main/java/io/github/dsheirer/gui/preference/call/CallManagementPreferenceEditor.java b/src/main/java/io/github/dsheirer/gui/preference/call/CallManagementPreferenceEditor.java index 37619c9f9..95e8af877 100644 --- a/src/main/java/io/github/dsheirer/gui/preference/call/CallManagementPreferenceEditor.java +++ b/src/main/java/io/github/dsheirer/gui/preference/call/CallManagementPreferenceEditor.java @@ -1,6 +1,6 @@ /* * ***************************************************************************** - * Copyright (C) 2014-2023 Dennis Sheirer + * Copyright (C) 2014-2024 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 @@ -56,7 +56,7 @@ public class CallManagementPreferenceEditor extends HBox */ public CallManagementPreferenceEditor(UserPreferences userPreferences) { - mPreference = userPreferences.getDuplicateCallDetectionPreference(); + mPreference = userPreferences.getCallManagementPreference(); HBox.setHgrow(getEditorPane(), Priority.ALWAYS); getChildren().add(getEditorPane()); diff --git a/src/main/java/io/github/dsheirer/preference/UserPreferences.java b/src/main/java/io/github/dsheirer/preference/UserPreferences.java index a2b628d01..10d91f13a 100644 --- a/src/main/java/io/github/dsheirer/preference/UserPreferences.java +++ b/src/main/java/io/github/dsheirer/preference/UserPreferences.java @@ -1,6 +1,6 @@ /* * ***************************************************************************** - * Copyright (C) 2014-2023 Dennis Sheirer + * Copyright (C) 2014-2024 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 @@ -60,7 +60,7 @@ public class UserPreferences implements Listener private ChannelMultiFrequencyPreference mChannelMultiFrequencyPreference; private DecodeEventPreference mDecodeEventPreference; private DirectoryPreference mDirectoryPreference; - private CallManagementPreference mDuplicateCallDetectionPreference; + private CallManagementPreference mCallManagementPreference; private JmbeLibraryPreference mJmbeLibraryPreference; private MP3Preference mMP3Preference; private PlaybackPreference mPlaybackPreference; @@ -206,11 +206,11 @@ public SwingPreference getSwingPreference() } /** - * Duplicate call detection preferences + * Call management and duplicate call detection preferences */ - public CallManagementPreference getDuplicateCallDetectionPreference() + public CallManagementPreference getCallManagementPreference() { - return mDuplicateCallDetectionPreference; + return mCallManagementPreference; } /** @@ -222,10 +222,9 @@ private void loadPreferenceTypes() mChannelMultiFrequencyPreference = new ChannelMultiFrequencyPreference(this::receive); mDecodeEventPreference = new DecodeEventPreference(this::receive); mDirectoryPreference = new DirectoryPreference(this::receive); - mDuplicateCallDetectionPreference = new CallManagementPreference(this::receive); + mCallManagementPreference = new CallManagementPreference(this::receive); mJmbeLibraryPreference = new JmbeLibraryPreference(this::receive); mMP3Preference = new MP3Preference(this::receive); - mPlaybackPreference = new PlaybackPreference(this::receive); mPlaylistPreference = new PlaylistPreference(this::receive, mDirectoryPreference); mRadioReferencePreference = new RadioReferencePreference(this::receive); mRecordPreference = new RecordPreference(this::receive); diff --git a/src/main/java/io/github/dsheirer/record/AudioRecordingManager.java b/src/main/java/io/github/dsheirer/record/AudioRecordingManager.java index f85173b14..967264cd5 100644 --- a/src/main/java/io/github/dsheirer/record/AudioRecordingManager.java +++ b/src/main/java/io/github/dsheirer/record/AudioRecordingManager.java @@ -140,7 +140,7 @@ private void processAudioSegments() while(audioSegment != null) { - if(audioSegment.isDuplicate() && mUserPreferences.getDuplicateCallDetectionPreference().isDuplicateRecordingSuppressionEnabled()) + if(audioSegment.isDuplicate() && mUserPreferences.getCallManagementPreference().isDuplicateRecordingSuppressionEnabled()) { audioSegment.decrementConsumerCount(); } diff --git a/src/test/java/io/github/dsheirer/audio/broadcast/AudioStreamingManagerTest.java b/src/test/java/io/github/dsheirer/audio/broadcast/AudioStreamingManagerTest.java new file mode 100644 index 000000000..66787ae94 --- /dev/null +++ b/src/test/java/io/github/dsheirer/audio/broadcast/AudioStreamingManagerTest.java @@ -0,0 +1,223 @@ +/* + * ***************************************************************************** + * Copyright (C) 2014-2024 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.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.alias.id.talkgroup.Talkgroup; +import io.github.dsheirer.audio.AudioSegment; +import io.github.dsheirer.dsp.oscillator.ScalarRealOscillator; +import io.github.dsheirer.identifier.patch.PatchGroup; +import io.github.dsheirer.identifier.radio.RadioIdentifier; +import io.github.dsheirer.identifier.talkgroup.TalkgroupIdentifier; +import io.github.dsheirer.message.TimeslotMessage; +import io.github.dsheirer.module.decode.p25.identifier.patch.APCO25PatchGroup; +import io.github.dsheirer.module.decode.p25.identifier.radio.APCO25RadioIdentifier; +import io.github.dsheirer.module.decode.p25.identifier.talkgroup.APCO25Talkgroup; +import io.github.dsheirer.preference.UserPreferences; +import io.github.dsheirer.protocol.Protocol; +import io.github.dsheirer.sample.Listener; +import java.io.IOException; +import java.nio.file.Files; +import java.nio.file.Path; +import java.util.concurrent.CountDownLatch; +import java.util.concurrent.TimeUnit; +import java.util.stream.Stream; +import org.junit.jupiter.api.Test; + +import static org.junit.jupiter.api.Assertions.assertTrue; + +/** + * Automated testing for the AudioStreamingManager that includes for testing streaming a patch group audio segment as + * an individual stream aliased against the patch group, or broken up into the set of patched talkgroups and streamed + * according to the aliases for each patched talkgroup. + */ +public class AudioStreamingManagerTest +{ + private static final int TALKGROUP_1 = 100; + private static final int TALKGROUP_2 = 200; + private static final int TALKGROUP_3 = 300; + private static final int RADIO_1 = 9999; + + @Test + public void testPatchGroupStreamingAsPatchGroup() + { + int expectedRecordingsCount = 1; + + //We use a countdown latch to count the number of expected audio recordings produced. + CountDownLatch latch = new CountDownLatch(expectedRecordingsCount); + Listener listener = audioRecording -> { + latch.countDown(); + }; + + UserPreferences userPreferences = new UserPreferences(); + userPreferences.getCallManagementPreference().setPatchGroupStreamingOption(PatchGroupStreamingOption.PATCH_GROUP); + AudioStreamingManager manager = new AudioStreamingManager(listener, BroadcastFormat.MP3, userPreferences); + manager.start(); + manager.receive(getAudioSegment()); + + boolean success = false; + + try + { + success = latch.await(5, TimeUnit.SECONDS); + } + catch(InterruptedException e) + { + throw new RuntimeException(e); + } + + cleanupStreamingDirectory(userPreferences.getDirectoryPreference().getDirectoryStreaming()); + + assertTrue(success, "Stream patch group audio as PATCHED GROUP failed to produce [" + + latch.getCount() + "/" + expectedRecordingsCount + "] streaming recordings"); + } + + @Test + public void testPatchGroupStreamingAsIndividualGroups() + { + int expectedRecordingsCount = 2; + + //We use a countdown latch to count the number of expected audio recordings produced. In this case, we expect + //two audio recordings, one for stream B and one for stream C associated with the two patched talkgroups. + CountDownLatch latch = new CountDownLatch(expectedRecordingsCount); + Listener listener = audioRecording -> { + latch.countDown(); + }; + + UserPreferences userPreferences = new UserPreferences(); + userPreferences.getCallManagementPreference().setPatchGroupStreamingOption(PatchGroupStreamingOption.TALKGROUPS); + AudioStreamingManager manager = new AudioStreamingManager(listener, BroadcastFormat.MP3, userPreferences); + manager.start(); + manager.receive(getAudioSegment()); + + boolean success = false; + + try + { + success = latch.await(5, TimeUnit.SECONDS); + } + catch(InterruptedException e) + { + throw new RuntimeException(e); + } + + cleanupStreamingDirectory(userPreferences.getDirectoryPreference().getDirectoryStreaming()); + + assertTrue(success, "Stream patch group audio as INDIVIDUAL TALKGROUPS failed to produce [" + + latch.getCount() + "/" + expectedRecordingsCount + "] streaming recordings"); + } + + /** + * Cleanup any generated streaming recordings. + * @param streamingDirectory + */ + private void cleanupStreamingDirectory(Path streamingDirectory) + { + if(Files.exists(streamingDirectory)) + { + try(Stream fileStream = Files.list(streamingDirectory)) + { + fileStream.forEach(path -> { + try + { + Files.delete(path); + } + catch(IOException e) + { + e.printStackTrace(); + } + }); + } + catch(IOException e) + { + e.printStackTrace(); + } + } + } + + /** + * Creates an audio segment with audio using the supplied alias list. + * @return audio segment + */ + private static AudioSegment getAudioSegment() + { + AliasList aliasList = getAliasList(); + + AudioSegment audioSegment = new AudioSegment(aliasList, TimeslotMessage.TIMESLOT_0); + ScalarRealOscillator oscillator = new ScalarRealOscillator(1000, 8000); + for(int x = 0; x < 100; x++) + { + audioSegment.addAudio(oscillator.generate(500)); + } + audioSegment.addIdentifier(getPatchGroup()); + audioSegment.addIdentifier(getRadio()); + audioSegment.completeProperty().set(true); + return audioSegment; + } + + private static AliasList getAliasList() + { + AliasList aliasList = new AliasList("test"); + + Alias patchAlias = new Alias("patch"); + patchAlias.addAliasID(new Talkgroup(Protocol.APCO25, 100)); + patchAlias.addAliasID(new BroadcastChannel("Stream A")); + aliasList.addAlias(patchAlias); + + Alias talkgroupAlias1 = new Alias("talkgroup1"); + talkgroupAlias1.addAliasID(new Talkgroup(Protocol.APCO25, 200)); + talkgroupAlias1.addAliasID(new BroadcastChannel("Stream B")); + aliasList.addAlias(talkgroupAlias1); + + Alias talkgroupAlias2 = new Alias("talkgroup2"); + talkgroupAlias2.addAliasID(new Talkgroup(Protocol.APCO25, 300)); + talkgroupAlias2.addAliasID(new BroadcastChannel("Stream C")); + aliasList.addAlias(talkgroupAlias2); + + return aliasList; + } + + /** + * Creates a patch group + * @return p25 patch group + */ + private static APCO25PatchGroup getPatchGroup() + { + TalkgroupIdentifier talkgroup1 = APCO25Talkgroup.create(TALKGROUP_1); + TalkgroupIdentifier talkgroup2 = APCO25Talkgroup.create(TALKGROUP_2); + TalkgroupIdentifier talkgroup3 = APCO25Talkgroup.create(TALKGROUP_3); + + PatchGroup pg = new PatchGroup(talkgroup1); + pg.addPatchedTalkgroup(talkgroup2); + pg.addPatchedTalkgroup(talkgroup3); + return APCO25PatchGroup.create(pg); + } + + /** + * Creates a source radio identifier. + * @return radio + */ + private static RadioIdentifier getRadio() + { + return APCO25RadioIdentifier.createFrom(RADIO_1); + } +}