#296 APCO25 Phase II Decoder Implementation (#589)

P25 Phase 2 Decoder implementation.
This commit is contained in:
Denny Sheirer
2019-11-17 06:56:52 -05:00
committed by GitHub
parent d5b802e794
commit f934676bd0
561 changed files with 35609 additions and 6215 deletions

View File

@@ -0,0 +1,59 @@
/*
*
* * ******************************************************************************
* * 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 <http://www.gnu.org/licenses/>
* * *****************************************************************************
*
*
*/
package io.github.dsheirer.audio.codec.mbe;
import io.github.dsheirer.preference.UserPreferences;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
public abstract class AmbeAudioModule extends JmbeAudioModule
{
private static final Logger mLog = LoggerFactory.getLogger(AmbeAudioModule.class);
private static final String AMBE_CODEC = "AMBE 3600 x 2450";
private static boolean sLibraryStatusLogged = false;
public AmbeAudioModule(UserPreferences userPreferences)
{
super(userPreferences);
if(!sLibraryStatusLogged)
{
if(getAudioCodec() != null)
{
mLog.info("AMBE CODEC successfully loaded - P25-2/DMR/NXDN audio will be available");
}
else
{
mLog.warn("AMBE CODEC not loaded - P25-2/DMR/NXDN audio will NOT be available");
}
sLibraryStatusLogged = true;
}
}
@Override
protected String getCodecName()
{
return AMBE_CODEC;
}
}

View File

@@ -0,0 +1,42 @@
/*
*
* * ******************************************************************************
* * 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 <http://www.gnu.org/licenses/>
* * *****************************************************************************
*
*
*/
package io.github.dsheirer.audio.codec.mbe;
import io.github.dsheirer.identifier.encryption.EncryptionKeyIdentifier;
/**
* P25 Encryption Parameters
*/
public interface IEncryptionSyncParameters
{
/**
* Encryption key identifier
*/
EncryptionKeyIdentifier getEncryptionKey();
/**
* Message Indicator
* @return key generator fill values
*/
String getMessageIndicator();
}

View File

@@ -0,0 +1,59 @@
/*
*
* * ******************************************************************************
* * 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 <http://www.gnu.org/licenses/>
* * *****************************************************************************
*
*
*/
package io.github.dsheirer.audio.codec.mbe;
import io.github.dsheirer.preference.UserPreferences;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
public abstract class ImbeAudioModule extends JmbeAudioModule
{
private static final Logger mLog = LoggerFactory.getLogger(ImbeAudioModule.class);
private static final String IMBE_CODEC = "IMBE";
private static boolean sLibraryStatusLogged = false;
public ImbeAudioModule(UserPreferences userPreferences)
{
super(userPreferences);
if(!sLibraryStatusLogged)
{
if(getAudioCodec() != null)
{
mLog.info("JMBE audio conversion library IMBE CODEC successfully loaded - P25-1 audio will be available");
}
else
{
mLog.warn("JMBE audio conversion library, IMBE CODEC not loaded - P25-1 audio will NOT be available");
}
sLibraryStatusLogged = true;
}
}
@Override
protected String getCodecName()
{
return IMBE_CODEC;
}
}

View File

