P25 Decoder - improved LSM decoding and IMBE audio decoding. Includes redesigned audio output class to handle automatic start/stop of audio playback based on availability of audio data.

This commit is contained in:
dennis.sheirer
2015-06-08 01:05:19 +00:00
parent 5dd4454c4c
commit 31482a1014
32 changed files with 366 additions and 432 deletions

View File

@@ -24,6 +24,6 @@
<classpathentry kind="lib" path="imports/logback-classic-1.1.2.jar"/>
<classpathentry kind="lib" path="imports/logback-core-1.1.2.jar"/>
<classpathentry kind="lib" path="imports/commons-io-2.4.jar"/>
<classpathentry kind="lib" path="/jmbe/library/jmbe-0.1.3.jar"/>
<classpathentry kind="lib" path="imports/jmbe-interface-0.2.0.jar"/>
<classpathentry kind="output" path="bin"/>
</classpath>

View File

@@ -6,7 +6,7 @@
<!-- version properties -->
<property name="major" value="0"/>
<property name="minor" value="1"/>
<property name="build" value="3"/>
<property name="build" value="4"/>
<!-- Build properties -->
<property name="classes" location="../classes"/>
@@ -89,18 +89,27 @@
</copy>
<!-- Set linux permissions on start script -->
<chmod file="${product}/run_sdrtrunk_linux" perm="777" />
<chmod file="${product}/run_sdrtrunk_linux.sh" perm="777" />
<tstamp prefix="build" />
<property name="filename" value="sdrtrunk_${build.DSTAMP}_${build.TSTAMP}_${major}.${minor}.${build}"/>
<!-- Bundle the files into .zip -->
<zip destfile="${product}/sdrtrunk_${build.DSTAMP}_${build.TSTAMP}_${major}.${minor}.${build}.zip"
basedir="${product}" />
<zip destfile="${product}/${filename}.zip" basedir="${product}" />
<!-- Bundle the files into .tar.gz to preserve linux script permissions -->
<tar destfile="${product}/${filename}.tar.gz" compression="gzip">
<tarfileset dir="${product}" filemode="777">
<include name="*.sh"/>
</tarfileset>
<tarfileset dir="${product}">
<exclude name="*.sh"/>
</tarfileset>
</tar>
</target>
<target name="clean"
description="Clean previous build artifacts" >
<!-- Delete the ${classes} and ${product} directory trees -->
<target name="clean" description="Clean previous build artifacts" >
<delete dir="${classes}"/>
<delete dir="${product}"/>
</target>

Binary file not shown.

View File

@@ -1,2 +0,0 @@
#!/bin/bash
java -cp "*:libs/*" gui.SDRTrunk&

View File

@@ -0,0 +1,2 @@
#!/bin/bash
java -XX:+UseG1GC -cp "*:libs/*" gui.SDRTrunk&

View File

@@ -1 +1 @@
java -cp "SDRTrunk.jar;*;libs/*;config/*;images/*" gui.SDRTrunk
java -XX:+UseG1GC -cp "SDRTrunk.jar;*;libs/*;config/*;images/*" gui.SDRTrunk

View File

