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