@@ -0,0 +1,248 @@
/*
*
* * ******************************************************************************
* * 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 <http://www.gnu.org/licenses/>
* * *****************************************************************************
*
*
*/
package io.github.dsheirer.audio.codec.mbe;
import com.google.common.eventbus.Subscribe;
import io.github.dsheirer.audio.AbstractAudioModule;
import io.github.dsheirer.eventbus.MyEventBus;
import io.github.dsheirer.message.IMessage;
import io.github.dsheirer.message.IMessageListener;
import io.github.dsheirer.preference.PreferenceType;
import io.github.dsheirer.preference.UserPreferences;
import io.github.dsheirer.sample.Listener;
import io.github.dsheirer.sample.buffer.ReusableAudioPacketQueue;
import jmbe.iface.IAudioCodec;
import jmbe.iface.IAudioCodecLibrary;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import java.lang.reflect.InvocationTargetException;
import java.net.MalformedURLException;
import java.net.URL;
import java.net.URLClassLoader;
import java.nio.file.Path;
import java.util.ArrayList;
import java.util.List;
public abstract class JmbeAudioModule extends AbstractAudioModule implements Listener<IMessage>, IMessageListener
{
private static final Logger mLog = LoggerFactory.getLogger(JmbeAudioModule.class);
private static final String JMBE_AUDIO_LIBRARY = "JMBE";
private static List<String> mLibraryLoadStatusLogged = new ArrayList<>();
private IAudioCodec mAudioCodec;
private UserPreferences mUserPreferences;
private ReusableAudioPacketQueue mAudioPacketQueue = new ReusableAudioPacketQueue("JmbeAudioModule");
public JmbeAudioModule(UserPreferences userPreferences)
{
mUserPreferences = userPreferences;
MyEventBus.getEventBus().register(this);
loadConverter();
}
protected IAudioCodec getAudioCodec()
{
return mAudioCodec;
}
/**
* Indicates that the JMBE audio library has been loaded and a suitable audio codec is usable (ie non-null)
*/
protected boolean hasAudioCodec()
{
return getAudioCodec() != null;
}
protected ReusableAudioPacketQueue getAudioPacketQueue()
{
return mAudioPacketQueue;
}
@Override
public Listener<IMessage> getMessageListener()
{
return this;
}
/**
* Receives notifications that the JMBE library preference has been updated via the Guava event bus
*
* @param preferenceType that was updated
*/
@Subscribe
public void preferenceUpdated(PreferenceType preferenceType)
{
if(preferenceType == PreferenceType.JMBE_LIBRARY)
{
mLibraryLoadStatusLogged.clear();
loadConverter();
}
}
/**
* Name of the CODEC to use from the JMBE library
*/
protected abstract String getCodecName();
/**
* Loads audio frame processing chain. Constructs an imbe targetdataline
* to receive the raw imbe frames. Adds an IMBE to 8k PCM format conversion
* stream wrapper. Finally, adds an upsampling (8k to 48k) stream wrapper.
*/
protected void loadConverter()
{
IAudioCodec audioConverter = null;
Path path = mUserPreferences.getJmbeLibraryPreference().getPathJmbeLibrary();
if(path != null)
{
try
{
if(!mLibraryLoadStatusLogged.contains(JMBE_AUDIO_LIBRARY))
{
mLog.info("Loading JMBE library from [" + path.toString() + "]");
}
URLClassLoader childClassLoader = new URLClassLoader(new URL[]{path.toUri().toURL()},
this.getClass().getClassLoader());
Class classToLoad = Class.forName("jmbe.JMBEAudioLibrary", true, childClassLoader);
Object instance = classToLoad.getDeclaredConstructor().newInstance();
if(instance instanceof IAudioCodecLibrary)
{
IAudioCodecLibrary library = (IAudioCodecLibrary)instance;
if((library.getMajorVersion() == 1 && library.getMinorVersion() >= 0 &&
library.getBuildVersion() >= 0) || library.getMajorVersion() >= 1)
{
audioConverter = library.getAudioConverter(getCodecName());
if(!mLibraryLoadStatusLogged.contains(JMBE_AUDIO_LIBRARY))
{
mLog.info("JMBE audio conversion library loaded: " + library.getVersion());
mLibraryLoadStatusLogged.add(JMBE_AUDIO_LIBRARY);
}
}
else
{
if(!mLibraryLoadStatusLogged.contains(JMBE_AUDIO_LIBRARY))
{
mLog.warn("JMBE library version 1.0.0 or higher is required - found: " + library.getVersion());
mLibraryLoadStatusLogged.add(JMBE_AUDIO_LIBRARY);
}
}
}
else
{
if(!mLibraryLoadStatusLogged.contains(JMBE_AUDIO_LIBRARY))
{
mLog.info("JMBE audio conversion library NOT FOUND");
mLibraryLoadStatusLogged.add(JMBE_AUDIO_LIBRARY);
}
}
}
catch(IllegalArgumentException iae)
{
if(!mLibraryLoadStatusLogged.contains(JMBE_AUDIO_LIBRARY + getCodecName()))
{
mLog.error("Couldn't load JMBE audio conversion library - " + iae.getMessage());
mLibraryLoadStatusLogged.add(JMBE_AUDIO_LIBRARY + getCodecName());
}
}
catch(NoSuchMethodException nsme)
{
if(!mLibraryLoadStatusLogged.contains(JMBE_AUDIO_LIBRARY))
{
mLog.error("Couldn't load JMBE audio conversion library - no such method exception");
mLibraryLoadStatusLogged.add(JMBE_AUDIO_LIBRARY);
}
}
catch(MalformedURLException mue)
{
if(!mLibraryLoadStatusLogged.contains(JMBE_AUDIO_LIBRARY))
{
mLog.error("Couldn't load JMBE audio conversion library from path [" + path + "]");
mLibraryLoadStatusLogged.add(JMBE_AUDIO_LIBRARY);
}
}
catch(ClassNotFoundException e1)
{
if(!mLibraryLoadStatusLogged.contains(JMBE_AUDIO_LIBRARY))
{
mLog.error("Couldn't load JMBE audio conversion library - class not found");
mLibraryLoadStatusLogged.add(JMBE_AUDIO_LIBRARY);
}
}
catch(InvocationTargetException ite)
{
if(!mLibraryLoadStatusLogged.contains(JMBE_AUDIO_LIBRARY))
{
mLog.error("Couldn't load JMBE audio conversion library - invocation target exception", ite);
mLibraryLoadStatusLogged.add(JMBE_AUDIO_LIBRARY);
}
}
catch(InstantiationException e1)
{
if(!mLibraryLoadStatusLogged.contains(JMBE_AUDIO_LIBRARY))
{
mLog.error("Couldn't load JMBE audio conversion library - instantiation exception", e1);
mLibraryLoadStatusLogged.add(JMBE_AUDIO_LIBRARY);
}
}
catch(IllegalAccessException e1)
{
if(!mLibraryLoadStatusLogged.contains(JMBE_AUDIO_LIBRARY))
{
mLog.error("Couldn't load JMBE audio conversion library - security restrictions");
mLibraryLoadStatusLogged.add(JMBE_AUDIO_LIBRARY);
}
}
}
else
{
if(!mLibraryLoadStatusLogged.contains(JMBE_AUDIO_LIBRARY))
{
mLog.warn("JMBE audio library path is NOT SET in your User Preferences.");
mLibraryLoadStatusLogged.add(JMBE_AUDIO_LIBRARY);
}
}
if(audioConverter != null)
{
mAudioCodec = audioConverter;
}
else
{
mAudioCodec = null;
}
}
@Override
public void dispose()
{
mAudioCodec = null;
}
}

