mirror of
https://github.com/DSheirer/sdrtrunk.git
synced 2026-11-13 09:07:27 -05:00
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:
@@ -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
|
||||
{
|
||||
|
||||
@@ -17,7 +17,7 @@
|
||||
******************************************************************************/
|
||||
package alias;
|
||||
|
||||
import audio.AudioType;
|
||||
import audio.inverted.AudioType;
|
||||
|
||||
public abstract class AliasID
|
||||
{
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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
|
||||
}
|
||||
@@ -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 );
|
||||
|
||||
}
|
||||
@@ -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,
|
||||
|
||||
@@ -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;
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1,6 +1,7 @@
|
||||
package audio;
|
||||
|
||||
import audio.SquelchListener.SquelchState;
|
||||
import audio.inverted.AudioType;
|
||||
|
||||
public interface IAudioOutput
|
||||
{
|
||||
|
||||
@@ -1,6 +0,0 @@
|
||||
package audio;
|
||||
|
||||
public interface IAudioPacketReceiver
|
||||
{
|
||||
|
||||
}
|
||||
@@ -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();
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -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;
|
||||
@@ -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;
|
||||
@@ -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
|
||||
{
|
||||
@@ -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
|
||||
{
|
||||
@@ -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;
|
||||
|
||||
@@ -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;
|
||||
}
|
||||
},
|
||||
|
||||
@@ -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;
|
||||
|
||||
@@ -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 );
|
||||
|
||||
@@ -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 );
|
||||
}
|
||||
|
||||
/**
|
||||
|
||||
@@ -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 );
|
||||
|
||||
|
||||
@@ -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 );
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
}
|
||||
@@ -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 */
|
||||
|
||||
@@ -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;
|
||||
|
||||
@@ -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 );
|
||||
|
||||
@@ -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();
|
||||
|
||||
Reference in New Issue
Block a user