@@ -24,7 +24,7 @@ import java.util.List;
import javax.xml.bind.annotation.XmlAttribute;
import alias.action.AliasAction;
import audio.AudioType;
import audio.inverted.AudioType;
public class Alias
{

View File

@@ -17,7 +17,7 @@
******************************************************************************/
package alias;
import audio.AudioType;
import audio.inverted.AudioType;
public abstract class AliasID
{

View File

@@ -21,7 +21,7 @@ import javax.xml.bind.annotation.XmlAttribute;
import alias.AliasID;
import alias.AliasIDType;
import audio.AudioType;
import audio.inverted.AudioType;
public class TalkgroupID extends AliasID

View File

@@ -31,7 +31,7 @@ import javax.swing.JTextArea;
import javax.swing.JTextField;
import net.miginfocom.swing.MigLayout;
import audio.AudioType;
import audio.inverted.AudioType;
import controller.ConfigurableNode;
public class TalkgroupIDEditor extends JPanel implements ActionListener

View File

@@ -1,20 +0,0 @@
package audio;
import javax.sound.sampled.AudioFormat;
public class Audio
{
public static final AudioFormat MONO_48K_16_LE =
new AudioFormat( 48000, //SampleRate
16, //Sample Size
1, //Channels
true, //Signed
false ); //Little Endian
public static final AudioFormat STEREO_48K_16_LE =
new AudioFormat( 48000, //SampleRate
16, //Sample Size
2, //Channels
true, //Signed
false ); //Little Endian
}

View File

@@ -1,8 +1,8 @@
package decode.p25.audio;
package audio;
import javax.sound.sampled.AudioFormat;
public class IMBEAudioFormat
public class AudioFormats
{
public static final AudioFormat.Encoding IMBE_ENCODING =
new AudioFormat.Encoding( "IMBE" );
@@ -47,5 +47,4 @@ public class IMBEAudioFormat
PCM_FRAME_SIZE_BYTES,
PCM_48KHZ_RATE,
LITTLE_ENDIAN );
}

View File

@@ -28,6 +28,9 @@ import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import sample.real.RealSampleListener;
import audio.inverted.AudioAdapter;
import audio.inverted.AudioType;
import audio.inverted.IAudioTypeListener;
import buffer.FloatSampleBufferAssembler;
public class AudioOutputImpl implements RealSampleListener,

View File

@@ -1,24 +1,49 @@
package audio;
import java.nio.ByteBuffer;
public class AudioPacket
{
private ByteBuffer mAudioBuffer;
private String mSource;
private Type mType;
private byte[] mAudioData;
private int mPriority;
public AudioPacket()
public AudioPacket( String source, Type type )
{
mSource = source;
mType = type;
}
public void setAudioBuffer( ByteBuffer audioBuffer )
public AudioPacket( String source, byte[] audio, int priority )
{
mAudioBuffer = audioBuffer;
this( source, Type.AUDIO );
mAudioData = audio;
mPriority = priority;
}
public ByteBuffer getAudioBuffer()
public String getSource()
{
return mAudioBuffer;
return mSource;
}
public Type getType()
{
return mType;
}
public byte[] getAudioData()
{
return mAudioData;
}
public int getPriority()
{
return mPriority;
}
public enum Type
{
AUDIO,
END;
}
}

View File

@@ -1,6 +1,7 @@
package audio;
import audio.SquelchListener.SquelchState;
import audio.inverted.AudioType;
public interface IAudioOutput
{

View File

@@ -1,6 +0,0 @@
package audio;
public interface IAudioPacketReceiver
{
}

View File

@@ -1,12 +1,183 @@
package audio;
public class MonoAudioOutput
import java.util.ArrayList;
import java.util.List;
import java.util.concurrent.Executors;
import java.util.concurrent.LinkedTransferQueue;
import java.util.concurrent.ScheduledExecutorService;
import java.util.concurrent.TimeUnit;
import java.util.concurrent.atomic.AtomicBoolean;
import javax.sound.sampled.AudioSystem;
import javax.sound.sampled.LineUnavailableException;
import javax.sound.sampled.SourceDataLine;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import sample.Listener;
import audio.AudioPacket.Type;
/**
* Audio output with automatic flow control based on the availability of
* audio data packets.
*/
public class MonoAudioOutput implements Listener<AudioPacket>
{
/**
* Buffered
*/
private final static Logger mLog = LoggerFactory.getLogger( MonoAudioOutput.class );
/* Output line buffer size is set at one second of audio at 48 kHz and
* two bytes per sample */
private final static int BUFFER_SIZE = 48000;
/* The queue processor will run every 20 milliseconds checking for inbound
* audio and automatically starting or stopping audio playback. We set the
* data line available space threshold to start when we're 20 ms away from
* being full, and the stop threshold when 20 ms away from being empty */
private final static int SAMPLE_SIZE_40_MS = 48000 / 25;
private final static int START_THRESHOLD = SAMPLE_SIZE_40_MS;
private final static int STOP_THRESHOLD = BUFFER_SIZE - SAMPLE_SIZE_40_MS;
private LinkedTransferQueue<AudioPacket> mBuffer = new LinkedTransferQueue<>();
private ScheduledExecutorService mExecutorService;
private SourceDataLine mOutput;
private boolean mCanProcessAudio;
private AtomicBoolean mMuted = new AtomicBoolean( true );
private AtomicBoolean mSquelched = new AtomicBoolean( false );
public MonoAudioOutput()
{
try
{
mOutput = AudioSystem.getSourceDataLine(
AudioFormats.PCM_SIGNED_48KHZ_16BITS );
if( mOutput != null )
{
mOutput.open( AudioFormats.PCM_SIGNED_48KHZ_16BITS, BUFFER_SIZE );
mCanProcessAudio = true;
mExecutorService = Executors.newSingleThreadScheduledExecutor();
/* Run the queue processor task every 40 milliseconds */
mExecutorService.scheduleAtFixedRate( new QueueProcessor(),
0, 40, TimeUnit.MILLISECONDS );
}
}
catch ( LineUnavailableException e )
{
mLog.error( "Couldn't obtain source data line for 48kHz PCM mono "
+ "audio output" );
}
}
public void setMuted( boolean muted )
{
mMuted.set( muted );
}
public void setSquelched( boolean squelched )
{
mSquelched.set( squelched );
}
public void dispose()
{
mCanProcessAudio = false;
mBuffer.clear();
if( mExecutorService != null )
{
mExecutorService.shutdown();
}
if( mOutput != null )
{
mOutput.close();
}
}
@Override
public void receive( AudioPacket packet )
{
if( mCanProcessAudio )
{
mBuffer.add( packet );
}
}
public class QueueProcessor implements Runnable
{
private AtomicBoolean mProcessing = new AtomicBoolean();
public QueueProcessor()
{
}
@Override
public void run()
{
/* The mProcessing flag ensures that only one instance of the
* processor can run at any given time */
if( mProcessing.compareAndSet( false, true ) )
{
List<AudioPacket> packets = new ArrayList<AudioPacket>();
mBuffer.drainTo( packets );
if( packets.size() > 0 )
{
for( AudioPacket packet: packets )
{
if( mCanProcessAudio )
{
if( mMuted.get() || mSquelched.get() )
{
checkStop();
}
else if( packet.getType() == Type.AUDIO )
{
mOutput.write( packet.getAudioData(), 0,
packet.getAudioData().length );
checkStart();
}
}
}
}
else
{
checkStop();
}
mProcessing.set( false );
}
}
private void checkStart()
{
if( mCanProcessAudio && !mMuted.get() && !mSquelched.get() &&
!mOutput.isRunning() && mOutput.available() <= START_THRESHOLD )
{
mOutput.start();
}
}
private void checkStop()
{
if( mCanProcessAudio && mOutput.isRunning() &&
( mOutput.available() >= STOP_THRESHOLD || mMuted.get() || mSquelched.get() ) )
{
mOutput.drain();
mOutput.stop();
}
}
}
}

View File

@@ -15,7 +15,7 @@
* 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 audio;
package audio.inverted;
import util.Oscillator;
import dsp.filter.FilterFactory;

View File

@@ -15,8 +15,9 @@
* 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 audio;
package audio.inverted;
import audio.InversionFrequency;
import sample.real.RealSampleListener;
import util.Oscillator;
import dsp.filter.Filters;

View File

@@ -15,7 +15,7 @@
* 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 audio;
package audio.inverted;
public enum AudioType
{

View File

@@ -15,7 +15,8 @@
* 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 audio;
package audio.inverted;
public interface IAudioTypeListener
{

View File

@@ -52,8 +52,8 @@ import source.tuner.frequency.AutomaticFrequencyControl;
import source.tuner.frequency.FrequencyCorrectionControl;
import alias.AliasList;
import audio.IAudioOutput;
import audio.IAudioTypeListener;
import audio.SquelchListener;
import audio.inverted.IAudioTypeListener;
import controller.ResourceManager;
import controller.ThreadPoolManager.ThreadType;
import controller.activity.CallEvent;

View File

@@ -35,10 +35,10 @@ import properties.SystemProperties;
import sample.Broadcaster;
import sample.Listener;
import alias.AliasList;
import audio.AudioType;
import audio.IAudioTypeListener;
import audio.SquelchListener;
import audio.SquelchListener.SquelchState;
import audio.inverted.AudioType;
import audio.inverted.IAudioTypeListener;
import controller.activity.CallEvent;
import controller.activity.CallEvent.CallEventType;
import controller.activity.CallEventAliasCellRenderer;
@@ -294,7 +294,7 @@ public abstract class ChannelState implements Listener<Message>
{
/* Let all CALL state changes go through in order to update the fade
* timer, otherwise, don't update the state if it hasn't changed */
if( mState != state || state == State.CALL )
if( mState != state || state == State.CALL || state == State.DATA || state == State.ENCRYPTED )
{
if( mState.canChangeTo( state ) )
{
@@ -314,6 +314,10 @@ public abstract class ChannelState implements Listener<Message>
mTimerService.stopResetTimer();
mTimerService.startFadeTimer();
break;
case ENCRYPTED:
setSquelchState( SquelchState.SQUELCH );
mTimerService.stopResetTimer();
mTimerService.startFadeTimer();
case NO_TUNER:
mTimerService.stopResetTimer();
if( getProcessingChain().getChannel()
@@ -569,12 +573,7 @@ public abstract class ChannelState implements Listener<Message>
@Override
public boolean canChangeTo( State state )
{
return state == IDLE ||
state == CALL ||
state == CONTROL ||
state == DATA ||
state == FADE ||
state == NO_TUNER;
return true;
}
},
CALL( "CALL" ) {
@@ -584,6 +583,7 @@ public abstract class ChannelState implements Listener<Message>
return state == CALL ||
state == CONTROL ||
state == DATA ||
state == ENCRYPTED ||
state == FADE;
}
},
@@ -594,6 +594,7 @@ public abstract class ChannelState implements Listener<Message>
return state == CALL ||
state == CONTROL ||
state == DATA ||
state == State.ENCRYPTED ||
state == FADE;
}
},

View File

@@ -42,10 +42,12 @@ import dsp.filter.ComplexFIRFilter;
import dsp.filter.FilterFactory;
import dsp.filter.FloatFIRFilter;
import dsp.filter.Window.WindowType;
import dsp.filter.equalizer.CMAEqualizer;
import dsp.gain.ComplexFeedForwardGainControl;
import dsp.gain.DirectGainControl;
import dsp.nbfm.FMDiscriminator;
import dsp.psk.CQPSKDemodulator;
import dsp.psk.QPSKPolarSlicer;
import dsp.psk.QPSKStarSlicer;
public class P25Decoder extends Decoder implements Instrumentable
@@ -76,16 +78,16 @@ public class P25Decoder extends Decoder implements Instrumentable
/* Gain */
private ComplexFeedForwardGainControl mAGC;
private DirectGainControl mDGC;
private CMAEqualizer mEqualizer;
/* Filters */
private ComplexFIRFilter mBasebandFilter;
private ComplexFIRFilter mRootRaisedCosineFilter;
private FloatFIRFilter mAudioFilter;
private C4FMSymbolFilter mSymbolFilter;
/* Slicers */
private C4FMSlicer mC4FMSlicer;
private QPSKStarSlicer mCQPSKSlicer;
private QPSKPolarSlicer mCQPSKSlicer;
/* Message framers and processors */
private P25MessageFramer mMessageFramer;
@@ -134,20 +136,17 @@ public class P25Decoder extends Decoder implements Instrumentable
48000, 7250, 8000, 60, WindowType.HANNING, true ), 1.0 );
this.addComplexListener( mBasebandFilter );
// mEqualizer = new CMAEqualizer( 1.0f, 0.005f );
// mBasebandFilter.setListener( mEqualizer );
mAGC = new ComplexFeedForwardGainControl( 32 );
// mEqualizer.setListener( mAGC );
mBasebandFilter.setListener( mAGC );
/* Root raised cosine filter using 34 symbol periods and 10
* samples per symbol with a roll-off (alpha) value of 0.2.
* This should produce a filter with 341 coefficients. */
mRootRaisedCosineFilter = new ComplexFIRFilter( FilterFactory
.getRootRaisedCosine( 10, 34, 0.2 ), 1.0 );
mAGC.setListener( mRootRaisedCosineFilter );
mRootRaisedCosineFilter.setListener( mCQPSKDemodulator );
mAGC.setListener( mCQPSKDemodulator );
mCQPSKSlicer = new QPSKStarSlicer();
mCQPSKSlicer = new QPSKPolarSlicer();
mCQPSKDemodulator.setListener( mCQPSKSlicer );
mCQPSKSlicer.addListener( mMessageFramer );
@@ -267,7 +266,7 @@ public class P25Decoder extends Decoder implements Instrumentable
case INSTRUMENT_BASEBAND_FILTER_OUTPUT:
ComplexTap baseband = (ComplexTap)tap;
mBasebandFilter.setListener( baseband );
baseband.setListener( mAGC );
baseband.setListener( mEqualizer );
break;
case INSTRUMENT_AGC_OUTPUT:
ComplexTap agcSymbol = (ComplexTap)tap;

View File

@@ -43,8 +43,8 @@ public class P25MessageFramer implements Listener<Dibit>
LoggerFactory.getLogger( P25MessageFramer.class );
/* Determines the threshold for sync pattern soft matching */
private static final int SYNC_MATCH_THRESHOLD = 4;
private static final int SYNC_IN_CALL_THRESHOLD = 8;
private static final int SYNC_MATCH_THRESHOLD = 2;
private static final int SYNC_IN_CALL_THRESHOLD = 4;
private SoftSyncDetector mPrimarySyncDetector = new SoftSyncDetector(
FrameSync.P25_PHASE1_NORMAL.getSync(), SYNC_MATCH_THRESHOLD );

View File

@@ -12,6 +12,8 @@ import javax.sound.sampled.TargetDataLine;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import audio.AudioFormats;
public class IMBETargetDataLine implements TargetDataLine
{
private final static Logger mLog =
@@ -35,7 +37,7 @@ public class IMBETargetDataLine implements TargetDataLine
public void open( AudioFormat format, int bufferSize )
throws LineUnavailableException
{
if( format != IMBEAudioFormat.IMBE_AUDIO_FORMAT )
if( format != AudioFormats.IMBE_AUDIO_FORMAT )
{
throw new LineUnavailableException( "Unsupported format" );
}
@@ -44,7 +46,7 @@ public class IMBETargetDataLine implements TargetDataLine
@Override
public void open() throws LineUnavailableException
{
open( IMBEAudioFormat.IMBE_AUDIO_FORMAT, BUFFER_SIZE );
open( AudioFormats.IMBE_AUDIO_FORMAT, BUFFER_SIZE );
}
@Override
@@ -161,7 +163,7 @@ public class IMBETargetDataLine implements TargetDataLine
@Override
public AudioFormat getFormat()
{
return IMBEAudioFormat.IMBE_AUDIO_FORMAT;
return AudioFormats.IMBE_AUDIO_FORMAT;
}
/**
@@ -227,7 +229,7 @@ public class IMBETargetDataLine implements TargetDataLine
public javax.sound.sampled.Line.Info getLineInfo()
{
return new DataLine.Info( IMBETargetDataLine.class,
IMBEAudioFormat.IMBE_AUDIO_FORMAT );
AudioFormats.IMBE_AUDIO_FORMAT );
}
/**

View File

@@ -11,6 +11,8 @@ import javax.sound.sampled.spi.FormatConversionProvider;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import audio.AudioFormats;
public class P25AudioCodecTest
{
private final static Logger mLog = LoggerFactory.getLogger( P25AudioCodecTest.class );
@@ -18,8 +20,8 @@ public class P25AudioCodecTest
public static void main( String[] args )
{
mLog.debug( "Starting ..." );
AudioFormat from = IMBEAudioFormat.IMBE_AUDIO_FORMAT;
AudioFormat to = IMBEAudioFormat.PCM_SIGNED_8KHZ_16BITS;
AudioFormat from = AudioFormats.IMBE_AUDIO_FORMAT;
AudioFormat to = AudioFormats.PCM_SIGNED_8KHZ_16BITS;
ServiceLoader<FormatConversionProvider> loader = ServiceLoader.load( FormatConversionProvider.class );

View File

@@ -1,128 +1,85 @@
package decode.p25.audio;
import java.io.IOException;
import java.util.concurrent.LinkedTransferQueue;
import java.util.concurrent.TimeUnit;
import java.util.concurrent.atomic.AtomicBoolean;
import javax.sound.sampled.AudioInputStream;
import javax.sound.sampled.AudioSystem;
import javax.sound.sampled.Line;
import javax.sound.sampled.LineUnavailableException;
import javax.sound.sampled.Mixer;
import javax.sound.sampled.SourceDataLine;
import jmbe.iface.AudioConversionLibrary;
import jmbe.iface.AudioConverter;
import message.Message;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import sample.Listener;
import audio.AudioType;
import audio.AudioFormats;
import audio.AudioPacket;
import audio.IAudioOutput;
import audio.IAudioTypeListener;
import audio.MonoAudioOutput;
import audio.SquelchListener;
import audio.inverted.AudioType;
import audio.inverted.IAudioTypeListener;
import controller.ResourceManager;
import controller.ThreadPoolManager;
import controller.ThreadPoolManager.ThreadType;
import decode.p25.message.ldu.LDUMessage;
import decode.p25.message.tdu.TDUMessage;
import decode.p25.message.tdu.lc.TDULinkControlMessage;
/**
* IMBE Audio Output converter. Processes 18-byte IMBE audio frames and
* produces PCM 48k MONO 16-bit sample output.
* P25 Phase 1 IMBE Audio Output converter. Processes 18-byte IMBE audio frames
* and produces PCM 48k MONO 16-bit sample output.
*
* Note: this class depends on an external imbe format conversion provider that
* is provided by an external library like JMBE, and that library must be on the
* classpath and discoverable at runtime.
* Note: this class depends on the external JMBE library.
*/
public class P25AudioOutput implements IAudioOutput, IAudioTypeListener,
SquelchListener, Listener<Message>
public class P25AudioOutput implements IAudioOutput, Listener<Message>,
SquelchListener, IAudioTypeListener
{
private final static Logger mLog = LoggerFactory.getLogger( P25AudioOutput.class );
private static final String IMBE_CODEC = "IMBE";
/* 1 IMBE frame = 160 samples/320 bytes @ 8kHz or 1920 bytes at 48kHz */
private static final int PROCESSED_8KHZ_AUDIO_FRAME_SIZE = 320;
private static final int PROCESSED_48KHZ_AUDIO_FRAME_SIZE = 1920;
private int mFrameSize = 1920;
private IMBETargetDataLine mIMBETargetDataLine = new IMBETargetDataLine();
private SourceDataLine mOutput;
private AudioInputStream mIMBEToPCMConverter;
private LinkedTransferQueue<byte[]> mIMBEFrameQueue =
new LinkedTransferQueue<byte[]>();
private LinkedTransferQueue<byte[]> mProcessedAudioQueue =
new LinkedTransferQueue<byte[]>();
private AtomicBoolean mConverting = new AtomicBoolean();
private AtomicBoolean mDispatching = new AtomicBoolean();
private AtomicBoolean mEnabled = new AtomicBoolean();
private boolean mCanProcessAudio = false;
private boolean mCanConvertAudio = false;
private boolean mEncryptedAudio = false;
private ThreadPoolManager mThreadPoolManager;
private AudioConverter mAudioConverter;
private MonoAudioOutput mAudioOutput = new MonoAudioOutput();
public P25AudioOutput( ResourceManager resourceManager )
{
loadConverter();
if( mCanProcessAudio )
{
mThreadPoolManager = resourceManager.getThreadPoolManager();
/* Schedule the imbe frame processor to run slightly faster than 6
* times a second. Each LDU message contains 9 frames and the
* frame rate is 50, so we need to run at least 6 times a second */
mThreadPoolManager.scheduleFixedRate( ThreadType.SOURCE_SAMPLE_PROCESSING,
new IMBEFrameConverter(), 40, TimeUnit.MILLISECONDS );
}
}
/**
* Primary inject point for p25 imbe audio frame messages. Each LDU audio
* message contains 9 imbe voice frames.
* message contains 9 imbe voice frames. If the audio is not encrypted,
* we insert each audio frame asynchronously into the audio converter.
*/
public void receive( Message message )
{
if( mCanProcessAudio )
if( mCanConvertAudio )
{
if( message instanceof LDUMessage )
{
LDUMessage ldu = (LDUMessage)message;
/* Check valid ldu frames for encryption. Set state to encrypted
* so that no encrypted frames are sent to the converter */
if( ldu.isValid() )
/* If we detect a valid LDU message with the encryption flag
* set to true, we toggle the encrypted audio state until we
* receive the first TDU or TDULC message, then reset it. */
if( ldu.isValid() && ldu.isEncrypted() )
{
mEncryptedAudio = ldu.isEncrypted();
mEncryptedAudio = true;
}
if( !mEncryptedAudio )
if( !mEncryptedAudio && mAudioConverter != null )
{
for( byte[] frame: ((LDUMessage)message).getIMBEFrames() )
{
if( !mIMBEFrameQueue.offer( frame ) )
{
mLog.debug( "IMBE frame queue full - throwing away "
+ "imbe audio frame" );
}
byte[] audio = mAudioConverter.convert( frame );
mAudioOutput.receive( new AudioPacket( "P25 Audio", audio, 1 ) );
}
}
}
}
}
/**
* Receives processed audio buffers from the imbe frame converter and queues
* them for dispatch. If the dispatcher is not currently running, this
* method will start it.
*/
private void receive( byte[] audio )
{
mProcessedAudioQueue.offer( audio );
if( !mDispatching.get() )
{
mThreadPoolManager.scheduleOnce( new AudioDispatcher(), 0, TimeUnit.MILLISECONDS );
else if( message instanceof TDUMessage || message instanceof TDULinkControlMessage )
{
mEncryptedAudio = false;
}
}
}
@@ -133,147 +90,60 @@ public class P25AudioOutput implements IAudioOutput, IAudioTypeListener,
*/
private void loadConverter()
{
try
{
mOutput = AudioSystem.getSourceDataLine(
IMBEAudioFormat.PCM_SIGNED_48KHZ_16BITS );
if( mOutput != null )
{
mFrameSize = PROCESSED_48KHZ_AUDIO_FRAME_SIZE;
/* Open the audio line with room for two buffers */
mOutput.open( IMBEAudioFormat.PCM_SIGNED_48KHZ_16BITS,
mFrameSize * 2 );
mCanProcessAudio = true;
}
else
{
mLog.debug( "Couldn't obtain a 48kHz source data line "
+ "- attempting 8kHz rate" );
mOutput = AudioSystem.getSourceDataLine(
IMBEAudioFormat.PCM_SIGNED_8KHZ_16BITS );
if( mOutput != null )
{
mFrameSize = PROCESSED_8KHZ_AUDIO_FRAME_SIZE;
/* Open the audio line with room for two buffers */
mOutput.open( IMBEAudioFormat.PCM_SIGNED_8KHZ_16BITS,
mFrameSize * 2 );
mCanProcessAudio = true;
}
else
{
mLog.error( "Couldn't obtain a 48 kHz or 8 kHz "
+ "source data line - no decoded IMBE audio "
+ "will be available" );
logSourceLines();
}
}
}
catch ( LineUnavailableException lue )
{
mLog.error( "AudioOutput - couldn't open audio output line "
+ "for playback", lue );
}
catch( IllegalArgumentException iae )
{
mLog.error( "Error fetching or opening audio output for "
+ "48kHz or 8kHz 16 bit audio", iae );
}
catch( Exception e )
{
mLog.error( "Error with audio output", e );
}
if( mCanProcessAudio )
{
try
{
if( mOutput.getFormat().getSampleRate() == IMBEAudioFormat.PCM_48KHZ_RATE )
{
mIMBEToPCMConverter = AudioSystem
.getAudioInputStream( IMBEAudioFormat.PCM_SIGNED_48KHZ_16BITS,
new AudioInputStream( mIMBETargetDataLine ) );
}
else
{
mIMBEToPCMConverter = AudioSystem
.getAudioInputStream( IMBEAudioFormat.PCM_SIGNED_8KHZ_16BITS,
new AudioInputStream( mIMBETargetDataLine ) );
}
if( mIMBEToPCMConverter != null )
{
mLog.info( "IMBE audio converter library loaded successfully" );
}
else
{
mLog.info( "could not load IMBE audio converter library" );
}
}
catch( IllegalArgumentException iae )
{
mLog.error( "could NOT find/load IMBE audio converter library", iae );
}
}
}
private void logSourceLines()
{
Mixer.Info[] infos = AudioSystem.getMixerInfo();
AudioConversionLibrary library = null;
for( Mixer.Info info: AudioSystem.getMixerInfo() )
try
{
Mixer mixer = AudioSystem.getMixer( info );
@SuppressWarnings( "rawtypes" )
Class temp = Class.forName( "jmbe.JMBEAudioLibrary" );
Line.Info[] lineInfos = mixer.getSourceLineInfo();
library = (AudioConversionLibrary)temp.newInstance();
mAudioConverter = library.getAudioConverter( IMBE_CODEC,
AudioFormats.PCM_SIGNED_48KHZ_16BITS );
for( Line.Info lineInfo: lineInfos )
if( mAudioConverter != null )
{
mLog.debug( "Source Line: " + lineInfo.toString() );
mCanConvertAudio = true;
mLog.info( "JMBE audio conversion library successfully loaded"
+ " - P25 audio will be available" );
}
else
{
mLog.info( "JMBE audio conversion library NOT FOUND" );
}
}
catch ( ClassNotFoundException e1 )
{
mLog.error( "Couldn't find/load JMBE audio conversion library", e1 );
}
catch ( InstantiationException e1 )
{
mLog.error( "Couldn't instantiate JMBE audio conversion library class" );
}
catch ( IllegalAccessException e1 )
{
mLog.error( "Couldn't load JMBE audio conversion library due to "
+ "security restrictions" );
}
}
@Override
public void setSquelch( SquelchState state )
{
/* Ignored */
}
@Override
public void setAudioPlaybackEnabled( boolean enabled )
{
mEnabled.set( enabled );
mAudioOutput.setMuted( !enabled );
}
@Override
public void dispose()
{
mCanProcessAudio = false;
mIMBEFrameQueue.clear();
mProcessedAudioQueue.clear();
mAudioOutput.dispose();
}
mConverting.set( false );
mDispatching.set( false );
mOutput.close();
mOutput = null;
mIMBETargetDataLine.close();
mIMBETargetDataLine = null;
// TODO Auto-generated method stub
@Override
public void setSquelch( SquelchState state )
{
/* not implemented */
}
@Override
@@ -281,122 +151,4 @@ public class P25AudioOutput implements IAudioOutput, IAudioTypeListener,
{
//Not implemented
}
private class IMBEFrameConverter implements Runnable
{
@Override
public void run()
{
if( mConverting.compareAndSet( false, true ) )
{
boolean processing = true;
while( processing )
{
byte[] frame = mIMBEFrameQueue.poll();
if( frame != null )
{
if( frame.length == 18 )
{
/* Insert an 18-byte imbe frame into the stream */
mIMBETargetDataLine.receive( frame );
/* Read one frame of converted audio from output */
byte[] audio = new byte[ mFrameSize ];
try
{
int read = mIMBEToPCMConverter.read( audio );
if( read > 0 )
{
receive( audio );
}
else
{
mLog.debug( "Couldn't read audio data from "
+ "conversion stream after inserting imbe "
+ "frame" );
}
}
catch ( IOException e )
{
mLog.error( "Error reading processed audio data "
+ "from imbe conversion stream", e );
}
}
else
{
mLog.equals( "We got an imbe frame that's not 18 bytes - length:" + ( frame == null ? "null" : frame.length ) );
}
}
else
{
processing = false;
}
}
mConverting.set( false );
}
}
}
private class AudioDispatcher implements Runnable
{
private int mEmptyFrameCount = 0;
@Override
public void run()
{
if( mDispatching.compareAndSet( false, true ) )
{
// mOutput.drain();
/* Write 2 frames of audio and then start the line */
writeAudioFrame();
writeAudioFrame();
mOutput.start();
while( mEmptyFrameCount < 4 )
{
writeAudioFrame();
}
mLog.debug( "Stopping ..." );
mOutput.stop();
mDispatching.set( false );
}
}
private void writeAudioFrame()
{
byte[] audio = mProcessedAudioQueue.poll();
if( audio == null )
{
mEmptyFrameCount++;
mLog.debug( " ... inserting empty" );
}
else
{
mEmptyFrameCount = 0;
}
/* Send empty audio if we're not currently enabled to playback */
if( audio == null || !mEnabled.get() )
{
audio = new byte[ mFrameSize ];
}
if( mOutput != null )
{
mOutput.write( audio, 0, audio.length );
}
}
}
}
}

View File

@@ -13,6 +13,7 @@ public enum Encryption
TRIPLE_DES_2_KEY( 0x82 ),
TRIPLE_DES_3_KEY( 0x83 ),
AES_256( 0x84 ),
AES_CBC( 0x85 ),
DES_XL( 0x9F ), /* Motorola Proprietary */
DVI_XL( 0xA0 ), /* Motorola Proprietary */
DVP_XL( 0xA1 ), /* Motorola Proprietary */

View File

@@ -27,8 +27,8 @@ import org.slf4j.LoggerFactory;
import message.Message;
import alias.Alias;
import alias.AliasList;
import audio.AudioType;
import audio.SquelchListener;
import audio.inverted.AudioType;
import controller.activity.CallEvent;
import controller.activity.CallEvent.CallEventType;
import controller.channel.ProcessingChain;

View File

@@ -48,6 +48,7 @@ import sample.complex.ComplexSample;
import source.tuner.frequency.FrequencyChangeListener;
import buffer.FloatAveragingBuffer;
import dsp.filter.interpolator.QPSKInterpolator;
import dsp.symbol.Dibit;
/**
* Implements a CQPSK demodulator using a Gardner Detector to determine optimal
@@ -221,7 +222,7 @@ public class CQPSKDemodulator implements Instrumentable,
/* Sampling point */
private float mMu = 10.0f;
private float mGainMu = 0.1f;
private float mGainMu = 0.05f;
/* Samples per symbol */
private float mOmega = 10.0f;
@@ -314,6 +315,9 @@ public class CQPSKDemodulator implements Instrumentable,
currentSymbol.quadrature() ) * middleSymbol.quadrature();
float gardnerError = normalize( errorInphase + errorQuadrature, 1.0f );
// /*debug*/
// gardnerError = 0.0f;
/* mOmega is samples per symbol and is constrained to floating
* between +/- .005 of the nominal 10.0 samples per symbol */
@@ -331,8 +335,9 @@ public class CQPSKDemodulator implements Instrumentable,
/* Update costas loop using phase error present in current
* symbol. The symbol is rotated from star orientation to polar
* orientation to simplify error calculation */
mCostasLoop.receive(
ComplexSample.multiply( currentSymbol, POINT_45_DEGREES ) );
mCostasLoop.receive( currentSymbol );
// mCostasLoop.receive(
// ComplexSample.multiply( currentSymbol, POINT_45_DEGREES ) );
/* Dispatch the differentiated symbol to the registered listener */
if( mListener != null )
@@ -442,7 +447,8 @@ public class CQPSKDemodulator implements Instrumentable,
*/
public ComplexSample getCurrentVector()
{
return ComplexSample.fromAngle( mLoopPhase + THETA );
// return ComplexSample.fromAngle( mLoopPhase + THETA );
return ComplexSample.fromAngle( mLoopPhase );
}
@Override
@@ -450,6 +456,9 @@ public class CQPSKDemodulator implements Instrumentable,
{
/* Calculate phase error */
float phaseError = getPhaseError( sample );
Dibit d = QPSKPolarSlicer.decide( sample );
// mLog.debug( "Dibit: " + d.name() + " Error: " + phaseError );
/* Adjust for phase error */
adjust( phaseError );

View File

@@ -105,9 +105,6 @@ public class SDRTrunk
*/
mResourceManager = new ResourceManager();
/* Log any available audio converter plugins */
logAvailableAudioPlugins();
mTitle = getTitle();
//Initialize the GUI
@@ -387,19 +384,6 @@ public class SDRTrunk
return homePath;
}
private void logAvailableAudioPlugins()
{
ServiceLoader<FormatConversionProvider> loader =
ServiceLoader.load( FormatConversionProvider.class );
Iterator<FormatConversionProvider> it = loader.iterator();
while( it.hasNext() )
{
mLog.info( "Available Audio Converter: " + it.next().getClass() );
}
}
private String getTitle()
{
StringBuilder sb = new StringBuilder();