View File

@@ -0,0 +1,291 @@
/*
*
* * ******************************************************************************
* * 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 <http://www.gnu.org/licenses/>
* * *****************************************************************************
*
*
*/
package io.github.dsheirer.audio.codec.mbe;
import com.fasterxml.jackson.annotation.JsonIgnore;
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.encryption.EncryptionKey;
import io.github.dsheirer.module.decode.p25.audio.VoiceFrame;
import java.util.ArrayList;
import java.util.List;
/**
* MBE Call Sequence containing one or more voice frames with optional encryption parameters and optional from and to
* radio identifiers.
*/
@JsonRootName("mbe_call")
@JsonPropertyOrder({"protocol", "call_type", "from", "to", "encrypted", "frames"})
@JsonInclude(JsonInclude.Include.NON_NULL)
public class MBECallSequence
{
private String mProtocol;
private String mFromIdentifier;
private String mToIdentifier;
private String mCallType;
private String mSystem;
private String mSite;
private boolean mEncrypted;
private List<VoiceFrame> mVoiceFrames = new ArrayList<>();
private IEncryptionSyncParameters mTemporaryEncryptionSyncParameters;
/**
* Constructs a call sequence
*/
public MBECallSequence(String protocol)
{
mProtocol = protocol;
}
public MBECallSequence()
{
//no-arg constructor for faster jackson deserialization
}
/**
* Protocol/format for the voice frames
*/
@JsonProperty("protocol")
public String getProtocol()
{
return mProtocol;
}
public void setProtocol(String protocol)
{
mProtocol = protocol;
}
/**
* Indicates if this sequences contains any audio frames
*/
@JsonIgnore
public boolean hasAudio()
{
return mVoiceFrames.size() > 4;
}
/**
* Indicates if this call sequence is encrypted
*/
@JsonProperty("encrypted")
public boolean isEncrypted()
{
return mEncrypted;
}
/**
* Sets the encrypted status of this call sequence
*
* @param encrypted status
*/
public void setEncrypted(boolean encrypted)
{
mEncrypted = encrypted;
}
/**
* Sets encryption sync parameters to use with the next sequence of voice frames. This is usually obtained from the
* HDU (phase1) or the PTT (phase2)
*
* @param encryptionSyncParameters to apply to the next set of voice frames
*/
public void setEncryptionSyncParameters(IEncryptionSyncParameters encryptionSyncParameters)
{
mTemporaryEncryptionSyncParameters = encryptionSyncParameters;
}
/**
* Sets the from radio identifier
*
* @param from id
*/
public void setFromIdentifier(String from)
{
if(from != null && !from.isEmpty() && !from.contentEquals("0"))
{
mFromIdentifier = from;
}
}
/**
* Radio identifier that originated the call
*
* @return from id
*/
@JsonProperty("from")
public String getFromIdentifier()
{
return mFromIdentifier;
}
/**
* Sets the to radio identifier
*
* @param to id
*/
public void setToIdentifier(String to)
{
if(to != null && !to.isEmpty() && !to.contentEquals("0"))
{
mToIdentifier = to;
}
}
/**
* Radio identifier that received the call
*
* @return to id
*/
@JsonProperty("to")
public String getToIdentifier()
{
return mToIdentifier;
}
/**
* Call type
*
* @param type from GROUP, INDIVIDUAL, or TELEPHONE INTERCONNECT
*/
public void setCallType(String type)
{
mCallType = type;
}
/**
* Sets the call type
*
* @return call type
*/
@JsonProperty("call_type")
public String getCallType()
{
return mCallType;
}
/**
* System name defined by the user
* @return
*/
@JsonProperty("system")
public String getSystem()
{
return mSystem;
}
/**
* Sets the system name
* @param system
*/
public void setSystem(String system)
{
mSystem = system;
}
/**
* Site name defined by the user
* @return
*/
@JsonProperty("site")
public String getSite()
{
return mSite;
}
/**
* Sets the site name
* @param site
*/
public void setSite(String site)
{
mSite = site;
}
/**
* Ordered list of voice frames for the call sequence
*
* @return list of unencrypted or encrypted audio frames
*/
@JsonProperty("frames")
public List<VoiceFrame> getVoiceFrames()
{
return mVoiceFrames;
}
public void setVoiceFrames(List<VoiceFrame> voiceFrames)
{
mVoiceFrames = voiceFrames;
}
/**
* Adds an unencrypted audio frame to this call sequence
*
* @param timestamp of the audio frame
* @param frame of hexadecimal values representing the transmitted audio frame and ecc bits
*/
public void addVoiceFrame(long timestamp, String frame)
{
if(mTemporaryEncryptionSyncParameters != null)
{
addEncryptedVoiceFrame(timestamp, frame, mTemporaryEncryptionSyncParameters);
mTemporaryEncryptionSyncParameters = null;
}
else
{
mVoiceFrames.add(new VoiceFrame(timestamp, frame));
}
}
/**
* Adds an encrypted audio frame to this call sequence
*
* @param timestamp of the audio frame
* @param frame of hexadecimal values representing the transmitted audio frame and ecc bits
* @param parameters identifying the encryption used for the frame
*/
public void addEncryptedVoiceFrame(long timestamp, String frame, IEncryptionSyncParameters parameters)
{
EncryptionKey encryption = parameters.getEncryptionKey().getValue();
addEncryptedVoiceFrame(timestamp, frame, encryption.getAlgorithm(), encryption.getKey(), parameters.getMessageIndicator());
setEncrypted(true);
}
/**
* Adds an encrypted audio frame to this call sequence
*
* @param timestamp of the audio frame
* @param frame of hexadecimal values representing the transmitted audio frame and ecc bits
* @param algorithm identifier for encryption
* @param keyid identifying which encryption key was used by the radio which may have multiple keys
* @param messageIndicator to seed the key generator
*/
public void addEncryptedVoiceFrame(long timestamp, String frame, int algorithm, int keyid, String messageIndicator)
{
mVoiceFrames.add(new VoiceFrame(timestamp, frame, algorithm, keyid, messageIndicator));
}
}

