mirror of
https://github.com/DSheirer/sdrtrunk.git
synced 2026-11-12 08:37:25 -05:00
Build 0.1.3 - adds support for 48 kHz audio output from JMBE library for P25 audio. Resolves issue with Automatic Frequency Control on P25 C4FM channels where the channel was not tracking changes in tuner frequency.
This commit is contained in:
@@ -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.2.jar"/>
|
||||
<classpathentry kind="lib" path="/jmbe/library/jmbe-0.1.3.jar"/>
|
||||
<classpathentry kind="output" path="bin"/>
|
||||
</classpath>
|
||||
|
||||
@@ -6,7 +6,7 @@
|
||||
<!-- version properties -->
|
||||
<property name="major" value="0"/>
|
||||
<property name="minor" value="1"/>
|
||||
<property name="build" value="2"/>
|
||||
<property name="build" value="3"/>
|
||||
|
||||
<!-- Build properties -->
|
||||
<property name="classes" location="../classes"/>
|
||||
@@ -94,7 +94,7 @@
|
||||
<tstamp prefix="build" />
|
||||
|
||||
<!-- Bundle the files into .zip -->
|
||||
<zip destfile="${product}/sdrtrunk_${build.DSTAMP}_${build.TSTAMP}.zip"
|
||||
<zip destfile="${product}/sdrtrunk_${build.DSTAMP}_${build.TSTAMP}_${major}.${minor}.${build}.zip"
|
||||
basedir="${product}" />
|
||||
</target>
|
||||
|
||||
|
||||
@@ -31,6 +31,11 @@ public class SoftSyncDetector implements ISyncProcessor
|
||||
}
|
||||
}
|
||||
|
||||
public void setThreshold( int threshold )
|
||||
{
|
||||
mThreshold = threshold;
|
||||
}
|
||||
|
||||
public void setListener( ISyncDetectListener listener )
|
||||
{
|
||||
mListener = listener;
|
||||
|
||||
@@ -234,6 +234,8 @@ public class C4FMSymbolFilter implements FrequencyCorrectionResetListener,
|
||||
{
|
||||
if( mReset )
|
||||
{
|
||||
mGainController.reset();
|
||||
|
||||
mCoarseFrequencyCorrection = 0.0f;
|
||||
mFineFrequencyCorrection = 0.0f;
|
||||
mSymbolSpread = 2.0f;
|
||||
@@ -346,11 +348,12 @@ public class C4FMSymbolFilter implements FrequencyCorrectionResetListener,
|
||||
/* send frequency correction */
|
||||
if( Math.abs( mCoarseFrequencyCorrection ) > COARSE_FREQUENCY_DEADBAND )
|
||||
{
|
||||
mFrequencyCorrectionControl.adjust(
|
||||
500 * ( mCoarseFrequencyCorrection > 0 ? 1 : -1 ) );
|
||||
int adjustment = 500 * ( mCoarseFrequencyCorrection > 0 ? 1 : -1 );
|
||||
|
||||
mFrequencyCorrectionControl.adjust( adjustment );
|
||||
|
||||
/* reset internal frequency error tracker */
|
||||
mCoarseFrequencyCorrection = 0;
|
||||
/* reset internal frequency error tracker */
|
||||
mCoarseFrequencyCorrection = 0;
|
||||
}
|
||||
|
||||
if( mSymbolSpreadTap != null )
|
||||
|
||||
@@ -128,13 +128,13 @@ public class P25Decoder extends Decoder implements Instrumentable
|
||||
* modulation (C4FM or CQPSK) */
|
||||
if( mSourceSampleType == SampleType.COMPLEX )
|
||||
{
|
||||
mBasebandFilter = new ComplexFIRFilter( FilterFactory.getLowPass(
|
||||
48000, 7250, 8000, 60, WindowType.HANNING, true ), 1.0 );
|
||||
|
||||
this.addComplexListener( mBasebandFilter );
|
||||
|
||||
if( modulation == Modulation.CQPSK )
|
||||
{
|
||||
mBasebandFilter = new ComplexFIRFilter( FilterFactory.getLowPass(
|
||||
48000, 7250, 8000, 60, WindowType.HANNING, true ), 1.0 );
|
||||
|
||||
this.addComplexListener( mBasebandFilter );
|
||||
|
||||
mAGC = new ComplexFeedForwardGainControl( 32 );
|
||||
mBasebandFilter.setListener( mAGC );
|
||||
|
||||
@@ -154,6 +154,11 @@ public class P25Decoder extends Decoder implements Instrumentable
|
||||
}
|
||||
else /* C4FM */
|
||||
{
|
||||
mBasebandFilter = new ComplexFIRFilter( FilterFactory.getLowPass(
|
||||
48000, 6750, 7500, 60, WindowType.HANNING, true ), 1.0 );
|
||||
|
||||
this.addComplexListener( mBasebandFilter );
|
||||
|
||||
mFMDemodulator = new FMDiscriminator( 1.0f );
|
||||
mBasebandFilter.setListener( mFMDemodulator );
|
||||
|
||||
@@ -161,8 +166,6 @@ public class P25Decoder extends Decoder implements Instrumentable
|
||||
* can process the output as if it were coming from any other
|
||||
* real sample source */
|
||||
mFMDemodulator.setListener( getRealReceiver() );
|
||||
|
||||
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@@ -43,7 +43,11 @@ public class P25MessageFramer implements Listener<Dibit>
|
||||
LoggerFactory.getLogger( P25MessageFramer.class );
|
||||
|
||||
/* Determines the threshold for sync pattern soft matching */
|
||||
private static final int SYNC_PATTERN_SOFT_MATCH_THRESHOLD = 4;
|
||||
private static final int SYNC_MATCH_THRESHOLD = 4;
|
||||
private static final int SYNC_IN_CALL_THRESHOLD = 8;
|
||||
|
||||
private SoftSyncDetector mPrimarySyncDetector = new SoftSyncDetector(
|
||||
FrameSync.P25_PHASE1_NORMAL.getSync(), SYNC_MATCH_THRESHOLD );
|
||||
|
||||
/* Costas Loop phase lock error correction values. A phase lock error of
|
||||
* 90 degrees requires a correction of 1/4 of the symbol rate (1200Hz). An
|
||||
@@ -89,10 +93,7 @@ public class P25MessageFramer implements Listener<Dibit>
|
||||
{
|
||||
mAliasList = aliasList;
|
||||
|
||||
SoftSyncDetector primarySyncDetector = new SoftSyncDetector(
|
||||
FrameSync.P25_PHASE1_NORMAL.getSync(), SYNC_PATTERN_SOFT_MATCH_THRESHOLD );
|
||||
|
||||
primarySyncDetector.setListener( new ISyncDetectListener()
|
||||
mPrimarySyncDetector.setListener( new ISyncDetectListener()
|
||||
{
|
||||
@Override
|
||||
public void syncDetected()
|
||||
@@ -108,7 +109,7 @@ public class P25MessageFramer implements Listener<Dibit>
|
||||
}
|
||||
} );
|
||||
|
||||
mMatcher.add( primarySyncDetector );
|
||||
mMatcher.add( mPrimarySyncDetector );
|
||||
|
||||
/**
|
||||
* We use two message assemblers to catch any sync detections, so that
|
||||
@@ -276,6 +277,9 @@ public class P25MessageFramer implements Listener<Dibit>
|
||||
case HDU:
|
||||
mComplete = true;
|
||||
dispatch( new HDUMessage( mMessage.copy(), mDUID, mAliasList ) );
|
||||
|
||||
/* We're in a call now, lower the sync match threshold */
|
||||
mPrimarySyncDetector.setThreshold( SYNC_IN_CALL_THRESHOLD );
|
||||
break;
|
||||
case LDU1:
|
||||
mComplete = true;
|
||||
@@ -285,10 +289,16 @@ public class P25MessageFramer implements Listener<Dibit>
|
||||
|
||||
/* Convert the LDU1 message into a link control LDU1 message */
|
||||
dispatch( LDULCMessageFactory.getMessage( ldu1 ) );
|
||||
|
||||
/* We're in a call now, lower the sync match threshold */
|
||||
mPrimarySyncDetector.setThreshold( SYNC_IN_CALL_THRESHOLD );
|
||||
break;
|
||||
case LDU2:
|
||||
mComplete = true;
|
||||
dispatch( new LDU2Message( mMessage.copy(), mDUID, mAliasList ) );
|
||||
|
||||
/* We're in a call now, lower the sync match threshold */
|
||||
mPrimarySyncDetector.setThreshold( SYNC_IN_CALL_THRESHOLD );
|
||||
break;
|
||||
case PDU0:
|
||||
|
||||
@@ -328,6 +338,10 @@ public class P25MessageFramer implements Listener<Dibit>
|
||||
{
|
||||
mComplete = true;
|
||||
}
|
||||
|
||||
/* Set sync match threshold to normal */
|
||||
mPrimarySyncDetector.setThreshold( SYNC_MATCH_THRESHOLD );
|
||||
|
||||
break;
|
||||
case PDU1:
|
||||
/* Remove interleaving */
|
||||
@@ -390,6 +404,9 @@ public class P25MessageFramer implements Listener<Dibit>
|
||||
{
|
||||
mComplete = true;
|
||||
}
|
||||
|
||||
/* Set sync match threshold to normal */
|
||||
mPrimarySyncDetector.setThreshold( SYNC_MATCH_THRESHOLD );
|
||||
break;
|
||||
case PDU3:
|
||||
/* Remove interleaving */
|
||||
@@ -456,10 +473,16 @@ public class P25MessageFramer implements Listener<Dibit>
|
||||
|
||||
mComplete = true;
|
||||
}
|
||||
|
||||
/* Set sync match threshold to normal */
|
||||
mPrimarySyncDetector.setThreshold( SYNC_MATCH_THRESHOLD );
|
||||
break;
|
||||
case TDU:
|
||||
dispatch( new TDUMessage( mMessage.copy(), mDUID, mAliasList ) );
|
||||
mComplete = true;
|
||||
|
||||
/* Set sync match threshold to normal */
|
||||
mPrimarySyncDetector.setThreshold( SYNC_MATCH_THRESHOLD );
|
||||
break;
|
||||
case TDULC:
|
||||
TDULinkControlMessage tdulc = new TDULinkControlMessage(
|
||||
@@ -470,6 +493,9 @@ public class P25MessageFramer implements Listener<Dibit>
|
||||
|
||||
dispatch( tdulc );
|
||||
mComplete = true;
|
||||
|
||||
/* Set sync match threshold to normal */
|
||||
mPrimarySyncDetector.setThreshold( SYNC_MATCH_THRESHOLD );
|
||||
break;
|
||||
case TSBK1:
|
||||
/* Remove interleaving */
|
||||
@@ -512,6 +538,9 @@ public class P25MessageFramer implements Listener<Dibit>
|
||||
{
|
||||
mComplete = true;
|
||||
}
|
||||
|
||||
/* Set sync match threshold to normal */
|
||||
mPrimarySyncDetector.setThreshold( SYNC_MATCH_THRESHOLD );
|
||||
break;
|
||||
case TSBK2:
|
||||
/* Remove interleaving */
|
||||
|
||||
@@ -12,9 +12,8 @@ public class IMBEAudioFormat
|
||||
|
||||
public static final float IMBE_FRAME_RATE = 50;
|
||||
public static final float IMBE_SAMPLE_RATE = 50;
|
||||
public static final float PCM_8KHZ_SAMPLE_RATE = 8000;
|
||||
public static final float PCM_48KHZ_SAMPLE_RATE = 48000;
|
||||
public static final float PCM_FRAME_RATE = 8000;
|
||||
public static final float PCM_8KHZ_RATE = 8000;
|
||||
public static final float PCM_48KHZ_RATE = 48000;
|
||||
|
||||
public static final int IMBE_FRAME_SIZE_BYTES = 18;
|
||||
public static final int IMBE_SAMPLE_SIZE_BITS = 144;
|
||||
@@ -33,20 +32,20 @@ public class IMBEAudioFormat
|
||||
|
||||
public static AudioFormat PCM_SIGNED_8KHZ_16BITS =
|
||||
new AudioFormat( AudioFormat.Encoding.PCM_SIGNED,
|
||||
PCM_8KHZ_SAMPLE_RATE,
|
||||
PCM_8KHZ_RATE,
|
||||
PCM_SAMPLE_SIZE_BITS,
|
||||
ONE_CHANNEL,
|
||||
PCM_FRAME_SIZE_BYTES,
|
||||
PCM_FRAME_RATE,
|
||||
PCM_8KHZ_RATE,
|
||||
LITTLE_ENDIAN );
|
||||
|
||||
public static AudioFormat PCM_SIGNED_48KHZ_16BITS =
|
||||
new AudioFormat( AudioFormat.Encoding.PCM_SIGNED,
|
||||
PCM_8KHZ_SAMPLE_RATE,
|
||||
PCM_48KHZ_RATE,
|
||||
PCM_SAMPLE_SIZE_BITS,
|
||||
ONE_CHANNEL,
|
||||
PCM_FRAME_SIZE_BYTES,
|
||||
PCM_FRAME_RATE,
|
||||
PCM_48KHZ_RATE,
|
||||
LITTLE_ENDIAN );
|
||||
|
||||
}
|
||||
|
||||
@@ -77,11 +77,11 @@ public class IMBETargetDataLine implements TargetDataLine
|
||||
return 0;
|
||||
}
|
||||
|
||||
private int getBytesAvailable()
|
||||
{
|
||||
return 1920;
|
||||
// private int getBytesAvailable()
|
||||
// {
|
||||
// return 1920;
|
||||
// return mFrame.length;
|
||||
}
|
||||
// }
|
||||
|
||||
/**
|
||||
* Primary inject point for submitting IMBE frame data into the audio
|
||||
|
||||
@@ -1,13 +1,15 @@
|
||||
package decode.p25.audio;
|
||||
|
||||
import java.io.IOException;
|
||||
import java.util.concurrent.ArrayBlockingQueue;
|
||||
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 message.Message;
|
||||
@@ -39,15 +41,17 @@ public class P25AudioOutput implements IAudioOutput, IAudioTypeListener,
|
||||
private final static Logger mLog = LoggerFactory.getLogger( P25AudioOutput.class );
|
||||
|
||||
/* 1 IMBE frame = 160 samples/320 bytes @ 8kHz or 1920 bytes at 48kHz */
|
||||
private static final int PROCESSED_AUDIO_FRAME_SIZE = 320;
|
||||
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 AudioInputStream mUpsampler;
|
||||
private ArrayBlockingQueue<byte[]> mIMBEFrameQueue =
|
||||
new ArrayBlockingQueue<byte[]>( 200 );
|
||||
private ArrayBlockingQueue<byte[]> mProcessedAudioQueue =
|
||||
new ArrayBlockingQueue<byte[]>( 200 );
|
||||
private LinkedTransferQueue<byte[]> mIMBEFrameQueue =
|
||||
new LinkedTransferQueue<byte[]>();
|
||||
private LinkedTransferQueue<byte[]> mProcessedAudioQueue =
|
||||
new LinkedTransferQueue<byte[]>();
|
||||
|
||||
private AtomicBoolean mConverting = new AtomicBoolean();
|
||||
private AtomicBoolean mDispatching = new AtomicBoolean();
|
||||
@@ -130,55 +134,110 @@ public class P25AudioOutput implements IAudioOutput, IAudioTypeListener,
|
||||
private void loadConverter()
|
||||
{
|
||||
try
|
||||
{
|
||||
mIMBEToPCMConverter = AudioSystem
|
||||
.getAudioInputStream( IMBEAudioFormat.PCM_SIGNED_8KHZ_16BITS,
|
||||
new AudioInputStream( mIMBETargetDataLine ) );
|
||||
|
||||
if( mIMBEToPCMConverter != null )
|
||||
{
|
||||
mLog.info( "IMBE audio converter library loaded successfully" );
|
||||
|
||||
try
|
||||
{
|
||||
mOutput = AudioSystem.getSourceDataLine(
|
||||
IMBEAudioFormat.PCM_SIGNED_8KHZ_16BITS );
|
||||
{
|
||||
mOutput = AudioSystem.getSourceDataLine(
|
||||
IMBEAudioFormat.PCM_SIGNED_48KHZ_16BITS );
|
||||
|
||||
if( mOutput != null )
|
||||
{
|
||||
/* Open the audio line with room for two buffers */
|
||||
mOutput.open( IMBEAudioFormat.PCM_SIGNED_8KHZ_16BITS,
|
||||
PROCESSED_AUDIO_FRAME_SIZE * 2 );
|
||||
|
||||
mCanProcessAudio = true;
|
||||
}
|
||||
else
|
||||
{
|
||||
mLog.debug( "Couldn't create output line" );
|
||||
}
|
||||
}
|
||||
catch ( LineUnavailableException lue )
|
||||
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( "AudioOutput - couldn't open audio output line "
|
||||
+ "for playback", lue );
|
||||
mLog.error( "Couldn't obtain a 48 kHz or 8 kHz "
|
||||
+ "source data line - no decoded IMBE audio "
|
||||
+ "will be available" );
|
||||
|
||||
logSourceLines();
|
||||
}
|
||||
catch( IllegalArgumentException iae )
|
||||
{
|
||||
mLog.debug( "Error fetching or opening audio output for 8kHz 16 bit audio", iae );
|
||||
}
|
||||
catch( Exception e )
|
||||
{
|
||||
mLog.error( "Error with audio output", e );
|
||||
}
|
||||
}
|
||||
else
|
||||
{
|
||||
mLog.info( "could not load IMBE audio converter library" );
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
catch ( LineUnavailableException lue )
|
||||
{
|
||||
mLog.error( "AudioOutput - couldn't open audio output line "
|
||||
+ "for playback", lue );
|
||||
}
|
||||
catch( IllegalArgumentException iae )
|
||||
{
|
||||
mLog.error( "could NOT find/load IMBE audio converter library", 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();
|
||||
|
||||
for( Mixer.Info info: AudioSystem.getMixerInfo() )
|
||||
{
|
||||
Mixer mixer = AudioSystem.getMixer( info );
|
||||
|
||||
Line.Info[] lineInfos = mixer.getSourceLineInfo();
|
||||
|
||||
for( Line.Info lineInfo: lineInfos )
|
||||
{
|
||||
mLog.debug( "Source Line: " + lineInfo.toString() );
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -240,11 +299,13 @@ public class P25AudioOutput implements IAudioOutput, IAudioTypeListener,
|
||||
{
|
||||
if( frame.length == 18 )
|
||||
{
|
||||
long start = System.currentTimeMillis();
|
||||
|
||||
/* Insert an 18-byte imbe frame into the stream */
|
||||
mIMBETargetDataLine.receive( frame );
|
||||
|
||||
/* Read 1920 bytes of converted audio from output */
|
||||
byte[] audio = new byte[ PROCESSED_AUDIO_FRAME_SIZE ];
|
||||
/* Read one frame of converted audio from output */
|
||||
byte[] audio = new byte[ mFrameSize ];
|
||||
|
||||
try
|
||||
{
|
||||
@@ -252,6 +313,8 @@ public class P25AudioOutput implements IAudioOutput, IAudioTypeListener,
|
||||
|
||||
if( read > 0 )
|
||||
{
|
||||
mLog.debug( "Conversion Time: " + ( System.currentTimeMillis() - start ) );
|
||||
|
||||
receive( audio );
|
||||
}
|
||||
else
|
||||
@@ -292,7 +355,7 @@ public class P25AudioOutput implements IAudioOutput, IAudioTypeListener,
|
||||
{
|
||||
if( mDispatching.compareAndSet( false, true ) )
|
||||
{
|
||||
mOutput.drain();
|
||||
// mOutput.drain();
|
||||
|
||||
/* Write 2 frames of audio and then start the line */
|
||||
writeAudioFrame();
|
||||
@@ -300,10 +363,12 @@ public class P25AudioOutput implements IAudioOutput, IAudioTypeListener,
|
||||
|
||||
mOutput.start();
|
||||
|
||||
while( mEmptyFrameCount < 3 )
|
||||
while( mEmptyFrameCount < 4 )
|
||||
{
|
||||
writeAudioFrame();
|
||||
}
|
||||
|
||||
mLog.debug( "Stopping ..." );
|
||||
|
||||
mOutput.stop();
|
||||
|
||||
@@ -318,12 +383,17 @@ public class P25AudioOutput implements IAudioOutput, IAudioTypeListener,
|
||||
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[ PROCESSED_AUDIO_FRAME_SIZE ];
|
||||
audio = new byte[ mFrameSize ];
|
||||
}
|
||||
|
||||
if( mOutput != null )
|
||||
|
||||
186
src/dsp/filter/PolyphaseFIRInterpolatingFilter.java
Normal file
186
src/dsp/filter/PolyphaseFIRInterpolatingFilter.java
Normal file
@@ -0,0 +1,186 @@
|
||||
package dsp.filter;
|
||||
|
||||
import java.util.Arrays;
|
||||
|
||||
import org.slf4j.Logger;
|
||||
import org.slf4j.LoggerFactory;
|
||||
|
||||
/**
|
||||
* Polyphase interpolating FIR filter with an internal circular buffer as
|
||||
* described by the dspGuru at:
|
||||
*
|
||||
* http://www.dspguru.com/dsp/faqs/multirate/interpolation
|
||||
* See sections 3.4.2 and 3.4.3 )
|
||||
*/
|
||||
public class PolyphaseFIRInterpolatingFilter
|
||||
{
|
||||
private final static Logger mLog =
|
||||
LoggerFactory.getLogger( PolyphaseFIRInterpolatingFilter.class );
|
||||
|
||||
private double[] mBuffer;
|
||||
private int mBufferPointer = 0;
|
||||
private double[] mTaps;
|
||||
private Phase[] mPhases;
|
||||
|
||||
/**
|
||||
* Constructs the interpolating filter. Coefficients length must be an
|
||||
* integer multiple of the interpolation value. Each sample processed by
|
||||
* the filter will generate a quantity of samples equal to the interpolation
|
||||
* value.
|
||||
*
|
||||
* @param coefficients - low pass filter coefficients sized for the desired
|
||||
* interpolated sample rate with a cutoff frequency of no more than half of
|
||||
* the original sample rate.
|
||||
*
|
||||
* @param interpolation - integral interpolation quantity
|
||||
*/
|
||||
public PolyphaseFIRInterpolatingFilter( double[] coefficients, int interpolation )
|
||||
{
|
||||
/* Ensure coefficients are an integral multiple of the interpolation factor */
|
||||
assert( coefficients.length % interpolation == 0 );
|
||||
|
||||
mTaps = coefficients;
|
||||
|
||||
mPhases = new Phase[ interpolation ];
|
||||
|
||||
int phaseSize = coefficients.length / interpolation;
|
||||
|
||||
mBuffer = new double[ phaseSize ];
|
||||
|
||||
for( int x = 0; x < interpolation; x++ )
|
||||
{
|
||||
int coefficientIndex = 0;
|
||||
|
||||
int[] indexes = new int[ phaseSize ];
|
||||
|
||||
for( int y = x; y < coefficients.length; y += interpolation )
|
||||
{
|
||||
indexes[ coefficientIndex++ ] = y;
|
||||
}
|
||||
|
||||
/* Create a phase using the tap indexes and the interpolation value
|
||||
* for the gain value, to offset the loss induced in upsampling from
|
||||
* one sample to (interpolation) samples. */
|
||||
mPhases[ interpolation - x - 1 ] = new Phase( indexes, interpolation );
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Inserts the sample into the filter and returns (interpolation) number
|
||||
* of interpolated samples in the return array.
|
||||
*
|
||||
* @param sample - float sample
|
||||
* @return - array of interpolated samples, size = interpolation quantity
|
||||
*/
|
||||
public float[] interpolate( float sample )
|
||||
{
|
||||
mBuffer[ mBufferPointer ] = (double)sample;
|
||||
|
||||
mBufferPointer++;
|
||||
mBufferPointer %= mBuffer.length;
|
||||
|
||||
float[] samples = new float[ mPhases.length ];
|
||||
|
||||
for( int x = 0; x < mPhases.length; x++ )
|
||||
{
|
||||
samples[ x ] = mPhases[ x ].filter();
|
||||
}
|
||||
|
||||
return samples;
|
||||
}
|
||||
|
||||
/**
|
||||
* Upsamples and interpolates the array of samples, returning an array that
|
||||
* is upsampled and low-pass filtered by the interpolation quantity.
|
||||
*
|
||||
* @param samples - array of float samples
|
||||
* @return - array of interpolated samples, size = interpolation * samples size
|
||||
*/
|
||||
public float[] interpolate( float[] samples )
|
||||
{
|
||||
int pointer = 0;
|
||||
float[] interpolated = new float[ samples.length * mPhases.length ];
|
||||
|
||||
for( float sample: samples )
|
||||
{
|
||||
mBuffer[ mBufferPointer ] = (double)sample;
|
||||
|
||||
mBufferPointer++;
|
||||
mBufferPointer %= mBuffer.length;
|
||||
|
||||
for( int x = 0; x < mPhases.length; x++ )
|
||||
{
|
||||
interpolated[ pointer ] = mPhases[ x ].filter();
|
||||
|
||||
pointer++;
|
||||
}
|
||||
}
|
||||
|
||||
return interpolated;
|
||||
}
|
||||
|
||||
/**
|
||||
* Single phase element of a polyphase interpolating FIR filter. Each phase
|
||||
* element shares a common buffer. The tap indexes argument defines the
|
||||
* filter taps used by this phase element to generate an interpolated value
|
||||
* from the shared circular buffer of samples.
|
||||
*/
|
||||
public class Phase
|
||||
{
|
||||
private int[] mIndexes;
|
||||
private double mGain;
|
||||
|
||||
public Phase( int[] tapIndexes, int gain )
|
||||
{
|
||||
mIndexes = tapIndexes;
|
||||
mGain = gain;
|
||||
}
|
||||
|
||||
public float filter()
|
||||
{
|
||||
double accumulator = 0;
|
||||
|
||||
for( int x = 0; x < mIndexes.length; x++ )
|
||||
{
|
||||
accumulator += mTaps[ mIndexes[ x ] ] *
|
||||
mBuffer[ ( x + mBufferPointer ) % mIndexes.length ];
|
||||
}
|
||||
|
||||
return (float)( accumulator * mGain );
|
||||
}
|
||||
}
|
||||
|
||||
// public static void main( String[] args )
|
||||
// {
|
||||
// int interpolation = 6;
|
||||
//
|
||||
// PolyphaseFIRInterpolatingFilter p =
|
||||
// new PolyphaseFIRInterpolatingFilter( IMBESynthesizer.INTERPOLATION_TAPS, interpolation );
|
||||
//
|
||||
// Oscillator o = new Oscillator( 500, 8000 );
|
||||
//
|
||||
// int iterations = 40;
|
||||
//
|
||||
// float[] in = new float[ iterations ];
|
||||
// float[] out = new float[ iterations * interpolation ];
|
||||
//
|
||||
// int inIndex = 0;
|
||||
// int outIndex = 0;
|
||||
//
|
||||
// for( int x = 0; x < iterations; x++ )
|
||||
// {
|
||||
// float next = o.nextFloat();
|
||||
//
|
||||
// in[ inIndex++ ] = next;
|
||||
//
|
||||
// float[] samples = p.interpolate( next );
|
||||
//
|
||||
// System.arraycopy( samples, 0, out, outIndex, samples.length );
|
||||
//
|
||||
// outIndex += interpolation;
|
||||
// }
|
||||
//
|
||||
// mLog.debug( "In: " + Arrays.toString( in ) );
|
||||
// mLog.debug( "Out: " + Arrays.toString( out ) );
|
||||
// }
|
||||
}
|
||||
99
src/dsp/filter/equalizer/CMAEqualizer.java
Normal file
99
src/dsp/filter/equalizer/CMAEqualizer.java
Normal file
@@ -0,0 +1,99 @@
|
||||
package dsp.filter.equalizer;
|
||||
|
||||
import org.slf4j.Logger;
|
||||
import org.slf4j.LoggerFactory;
|
||||
|
||||
import sample.Listener;
|
||||
import sample.complex.ComplexSample;
|
||||
|
||||
public class CMAEqualizer implements Listener<ComplexSample>
|
||||
{
|
||||
private final static Logger mLog = LoggerFactory.getLogger( CMAEqualizer.class );
|
||||
|
||||
public static final int TAP_COUNT = 4;
|
||||
|
||||
private float mModulus;
|
||||
private float mMu;
|
||||
private ComplexSample mError = new ComplexSample( 0.0f, 0.0f );
|
||||
|
||||
private ComplexSample[] mTaps = new ComplexSample[ TAP_COUNT ];
|
||||
private ComplexSample[] mBuffer = new ComplexSample[ TAP_COUNT ];
|
||||
|
||||
private int mBufferPointer = 0;
|
||||
|
||||
private Listener<ComplexSample> mListener;
|
||||
|
||||
public CMAEqualizer( float modulus, float mu )
|
||||
{
|
||||
mModulus = modulus;
|
||||
mMu = mu;
|
||||
|
||||
mTaps[ 0 ] = new ComplexSample( 1.0f, 1.0f );
|
||||
mBuffer[ 0 ] = new ComplexSample( 1.0f, 1.0f );
|
||||
|
||||
for( int x = 1; x < TAP_COUNT; x++ )
|
||||
{
|
||||
mTaps[ x ] = new ComplexSample( 0.0f, 0.0f );
|
||||
mBuffer[ x ] = new ComplexSample( 1.0f, 1.0f );
|
||||
}
|
||||
}
|
||||
|
||||
private void updateTaps()
|
||||
{
|
||||
for( int x = 0; x < TAP_COUNT; x++ )
|
||||
{
|
||||
// tap -= mu * conj(sample) * error
|
||||
|
||||
mTaps[ x ] = ComplexSample.subtract( mTaps[ x ],
|
||||
ComplexSample.multiply( ComplexSample.multiply(
|
||||
mBuffer[ x ].conjugate(), mMu ), mError ) );
|
||||
}
|
||||
}
|
||||
|
||||
@Override
|
||||
public void receive( ComplexSample sample )
|
||||
{
|
||||
mBuffer[ mBufferPointer ] = sample;
|
||||
|
||||
mBufferPointer++;
|
||||
|
||||
mBufferPointer = mBufferPointer % TAP_COUNT;
|
||||
|
||||
ComplexSample equalized = filter();
|
||||
|
||||
calculateError( equalized );
|
||||
|
||||
updateTaps();
|
||||
|
||||
if( mListener != null )
|
||||
{
|
||||
mListener.receive( equalized );
|
||||
}
|
||||
|
||||
mLog.debug( "Tap 0:" + mTaps[ 0 ].toString() + " error:" + mError.toString() );
|
||||
}
|
||||
|
||||
private ComplexSample filter()
|
||||
{
|
||||
ComplexSample sum = new ComplexSample( 0.0f, 0.0f );
|
||||
|
||||
for( int x = 0; x < TAP_COUNT; x++ )
|
||||
{
|
||||
sum = ComplexSample.add( sum,
|
||||
ComplexSample.multiply( mBuffer[ x ], mTaps[ x ] ) );
|
||||
}
|
||||
|
||||
return sum;
|
||||
}
|
||||
|
||||
public void setListener( Listener<ComplexSample> listener )
|
||||
{
|
||||
mListener = listener;
|
||||
}
|
||||
|
||||
private void calculateError( ComplexSample sample )
|
||||
{
|
||||
mError = ComplexSample.multiply( sample, sample.norm() - mModulus );
|
||||
mError.clip( 1.0f );
|
||||
}
|
||||
}
|
||||
@@ -27,6 +27,7 @@ public class DirectGainControl implements GainController, RealSampleListener
|
||||
private final static Logger mLog =
|
||||
LoggerFactory.getLogger( DirectGainControl.class );
|
||||
|
||||
private float mDefaultGain;
|
||||
private float mGain;
|
||||
private float mMinimum;
|
||||
private float mMaximum;
|
||||
@@ -49,7 +50,8 @@ public class DirectGainControl implements GainController, RealSampleListener
|
||||
public DirectGainControl( float gain, float minimum, float maximum, float increment )
|
||||
{
|
||||
assert( minimum > 0 );
|
||||
|
||||
|
||||
mDefaultGain = gain;
|
||||
mGain = gain;
|
||||
mMinimum = minimum;
|
||||
mMaximum = maximum;
|
||||
@@ -62,6 +64,12 @@ public class DirectGainControl implements GainController, RealSampleListener
|
||||
mListener.receive( (float)( sample * mGain ) );
|
||||
}
|
||||
|
||||
@Override
|
||||
public void reset()
|
||||
{
|
||||
mGain = mDefaultGain;
|
||||
}
|
||||
|
||||
@Override
|
||||
public void increase()
|
||||
{
|
||||
|
||||
@@ -5,4 +5,6 @@ public interface GainController
|
||||
public abstract void increase();
|
||||
|
||||
public abstract void decrease();
|
||||
|
||||
public abstract void reset();
|
||||
}
|
||||
@@ -203,7 +203,7 @@ public class TunerChannelSource extends ComplexSource
|
||||
long frequencyErrorOffset = mTunerFrequency -
|
||||
mTunerChannel.getFrequency() -
|
||||
mTunerFrequencyError;
|
||||
|
||||
|
||||
mSineWaveGenerator.setFrequency( frequencyErrorOffset );
|
||||
break;
|
||||
case FREQUENCY:
|
||||
@@ -211,6 +211,8 @@ public class TunerChannelSource extends ComplexSource
|
||||
|
||||
/* If the frequency is updated, the AFC will also get reset, so
|
||||
* we don't include the frequency error value here */
|
||||
mTunerFrequencyError = 0;
|
||||
|
||||
long frequencyOffset = frequency - mTunerChannel.getFrequency();
|
||||
|
||||
mSineWaveGenerator.setFrequency( frequencyOffset );
|
||||
|
||||
Reference in New Issue
Block a user