mirror of
https://github.com/DSheirer/sdrtrunk.git
synced 2026-11-13 09:07:27 -05:00
P25 Phase 2 Decoder implementation.
This commit is contained in:
@@ -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;
|
||||
}
|
||||
}
|
||||
@@ -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();
|
||||
}
|
||||
@@ -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;
|
||||
}
|
||||
}
|
||||
@@ -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;
|
||||
}
|
||||
}
|
||||
@@ -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));
|
||||
}
|
||||
}
|
||||
@@ -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);
|
||||
// }
|
||||
}
|
||||
}
|
||||
@@ -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();
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -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;
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user