View File

@@ -0,0 +1,122 @@
/*
*
* * ******************************************************************************
* * 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 <http://www.gnu.org/licenses/>
* * *****************************************************************************
*
*
*/
package io.github.dsheirer.audio.codec.mbe;
import com.fasterxml.jackson.databind.ObjectMapper;
import io.github.dsheirer.audio.convert.thumbdv.ThumbDv;
import io.github.dsheirer.audio.convert.thumbdv.message.response.AmbeResponse;
import io.github.dsheirer.module.decode.p25.audio.VoiceFrame;
import io.github.dsheirer.record.wave.AudioPacketWaveRecorder;
import io.github.dsheirer.record.wave.WaveMetadata;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import java.io.IOException;
import java.io.InputStream;
import java.nio.file.Files;
import java.nio.file.Path;
import java.nio.file.Paths;
/**
* Utility for converting MBE call sequences (*.mbe) to PCM wave audio format
*/
public class MBECallSequenceConverter
{
private final static Logger mLog = LoggerFactory.getLogger(MBECallSequenceConverter.class);
public static void convert(Path input, Path output) throws IOException
{
InputStream inputStream = Files.newInputStream(input);
ObjectMapper mapper = new ObjectMapper();
MBECallSequence sequence = mapper.readValue(inputStream, MBECallSequence.class);
convert(sequence, output);
}
public static void convert(MBECallSequence callSequence, Path outputPath)
{
if(callSequence == null || callSequence.isEncrypted())
{
throw new IllegalArgumentException("Cannot decode null or encrypted call sequence");
}
if(callSequence != null && !callSequence.isEncrypted())
{
ThumbDv.AudioProtocol protocol = ThumbDv.AudioProtocol.P25_PHASE2;
AudioPacketWaveRecorder recorder = new AudioPacketWaveRecorder(outputPath);
recorder.start();
long delayMillis = 0;
try(ThumbDv thumbDv = new ThumbDv(protocol, recorder))
{
thumbDv.start();
for(VoiceFrame voiceFrame: callSequence.getVoiceFrames())
{
mLog.debug("Frame [" + voiceFrame.getFrame() + "] + Hex [" + AmbeResponse.toHex(voiceFrame.getFrameBytes()) + "]");
thumbDv.decode(voiceFrame.getFrameBytes());
delayMillis += 30;
}
if(delayMillis > 0)
{
delayMillis += 1000;
try
{
Thread.sleep(delayMillis);
}
catch(InterruptedException ie)
{
}
}
}
catch(IOException ioe)
{
mLog.error("Error", ioe);
}
recorder.stop(Paths.get(outputPath.toString().replace(".tmp", ".wav")), new WaveMetadata());
}
}
public static void main(String[] args)
{
// String mbe = "/home/denny/SDRTrunk/recordings/20190331085324_154250000_3_TS0_65084_6591007.mbe";
// String mbe = "/home/denny/SDRTrunk/recordings/20190331085324_154250000_2_TS1_65035.mbe";
//
// Path input = Paths.get(mbe);
// Path output = Paths.get(mbe.replace(".mbe", ".tmp"));
//
// mLog.info("Converting: " + mbe);
//
// try
// {
// MBECallSequenceConverter.convert(input, output);
// }
// catch(IOException ioe)
// {
// mLog.error("Error", ioe);
// }
}
}

