diff --git a/src/main/java/io/github/dsheirer/identifier/tone/ToneIdentifierMessage.java b/src/main/java/io/github/dsheirer/identifier/tone/ToneIdentifierMessage.java new file mode 100644 index 00000000..17151e5c --- /dev/null +++ b/src/main/java/io/github/dsheirer/identifier/tone/ToneIdentifierMessage.java @@ -0,0 +1,74 @@ +/* + * ***************************************************************************** + * Copyright (C) 2014-2022 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.identifier.tone; + +import io.github.dsheirer.identifier.Identifier; +import io.github.dsheirer.message.IMessage; +import io.github.dsheirer.protocol.Protocol; +import java.util.Collections; +import java.util.List; + +/** + * IMessage wrapper for a ToneIdentifier decoded from a sequence of AMBE audio frames. + * @param protocol of the decoded tone + * @param timeslot where the tone occurred + * @param timestamp of the event + * @param toneIdentifier that was decoded + * @param messageText to convey with the tone identifier + */ +public record ToneIdentifierMessage(Protocol protocol, int timeslot, long timestamp, ToneIdentifier toneIdentifier, + String messageText) implements IMessage +{ + @Override + public long getTimestamp() + { + return timestamp(); + } + + @Override + public boolean isValid() + { + return true; + } + + @Override + public Protocol getProtocol() + { + return protocol(); + } + + @Override + public int getTimeslot() + { + return timeslot(); + } + + @Override + public List getIdentifiers() + { + return Collections.singletonList(toneIdentifier()); + } + + @Override + public String toString() + { + return messageText(); + } +} diff --git a/src/main/java/io/github/dsheirer/module/decode/dmr/audio/DMRAudioModule.java b/src/main/java/io/github/dsheirer/module/decode/dmr/audio/DMRAudioModule.java index fdc3fa78..21c423db 100644 --- a/src/main/java/io/github/dsheirer/module/decode/dmr/audio/DMRAudioModule.java +++ b/src/main/java/io/github/dsheirer/module/decode/dmr/audio/DMRAudioModule.java @@ -22,14 +22,16 @@ import io.github.dsheirer.alias.AliasList; import io.github.dsheirer.audio.codec.mbe.AmbeAudioModule; import io.github.dsheirer.audio.squelch.SquelchState; import io.github.dsheirer.audio.squelch.SquelchStateEvent; -import io.github.dsheirer.identifier.Identifier; import io.github.dsheirer.identifier.IdentifierUpdateNotification; import io.github.dsheirer.identifier.IdentifierUpdateProvider; import io.github.dsheirer.identifier.Role; import io.github.dsheirer.identifier.tone.AmbeTone; import io.github.dsheirer.identifier.tone.Tone; +import io.github.dsheirer.identifier.tone.ToneIdentifier; +import io.github.dsheirer.identifier.tone.ToneIdentifierMessage; import io.github.dsheirer.identifier.tone.ToneSequence; import io.github.dsheirer.message.IMessage; +import io.github.dsheirer.message.IMessageProvider; import io.github.dsheirer.module.decode.dmr.identifier.DMRToneIdentifier; import io.github.dsheirer.module.decode.dmr.message.data.header.VoiceHeader; import io.github.dsheirer.module.decode.dmr.message.data.lc.LCMessage; @@ -40,19 +42,19 @@ import io.github.dsheirer.module.decode.dmr.message.type.ServiceOptions; import io.github.dsheirer.module.decode.dmr.message.voice.VoiceEMBMessage; import io.github.dsheirer.module.decode.dmr.message.voice.VoiceMessage; import io.github.dsheirer.preference.UserPreferences; +import io.github.dsheirer.protocol.Protocol; import io.github.dsheirer.sample.Listener; +import java.util.ArrayList; +import java.util.List; +import java.util.Map; import jmbe.iface.IAudioWithMetadata; import org.slf4j.Logger; import org.slf4j.LoggerFactory; -import java.util.ArrayList; -import java.util.List; -import java.util.Map; - /** * DMR Audio Module for converting transmitted AMBE audio frames to 8 kHz PCM audio */ -public class DMRAudioModule extends AmbeAudioModule implements IdentifierUpdateProvider +public class DMRAudioModule extends AmbeAudioModule implements IdentifierUpdateProvider, IMessageProvider { private final static Logger mLog = LoggerFactory.getLogger(DMRAudioModule.class); private SquelchStateListener mSquelchStateListener = new SquelchStateListener(); @@ -61,6 +63,7 @@ public class DMRAudioModule extends AmbeAudioModule implements IdentifierUpdateP private List mQueuedAmbeFrames = new ArrayList<>(); private boolean mEncryptedCallStateEstablished = false; private boolean mEncryptedCall = false; + private Listener mMessageListener; /** * Constructs an instance @@ -111,7 +114,7 @@ public class DMRAudioModule extends AmbeAudioModule implements IdentifierUpdateP List frames = vm.getAMBEFrames(); for(byte[] frame: frames) { - processAudio(frame); + processAudio(frame, message.getTimestamp()); } } //Both Motorola and Hytera signal their Basic Privacy (BP) scrambling in some of the Voice B-F frames @@ -127,7 +130,7 @@ public class DMRAudioModule extends AmbeAudioModule implements IdentifierUpdateP List frames = voiceMessage.getAMBEFrames(); for(byte[] frame: frames) { - processAudio(frame); + processAudio(frame, message.getTimestamp()); } } else if(message instanceof VoiceMessage voiceMessage) @@ -135,7 +138,7 @@ public class DMRAudioModule extends AmbeAudioModule implements IdentifierUpdateP List frames = voiceMessage.getAMBEFrames(); for(byte[] frame: frames) { - processAudio(frame); + processAudio(frame, message.getTimestamp()); } } else if(!mEncryptedCallStateEstablished && message instanceof VoiceHeader voiceHeader) @@ -171,7 +174,7 @@ public class DMRAudioModule extends AmbeAudioModule implements IdentifierUpdateP * Processes the audio frame. Queues the frame until encryption state is determined. Once determined, the audio * frames are dequeued and audio is generated. */ - private void processAudio(byte[] frame) + private void processAudio(byte[] frame, long timestamp) { if(mEncryptedCallStateEstablished) { @@ -182,7 +185,7 @@ public class DMRAudioModule extends AmbeAudioModule implements IdentifierUpdateP { for(byte[] queuedFrame: mQueuedAmbeFrames) { - produceAudio(queuedFrame); + produceAudio(queuedFrame, timestamp); } mQueuedAmbeFrames.clear(); @@ -191,7 +194,7 @@ public class DMRAudioModule extends AmbeAudioModule implements IdentifierUpdateP mQueuedAmbeFrames.clear(); } - produceAudio(frame); + produceAudio(frame, timestamp); } else { @@ -199,13 +202,13 @@ public class DMRAudioModule extends AmbeAudioModule implements IdentifierUpdateP } } - private void produceAudio(byte[] frame) + private void produceAudio(byte[] frame, long timestamp) { try { IAudioWithMetadata audioWithMetadata = getAudioCodec().getAudioWithMetadata(frame); addAudio(audioWithMetadata.getAudio()); - processMetadata(audioWithMetadata); + processMetadata(audioWithMetadata, timestamp); } catch(Exception e) { @@ -217,7 +220,7 @@ public class DMRAudioModule extends AmbeAudioModule implements IdentifierUpdateP * Processes optional metadata that can be included with decoded audio (ie dtmf, tones, knox, etc.) so that the * tone metadata can be converted into a FROM identifier and included with any call segment. */ - private void processMetadata(IAudioWithMetadata audioWithMetadata) + private void processMetadata(IAudioWithMetadata audioWithMetadata, long timestamp) { if(audioWithMetadata.hasMetadata()) { @@ -225,11 +228,11 @@ public class DMRAudioModule extends AmbeAudioModule implements IdentifierUpdateP for(Map.Entry entry: audioWithMetadata.getMetadata().entrySet()) { //Each metadata map entry contains a tone-type (key) and tone (value) - Identifier metadataIdentifier = mToneMetadataProcessor.process(entry.getKey(), entry.getValue()); + ToneIdentifier metadataIdentifier = mToneMetadataProcessor.process(entry.getKey(), entry.getValue()); if(metadataIdentifier != null) { - broadcast(metadataIdentifier); + broadcast(metadataIdentifier, timestamp); } } } @@ -242,13 +245,25 @@ public class DMRAudioModule extends AmbeAudioModule implements IdentifierUpdateP /** * Broadcasts the identifier to a registered listener */ - private void broadcast(Identifier identifier) + private void broadcast(ToneIdentifier identifier, long timestamp) { if(mIdentifierUpdateNotificationListener != null) { mIdentifierUpdateNotificationListener.receive(new IdentifierUpdateNotification(identifier, IdentifierUpdateNotification.Operation.ADD, getTimeslot())); } + + if(mMessageListener != null) + { + StringBuilder sb = new StringBuilder(); + sb.append("DMR Timeslot "); + sb.append(getTimeslot()); + sb.append("Audio Tone Sequence Decoded: "); + sb.append(identifier.toString()); + + mMessageListener.receive(new ToneIdentifierMessage(Protocol.DMR, getTimeslot(), timestamp, + identifier, sb.toString())); + } } /** @@ -269,6 +284,25 @@ public class DMRAudioModule extends AmbeAudioModule implements IdentifierUpdateP mIdentifierUpdateNotificationListener = null; } + /** + * Registers the listener to receive tone identifier messages from this module. + * @param listener to receive messages + */ + @Override + public void setMessageListener(Listener listener) + { + mMessageListener = listener; + } + + /** + * Unregisters the message listener + */ + @Override + public void removeMessageListener() + { + mMessageListener = null; + } + /** * Wrapper for squelch state to process end of call actions. At call end the encrypted call state established * flag is reset so that the encrypted audio state for the next call can be properly detected and we send an @@ -310,7 +344,7 @@ public class DMRAudioModule extends AmbeAudioModule implements IdentifierUpdateP * @param value of tone * @return an identifier with the accumulated tone metadata set */ - public Identifier process(String type, String value) + public ToneIdentifier process(String type, String value) { if(type == null || value == null) { diff --git a/src/main/java/io/github/dsheirer/module/decode/p25/audio/P25P2AudioModule.java b/src/main/java/io/github/dsheirer/module/decode/p25/audio/P25P2AudioModule.java index 28d40449..7abe7025 100644 --- a/src/main/java/io/github/dsheirer/module/decode/p25/audio/P25P2AudioModule.java +++ b/src/main/java/io/github/dsheirer/module/decode/p25/audio/P25P2AudioModule.java @@ -24,31 +24,33 @@ import io.github.dsheirer.audio.codec.mbe.AmbeAudioModule; import io.github.dsheirer.audio.squelch.SquelchState; import io.github.dsheirer.audio.squelch.SquelchStateEvent; import io.github.dsheirer.bits.BinaryMessage; -import io.github.dsheirer.identifier.Identifier; import io.github.dsheirer.identifier.IdentifierUpdateNotification; import io.github.dsheirer.identifier.IdentifierUpdateProvider; import io.github.dsheirer.identifier.Role; import io.github.dsheirer.identifier.tone.AmbeTone; import io.github.dsheirer.identifier.tone.P25ToneIdentifier; import io.github.dsheirer.identifier.tone.Tone; +import io.github.dsheirer.identifier.tone.ToneIdentifier; +import io.github.dsheirer.identifier.tone.ToneIdentifierMessage; import io.github.dsheirer.identifier.tone.ToneSequence; import io.github.dsheirer.message.IMessage; +import io.github.dsheirer.message.IMessageProvider; import io.github.dsheirer.module.decode.p25.phase2.message.EncryptionSynchronizationSequence; import io.github.dsheirer.module.decode.p25.phase2.message.mac.structure.PushToTalk; import io.github.dsheirer.module.decode.p25.phase2.timeslot.AbstractVoiceTimeslot; import io.github.dsheirer.preference.UserPreferences; +import io.github.dsheirer.protocol.Protocol; import io.github.dsheirer.sample.Listener; -import jmbe.iface.IAudioWithMetadata; -import org.slf4j.Logger; -import org.slf4j.LoggerFactory; - import java.util.ArrayDeque; import java.util.ArrayList; import java.util.List; import java.util.Map; import java.util.Queue; +import jmbe.iface.IAudioWithMetadata; +import org.slf4j.Logger; +import org.slf4j.LoggerFactory; -public class P25P2AudioModule extends AmbeAudioModule implements IdentifierUpdateProvider +public class P25P2AudioModule extends AmbeAudioModule implements IdentifierUpdateProvider, IMessageProvider { private final static Logger mLog = LoggerFactory.getLogger(P25P2AudioModule.class); @@ -58,6 +60,7 @@ public class P25P2AudioModule extends AmbeAudioModule implements IdentifierUpdat private Queue mQueuedAudioTimeslots = new ArrayDeque<>(); private boolean mEncryptedCallStateEstablished = false; private boolean mEncryptedCall = false; + private Listener mMessageListener; public P25P2AudioModule(UserPreferences userPreferences, int timeslot, AliasList aliasList) { @@ -116,7 +119,7 @@ public class P25P2AudioModule extends AmbeAudioModule implements IdentifierUpdat { if(!mEncryptedCall) { - processAudio(abstractVoiceTimeslot.getVoiceFrames()); + processAudio(abstractVoiceTimeslot.getVoiceFrames(), message.getTimestamp()); } } else @@ -156,7 +159,12 @@ public class P25P2AudioModule extends AmbeAudioModule implements IdentifierUpdat } } - private void processAudio(List voiceFrames) + /** + * Process the audio voice frames + * @param voiceFrames to process + * @param timestamp of the carrier message + */ + private void processAudio(List voiceFrames, long timestamp) { if(hasAudioCodec()) { @@ -168,7 +176,7 @@ public class P25P2AudioModule extends AmbeAudioModule implements IdentifierUpdat { IAudioWithMetadata audioWithMetadata = getAudioCodec().getAudioWithMetadata(voiceFrameBytes); addAudio(audioWithMetadata.getAudio()); - processMetadata(audioWithMetadata); + processMetadata(audioWithMetadata, timestamp); } catch(Exception e) { @@ -182,7 +190,7 @@ public class P25P2AudioModule extends AmbeAudioModule implements IdentifierUpdat * Processes optional metadata that can be included with decoded audio (ie dtmf, tones, knox, etc.) so that the * tone metadata can be converted into a FROM identifier and included with any call segment. */ - private void processMetadata(IAudioWithMetadata audioWithMetadata) + private void processMetadata(IAudioWithMetadata audioWithMetadata, long timestamp) { if(audioWithMetadata.hasMetadata()) { @@ -190,11 +198,11 @@ public class P25P2AudioModule extends AmbeAudioModule implements IdentifierUpdat for(Map.Entry entry: audioWithMetadata.getMetadata().entrySet()) { //Each metadata map entry contains a tone-type (key) and tone (value) - Identifier metadataIdentifier = mToneMetadataProcessor.process(entry.getKey(), entry.getValue()); + ToneIdentifier toneIdentifier = mToneMetadataProcessor.process(entry.getKey(), entry.getValue()); - if(metadataIdentifier != null) + if(toneIdentifier != null) { - broadcast(metadataIdentifier); + broadcast(toneIdentifier, timestamp); } } } @@ -205,15 +213,28 @@ public class P25P2AudioModule extends AmbeAudioModule implements IdentifierUpdat } /** - * Broadcasts the identifier to a registered listener + * Broadcasts the identifier to a registered listener and creates a new AMBE tone identifier message when tones are + * present to send to the alias action manager */ - private void broadcast(Identifier identifier) + private void broadcast(ToneIdentifier identifier, long timestamp) { if(mIdentifierUpdateNotificationListener != null) { mIdentifierUpdateNotificationListener.receive(new IdentifierUpdateNotification(identifier, IdentifierUpdateNotification.Operation.ADD, getTimeslot())); } + + if(mMessageListener != null) + { + StringBuilder sb = new StringBuilder(); + sb.append("P25.2 Timeslot "); + sb.append(getTimeslot()); + sb.append("Audio Tone Sequence Decoded: "); + sb.append(identifier.toString()); + + mMessageListener.receive(new ToneIdentifierMessage(Protocol.APCO25_PHASE2, getTimeslot(), timestamp, + identifier, sb.toString())); + } } /** @@ -234,6 +255,25 @@ public class P25P2AudioModule extends AmbeAudioModule implements IdentifierUpdat mIdentifierUpdateNotificationListener = null; } + /** + * Registers a message listener to receive AMBE tone identifier messages. + * @param listener to register + */ + @Override + public void setMessageListener(Listener listener) + { + mMessageListener = listener; + } + + /** + * Removes the message listener + */ + @Override + public void removeMessageListener() + { + mMessageListener = null; + } + /** * Process AMBE audio frame tone metadata. Tracks the count of sequential frames containing tone metadata to * provide a list of each unique tone and a time duration (milliseconds) for the tone. Tones are concatenated into @@ -258,7 +298,7 @@ public class P25P2AudioModule extends AmbeAudioModule implements IdentifierUpdat * @param value of tone * @return an identifier with the accumulated tone metadata set */ - public Identifier process(String type, String value) + public ToneIdentifier process(String type, String value) { if(type == null || value == null) { diff --git a/src/main/java/io/github/dsheirer/module/decode/p25/phase1/P25P1MessageFramer.java b/src/main/java/io/github/dsheirer/module/decode/p25/phase1/P25P1MessageFramer.java index 25ac11b8..a9022585 100644 --- a/src/main/java/io/github/dsheirer/module/decode/p25/phase1/P25P1MessageFramer.java +++ b/src/main/java/io/github/dsheirer/module/decode/p25/phase1/P25P1MessageFramer.java @@ -52,9 +52,6 @@ import io.github.dsheirer.protocol.Protocol; import io.github.dsheirer.record.AudioRecordingManager; import io.github.dsheirer.record.binary.BinaryReader; import io.github.dsheirer.sample.Listener; -import org.slf4j.Logger; -import org.slf4j.LoggerFactory; - import java.io.IOException; import java.io.OutputStream; import java.nio.ByteBuffer; @@ -63,6 +60,8 @@ import java.nio.file.Files; import java.nio.file.Path; import java.nio.file.Paths; import java.util.function.Consumer; +import org.slf4j.Logger; +import org.slf4j.LoggerFactory; /** * P25 Sync Detector and Message Framer. Includes capability to detect PLL out-of-phase lock errors