diff --git a/.classpath b/.classpath index cecdafa7..495e4bc4 100644 --- a/.classpath +++ b/.classpath @@ -24,6 +24,6 @@ - + diff --git a/build/build.xml b/build/build.xml index 5b068185..8eb04324 100644 --- a/build/build.xml +++ b/build/build.xml @@ -6,7 +6,7 @@ - + @@ -89,18 +89,27 @@ - + + + - + + + + + + + + + + + - - + diff --git a/imports/jmbe-interface-0.2.0.jar b/imports/jmbe-interface-0.2.0.jar new file mode 100644 index 00000000..85add8bd Binary files /dev/null and b/imports/jmbe-interface-0.2.0.jar differ diff --git a/scripts/run_sdrtrunk_linux b/scripts/run_sdrtrunk_linux deleted file mode 100644 index 735cba6c..00000000 --- a/scripts/run_sdrtrunk_linux +++ /dev/null @@ -1,2 +0,0 @@ -#!/bin/bash -java -cp "*:libs/*" gui.SDRTrunk& \ No newline at end of file diff --git a/scripts/run_sdrtrunk_linux.sh b/scripts/run_sdrtrunk_linux.sh new file mode 100644 index 00000000..00355174 --- /dev/null +++ b/scripts/run_sdrtrunk_linux.sh @@ -0,0 +1,2 @@ +#!/bin/bash +java -XX:+UseG1GC -cp "*:libs/*" gui.SDRTrunk& \ No newline at end of file diff --git a/scripts/run_sdrtrunk_windows.bat b/scripts/run_sdrtrunk_windows.bat index e911942d..1206e039 100644 --- a/scripts/run_sdrtrunk_windows.bat +++ b/scripts/run_sdrtrunk_windows.bat @@ -1 +1 @@ -java -cp "SDRTrunk.jar;*;libs/*;config/*;images/*" gui.SDRTrunk \ No newline at end of file +java -XX:+UseG1GC -cp "SDRTrunk.jar;*;libs/*;config/*;images/*" gui.SDRTrunk \ No newline at end of file diff --git a/src/alias/Alias.java b/src/alias/Alias.java index 26404dc9..acccd6b5 100644 --- a/src/alias/Alias.java +++ b/src/alias/Alias.java @@ -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 { diff --git a/src/alias/AliasID.java b/src/alias/AliasID.java index 090d091e..998896df 100644 --- a/src/alias/AliasID.java +++ b/src/alias/AliasID.java @@ -17,7 +17,7 @@ ******************************************************************************/ package alias; -import audio.AudioType; +import audio.inverted.AudioType; public abstract class AliasID { diff --git a/src/alias/talkgroup/TalkgroupID.java b/src/alias/talkgroup/TalkgroupID.java index a6f79940..6fca233c 100644 --- a/src/alias/talkgroup/TalkgroupID.java +++ b/src/alias/talkgroup/TalkgroupID.java @@ -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 diff --git a/src/alias/talkgroup/TalkgroupIDEditor.java b/src/alias/talkgroup/TalkgroupIDEditor.java index 344d013c..6fb73d03 100644 --- a/src/alias/talkgroup/TalkgroupIDEditor.java +++ b/src/alias/talkgroup/TalkgroupIDEditor.java @@ -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 diff --git a/src/audio/Audio.java b/src/audio/Audio.java deleted file mode 100644 index 68b624f6..00000000 --- a/src/audio/Audio.java +++ /dev/null @@ -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 -} diff --git a/src/decode/p25/audio/IMBEAudioFormat.java b/src/audio/AudioFormats.java similarity index 96% rename from src/decode/p25/audio/IMBEAudioFormat.java rename to src/audio/AudioFormats.java index 348640c7..e9ab4add 100644 --- a/src/decode/p25/audio/IMBEAudioFormat.java +++ b/src/audio/AudioFormats.java @@ -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 ); - } diff --git a/src/audio/AudioOutputImpl.java b/src/audio/AudioOutputImpl.java index ae833592..78d26ad0 100644 --- a/src/audio/AudioOutputImpl.java +++ b/src/audio/AudioOutputImpl.java @@ -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, diff --git a/src/audio/AudioPacket.java b/src/audio/AudioPacket.java index faa232c1..17de2efa 100644 --- a/src/audio/AudioPacket.java +++ b/src/audio/AudioPacket.java @@ -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; + } } diff --git a/src/audio/IAudioOutput.java b/src/audio/IAudioOutput.java index f9add19c..790f45ee 100644 --- a/src/audio/IAudioOutput.java +++ b/src/audio/IAudioOutput.java @@ -1,6 +1,7 @@ package audio; import audio.SquelchListener.SquelchState; +import audio.inverted.AudioType; public interface IAudioOutput { diff --git a/src/audio/IAudioPacketReceiver.java b/src/audio/IAudioPacketReceiver.java deleted file mode 100644 index 1bab1254..00000000 --- a/src/audio/IAudioPacketReceiver.java +++ /dev/null @@ -1,6 +0,0 @@ -package audio; - -public interface IAudioPacketReceiver -{ - -} diff --git a/src/audio/MonoAudioOutput.java b/src/audio/MonoAudioOutput.java index f771a709..2148509f 100644 --- a/src/audio/MonoAudioOutput.java +++ b/src/audio/MonoAudioOutput.java @@ -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 { - /** - * 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 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 packets = new ArrayList(); + + 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(); + } + } } } diff --git a/src/audio/AudioAdapter.java b/src/audio/inverted/AudioAdapter.java similarity index 99% rename from src/audio/AudioAdapter.java rename to src/audio/inverted/AudioAdapter.java index 1cfa9377..2d07a4e8 100644 --- a/src/audio/AudioAdapter.java +++ b/src/audio/inverted/AudioAdapter.java @@ -15,7 +15,7 @@ * You should have received a copy of the GNU General Public License * along with this program. If not, see ******************************************************************************/ -package audio; +package audio.inverted; import util.Oscillator; import dsp.filter.FilterFactory; diff --git a/src/audio/AudioInverter.java b/src/audio/inverted/AudioInverter.java similarity index 97% rename from src/audio/AudioInverter.java rename to src/audio/inverted/AudioInverter.java index 70898f73..26408556 100644 --- a/src/audio/AudioInverter.java +++ b/src/audio/inverted/AudioInverter.java @@ -15,8 +15,9 @@ * You should have received a copy of the GNU General Public License * along with this program. If not, see ******************************************************************************/ -package audio; +package audio.inverted; +import audio.InversionFrequency; import sample.real.RealSampleListener; import util.Oscillator; import dsp.filter.Filters; diff --git a/src/audio/AudioType.java b/src/audio/inverted/AudioType.java similarity index 99% rename from src/audio/AudioType.java rename to src/audio/inverted/AudioType.java index 643f5ce5..706ee683 100644 --- a/src/audio/AudioType.java +++ b/src/audio/inverted/AudioType.java @@ -15,7 +15,7 @@ * You should have received a copy of the GNU General Public License * along with this program. If not, see ******************************************************************************/ -package audio; +package audio.inverted; public enum AudioType { diff --git a/src/audio/IAudioTypeListener.java b/src/audio/inverted/IAudioTypeListener.java similarity index 97% rename from src/audio/IAudioTypeListener.java rename to src/audio/inverted/IAudioTypeListener.java index 92c794db..52321dcf 100644 --- a/src/audio/IAudioTypeListener.java +++ b/src/audio/inverted/IAudioTypeListener.java @@ -15,7 +15,8 @@ * You should have received a copy of the GNU General Public License * along with this program. If not, see ******************************************************************************/ -package audio; +package audio.inverted; + public interface IAudioTypeListener { diff --git a/src/controller/channel/ProcessingChain.java b/src/controller/channel/ProcessingChain.java index 92a0a9c7..38a54674 100644 --- a/src/controller/channel/ProcessingChain.java +++ b/src/controller/channel/ProcessingChain.java @@ -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; diff --git a/src/controller/state/ChannelState.java b/src/controller/state/ChannelState.java index 766693bc..3f395086 100644 --- a/src/controller/state/ChannelState.java +++ b/src/controller/state/ChannelState.java @@ -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 { /* 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 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 @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 return state == CALL || state == CONTROL || state == DATA || + state == ENCRYPTED || state == FADE; } }, @@ -594,6 +594,7 @@ public abstract class ChannelState implements Listener return state == CALL || state == CONTROL || state == DATA || + state == State.ENCRYPTED || state == FADE; } }, diff --git a/src/decode/p25/P25Decoder.java b/src/decode/p25/P25Decoder.java index 63cb42f7..57b3c89e 100644 --- a/src/decode/p25/P25Decoder.java +++ b/src/decode/p25/P25Decoder.java @@ -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; diff --git a/src/decode/p25/P25MessageFramer.java b/src/decode/p25/P25MessageFramer.java index 7d02e706..033a3a84 100644 --- a/src/decode/p25/P25MessageFramer.java +++ b/src/decode/p25/P25MessageFramer.java @@ -43,8 +43,8 @@ public class P25MessageFramer implements Listener 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 ); diff --git a/src/decode/p25/audio/IMBETargetDataLine.java b/src/decode/p25/audio/IMBETargetDataLine.java index b3bfd088..041e233f 100644 --- a/src/decode/p25/audio/IMBETargetDataLine.java +++ b/src/decode/p25/audio/IMBETargetDataLine.java @@ -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 ); } /** diff --git a/src/decode/p25/audio/P25AudioCodecTest.java b/src/decode/p25/audio/P25AudioCodecTest.java index 56640fed..c35f330d 100644 --- a/src/decode/p25/audio/P25AudioCodecTest.java +++ b/src/decode/p25/audio/P25AudioCodecTest.java @@ -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 loader = ServiceLoader.load( FormatConversionProvider.class ); diff --git a/src/decode/p25/audio/P25AudioOutput.java b/src/decode/p25/audio/P25AudioOutput.java index 70657d86..51af5cb2 100644 --- a/src/decode/p25/audio/P25AudioOutput.java +++ b/src/decode/p25/audio/P25AudioOutput.java @@ -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 +public class P25AudioOutput implements IAudioOutput, Listener, + 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 mIMBEFrameQueue = - new LinkedTransferQueue(); - private LinkedTransferQueue mProcessedAudioQueue = - new LinkedTransferQueue(); - - 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 ); - } - } - } -} - +} \ No newline at end of file diff --git a/src/decode/p25/reference/Encryption.java b/src/decode/p25/reference/Encryption.java index 773f9eb3..2606c597 100644 --- a/src/decode/p25/reference/Encryption.java +++ b/src/decode/p25/reference/Encryption.java @@ -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 */ diff --git a/src/decode/passport/PassportChannelState.java b/src/decode/passport/PassportChannelState.java index 39131b3b..f64d34ea 100644 --- a/src/decode/passport/PassportChannelState.java +++ b/src/decode/passport/PassportChannelState.java @@ -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; diff --git a/src/dsp/psk/CQPSKDemodulator.java b/src/dsp/psk/CQPSKDemodulator.java index 91ba9870..19a9c2fc 100644 --- a/src/dsp/psk/CQPSKDemodulator.java +++ b/src/dsp/psk/CQPSKDemodulator.java @@ -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 ); diff --git a/src/gui/SDRTrunk.java b/src/gui/SDRTrunk.java index ac68ea20..57aacf78 100644 --- a/src/gui/SDRTrunk.java +++ b/src/gui/SDRTrunk.java @@ -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 loader = - ServiceLoader.load( FormatConversionProvider.class ); - - Iterator it = loader.iterator(); - - while( it.hasNext() ) - { - mLog.info( "Available Audio Converter: " + it.next().getClass() ); - } - } - private String getTitle() { StringBuilder sb = new StringBuilder();