View File

@@ -0,0 +1,79 @@
/*
*
* * ******************************************************************************
* * 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 <http://www.gnu.org/licenses/>
* * *****************************************************************************
*
*
*/
package io.github.dsheirer.audio.codec.mbe;
import com.fasterxml.jackson.databind.ObjectMapper;
import com.google.common.base.Joiner;
import io.github.dsheirer.module.decode.p25.audio.VoiceFrame;
import java.io.IOException;
import java.nio.file.Files;
import java.nio.file.Path;
import java.util.ArrayList;
import java.util.Collections;
import java.util.List;
/**
* Reader for MBE call sequence recordings
*/
public class MBECallSequenceReader
{
public static List<String> getAudioFrames(Path path) throws IOException
{
ObjectMapper mapper = new ObjectMapper();
Object obj = mapper.readValue(path.toFile(), MBECallSequence.class);
if(obj instanceof MBECallSequence)
{
MBECallSequence sequence = (MBECallSequence)obj;
List<String> audioFrames = new ArrayList<>();
for(VoiceFrame voiceFrame: sequence.getVoiceFrames())
{
audioFrames.add(voiceFrame.getFrame());
}
return audioFrames;
}
return Collections.emptyList();
}
public static void main(String[] args)
{
Path path = Path.of("/home/denny/SDRTrunk/recordings/20190706063149_154250000_7_TS1_65084_6591001.mbe");
try
{
List<String> frames = MBECallSequenceReader.getAudioFrames(path);
Files.writeString(Path.of("/home/denny/SDRTrunk/recordings/mbe_frames.txt"), Joiner.on("\",\n\"").join(frames));
}
catch(Exception e)
{
e.printStackTrace();
}
}
}

