diff --git a/src/main/java/io/github/dsheirer/audio/codec/mbe/MBECallSequence.java b/src/main/java/io/github/dsheirer/audio/codec/mbe/MBECallSequence.java
index 15776dd5..05b71c6a 100644
--- a/src/main/java/io/github/dsheirer/audio/codec/mbe/MBECallSequence.java
+++ b/src/main/java/io/github/dsheirer/audio/codec/mbe/MBECallSequence.java
@@ -1,23 +1,20 @@
/*
+ * *****************************************************************************
+ * Copyright (C) 2014-2021 Dennis Sheirer
*
- * * ******************************************************************************
- * * Copyright (C) 2014-2019 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
- * * *****************************************************************************
+ * 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.codec.mbe;
@@ -27,6 +24,7 @@ import com.fasterxml.jackson.annotation.JsonInclude;
import com.fasterxml.jackson.annotation.JsonProperty;
import com.fasterxml.jackson.annotation.JsonPropertyOrder;
import com.fasterxml.jackson.annotation.JsonRootName;
+import io.github.dsheirer.identifier.Identifier;
import io.github.dsheirer.identifier.encryption.EncryptionKey;
import io.github.dsheirer.module.decode.p25.audio.VoiceFrame;
@@ -132,6 +130,18 @@ public class MBECallSequence
}
}
+ /**
+ * Sets the from radio identifier
+ * @param identifier for from entity
+ */
+ public void setFromIdentifier(Identifier identifier)
+ {
+ if(identifier != null)
+ {
+ setFromIdentifier(identifier.toString());
+ }
+ }
+
/**
* Radio identifier that originated the call
*
@@ -156,6 +166,18 @@ public class MBECallSequence
}
}
+ /**
+ * Sets the TO identifier
+ * @param identifier for the TO entity
+ */
+ public void setToIdentifier(Identifier identifier)
+ {
+ if(identifier != null)
+ {
+ setToIdentifier(identifier.toString());
+ }
+ }
+
/**
* Radio identifier that received the call
*
diff --git a/src/main/java/io/github/dsheirer/module/decode/DecoderFactory.java b/src/main/java/io/github/dsheirer/module/decode/DecoderFactory.java
index aca18a9c..770090f3 100644
--- a/src/main/java/io/github/dsheirer/module/decode/DecoderFactory.java
+++ b/src/main/java/io/github/dsheirer/module/decode/DecoderFactory.java
@@ -1,6 +1,6 @@
/*
* *****************************************************************************
- * Copyright (C) 2014-2020 Dennis Sheirer
+ * Copyright (C) 2014-2021 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
@@ -43,7 +43,7 @@ import io.github.dsheirer.module.decode.dmr.DMRDecoder;
import io.github.dsheirer.module.decode.dmr.DMRDecoderState;
import io.github.dsheirer.module.decode.dmr.DMRTrafficChannelManager;
import io.github.dsheirer.module.decode.dmr.DecodeConfigDMR;
-import io.github.dsheirer.module.decode.dmr.message.voice.DMRAudioModule;
+import io.github.dsheirer.module.decode.dmr.audio.DMRAudioModule;
import io.github.dsheirer.module.decode.fleetsync2.Fleetsync2Decoder;
import io.github.dsheirer.module.decode.fleetsync2.Fleetsync2DecoderState;
import io.github.dsheirer.module.decode.fleetsync2.FleetsyncMessageFilter;
diff --git a/src/main/java/io/github/dsheirer/module/decode/dmr/message/voice/DMRAudioModule.java b/src/main/java/io/github/dsheirer/module/decode/dmr/audio/DMRAudioModule.java
similarity index 91%
rename from src/main/java/io/github/dsheirer/module/decode/dmr/message/voice/DMRAudioModule.java
rename to src/main/java/io/github/dsheirer/module/decode/dmr/audio/DMRAudioModule.java
index dddebb0a..1007554d 100644
--- a/src/main/java/io/github/dsheirer/module/decode/dmr/message/voice/DMRAudioModule.java
+++ b/src/main/java/io/github/dsheirer/module/decode/dmr/audio/DMRAudioModule.java
@@ -1,25 +1,22 @@
/*
+ * *****************************************************************************
+ * Copyright (C) 2014-2021 Dennis Sheirer
*
- * * ******************************************************************************
- * * Copyright (C) 2014-2020 Dennis Sheirer, Zhenyu Mao
- * *
- * * 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
- * * *****************************************************************************
+ * 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.module.decode.dmr.message.voice;
+package io.github.dsheirer.module.decode.dmr.audio;
import io.github.dsheirer.alias.AliasList;
import io.github.dsheirer.audio.codec.mbe.AmbeAudioModule;
@@ -40,6 +37,8 @@ import io.github.dsheirer.module.decode.dmr.message.data.lc.full.AbstractVoiceCh
import io.github.dsheirer.module.decode.dmr.message.data.lc.full.FullLCMessage;
import io.github.dsheirer.module.decode.dmr.message.data.terminator.Terminator;
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.sample.Listener;
import jmbe.iface.IAudioWithMetadata;
diff --git a/src/main/java/io/github/dsheirer/module/decode/dmr/audio/DMRCallSequenceRecorder.java b/src/main/java/io/github/dsheirer/module/decode/dmr/audio/DMRCallSequenceRecorder.java
new file mode 100644
index 00000000..8b81955d
--- /dev/null
+++ b/src/main/java/io/github/dsheirer/module/decode/dmr/audio/DMRCallSequenceRecorder.java
@@ -0,0 +1,214 @@
+/*
+ * *****************************************************************************
+ * Copyright (C) 2014-2021 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.module.decode.dmr.audio;
+
+
+import io.github.dsheirer.audio.codec.mbe.MBECallSequence;
+import io.github.dsheirer.audio.codec.mbe.MBECallSequenceRecorder;
+import io.github.dsheirer.bits.BinaryMessage;
+import io.github.dsheirer.message.IMessage;
+import io.github.dsheirer.module.decode.dmr.message.DMRMessage;
+import io.github.dsheirer.module.decode.dmr.message.data.lc.LCMessage;
+import io.github.dsheirer.module.decode.dmr.message.data.lc.full.FullLCMessage;
+import io.github.dsheirer.module.decode.dmr.message.data.lc.full.GroupVoiceChannelUser;
+import io.github.dsheirer.module.decode.dmr.message.data.lc.full.UnitToUnitVoiceChannelUser;
+import io.github.dsheirer.module.decode.dmr.message.data.lc.full.hytera.HyteraGroupVoiceChannelUser;
+import io.github.dsheirer.module.decode.dmr.message.data.lc.full.hytera.HyteraUnitToUnitVoiceChannelUser;
+import io.github.dsheirer.module.decode.dmr.message.data.lc.full.motorola.CapacityPlusGroupVoiceChannelUser;
+import io.github.dsheirer.module.decode.dmr.message.data.lc.full.motorola.CapacityPlusWideAreaVoiceChannelUser;
+import io.github.dsheirer.module.decode.dmr.message.data.terminator.Terminator;
+import io.github.dsheirer.module.decode.dmr.message.voice.VoiceMessage;
+import io.github.dsheirer.preference.UserPreferences;
+import org.slf4j.Logger;
+import org.slf4j.LoggerFactory;
+
+import java.util.List;
+
+/**
+ * DMR AMBE Frame recorder generates call sequence recordings containing JSON representations of audio
+ * frames, optional encryption and call identifiers.
+ */
+public class DMRCallSequenceRecorder extends MBECallSequenceRecorder
+{
+ private final static Logger mLog = LoggerFactory.getLogger(DMRCallSequenceRecorder.class);
+
+ private static final String PROTOCOL = "DMR";
+
+ private MBECallSequence mCallSequence;
+
+ /**
+ * Constructs a DMR MBE call sequence recorder.
+ *
+ * @param userPreferences to obtain the recording directory
+ * @param channelFrequency for the channel to record
+ * @param system defined by the user
+ * @param site defined by the user
+ */
+ public DMRCallSequenceRecorder(UserPreferences userPreferences, long channelFrequency, String system, String site)
+ {
+ super(userPreferences, channelFrequency, system, site);
+ }
+
+ /**
+ * Stops and flushes any partial frame sequence from the processors
+ */
+ @Override
+ public void stop()
+ {
+ flush();
+ }
+
+ /**
+ * Primary message interface for receiving frames and metadata messages to record
+ */
+ @Override
+ public void receive(IMessage message)
+ {
+ if(message instanceof DMRMessage dmr)
+ {
+ if(dmr.isValid())
+ {
+ process(dmr);
+ }
+ }
+ }
+
+
+ /**
+ * Flushes any partial call sequence
+ */
+ public void flush()
+ {
+ if(mCallSequence != null)
+ {
+ writeCallSequence(mCallSequence);
+ mCallSequence = null;
+ }
+ }
+
+ /**
+ * Processes any DMR audio and terminator messages
+ */
+ public void process(DMRMessage message)
+ {
+ if(message instanceof VoiceMessage voiceMessage)
+ {
+ process(voiceMessage);
+ }
+ else if(message instanceof FullLCMessage fullLCMessage)
+ {
+ process(fullLCMessage);
+ }
+ else if(message instanceof Terminator)
+ {
+ flush();
+ }
+ }
+
+ /**
+ * Processes Voice messages
+ */
+ private void process(VoiceMessage voiceMessage)
+ {
+ if(mCallSequence == null)
+ {
+ mCallSequence = new MBECallSequence(PROTOCOL);
+ }
+
+ List voiceFrames = voiceMessage.getAMBEFrames();
+
+ long baseTimestamp = voiceMessage.getTimestamp();
+
+ for(byte[] frame : voiceFrames)
+ {
+ BinaryMessage frameBits = BinaryMessage.from(frame);
+ mCallSequence.addVoiceFrame(baseTimestamp, frameBits.toHexString());
+
+ //Voice frames are 20 milliseconds each, so we increment the timestamp by 20 for each one
+ baseTimestamp += 20;
+ }
+ }
+
+ /**
+ * Process full link control messages to extract call details
+ */
+ private void process(FullLCMessage message)
+ {
+ if(message.isValid() && mCallSequence != null)
+ {
+ switch(message.getOpcode())
+ {
+ case FULL_CAPACITY_PLUS_GROUP_VOICE_CHANNEL_USER:
+ if(message instanceof CapacityPlusGroupVoiceChannelUser cpvcu)
+ {
+ mCallSequence.setFromIdentifier(cpvcu.getRadio());
+ mCallSequence.setToIdentifier(cpvcu.getTalkgroup());
+ mCallSequence.setEncrypted(cpvcu.getServiceOptions().isEncrypted());
+ mCallSequence.setCallType(CALL_TYPE_GROUP);
+ }
+ break;
+ case FULL_CAPACITY_PLUS_WIDE_AREA_VOICE_CHANNEL_USER:
+ if(message instanceof CapacityPlusWideAreaVoiceChannelUser cpwavcu)
+ {
+ mCallSequence.setToIdentifier(cpwavcu.getTalkgroup());
+ mCallSequence.setEncrypted(cpwavcu.getServiceOptions().isEncrypted());
+ mCallSequence.setCallType(CALL_TYPE_GROUP);
+ }
+ break;
+ case FULL_HYTERA_GROUP_VOICE_CHANNEL_USER:
+ if(message instanceof HyteraGroupVoiceChannelUser hgvcu)
+ {
+ mCallSequence.setToIdentifier(hgvcu.getTalkgroup());
+ mCallSequence.setFromIdentifier(hgvcu.getSourceRadio());
+ mCallSequence.setCallType(CALL_TYPE_GROUP);
+ mCallSequence.setEncrypted(hgvcu.isEncrypted());
+ }
+ break;
+ case FULL_HYTERA_UNIT_TO_UNIT_VOICE_CHANNEL_USER:
+ if(message instanceof HyteraUnitToUnitVoiceChannelUser huuvcu)
+ {
+ mCallSequence.setToIdentifier(huuvcu.getTargetRadio());
+ mCallSequence.setFromIdentifier(huuvcu.getSourceRadio());
+ mCallSequence.setCallType(CALL_TYPE_INDIVIDUAL);
+ mCallSequence.setEncrypted(huuvcu.isEncrypted());
+ }
+ break;
+ case FULL_STANDARD_GROUP_VOICE_CHANNEL_USER:
+ if(message instanceof GroupVoiceChannelUser gvcu)
+ {
+ mCallSequence.setFromIdentifier(gvcu.getRadio());
+ mCallSequence.setToIdentifier(gvcu.getTalkgroup());
+ mCallSequence.setCallType(CALL_TYPE_GROUP);
+ mCallSequence.setEncrypted(gvcu.getServiceOptions().isEncrypted());
+ }
+ break;
+ case FULL_STANDARD_UNIT_TO_UNIT_VOICE_CHANNEL_USER:
+ if(message instanceof UnitToUnitVoiceChannelUser uuvcu)
+ {
+ mCallSequence.setFromIdentifier(uuvcu.getSourceRadio());
+ mCallSequence.setToIdentifier(uuvcu.getTargetRadio());
+ mCallSequence.setCallType(CALL_TYPE_INDIVIDUAL);
+ mCallSequence.setEncrypted(uuvcu.getServiceOptions().isEncrypted());
+ }
+ break;
+ }
+ }
+ }
+}