#1358 Resolves issue where decoded AMBE (DMR & P25-2) tone identifiers are not sent to the alias action manager. (#1359)

Co-authored-by: Dennis Sheirer <dsheirer@github.com>
This commit is contained in:
Denny Sheirer
2022-12-17 07:35:16 -05:00
committed by GitHub
parent bc462b9652
commit 24f4c22cdd
4 changed files with 185 additions and 38 deletions

View File

@@ -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 <http://www.gnu.org/licenses/>
* ****************************************************************************
*/
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<Identifier> getIdentifiers()
{
return Collections.singletonList(toneIdentifier());
}
@Override
public String toString()
{
return messageText();
}
}

View File

@@ -22,14 +22,16 @@ import io.github.dsheirer.alias.AliasList;
import io.github.dsheirer.audio.codec.mbe.AmbeAudioModule; import io.github.dsheirer.audio.codec.mbe.AmbeAudioModule;
import io.github.dsheirer.audio.squelch.SquelchState; import io.github.dsheirer.audio.squelch.SquelchState;
import io.github.dsheirer.audio.squelch.SquelchStateEvent; import io.github.dsheirer.audio.squelch.SquelchStateEvent;
import io.github.dsheirer.identifier.Identifier;
import io.github.dsheirer.identifier.IdentifierUpdateNotification; import io.github.dsheirer.identifier.IdentifierUpdateNotification;
import io.github.dsheirer.identifier.IdentifierUpdateProvider; import io.github.dsheirer.identifier.IdentifierUpdateProvider;
import io.github.dsheirer.identifier.Role; import io.github.dsheirer.identifier.Role;
import io.github.dsheirer.identifier.tone.AmbeTone; import io.github.dsheirer.identifier.tone.AmbeTone;
import io.github.dsheirer.identifier.tone.Tone; 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.identifier.tone.ToneSequence;
import io.github.dsheirer.message.IMessage; 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.identifier.DMRToneIdentifier;
import io.github.dsheirer.module.decode.dmr.message.data.header.VoiceHeader; import io.github.dsheirer.module.decode.dmr.message.data.header.VoiceHeader;
import io.github.dsheirer.module.decode.dmr.message.data.lc.LCMessage; 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.VoiceEMBMessage;
import io.github.dsheirer.module.decode.dmr.message.voice.VoiceMessage; import io.github.dsheirer.module.decode.dmr.message.voice.VoiceMessage;
import io.github.dsheirer.preference.UserPreferences; import io.github.dsheirer.preference.UserPreferences;
import io.github.dsheirer.protocol.Protocol;
import io.github.dsheirer.sample.Listener; import io.github.dsheirer.sample.Listener;
import java.util.ArrayList;
import java.util.List;
import java.util.Map;
import jmbe.iface.IAudioWithMetadata; import jmbe.iface.IAudioWithMetadata;
import org.slf4j.Logger; import org.slf4j.Logger;
import org.slf4j.LoggerFactory; 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 * 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 final static Logger mLog = LoggerFactory.getLogger(DMRAudioModule.class);
private SquelchStateListener mSquelchStateListener = new SquelchStateListener(); private SquelchStateListener mSquelchStateListener = new SquelchStateListener();
@@ -61,6 +63,7 @@ public class DMRAudioModule extends AmbeAudioModule implements IdentifierUpdateP
private List<byte[]> mQueuedAmbeFrames = new ArrayList<>(); private List<byte[]> mQueuedAmbeFrames = new ArrayList<>();
private boolean mEncryptedCallStateEstablished = false; private boolean mEncryptedCallStateEstablished = false;
private boolean mEncryptedCall = false; private boolean mEncryptedCall = false;
private Listener<IMessage> mMessageListener;
/** /**
* Constructs an instance * Constructs an instance
@@ -111,7 +114,7 @@ public class DMRAudioModule extends AmbeAudioModule implements IdentifierUpdateP
List<byte[]> frames = vm.getAMBEFrames(); List<byte[]> frames = vm.getAMBEFrames();
for(byte[] frame: frames) 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 //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<byte[]> frames = voiceMessage.getAMBEFrames(); List<byte[]> frames = voiceMessage.getAMBEFrames();
for(byte[] frame: frames) for(byte[] frame: frames)
{ {
processAudio(frame); processAudio(frame, message.getTimestamp());
} }
} }
else if(message instanceof VoiceMessage voiceMessage) else if(message instanceof VoiceMessage voiceMessage)
@@ -135,7 +138,7 @@ public class DMRAudioModule extends AmbeAudioModule implements IdentifierUpdateP
List<byte[]> frames = voiceMessage.getAMBEFrames(); List<byte[]> frames = voiceMessage.getAMBEFrames();
for(byte[] frame: frames) for(byte[] frame: frames)
{ {
processAudio(frame); processAudio(frame, message.getTimestamp());
} }
} }
else if(!mEncryptedCallStateEstablished && message instanceof VoiceHeader voiceHeader) 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 * Processes the audio frame. Queues the frame until encryption state is determined. Once determined, the audio
* frames are dequeued and audio is generated. * frames are dequeued and audio is generated.
*/ */
private void processAudio(byte[] frame) private void processAudio(byte[] frame, long timestamp)
{ {
if(mEncryptedCallStateEstablished) if(mEncryptedCallStateEstablished)
{ {
@@ -182,7 +185,7 @@ public class DMRAudioModule extends AmbeAudioModule implements IdentifierUpdateP
{ {
for(byte[] queuedFrame: mQueuedAmbeFrames) for(byte[] queuedFrame: mQueuedAmbeFrames)
{ {
produceAudio(queuedFrame); produceAudio(queuedFrame, timestamp);
} }
mQueuedAmbeFrames.clear(); mQueuedAmbeFrames.clear();
@@ -191,7 +194,7 @@ public class DMRAudioModule extends AmbeAudioModule implements IdentifierUpdateP
mQueuedAmbeFrames.clear(); mQueuedAmbeFrames.clear();
} }
produceAudio(frame); produceAudio(frame, timestamp);
} }
else 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 try
{ {
IAudioWithMetadata audioWithMetadata = getAudioCodec().getAudioWithMetadata(frame); IAudioWithMetadata audioWithMetadata = getAudioCodec().getAudioWithMetadata(frame);
addAudio(audioWithMetadata.getAudio()); addAudio(audioWithMetadata.getAudio());
processMetadata(audioWithMetadata); processMetadata(audioWithMetadata, timestamp);
} }
catch(Exception e) 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 * 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. * 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()) if(audioWithMetadata.hasMetadata())
{ {
@@ -225,11 +228,11 @@ public class DMRAudioModule extends AmbeAudioModule implements IdentifierUpdateP
for(Map.Entry<String,String> entry: audioWithMetadata.getMetadata().entrySet()) for(Map.Entry<String,String> entry: audioWithMetadata.getMetadata().entrySet())
{ {
//Each metadata map entry contains a tone-type (key) and tone (value) //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) 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 * Broadcasts the identifier to a registered listener
*/ */
private void broadcast(Identifier identifier) private void broadcast(ToneIdentifier identifier, long timestamp)
{ {
if(mIdentifierUpdateNotificationListener != null) if(mIdentifierUpdateNotificationListener != null)
{ {
mIdentifierUpdateNotificationListener.receive(new IdentifierUpdateNotification(identifier, mIdentifierUpdateNotificationListener.receive(new IdentifierUpdateNotification(identifier,
IdentifierUpdateNotification.Operation.ADD, getTimeslot())); 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; mIdentifierUpdateNotificationListener = null;
} }
/**
* Registers the listener to receive tone identifier messages from this module.
* @param listener to receive messages
*/
@Override
public void setMessageListener(Listener<IMessage> 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 * 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 * 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 * @param value of tone
* @return an identifier with the accumulated tone metadata set * @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) if(type == null || value == null)
{ {

View File

@@ -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.SquelchState;
import io.github.dsheirer.audio.squelch.SquelchStateEvent; import io.github.dsheirer.audio.squelch.SquelchStateEvent;
import io.github.dsheirer.bits.BinaryMessage; import io.github.dsheirer.bits.BinaryMessage;
import io.github.dsheirer.identifier.Identifier;
import io.github.dsheirer.identifier.IdentifierUpdateNotification; import io.github.dsheirer.identifier.IdentifierUpdateNotification;
import io.github.dsheirer.identifier.IdentifierUpdateProvider; import io.github.dsheirer.identifier.IdentifierUpdateProvider;
import io.github.dsheirer.identifier.Role; import io.github.dsheirer.identifier.Role;
import io.github.dsheirer.identifier.tone.AmbeTone; import io.github.dsheirer.identifier.tone.AmbeTone;
import io.github.dsheirer.identifier.tone.P25ToneIdentifier; import io.github.dsheirer.identifier.tone.P25ToneIdentifier;
import io.github.dsheirer.identifier.tone.Tone; 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.identifier.tone.ToneSequence;
import io.github.dsheirer.message.IMessage; 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.EncryptionSynchronizationSequence;
import io.github.dsheirer.module.decode.p25.phase2.message.mac.structure.PushToTalk; 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.module.decode.p25.phase2.timeslot.AbstractVoiceTimeslot;
import io.github.dsheirer.preference.UserPreferences; import io.github.dsheirer.preference.UserPreferences;
import io.github.dsheirer.protocol.Protocol;
import io.github.dsheirer.sample.Listener; 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.ArrayDeque;
import java.util.ArrayList; import java.util.ArrayList;
import java.util.List; import java.util.List;
import java.util.Map; import java.util.Map;
import java.util.Queue; 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); private final static Logger mLog = LoggerFactory.getLogger(P25P2AudioModule.class);
@@ -58,6 +60,7 @@ public class P25P2AudioModule extends AmbeAudioModule implements IdentifierUpdat
private Queue<AbstractVoiceTimeslot> mQueuedAudioTimeslots = new ArrayDeque<>(); private Queue<AbstractVoiceTimeslot> mQueuedAudioTimeslots = new ArrayDeque<>();
private boolean mEncryptedCallStateEstablished = false; private boolean mEncryptedCallStateEstablished = false;
private boolean mEncryptedCall = false; private boolean mEncryptedCall = false;
private Listener<IMessage> mMessageListener;
public P25P2AudioModule(UserPreferences userPreferences, int timeslot, AliasList aliasList) public P25P2AudioModule(UserPreferences userPreferences, int timeslot, AliasList aliasList)
{ {
@@ -116,7 +119,7 @@ public class P25P2AudioModule extends AmbeAudioModule implements IdentifierUpdat
{ {
if(!mEncryptedCall) if(!mEncryptedCall)
{ {
processAudio(abstractVoiceTimeslot.getVoiceFrames()); processAudio(abstractVoiceTimeslot.getVoiceFrames(), message.getTimestamp());
} }
} }
else else
@@ -156,7 +159,12 @@ public class P25P2AudioModule extends AmbeAudioModule implements IdentifierUpdat
} }
} }
private void processAudio(List<BinaryMessage> voiceFrames) /**
* Process the audio voice frames
* @param voiceFrames to process
* @param timestamp of the carrier message
*/
private void processAudio(List<BinaryMessage> voiceFrames, long timestamp)
{ {
if(hasAudioCodec()) if(hasAudioCodec())
{ {
@@ -168,7 +176,7 @@ public class P25P2AudioModule extends AmbeAudioModule implements IdentifierUpdat
{ {
IAudioWithMetadata audioWithMetadata = getAudioCodec().getAudioWithMetadata(voiceFrameBytes); IAudioWithMetadata audioWithMetadata = getAudioCodec().getAudioWithMetadata(voiceFrameBytes);
addAudio(audioWithMetadata.getAudio()); addAudio(audioWithMetadata.getAudio());
processMetadata(audioWithMetadata); processMetadata(audioWithMetadata, timestamp);
} }
catch(Exception e) 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 * 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. * 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()) if(audioWithMetadata.hasMetadata())
{ {
@@ -190,11 +198,11 @@ public class P25P2AudioModule extends AmbeAudioModule implements IdentifierUpdat
for(Map.Entry<String,String> entry: audioWithMetadata.getMetadata().entrySet()) for(Map.Entry<String,String> entry: audioWithMetadata.getMetadata().entrySet())
{ {
//Each metadata map entry contains a tone-type (key) and tone (value) //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) if(mIdentifierUpdateNotificationListener != null)
{ {
mIdentifierUpdateNotificationListener.receive(new IdentifierUpdateNotification(identifier, mIdentifierUpdateNotificationListener.receive(new IdentifierUpdateNotification(identifier,
IdentifierUpdateNotification.Operation.ADD, getTimeslot())); 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; mIdentifierUpdateNotificationListener = null;
} }
/**
* Registers a message listener to receive AMBE tone identifier messages.
* @param listener to register
*/
@Override
public void setMessageListener(Listener<IMessage> 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 * 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 * 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 * @param value of tone
* @return an identifier with the accumulated tone metadata set * @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) if(type == null || value == null)
{ {

View File

@@ -52,9 +52,6 @@ import io.github.dsheirer.protocol.Protocol;
import io.github.dsheirer.record.AudioRecordingManager; import io.github.dsheirer.record.AudioRecordingManager;
import io.github.dsheirer.record.binary.BinaryReader; import io.github.dsheirer.record.binary.BinaryReader;
import io.github.dsheirer.sample.Listener; import io.github.dsheirer.sample.Listener;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import java.io.IOException; import java.io.IOException;
import java.io.OutputStream; import java.io.OutputStream;
import java.nio.ByteBuffer; import java.nio.ByteBuffer;
@@ -63,6 +60,8 @@ import java.nio.file.Files;
import java.nio.file.Path; import java.nio.file.Path;
import java.nio.file.Paths; import java.nio.file.Paths;
import java.util.function.Consumer; 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 * P25 Sync Detector and Message Framer. Includes capability to detect PLL out-of-phase lock errors