View File

@@ -0,0 +1,163 @@
/*
*
* * ******************************************************************************
* * 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 <http://www.gnu.org/licenses/>
* * *****************************************************************************
*
*
*/
package io.github.dsheirer.audio.codec.mbe;
import com.fasterxml.jackson.databind.ObjectMapper;
import io.github.dsheirer.message.IMessage;
import io.github.dsheirer.message.IMessageListener;
import io.github.dsheirer.module.Module;
import io.github.dsheirer.preference.TimestampFormat;
import io.github.dsheirer.preference.UserPreferences;
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.file.Files;
import java.nio.file.Path;
import java.util.Date;
/**
* Records MBE audio frame call sequences and metadata to a JSON format recording file
*/
public abstract class MBECallSequenceRecorder extends Module implements IMessageListener, Listener<IMessage>
{
private final static Logger mLog = LoggerFactory.getLogger(MBECallSequenceRecorder.class);
protected static final String CALL_TYPE_GROUP = "GROUP";
protected static final String CALL_TYPE_INDIVIDUAL = "INDIVIDUAL";
protected static final String CALL_TYPE_TELEPHONE_INTERCONNECT = "TELEPHONE INTERCONNECT";
protected UserPreferences mUserPreferences;
protected long mChannelFrequency;
protected String mSystem;
protected String mSite;
private int mCallNumber = 1;
/**
* Constructs an instance
* @param userPreferences to obtain recording directory
* @param channelFrequency for the channel to record
*/
public MBECallSequenceRecorder(UserPreferences userPreferences, long channelFrequency, String system, String site)
{
mUserPreferences = userPreferences;
mChannelFrequency = channelFrequency;
mSystem = system;
mSite = site;
}
@Override
public void reset()
{
}
@Override
public void start()
{
}
@Override
public void dispose()
{
}
/**
* Writes an MBE call sequence recording to the recording directory
* @param sequence to write
*/
protected void writeCallSequence(MBECallSequence sequence)
{
writeCallSequence(sequence, null);
}
/**
* Writes an MBE call sequence recording to the recording directory.
*
* @param optionalChannelTag to include in the filename
* @param sequence containing voice frames
*/
protected void writeCallSequence(MBECallSequence sequence, String optionalChannelTag)
{
if(sequence != null && sequence.hasAudio())
{
sequence.setSystem(mSystem);
sequence.setSite(mSite);
StringBuilder sb = new StringBuilder();
sb.append(TimestampFormat.TIMESTAMP_COMPACT.getFormatter().format(new Date(System.currentTimeMillis())));
sb.append("_").append(mChannelFrequency);
sb.append("_").append(mCallNumber++);
if(mCallNumber < 1)
{
mCallNumber = 1;
}
if(optionalChannelTag != null)
{
sb.append("_").append(optionalChannelTag);
}
if(sequence.getToIdentifier() != null)
{
sb.append("_").append(sequence.getToIdentifier().replace(":", ""));
}
if(sequence.getFromIdentifier() != null)
{
sb.append("_").append(sequence.getFromIdentifier().replace(":", ""));
}
if(sequence.isEncrypted())
{
sb.append("_encrypted");
}
sb.append(".mbe");
Path recordingDirectory = mUserPreferences.getDirectoryPreference().getDirectoryRecording();
Path filePath = recordingDirectory.resolve(sb.toString());
try
{
OutputStream outputStream = Files.newOutputStream(filePath);
ObjectMapper mapper = new ObjectMapper();
mapper.writerWithDefaultPrettyPrinter().writeValue(outputStream, sequence);
outputStream.close();
}
catch(IOException ioe)
{
mLog.error("Couldn't write MBE call sequence to path [" + filePath.toString() + "]", ioe);
}
}
}
/**
* Implementation of IMessageListener interface
*/
@Override
public Listener<IMessage> getMessageListener()
{
return this;
}
}