diff --git a/src/main/java/io/github/dsheirer/gui/viewer/MessagePackage.java b/src/main/java/io/github/dsheirer/gui/viewer/MessagePackage.java index a912f88c8..ec49879b9 100644 --- a/src/main/java/io/github/dsheirer/gui/viewer/MessagePackage.java +++ b/src/main/java/io/github/dsheirer/gui/viewer/MessagePackage.java @@ -19,11 +19,11 @@ package io.github.dsheirer.gui.viewer; +import io.github.dsheirer.audio.AudioSegment; import io.github.dsheirer.channel.state.DecoderStateEvent; import io.github.dsheirer.controller.channel.event.ChannelStartProcessingRequest; import io.github.dsheirer.message.IMessage; import io.github.dsheirer.module.decode.event.DecodeEventSnapshot; -import io.github.dsheirer.module.decode.event.IDecodeEvent; import java.util.ArrayList; import java.util.List; @@ -37,6 +37,7 @@ public class MessagePackage private List mDecoderStateEvents = new ArrayList<>(); private List mDecodeEvents = new ArrayList<>(); private ChannelStartProcessingRequest mChannelStartProcessingRequest; + private AudioSegment mAudioSegment; /** * Constructs an instance @@ -152,8 +153,39 @@ public int getChannelStartProcessingRequestCount() return mChannelStartProcessingRequest == null ? 0 : 1; } + /** + * Count (0 or 1) of audio segment. + */ + public int getAudioSegmentCount() + { + return mAudioSegment == null ? 0 : 1; + } + public ChannelStartProcessingRequest getChannelStartProcessingRequest() { return mChannelStartProcessingRequest; } + + /** + * Generated audio segment. + * @return segment or null. + */ + public AudioSegment getAudioSegment() + { + return mAudioSegment; + } + + /** + * Adds the audio segment to the package + * @param audioSegment to add + */ + public void add(AudioSegment audioSegment) + { + if(mAudioSegment != null) + { + throw new IllegalStateException("AudioSegment already set"); + } + + mAudioSegment = audioSegment; + } } diff --git a/src/main/java/io/github/dsheirer/gui/viewer/MessagePackageViewer.java b/src/main/java/io/github/dsheirer/gui/viewer/MessagePackageViewer.java index 061c71c05..a28cfda58 100644 --- a/src/main/java/io/github/dsheirer/gui/viewer/MessagePackageViewer.java +++ b/src/main/java/io/github/dsheirer/gui/viewer/MessagePackageViewer.java @@ -22,6 +22,8 @@ import io.github.dsheirer.channel.state.DecoderStateEvent; import io.github.dsheirer.module.decode.event.DecodeEventSnapshot; import javafx.scene.control.Label; +import javafx.scene.control.Tab; +import javafx.scene.control.TabPane; import javafx.scene.control.TableColumn; import javafx.scene.control.TableView; import javafx.scene.control.cell.PropertyValueFactory; @@ -38,6 +40,7 @@ public class MessagePackageViewer extends VBox private TableView mDecoderStateEventTableView; private TableView mDecodeEventTableView; private IdentifierCollectionViewer mIdentifierCollectionViewer; + private IdentifierCollectionViewer mAudioSegmentIdCollectionViewer; private ChannelStartProcessingRequestViewer mChannelStartProcessingRequestViewer; /** @@ -63,8 +66,14 @@ public MessagePackageViewer() GridPane.setHgrow(getDecodeEventTableView(), Priority.ALWAYS); gridPane.add(getDecodeEventTableView(), 1, 2); - gridPane.add(new Label("Channel Start Processing Request"), 0, 3); - gridPane.add(getChannelStartProcessingRequestViewer(), 0, 4); + TabPane tabPane = new TabPane(); + Tab startTab = new Tab("Channel Start Processing Request"); + startTab.setContent(getChannelStartProcessingRequestViewer()); + Tab audioTab = new Tab("Audio Segment"); + audioTab.setContent(getAudioSegmentIdCollectionViewer()); + tabPane.getTabs().addAll(audioTab, startTab); + + gridPane.add(tabPane, 0, 4); getIdentifierCollectionViewer().setPrefHeight(120); gridPane.add(new Label("Selected Decode Event Identifiers"), 1, 3); @@ -96,6 +105,15 @@ public void set(MessagePackage messagePackage) { getDecodeEventTableView().getSelectionModel().select(0); } + + if(messagePackage.getAudioSegment() != null) + { + getAudioSegmentIdCollectionViewer().set(messagePackage.getAudioSegment().getIdentifierCollection()); + } + else + { + getAudioSegmentIdCollectionViewer().set(null); + } } } @@ -109,6 +127,16 @@ private IdentifierCollectionViewer getIdentifierCollectionViewer() return mIdentifierCollectionViewer; } + private IdentifierCollectionViewer getAudioSegmentIdCollectionViewer() + { + if(mAudioSegmentIdCollectionViewer == null) + { + mAudioSegmentIdCollectionViewer = new IdentifierCollectionViewer(); + } + + return mAudioSegmentIdCollectionViewer; + } + private Label getMessageLabel() { if(mMessageLabel == null) diff --git a/src/main/java/io/github/dsheirer/gui/viewer/MessagePackager.java b/src/main/java/io/github/dsheirer/gui/viewer/MessagePackager.java index ef5fb0744..b4c131d1b 100644 --- a/src/main/java/io/github/dsheirer/gui/viewer/MessagePackager.java +++ b/src/main/java/io/github/dsheirer/gui/viewer/MessagePackager.java @@ -20,13 +20,13 @@ package io.github.dsheirer.gui.viewer; import com.google.common.eventbus.Subscribe; +import io.github.dsheirer.audio.AudioSegment; import io.github.dsheirer.channel.state.DecoderStateEvent; import io.github.dsheirer.controller.channel.event.ChannelStartProcessingRequest; import io.github.dsheirer.message.IMessage; import io.github.dsheirer.module.decode.event.DecodeEvent; import io.github.dsheirer.module.decode.event.DecodeEventSnapshot; import io.github.dsheirer.module.decode.event.IDecodeEvent; -import io.github.dsheirer.module.decode.event.IDecodeEventListener; /** * Utility for combining a message and decoder state events. @@ -42,6 +42,18 @@ public MessagePackager() { } + /** + * Adds an audio segment. + * @param audioSegment to add + */ + public void add(AudioSegment audioSegment) + { + if(mMessagePackage != null) + { + mMessagePackage.add(audioSegment); + } + } + /** * Adds the message and creates a new MessageWithEvents instance, wrapping the message, ready to also receive any * decode events and decoder state events. The previous message with events is overwritten. diff --git a/src/main/java/io/github/dsheirer/gui/viewer/MessageRecordingViewer.java b/src/main/java/io/github/dsheirer/gui/viewer/MessageRecordingViewer.java index 6729fb1a4..c7ff473ef 100644 --- a/src/main/java/io/github/dsheirer/gui/viewer/MessageRecordingViewer.java +++ b/src/main/java/io/github/dsheirer/gui/viewer/MessageRecordingViewer.java @@ -19,6 +19,7 @@ package io.github.dsheirer.gui.viewer; +import io.github.dsheirer.preference.UserPreferences; import javafx.application.Application; import javafx.application.Platform; import javafx.scene.Node; @@ -51,6 +52,7 @@ public class MessageRecordingViewer extends VBox private int mTabCounterDmr = 1; private int mTabCounterP25P1 = 1; private int mTabCounterP25P2 = 1; + private UserPreferences mUserPreferences = new UserPreferences(); /** * Constructs an instance @@ -77,7 +79,7 @@ public MenuBar getMenuBar() }); MenuItem p25p1MenuItem = new MenuItem("P25 Phase 1"); p25p1MenuItem.onActionProperty().set(event -> { - Tab tab = new LabeledTab("P25P1-" + mTabCounterP25P1++, new P25P1Viewer()); + Tab tab = new LabeledTab("P25P1-" + mTabCounterP25P1++, new P25P1Viewer(mUserPreferences)); getTabPane().getTabs().add(tab); getTabPane().getSelectionModel().select(tab); }); @@ -108,7 +110,7 @@ public TabPane getTabPane() mTabPane = new TabPane(); mTabPane.setMaxHeight(Double.MAX_VALUE); mTabPane.getTabs().add(new LabeledTab("DMR-" + mTabCounterDmr++, new DmrViewer())); - mTabPane.getTabs().add(new LabeledTab("P25P1-" + mTabCounterP25P1++, new P25P1Viewer())); + mTabPane.getTabs().add(new LabeledTab("P25P1-" + mTabCounterP25P1++, new P25P1Viewer(mUserPreferences))); mTabPane.getTabs().add(new LabeledTab("P25P2-" + mTabCounterP25P2++, new P25P2Viewer())); } diff --git a/src/main/java/io/github/dsheirer/gui/viewer/P25P1Viewer.java b/src/main/java/io/github/dsheirer/gui/viewer/P25P1Viewer.java index d50c9d555..7937e54e9 100644 --- a/src/main/java/io/github/dsheirer/gui/viewer/P25P1Viewer.java +++ b/src/main/java/io/github/dsheirer/gui/viewer/P25P1Viewer.java @@ -20,16 +20,24 @@ package io.github.dsheirer.gui.viewer; import com.google.common.eventbus.EventBus; +import io.github.dsheirer.alias.AliasList; +import io.github.dsheirer.alias.AliasModel; +import io.github.dsheirer.channel.state.DecoderStateEvent; +import io.github.dsheirer.channel.state.SingleChannelState; import io.github.dsheirer.controller.channel.Channel; import io.github.dsheirer.identifier.IdentifierUpdateNotification; import io.github.dsheirer.identifier.configuration.FrequencyConfigurationIdentifier; +import io.github.dsheirer.message.IMessage; import io.github.dsheirer.message.StuffBitsMessage; import io.github.dsheirer.module.decode.p25.P25TrafficChannelManager; +import io.github.dsheirer.module.decode.p25.audio.P25P1AudioModule; import io.github.dsheirer.module.decode.p25.phase1.DecodeConfigP25Phase1; import io.github.dsheirer.module.decode.p25.phase1.P25P1DecoderState; import io.github.dsheirer.module.decode.p25.phase1.P25P1MessageFramer; import io.github.dsheirer.module.decode.p25.phase1.P25P1MessageProcessor; +import io.github.dsheirer.preference.UserPreferences; import io.github.dsheirer.record.binary.BinaryReader; +import io.github.dsheirer.sample.Broadcaster; import io.github.dsheirer.util.ThreadPool; import java.io.File; import java.nio.ByteBuffer; @@ -94,9 +102,11 @@ public class P25P1Viewer extends VBox private ProgressIndicator mLoadingIndicator; private MessagePackageViewer mMessagePackageViewer; private StringProperty mLoadedFile = new SimpleStringProperty(); + private UserPreferences mUserPreferences; - public P25P1Viewer() + public P25P1Viewer(UserPreferences userPreferences) { + mUserPreferences = userPreferences; setPadding(new Insets(5)); setSpacing(5); @@ -155,7 +165,9 @@ private void load(File file) List messagePackages = new ArrayList<>(); P25P1MessageFramer messageFramer = new P25P1MessageFramer(null, 9600); P25P1MessageProcessor messageProcessor = new P25P1MessageProcessor(); - messageFramer.setListener(messageProcessor); + Broadcaster messageBroadcaster = new Broadcaster<>(); + messageFramer.setListener(messageBroadcaster); + messageBroadcaster.addListener(messageProcessor); Channel empty = new Channel("Empty"); empty.setDecodeConfiguration(new DecodeConfigP25Phase1()); @@ -169,10 +181,14 @@ private void load(File file) trafficChannelManager.setInterModuleEventBus(eventBus); //Register to receive events - trafficChannelManager.addDecodeEventListener(decodeEvent -> messagePackager.add(decodeEvent)); + trafficChannelManager.addDecodeEventListener(messagePackager::add); P25P1DecoderState decoderState = new P25P1DecoderState(empty, trafficChannelManager); - decoderState.setDecoderStateListener(decoderStateEvent -> messagePackager.add(decoderStateEvent)); - decoderState.addDecodeEventListener(decodeEvent -> messagePackager.add(decodeEvent)); + + Broadcaster decoderStateEventBroadcaster = new Broadcaster<>(); + decoderState.setDecoderStateListener(decoderStateEventBroadcaster); + + decoderStateEventBroadcaster.addListener(messagePackager::add); + decoderState.addDecodeEventListener(messagePackager::add); decoderState.start(); long frequency = getFrequencyFromFile(mLoadedFile.get()); @@ -197,6 +213,16 @@ private void load(File file) } }); + P25P1AudioModule audioModule = new P25P1AudioModule(mUserPreferences, new AliasList("debug")); + decoderState.setIdentifierUpdateListener(audioModule.getIdentifierUpdateListener()); + audioModule.setAudioSegmentListener(messagePackager::add); + messageBroadcaster.addListener(audioModule); + audioModule.start(); + SingleChannelState singleChannelState = new SingleChannelState(empty, new AliasModel()); + singleChannelState.setSquelchStateListener(squelchStateEvent -> audioModule.getSquelchStateListener().receive(squelchStateEvent)); + decoderStateEventBroadcaster.addListener(singleChannelState.getDecoderStateListener()); + singleChannelState.start(); + try(BinaryReader reader = new BinaryReader(file.toPath(), 200)) { while(reader.hasNext()) @@ -210,6 +236,10 @@ private void load(File file) ioe.printStackTrace(); } + audioModule.stop(); + decoderState.stop(); + singleChannelState.stop(); + Platform.runLater(() -> { getLoadingIndicator().setVisible(false); getSelectedFileLabel().setText(file.getName()); @@ -444,8 +474,13 @@ private TableView getMessagePackageTableView() channelStartCountColumn.setText("Starts"); channelStartCountColumn.setCellValueFactory(new PropertyValueFactory<>("channelStartProcessingRequestCount")); + TableColumn audioSegmentCountColumn = new TableColumn(); + audioSegmentCountColumn.setPrefWidth(50); + audioSegmentCountColumn.setText("Audio"); + audioSegmentCountColumn.setCellValueFactory(new PropertyValueFactory<>("audioSegmentCount")); + mMessagePackageTableView.getColumns().addAll(timestampColumn, validColumn, timeslotColumn, messageColumn, - decodeEventCountColumn, decoderStateEventCountColumn, channelStartCountColumn); + decodeEventCountColumn, decoderStateEventCountColumn, channelStartCountColumn, audioSegmentCountColumn); } return mMessagePackageTableView; diff --git a/src/main/java/io/github/dsheirer/module/decode/p25/audio/P25P1AudioModule.java b/src/main/java/io/github/dsheirer/module/decode/p25/audio/P25P1AudioModule.java index 41ab474cc..d15be3684 100644 --- a/src/main/java/io/github/dsheirer/module/decode/p25/audio/P25P1AudioModule.java +++ b/src/main/java/io/github/dsheirer/module/decode/p25/audio/P25P1AudioModule.java @@ -1,6 +1,6 @@ /* * ***************************************************************************** - * Copyright (C) 2014-2020 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 @@ -30,6 +30,8 @@ import io.github.dsheirer.module.decode.p25.phase1.message.ldu.LDUMessage; import io.github.dsheirer.preference.UserPreferences; import io.github.dsheirer.sample.Listener; +import java.util.ArrayList; +import java.util.List; public class P25P1AudioModule extends ImbeAudioModule { @@ -38,7 +40,7 @@ public class P25P1AudioModule extends ImbeAudioModule private SquelchStateListener mSquelchStateListener = new SquelchStateListener(); private NonClippingGain mGain = new NonClippingGain(5.0f, 0.95f); - private LDU1Message mCachedLDU1Message = null; + private List mCachedLDUMessages = new ArrayList<>(); public P25P1AudioModule(UserPreferences userPreferences, AliasList aliasList) { @@ -81,37 +83,46 @@ public void receive(IMessage message) { if(mEncryptedCallStateEstablished) { - if(message instanceof LDUMessage) + if(message instanceof LDUMessage ldu) { - processAudio((LDUMessage)message); + processAudio(ldu); } } else { - if(message instanceof HDUMessage) + if(message instanceof HDUMessage hdu && hdu.isValid()) { mEncryptedCallStateEstablished = true; - mEncryptedCall = ((HDUMessage)message).getHeaderData().isEncryptedAudio(); + mEncryptedCall = hdu.getHeaderData().isEncryptedAudio(); } - else if(message instanceof LDU1Message) + else if(message instanceof LDU1Message ldu1) { //When we receive an LDU1 message without first receiving the HDU message, cache the LDU1 Message //until we can determine the encrypted call state from the next LDU2 message - mCachedLDU1Message = (LDU1Message)message; + mCachedLDUMessages.add(ldu1); } - else if(message instanceof LDU2Message) + else if(message instanceof LDU2Message ldu2) { - mEncryptedCallStateEstablished = true; - LDU2Message ldu2 = (LDU2Message)message; - mEncryptedCall = ldu2.getEncryptionSyncParameters().isEncryptedAudio(); - - if(mCachedLDU1Message != null) + if(ldu2.getEncryptionSyncParameters().isValid()) { - processAudio(mCachedLDU1Message); - mCachedLDU1Message = null; + mEncryptedCallStateEstablished = true; + mEncryptedCall = ldu2.getEncryptionSyncParameters().isEncryptedAudio(); } - processAudio(ldu2); + if(mEncryptedCallStateEstablished) + { + for(LDUMessage cachedLdu : mCachedLDUMessages) + { + processAudio(cachedLdu); + } + + mCachedLDUMessages.clear(); + processAudio(ldu2); + } + else + { + mCachedLDUMessages.add(ldu2); + } } } } @@ -152,7 +163,7 @@ public void receive(SquelchStateEvent event) closeAudioSegment(); mEncryptedCallStateEstablished = false; mEncryptedCall = false; - mCachedLDU1Message = null; + mCachedLDUMessages.clear(); } } } diff --git a/src/main/java/io/github/dsheirer/module/decode/p25/phase1/P25P1DecoderState.java b/src/main/java/io/github/dsheirer/module/decode/p25/phase1/P25P1DecoderState.java index 9c9d2d229..c20c61202 100644 --- a/src/main/java/io/github/dsheirer/module/decode/p25/phase1/P25P1DecoderState.java +++ b/src/main/java/io/github/dsheirer/module/decode/p25/phase1/P25P1DecoderState.java @@ -60,7 +60,6 @@ import io.github.dsheirer.module.decode.p25.phase1.message.P25P1Message; import io.github.dsheirer.module.decode.p25.phase1.message.hdu.HDUMessage; import io.github.dsheirer.module.decode.p25.phase1.message.hdu.HeaderData; -import io.github.dsheirer.module.decode.p25.phase1.message.lc.LinkControlOpcode; import io.github.dsheirer.module.decode.p25.phase1.message.lc.LinkControlWord; import io.github.dsheirer.module.decode.p25.phase1.message.lc.l3harris.LCHarrisReturnToControlChannel; import io.github.dsheirer.module.decode.p25.phase1.message.lc.motorola.LCMotorolaEmergencyAlarmActivation; @@ -891,15 +890,9 @@ private void processTDULC(P25P1Message message) if(lcw != null && lcw.isValid()) { - //Send an ACTIVE decoder state event for everything except the CALL TERMINATION opcode which is - //handled by the processLC() method. - if(lcw.getOpcode() != LinkControlOpcode.CALL_TERMINATION_OR_CANCELLATION) - { - //Set the state to ACTIVE while the call continues in hangtime. The processLC() method will signal - // the channel teardown. - broadcast(new DecoderStateEvent(this, Event.DECODE, State.ACTIVE)); - } - + //Set the state to ACTIVE while the call continues in hangtime. The processLC() method will signal + // the channel teardown when that happens. + broadcast(new DecoderStateEvent(this, Event.DECODE, State.ACTIVE)); processLC(lcw, message.getTimestamp(), true); } } diff --git a/src/main/java/io/github/dsheirer/module/decode/p25/phase1/message/hdu/HDUMessage.java b/src/main/java/io/github/dsheirer/module/decode/p25/phase1/message/hdu/HDUMessage.java index ccce03547..f0f477a9d 100644 --- a/src/main/java/io/github/dsheirer/module/decode/p25/phase1/message/hdu/HDUMessage.java +++ b/src/main/java/io/github/dsheirer/module/decode/p25/phase1/message/hdu/HDUMessage.java @@ -184,7 +184,11 @@ private void extractHeaderData() if(irrecoverableErrors) { + //Set the header data as invalid mHeaderData.setValid(false); + + //Est the whole HDU message as invalid. + setValid(false); } else {