mirror of
https://github.com/DSheirer/sdrtrunk.git
synced 2026-11-12 08:37:25 -05:00
Instrumentation updates for eye diagram data and removed stack trace logging when JMBE library is not discovered.
This commit is contained in:
@@ -48,7 +48,6 @@ 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
|
||||
{
|
||||
@@ -214,8 +213,12 @@ public class P25Decoder extends Decoder implements Instrumentable
|
||||
|
||||
if( mSourceSampleType == SampleType.COMPLEX )
|
||||
{
|
||||
mAvailableTaps.add( new ComplexTap( INSTRUMENT_COMPLEX_INPUT, 0, 1.0f ) );
|
||||
mAvailableTaps.add( new ComplexTap( INSTRUMENT_BASEBAND_FILTER_OUTPUT, 0, 1.0f ) );
|
||||
if( mCQPSKDemodulator != null )
|
||||
{
|
||||
mAvailableTaps.addAll( mCQPSKDemodulator.getTaps() );
|
||||
}
|
||||
// mAvailableTaps.add( new ComplexTap( INSTRUMENT_COMPLEX_INPUT, 0, 1.0f ) );
|
||||
// mAvailableTaps.add( new ComplexTap( INSTRUMENT_BASEBAND_FILTER_OUTPUT, 0, 1.0f ) );
|
||||
}
|
||||
else
|
||||
{
|
||||
@@ -238,7 +241,6 @@ public class P25Decoder extends Decoder implements Instrumentable
|
||||
mAvailableTaps.add( new ComplexTap( INSTRUMENT_AGC_OUTPUT, 0, 1.0f ) );
|
||||
mAvailableTaps.add( new QPSKTap( INSTRUMENT_QPSK_DEMODULATOR_OUTPUT, 0, 1.0f ) );
|
||||
mAvailableTaps.add( new DibitTap( INSTRUMENT_CQPSK_SLICER_OUTPUT, 0, 0.1f ) );
|
||||
mAvailableTaps.addAll( mCQPSKDemodulator.getTaps() );
|
||||
}
|
||||
}
|
||||
|
||||
@@ -266,7 +268,7 @@ public class P25Decoder extends Decoder implements Instrumentable
|
||||
case INSTRUMENT_BASEBAND_FILTER_OUTPUT:
|
||||
ComplexTap baseband = (ComplexTap)tap;
|
||||
mBasebandFilter.setListener( baseband );
|
||||
baseband.setListener( mEqualizer );
|
||||
baseband.setListener( mAGC );
|
||||
break;
|
||||
case INSTRUMENT_AGC_OUTPUT:
|
||||
ComplexTap agcSymbol = (ComplexTap)tap;
|
||||
@@ -323,7 +325,7 @@ public class P25Decoder extends Decoder implements Instrumentable
|
||||
|
||||
if( mCQPSKDemodulator != null )
|
||||
{
|
||||
mCQPSKDemodulator.addTap( tap );
|
||||
mCQPSKDemodulator.removeTap( tap );
|
||||
}
|
||||
|
||||
switch( tap.getName() )
|
||||
@@ -361,11 +363,6 @@ public class P25Decoder extends Decoder implements Instrumentable
|
||||
{
|
||||
mC4FMSlicer.removeListener( slicerTap );
|
||||
}
|
||||
|
||||
// if( mCQPSKSlicer != null )
|
||||
// {
|
||||
// mCQPSKSlicer.removeListener( slicerTap );
|
||||
// }
|
||||
break;
|
||||
}
|
||||
}
|
||||
|
||||
@@ -31,6 +31,7 @@ import decode.p25.message.tsbk.TSBKMessage;
|
||||
import decode.p25.message.tsbk.TSBKMessageFactory;
|
||||
import decode.p25.reference.DataUnitID;
|
||||
import dsp.psk.CQPSKDemodulator;
|
||||
import dsp.psk.CQPSKDemodulator2;
|
||||
import dsp.symbol.Dibit;
|
||||
import dsp.symbol.FrameSync;
|
||||
import edac.BCH_63_16_11;
|
||||
|
||||
@@ -115,7 +115,7 @@ public class P25AudioOutput implements IAudioOutput, Listener<Message>,
|
||||
}
|
||||
catch ( ClassNotFoundException e1 )
|
||||
{
|
||||
mLog.error( "Couldn't find/load JMBE audio conversion library", e1 );
|
||||
mLog.error( "Couldn't find/load JMBE audio conversion library" );
|
||||
}
|
||||
catch ( InstantiationException e1 )
|
||||
{
|
||||
|
||||
@@ -28,7 +28,9 @@ public class CMAEqualizer implements Listener<ComplexSample>
|
||||
mModulus = modulus;
|
||||
mMu = mu;
|
||||
|
||||
/* Set first tap to 1,0 as a start */
|
||||
/* Set first tap to 1,0 and all other taps to 0,0 as a starting point.
|
||||
* This means that the output is solely determined by the last sample
|
||||
* entering the filter, initially. */
|
||||
mTaps[ 0 ] = new ComplexSample( 1.0f, 0.0f );
|
||||
|
||||
for( int x = 1; x < TAP_COUNT; x++ )
|
||||
|
||||
@@ -32,6 +32,7 @@ package dsp.psk;
|
||||
|
||||
import instrument.Instrumentable;
|
||||
import instrument.tap.Tap;
|
||||
import instrument.tap.stream.EyeDiagramData;
|
||||
import instrument.tap.stream.EyeDiagramDataTap;
|
||||
|
||||
import java.text.DecimalFormat;
|
||||
@@ -299,14 +300,6 @@ public class CQPSKDemodulator implements Instrumentable,
|
||||
middleSymbol.normalize();
|
||||
currentSymbol.normalize();
|
||||
|
||||
if( mEyeDiagramDataTap != null )
|
||||
{
|
||||
mEyeDiagramDataTap.receive(
|
||||
new EyeDiagramData( Arrays.copyOfRange( mDelayLine,
|
||||
mDelayLinePointer, mDelayLinePointer + 20 ),
|
||||
mMu, (float)half_sps + half_mu ) );
|
||||
}
|
||||
|
||||
/* Gardner timing error calculations */
|
||||
float errorInphase = ( mPreviousSymbol.inphase() -
|
||||
currentSymbol.inphase() ) * middleSymbol.inphase();
|
||||
@@ -316,9 +309,13 @@ public class CQPSKDemodulator implements Instrumentable,
|
||||
|
||||
float gardnerError = normalize( errorInphase + errorQuadrature, 1.0f );
|
||||
|
||||
// /*debug*/
|
||||
// gardnerError = 0.0f;
|
||||
|
||||
if( mEyeDiagramDataTap != null )
|
||||
{
|
||||
mEyeDiagramDataTap.receive(
|
||||
new EyeDiagramData( Arrays.copyOfRange( mDelayLine,
|
||||
0, 20 ), mMu, (float)half_sps + half_mu, gardnerError ) );
|
||||
}
|
||||
|
||||
/* mOmega is samples per symbol and is constrained to floating
|
||||
* between +/- .005 of the nominal 10.0 samples per symbol */
|
||||
mOmega = mOmega + mGainOmega * gardnerError;
|
||||
|
||||
561
src/dsp/psk/CQPSKDemodulator2.java
Normal file
561
src/dsp/psk/CQPSKDemodulator2.java
Normal file
@@ -0,0 +1,561 @@
|
||||
package dsp.psk;
|
||||
|
||||
/*******************************************************************************
|
||||
* SDR Trunk
|
||||
* Copyright (C) 2014, 2015 Dennis Sheirer
|
||||
*
|
||||
* Gardner detector, Costas loop and Interpolator derived from:
|
||||
*
|
||||
* OP25 - gardner_costas_cc_impl.cc
|
||||
* Copyright 2010, 2011, 2012, 2013 KA1RBI (gardner_costas_cc_impl.cc)
|
||||
*
|
||||
* OP25 - scope.py
|
||||
* Copyright 2008-2011 Steve Glass
|
||||
* Copyright 2011, 2012, 2013 KA1RBI
|
||||
*
|
||||
* GNURadio - control_loop and mpsk_receiver_cc
|
||||
* Copyright 2011,2013 Free Software Foundation, Inc.
|
||||
*
|
||||
* This program is free software: you can redistribute it and/or modify
|
||||
* it under the terms of the GNU General Public License as published by
|
||||
* the Free Software Foundation, either version 3 of the License, or
|
||||
* (at your option) any later version.
|
||||
*
|
||||
* This program is distributed in the hope that it will be useful,
|
||||
* but WITHOUT ANY WARRANTY; without even the implied warranty of
|
||||
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
|
||||
* GNU General Public License for more details.
|
||||
*
|
||||
* You should have received a copy of the GNU General Public License
|
||||
* along with this program. If not, see <http://www.gnu.org/licenses/>
|
||||
******************************************************************************/
|
||||
|
||||
import instrument.Instrumentable;
|
||||
import instrument.tap.Tap;
|
||||
import instrument.tap.stream.EyeDiagramData;
|
||||
import instrument.tap.stream.EyeDiagramDataTap;
|
||||
|
||||
import java.text.DecimalFormat;
|
||||
import java.util.ArrayList;
|
||||
import java.util.Arrays;
|
||||
import java.util.List;
|
||||
|
||||
import org.slf4j.Logger;
|
||||
import org.slf4j.LoggerFactory;
|
||||
|
||||
import sample.Listener;
|
||||
import sample.Provider;
|
||||
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 an early/late symbol timing error detector
|
||||
* and a Costas Loop as a phase locked loop synchronized with the incoming
|
||||
* signal carrier frequency.
|
||||
*
|
||||
* Sample Rate: 48000
|
||||
* Symbol Rate: 4800
|
||||
*/
|
||||
public class CQPSKDemodulator2 implements Instrumentable,
|
||||
Listener<ComplexSample>,
|
||||
Provider<ComplexSample>
|
||||
{
|
||||
private final static Logger mLog =
|
||||
LoggerFactory.getLogger( CQPSKDemodulator2.class );
|
||||
|
||||
/* 45 degree rotation angle */
|
||||
public static final float THETA = (float)( Math.PI / 4.0d );
|
||||
|
||||
/* 45 degree point */
|
||||
public static final ComplexSample POINT_45_DEGREES =
|
||||
new ComplexSample( (float)Math.sin( THETA ), (float)Math.cos( THETA ) );
|
||||
|
||||
private DecimalFormat mDecimalFormat = new DecimalFormat( "0.000000" );
|
||||
|
||||
private Listener<ComplexSample> mListener;
|
||||
|
||||
private EarlyLateSymbolTiming mGardnerDetector = new EarlyLateSymbolTiming();
|
||||
|
||||
private CostasLoop mCostasLoop = new CostasLoop();
|
||||
|
||||
private FrequencyChangeListener mFrequencyChangeListener;
|
||||
|
||||
private EyeDiagramDataTap mEyeDiagramDataTap;
|
||||
|
||||
public CQPSKDemodulator2()
|
||||
{
|
||||
}
|
||||
|
||||
@Override
|
||||
public void receive( ComplexSample sample )
|
||||
{
|
||||
mGardnerDetector.receive( sample );
|
||||
}
|
||||
|
||||
/**
|
||||
* Applies a phase correction value to the costas loop to correct when a
|
||||
* phase lock error is detected in the binary output stream.
|
||||
*
|
||||
* @param correction - value in radians
|
||||
*/
|
||||
public void correctPhaseError( double correction )
|
||||
{
|
||||
mCostasLoop.correctPhaseError( correction );
|
||||
}
|
||||
|
||||
/**
|
||||
* Constrains value to the range of ( -maximum <> maximum )
|
||||
*/
|
||||
public static float clip( float value, float maximum )
|
||||
{
|
||||
if( value > maximum )
|
||||
{
|
||||
return maximum;
|
||||
}
|
||||
else if( value < -maximum )
|
||||
{
|
||||
return -maximum;
|
||||
}
|
||||
|
||||
return value;
|
||||
}
|
||||
|
||||
/**
|
||||
* Constrains timing error to +/- the maximum value and corrects any
|
||||
* floating point invalid numbers
|
||||
*/
|
||||
private float normalize( float error, float maximum )
|
||||
{
|
||||
if( Float.isNaN( error ) )
|
||||
{
|
||||
return 0.0f;
|
||||
}
|
||||
else
|
||||
{
|
||||
return clip( error, maximum );
|
||||
}
|
||||
}
|
||||
|
||||
public void addListener( FrequencyChangeListener listener )
|
||||
{
|
||||
mFrequencyChangeListener = listener;
|
||||
}
|
||||
|
||||
public void removeListener( FrequencyChangeListener listener )
|
||||
{
|
||||
mFrequencyChangeListener = null;
|
||||
}
|
||||
|
||||
/**
|
||||
* Processes the current loop frequency of the costas loop and broadcasts
|
||||
* tuner frequency offset adjustments 10 times a second as needed to keep
|
||||
* the frequency offset below half of the maximum frequency
|
||||
*/
|
||||
public class FrequencyControl
|
||||
{
|
||||
/* Set the trigger threshold at half of the maximum costas loop control
|
||||
* frequency */
|
||||
private static final float CORRECTION_THRESHOLD_FREQUENCY =
|
||||
( 2.0f * (float)Math.PI * 600.0f ) / 48000.0f;
|
||||
|
||||
private FloatAveragingBuffer mBuffer = new FloatAveragingBuffer( 40 );
|
||||
|
||||
private long mFrequencyError = 0;
|
||||
|
||||
private int mCounter = 0;
|
||||
|
||||
public void receive( float frequency )
|
||||
{
|
||||
// mCounter++;
|
||||
//
|
||||
// if( mCounter >= 480 )
|
||||
// {
|
||||
// float average = mBuffer.get( frequency );
|
||||
// mLog.debug( "Avg:" + average + " Threshold:" + CORRECTION_THRESHOLD_FREQUENCY);
|
||||
//
|
||||
// boolean correctionNeeded = false;
|
||||
//
|
||||
// if( average > CORRECTION_THRESHOLD_FREQUENCY )
|
||||
// {
|
||||
// mFrequencyError--;
|
||||
//
|
||||
// correctionNeeded = true;
|
||||
// }
|
||||
// else if( average < -CORRECTION_THRESHOLD_FREQUENCY )
|
||||
// {
|
||||
// mFrequencyError++;
|
||||
//
|
||||
// correctionNeeded = true;
|
||||
// }
|
||||
//
|
||||
// if( correctionNeeded && mFrequencyChangeListener != null )
|
||||
// {
|
||||
// mLog.debug( "Issuing Frequency Correction: " + mFrequencyError );
|
||||
// mFrequencyChangeListener.frequencyChanged(
|
||||
// new FrequencyChangeEvent( Attribute.FREQUENCY_ERROR,
|
||||
// mFrequencyError ) );
|
||||
// }
|
||||
//
|
||||
// mCounter = 0;
|
||||
// }
|
||||
// else
|
||||
// {
|
||||
// mBuffer.get( frequency );
|
||||
// }
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Early/Late symbol timing error detector. Linear Simulcast Modulation (LSM)
|
||||
* uses a Raised Cosine Filter at the transmitter which causes the
|
||||
* signal magnitude to rise to unity at the optimal sampling point and drop
|
||||
* to zero or a midpoint between unity and zero between each symbol period.
|
||||
*
|
||||
* This error detector continually adjusts symbol timing by continuously
|
||||
* attempting to find the largest magnitude value between two interpolated
|
||||
* sample points during each symbol period.
|
||||
*/
|
||||
public class EarlyLateSymbolTiming implements Listener<ComplexSample>
|
||||
{
|
||||
public static final int SAMPLES_PER_SYMBOL = 10;
|
||||
public static final int TWICE_SAMPLES_PER_SYMBOL = 20;
|
||||
|
||||
private ComplexSample[] mDelayLine =
|
||||
new ComplexSample[ 2 * TWICE_SAMPLES_PER_SYMBOL ];
|
||||
private int mDelayLinePointer = 0;
|
||||
|
||||
/* Sampling point */
|
||||
private float mMu = 10.0f;
|
||||
private float mGainMu = 0.5f;
|
||||
|
||||
/* Samples per symbol */
|
||||
private float mOmega = 10.0f;
|
||||
private float mGainOmega = 0.1f * mGainMu * mGainMu;
|
||||
private float mOmegaRel = 0.005f;
|
||||
private float mOmegaMid = 10.0f;
|
||||
|
||||
private QPSKInterpolator mInterpolator = new QPSKInterpolator( 1.0f );
|
||||
|
||||
private ComplexSample mPreviousSample = new ComplexSample( 0.0f, 0.0f );
|
||||
|
||||
/**
|
||||
* Provides symbol sampling timing control
|
||||
*/
|
||||
public EarlyLateSymbolTiming()
|
||||
{
|
||||
for( int x = 0; x < ( 2 * TWICE_SAMPLES_PER_SYMBOL ); x++ )
|
||||
{
|
||||
mDelayLine[ x ] = new ComplexSample( 0.0f, 0.0f );
|
||||
}
|
||||
}
|
||||
|
||||
@Override
|
||||
public void receive( ComplexSample sample )
|
||||
{
|
||||
/* Count down samples per symbol until we calculate the symbol */
|
||||
mMu--;
|
||||
|
||||
mCostasLoop.increment();
|
||||
|
||||
/* Mix incoming sample with costas loop to remove any rotation
|
||||
* that is present from a mis-tuned carrier frequency */
|
||||
ComplexSample derotatedSample = ComplexSample
|
||||
.multiply( mCostasLoop.getCurrentVector(), sample );
|
||||
|
||||
/* Fill up the delay line to use with the interpolator */
|
||||
mDelayLine[ mDelayLinePointer ] = derotatedSample;
|
||||
mDelayLine[ mDelayLinePointer + TWICE_SAMPLES_PER_SYMBOL ] = derotatedSample;
|
||||
|
||||
/* Imcrement pointer and keep pointer in bounds */
|
||||
mDelayLinePointer = ( mDelayLinePointer + 1 ) % TWICE_SAMPLES_PER_SYMBOL;
|
||||
|
||||
/* Calculate the symbol once we've stored enough samples */
|
||||
if( mMu <= 1.0f )
|
||||
{
|
||||
ComplexSample earlySample = mInterpolator
|
||||
.filter( mDelayLine, mDelayLinePointer, mMu );
|
||||
|
||||
ComplexSample middleSample = mInterpolator
|
||||
.filter( mDelayLine, mDelayLinePointer + 1, mMu );
|
||||
|
||||
ComplexSample lateSample = mInterpolator
|
||||
.filter( mDelayLine, mDelayLinePointer + 2, mMu );
|
||||
|
||||
ComplexSample currentSymbol = ComplexSample.multiply(
|
||||
middleSample, mPreviousSample.conjugate() );
|
||||
|
||||
/* Set symbol gain to unity */
|
||||
currentSymbol.normalize();
|
||||
|
||||
float earlyLateError =
|
||||
lateSample.magnitude() - earlySample.magnitude();
|
||||
|
||||
if( mEyeDiagramDataTap != null )
|
||||
{
|
||||
mEyeDiagramDataTap.receive(
|
||||
new EyeDiagramData( Arrays.copyOfRange( mDelayLine, 0, 20 ),
|
||||
(float)mDelayLinePointer + 4.0f + mMu,
|
||||
(float)mDelayLinePointer + 4.0f + mMu + 0.25f, earlyLateError ) );
|
||||
}
|
||||
|
||||
/* mOmega is samples per symbol and is constrained to floating
|
||||
* between +/- .005 of the nominal 10.0 samples per symbol */
|
||||
mOmega = mOmega + mGainOmega * earlyLateError;
|
||||
mOmega = mOmegaMid + clip( mOmega - mOmegaMid, mOmegaRel );
|
||||
|
||||
|
||||
/* Adjust sample timing based on error of current sample */
|
||||
mMu += mOmega + ( mGainMu * earlyLateError );
|
||||
|
||||
|
||||
/* Store current samples/symbols to use for the next period */
|
||||
mPreviousSample = middleSample;
|
||||
|
||||
/* 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( currentSymbol );
|
||||
// mCostasLoop.receive(
|
||||
// ComplexSample.multiply( currentSymbol, POINT_45_DEGREES ) );
|
||||
|
||||
/* Dispatch the differentiated symbol to the registered listener */
|
||||
if( mListener != null )
|
||||
{
|
||||
mListener.receive( currentSymbol );
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Costas Loop - phase locked loop synchronized to the frequency offset of
|
||||
* the incoming signal to enable the signal to be mixed down to zero offset
|
||||
* for proper synchronization. The mFrequency value indicates the detected
|
||||
* frequency offset. We attempt to keep that value close to zero by issuing
|
||||
* frequency adjustments to the tuner channel source.
|
||||
*
|
||||
* Most of the costas loop code was ported from gnuradio/control_loop and
|
||||
* gnuradio/mpsk_receiver_cc using initialization values from KA1RBI's
|
||||
* OP25/cqpsk.py
|
||||
*/
|
||||
public class CostasLoop implements Listener<ComplexSample>
|
||||
{
|
||||
public static final double TWO_PI = 2.0 * Math.PI;
|
||||
|
||||
private static final float MAXIMUM_FREQUENCY =
|
||||
( 2400.0f * (float)TWO_PI ) / 48000.0f;
|
||||
|
||||
/* http://www.trondeau.com/blog/2011/8/13/control-loop-gain-values.html */
|
||||
private float mDamping = (float)Math.sqrt( 2.0 ) / 2.0f;
|
||||
|
||||
/* Use denominator between 100 (most) and 400 (least) to adjust costas
|
||||
* loop control level */
|
||||
private float mLoopBandwidth = (float)( TWO_PI / 400.0d );
|
||||
|
||||
private float mAlphaGain = ( 4.0f * mDamping * mLoopBandwidth ) /
|
||||
( 1.0f + ( 2.0f * mDamping * mLoopBandwidth ) +
|
||||
( mLoopBandwidth * mLoopBandwidth ) );
|
||||
|
||||
private float mBetaGain = ( 4.0f * mLoopBandwidth * mLoopBandwidth ) /
|
||||
( 1.0f + ( 2.0f * mDamping * mLoopBandwidth ) +
|
||||
( mLoopBandwidth * mLoopBandwidth ) );
|
||||
|
||||
private float mLoopPhase = 0.0f;
|
||||
|
||||
private float mLoopFrequency = 0.0f;
|
||||
|
||||
private FrequencyControl mFrequencyControl = new FrequencyControl();
|
||||
|
||||
public CostasLoop()
|
||||
{
|
||||
}
|
||||
|
||||
/**
|
||||
* Applies a phase correction value to the current phasor. Use this
|
||||
* method to apply correction when a +/- 90, or 180 degree phase error
|
||||
* is detected in the binary output stream.
|
||||
*
|
||||
* If the supplied correction value places the loop frequency outside
|
||||
* of the max frequency, then the frequency will be corrected 360
|
||||
* degrees in the opposite direction to maintain within the max
|
||||
* frequency bounds.
|
||||
*
|
||||
* @param correction - correction value in radians
|
||||
*/
|
||||
public void correctPhaseError( double correction )
|
||||
{
|
||||
mLoopFrequency += correction;
|
||||
|
||||
if( mLoopFrequency > MAXIMUM_FREQUENCY )
|
||||
{
|
||||
mLoopFrequency -= 2.0d * MAXIMUM_FREQUENCY;
|
||||
}
|
||||
|
||||
if( mLoopFrequency < -MAXIMUM_FREQUENCY )
|
||||
{
|
||||
mLoopFrequency += 2.0d * MAXIMUM_FREQUENCY;
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Increments the phase of the loop
|
||||
*/
|
||||
public void increment()
|
||||
{
|
||||
mLoopPhase += mLoopFrequency;
|
||||
|
||||
/* Keep the loop phase in bounds */
|
||||
unwrapPhase();
|
||||
}
|
||||
|
||||
private void unwrapPhase()
|
||||
{
|
||||
while( mLoopPhase > TWO_PI )
|
||||
{
|
||||
mLoopPhase -= TWO_PI;
|
||||
}
|
||||
|
||||
while( mLoopPhase < -TWO_PI )
|
||||
{
|
||||
mLoopPhase += TWO_PI;
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Current vector of the loop rotated by THETA degrees.
|
||||
*/
|
||||
public ComplexSample getCurrentVector()
|
||||
{
|
||||
// return ComplexSample.fromAngle( mLoopPhase + THETA );
|
||||
return ComplexSample.fromAngle( mLoopPhase );
|
||||
}
|
||||
|
||||
@Override
|
||||
public void receive( ComplexSample sample )
|
||||
{
|
||||
/* 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 );
|
||||
}
|
||||
|
||||
/**
|
||||
* Updates the costas loop frequency and phase to adjust for the phase
|
||||
* error value
|
||||
*
|
||||
* @param phase_error - (-)= late and (+)= early
|
||||
*/
|
||||
private void adjust( float phase_error )
|
||||
{
|
||||
mLoopFrequency += mBetaGain * phase_error;
|
||||
mLoopPhase += mLoopFrequency + mAlphaGain * phase_error;
|
||||
|
||||
/* Maintain phase between +/- 2 * PI */
|
||||
unwrapPhase();
|
||||
|
||||
/* Limit frequency to +/- maximum loop frequency */
|
||||
limitFrequency();
|
||||
}
|
||||
|
||||
/**
|
||||
* Constrains the frequency within the bounds of +/- loop frequency
|
||||
*/
|
||||
private void limitFrequency()
|
||||
{
|
||||
/* Check for and issue tuner offset correction */
|
||||
mFrequencyControl.receive( mLoopFrequency );
|
||||
|
||||
if( mLoopFrequency > MAXIMUM_FREQUENCY )
|
||||
{
|
||||
mLoopFrequency = MAXIMUM_FREQUENCY;
|
||||
}
|
||||
|
||||
if( mLoopFrequency < -MAXIMUM_FREQUENCY )
|
||||
{
|
||||
mLoopFrequency = -MAXIMUM_FREQUENCY;
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Detects rotational error present when the costas loop is not aligned
|
||||
* with the carrier frequency. Provides error feedback to adjust
|
||||
* mixer frequency.
|
||||
*/
|
||||
public float getPhaseError( ComplexSample sample )
|
||||
{
|
||||
float phase_error = 0;
|
||||
|
||||
if( Math.abs( sample.real() ) > Math.abs( sample.imaginary() ) )
|
||||
{
|
||||
if( sample.real() > 0 )
|
||||
{
|
||||
phase_error = -sample.imaginary();
|
||||
}
|
||||
else
|
||||
{
|
||||
phase_error = sample.imaginary();
|
||||
}
|
||||
}
|
||||
else
|
||||
{
|
||||
if( sample.imaginary() > 0 )
|
||||
{
|
||||
phase_error = sample.real();
|
||||
}
|
||||
else
|
||||
{
|
||||
phase_error = -sample.real();
|
||||
}
|
||||
}
|
||||
|
||||
return phase_error;
|
||||
}
|
||||
}
|
||||
|
||||
@Override
|
||||
public void setListener( Listener<ComplexSample> listener )
|
||||
{
|
||||
mListener = listener;
|
||||
}
|
||||
|
||||
@Override
|
||||
public void removeListener( Listener<ComplexSample> listener )
|
||||
{
|
||||
mListener = null;
|
||||
}
|
||||
|
||||
@Override
|
||||
public List<Tap> getTaps()
|
||||
{
|
||||
List<Tap> taps = new ArrayList<Tap>();
|
||||
|
||||
taps.add( new EyeDiagramDataTap( "Eye Diagram", 0, 4800 ) );
|
||||
|
||||
return taps;
|
||||
}
|
||||
|
||||
@Override
|
||||
public void addTap( Tap tap )
|
||||
{
|
||||
if( tap instanceof EyeDiagramDataTap )
|
||||
{
|
||||
mEyeDiagramDataTap = (EyeDiagramDataTap)tap;
|
||||
}
|
||||
}
|
||||
|
||||
@Override
|
||||
public void removeTap( Tap tap )
|
||||
{
|
||||
if( tap instanceof EyeDiagramDataTap )
|
||||
{
|
||||
mEyeDiagramDataTap = null;
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -1,33 +0,0 @@
|
||||
package dsp.psk;
|
||||
|
||||
import sample.complex.ComplexSample;
|
||||
|
||||
public class EyeDiagramData
|
||||
{
|
||||
private ComplexSample[] mSamples;
|
||||
private float mSamplePoint;
|
||||
private float mMiddlePoint;
|
||||
|
||||
public EyeDiagramData( ComplexSample[] samples, float samplePoint, float middlePoint )
|
||||
{
|
||||
mSamples = samples;
|
||||
|
||||
mSamplePoint = samplePoint;
|
||||
mMiddlePoint = middlePoint;
|
||||
}
|
||||
|
||||
public ComplexSample[] getSamples()
|
||||
{
|
||||
return mSamples;
|
||||
}
|
||||
|
||||
public float getSamplePoint()
|
||||
{
|
||||
return mSamplePoint;
|
||||
}
|
||||
|
||||
public float getMiddlePoint()
|
||||
{
|
||||
return mMiddlePoint;
|
||||
}
|
||||
}
|
||||
@@ -17,11 +17,18 @@
|
||||
******************************************************************************/
|
||||
package instrument.gui;
|
||||
|
||||
import java.awt.AWTException;
|
||||
import java.awt.EventQueue;
|
||||
import java.awt.Robot;
|
||||
import java.awt.event.ActionEvent;
|
||||
import java.awt.event.ActionListener;
|
||||
import java.awt.image.BufferedImage;
|
||||
import java.io.File;
|
||||
import java.io.IOException;
|
||||
import java.nio.file.Files;
|
||||
import java.nio.file.Path;
|
||||
|
||||
import javax.imageio.ImageIO;
|
||||
import javax.swing.JDesktopPane;
|
||||
import javax.swing.JFileChooser;
|
||||
import javax.swing.JFrame;
|
||||
@@ -34,10 +41,12 @@ import javax.swing.filechooser.FileFilter;
|
||||
import org.slf4j.Logger;
|
||||
import org.slf4j.LoggerFactory;
|
||||
|
||||
import properties.SystemProperties;
|
||||
import source.Source;
|
||||
import source.wave.ComplexWaveSource;
|
||||
import source.wave.FloatWaveSource;
|
||||
import source.wave.WaveSource;
|
||||
import util.TimeStamp;
|
||||
|
||||
public class Viewer
|
||||
{
|
||||
@@ -101,24 +110,67 @@ public class Viewer
|
||||
|
||||
fileMenu.add( exitMenu );
|
||||
|
||||
// JMenuItem windowItem = new JMenuItem( "New Window" );
|
||||
// windowItem.addActionListener( new ActionListener()
|
||||
// {
|
||||
// @Override
|
||||
// public void actionPerformed( ActionEvent arg0 )
|
||||
// {
|
||||
// JInternalFrame frame =
|
||||
// new JInternalFrame( "New", true, true, true, true );
|
||||
//
|
||||
// frame.add( new JLabel( "This is a test" ) );
|
||||
// frame.pack();
|
||||
//
|
||||
// mDesktop.add( frame );
|
||||
//
|
||||
// frame.setVisible( true );
|
||||
// }
|
||||
// } );
|
||||
// menuBar.add( windowItem );
|
||||
JMenuItem screenCaptureItem = new JMenuItem( "Screen Capture" );
|
||||
screenCaptureItem.addActionListener( new ActionListener()
|
||||
{
|
||||
@Override
|
||||
public void actionPerformed( ActionEvent arg0 )
|
||||
{
|
||||
try
|
||||
{
|
||||
Robot robot = new Robot();
|
||||
|
||||
final BufferedImage image =
|
||||
robot.createScreenCapture( mFrame.getBounds() );
|
||||
|
||||
SystemProperties props = SystemProperties.getInstance();
|
||||
|
||||
Path capturePath = props.getApplicationFolder( "screen_captures" );
|
||||
|
||||
if( !Files.exists( capturePath ) )
|
||||
{
|
||||
try
|
||||
{
|
||||
Files.createDirectory( capturePath );
|
||||
}
|
||||
catch ( IOException e )
|
||||
{
|
||||
mLog.error( "Couldn't create 'screen_captures' "
|
||||
+ "subdirectory in the " +
|
||||
"SDRTrunk application directory", e );
|
||||
}
|
||||
}
|
||||
|
||||
String filename = TimeStamp.getTimeStamp( "_" ) +
|
||||
"_screen_capture.png";
|
||||
|
||||
final Path captureFile = capturePath.resolve( filename );
|
||||
|
||||
EventQueue.invokeLater( new Runnable()
|
||||
{
|
||||
@Override
|
||||
public void run()
|
||||
{
|
||||
try
|
||||
{
|
||||
ImageIO.write( image, "png",
|
||||
captureFile.toFile() );
|
||||
}
|
||||
catch ( IOException e )
|
||||
{
|
||||
mLog.error( "Couldn't write screen capture to "
|
||||
+ "file [" + captureFile.toString() + "]", e );
|
||||
}
|
||||
}} );
|
||||
}
|
||||
catch ( AWTException e )
|
||||
{
|
||||
mLog.error( "Exception while taking screen capture", e );
|
||||
}
|
||||
}
|
||||
} );
|
||||
|
||||
menuBar.add( screenCaptureItem );
|
||||
}
|
||||
|
||||
private void setSourceFile( File file )
|
||||
|
||||
40
src/instrument/tap/stream/EyeDiagramData.java
Normal file
40
src/instrument/tap/stream/EyeDiagramData.java
Normal file
@@ -0,0 +1,40 @@
|
||||
package instrument.tap.stream;
|
||||
|
||||
import sample.complex.ComplexSample;
|
||||
|
||||
public class EyeDiagramData
|
||||
{
|
||||
private ComplexSample[] mSamples;
|
||||
private float mLeftPoint;
|
||||
private float mRightPoint;
|
||||
private float mError;
|
||||
|
||||
public EyeDiagramData( ComplexSample[] samples, float leftPoint,
|
||||
float rightPoint, float error )
|
||||
{
|
||||
mSamples = samples;
|
||||
mLeftPoint = leftPoint;
|
||||
mRightPoint = rightPoint;
|
||||
mError = error;
|
||||
}
|
||||
|
||||
public ComplexSample[] getSamples()
|
||||
{
|
||||
return mSamples;
|
||||
}
|
||||
|
||||
public float getLeftPoint()
|
||||
{
|
||||
return mLeftPoint;
|
||||
}
|
||||
|
||||
public float getRightPoint()
|
||||
{
|
||||
return mRightPoint;
|
||||
}
|
||||
|
||||
public float getError()
|
||||
{
|
||||
return mError;
|
||||
}
|
||||
}
|
||||
@@ -17,13 +17,18 @@
|
||||
******************************************************************************/
|
||||
package instrument.tap.stream;
|
||||
|
||||
import org.slf4j.Logger;
|
||||
import org.slf4j.LoggerFactory;
|
||||
|
||||
import decode.p25.P25Decoder;
|
||||
import instrument.tap.TapListener;
|
||||
import instrument.tap.TapType;
|
||||
import sample.Listener;
|
||||
import dsp.psk.EyeDiagramData;
|
||||
|
||||
public class EyeDiagramDataTap extends StreamTap implements Listener<EyeDiagramData>
|
||||
{
|
||||
private final static Logger mLog = LoggerFactory.getLogger( EyeDiagramDataTap.class );
|
||||
|
||||
private Listener<EyeDiagramData> mListener;
|
||||
|
||||
public EyeDiagramDataTap( String name, int delay, float sampleRate )
|
||||
|
||||
@@ -33,7 +33,6 @@ import org.slf4j.Logger;
|
||||
import org.slf4j.LoggerFactory;
|
||||
|
||||
import sample.complex.ComplexSample;
|
||||
import dsp.psk.EyeDiagramData;
|
||||
|
||||
public class EyeDiagramDataTapPanel extends TapViewPanel
|
||||
{
|
||||
@@ -69,135 +68,158 @@ public class EyeDiagramDataTapPanel extends TapViewPanel
|
||||
|
||||
Graphics2D g2 = (Graphics2D)g;
|
||||
|
||||
|
||||
int min = ( getHeight() < getWidth() ? getHeight() : getWidth() );
|
||||
|
||||
|
||||
float halfWidth = getWidth() / 2.0f;
|
||||
float third = getWidth() / 3.0f;
|
||||
float two_thirds = 2.0f * third;
|
||||
float halfHeight = getHeight() / 2.0f;
|
||||
float xAxisIncrement = getWidth() / 19.0f / 4.0f;
|
||||
float middle = 6.0f * xAxisIncrement;
|
||||
|
||||
g2.setColor( Color.BLUE );
|
||||
g2.drawString( "Eye Diagram - I Leg", 5 , 15 );
|
||||
g2.drawString( "Eye Diagram - Q Leg", halfWidth , 15 );
|
||||
|
||||
float xAxisIncrement = third / 20.0f;
|
||||
|
||||
/* Vertical Line */
|
||||
g2.drawLine( getWidth() / 8, 5, getWidth() / 8, getHeight() - 5 );
|
||||
g2.drawLine( (int)( halfWidth + ( getWidth() / 8 ) ), 5,
|
||||
(int)( halfWidth + ( getWidth() / 8 ) ), getHeight() - 5 );
|
||||
|
||||
g2.setColor( Color.LIGHT_GRAY );
|
||||
|
||||
g2.drawLine( (int)( getWidth() / 8 - xAxisIncrement ), 5, (int)( getWidth() / 8 - xAxisIncrement ), getHeight() - 5 );
|
||||
g2.drawLine( (int)( halfWidth + ( getWidth() / 8 ) - xAxisIncrement ), 5,
|
||||
(int)( halfWidth + ( getWidth() / 8 ) - xAxisIncrement ), getHeight() - 5 );
|
||||
|
||||
g2.drawLine( (int)( getWidth() / 8 + xAxisIncrement ), 5, (int)( getWidth() / 8 + xAxisIncrement ), getHeight() - 5 );
|
||||
g2.drawLine( (int)( halfWidth + ( getWidth() / 8 ) + xAxisIncrement ), 5,
|
||||
(int)( halfWidth + ( getWidth() / 8 ) + xAxisIncrement ), getHeight() - 5 );
|
||||
|
||||
/* Horizontal Line */
|
||||
g2.drawLine( 0, getHeight() / 2, getWidth() / 4, getHeight() / 2 );
|
||||
g2.drawLine( (int)halfWidth, getHeight() / 2, (int)(getWidth() * .75),
|
||||
getHeight() / 2 );
|
||||
|
||||
if( mSamples != null &&
|
||||
mSamples.size() > 0 )
|
||||
if( mSamples != null && mSamples.size() > 0 )
|
||||
{
|
||||
|
||||
g2.setColor( Color.BLUE );
|
||||
|
||||
/* Draw each of the sample sets */
|
||||
for( int z = 0; z < mSamples.size(); z++ )
|
||||
{
|
||||
EyeDiagramData data = mSamples.get( z );
|
||||
|
||||
if( z < mSamples.size() * .75 )
|
||||
{
|
||||
g2.setColor( Color.LIGHT_GRAY );
|
||||
}
|
||||
else if( z == mSamples.size() - 1 )
|
||||
if( z == mSamples.size() - 1 )
|
||||
{
|
||||
g2.setColor( Color.BLUE );
|
||||
}
|
||||
else if( z > ( mSamples.size() * .75 ) )
|
||||
{
|
||||
g2.setColor( Color.DARK_GRAY );
|
||||
}
|
||||
else
|
||||
{
|
||||
g2.setColor( Color.GRAY );
|
||||
g2.setColor( Color.LIGHT_GRAY );
|
||||
}
|
||||
|
||||
EyeDiagramData data = mSamples.get( z );
|
||||
|
||||
ComplexSample[] samples = data.getSamples();
|
||||
|
||||
Path2D.Float iLine = new Path2D.Float( GeneralPath.WIND_EVEN_ODD, 20 );
|
||||
Path2D.Float qLine = new Path2D.Float( GeneralPath.WIND_EVEN_ODD, 20 );
|
||||
|
||||
float xAxis = middle - ( data.getMiddlePoint() * xAxisIncrement );
|
||||
|
||||
iLine.moveTo( xAxis, halfHeight );
|
||||
qLine.moveTo( xAxis + halfWidth, halfHeight );
|
||||
|
||||
for( int x = 0; x < samples.length; x++ )
|
||||
/* Draw the sample set containing two symbol periods of data */
|
||||
if( samples.length >= 1 )
|
||||
{
|
||||
float i = samples[ x ].inphase();
|
||||
Path2D.Float iLine = new Path2D.Float( GeneralPath.WIND_EVEN_ODD, 20 );
|
||||
Path2D.Float qLine = new Path2D.Float( GeneralPath.WIND_EVEN_ODD, 20 );
|
||||
Path2D.Float mLine = new Path2D.Float( GeneralPath.WIND_EVEN_ODD, 20 );
|
||||
|
||||
if( i > 1.0 )
|
||||
float xAxis = 0;
|
||||
|
||||
iLine.moveTo( xAxis, getSampleHeight( halfHeight, samples[ 0 ].inphase() ) );
|
||||
qLine.moveTo( xAxis + third, getSampleHeight( halfHeight, samples[ 0 ].quadrature() ) );
|
||||
mLine.moveTo( xAxis + ( two_thirds ), getMagnitudeHeight( samples[ 0 ].magnitude() ) );
|
||||
|
||||
for( int x = 1; x < samples.length; x++ )
|
||||
{
|
||||
i = 1.0f;
|
||||
}
|
||||
if( i < -1.0 )
|
||||
{
|
||||
i = -1.0f;
|
||||
xAxis += xAxisIncrement;
|
||||
|
||||
iLine.lineTo( xAxis, getSampleHeight( halfHeight, samples[ x ].inphase() ) );
|
||||
qLine.lineTo( xAxis + third, getSampleHeight( halfHeight, samples[ x ].quadrature() ) );
|
||||
mLine.lineTo( xAxis + two_thirds, getMagnitudeHeight( samples[ x ].magnitude() ) );
|
||||
}
|
||||
|
||||
float iy = halfHeight + ( .85f * ( halfHeight * i ) );
|
||||
g2.draw( iLine );
|
||||
g2.draw( qLine );
|
||||
g2.draw( mLine );
|
||||
|
||||
iLine.lineTo( xAxis, iy );
|
||||
|
||||
float q = samples[ x ].quadrature();
|
||||
|
||||
if( q > 1.0 )
|
||||
/* Left Sample line */
|
||||
if( z == mSamples.size() - 1 )
|
||||
{
|
||||
q = 1.0f;
|
||||
g2.setColor( Color.GREEN );
|
||||
}
|
||||
if( q < -1.0 )
|
||||
else
|
||||
{
|
||||
q = -1.0f;
|
||||
g2.setColor( Color.LIGHT_GRAY );
|
||||
}
|
||||
xAxis = xAxisIncrement * data.getLeftPoint();
|
||||
g2.drawLine( (int)xAxis, 5, (int)xAxis, getHeight() - 5 );
|
||||
g2.drawLine( (int)( xAxis + third ), 5, (int)( xAxis + third ), getHeight() - 5 );
|
||||
g2.drawLine( (int)( xAxis + two_thirds ), 5, (int)( xAxis + two_thirds ), getHeight() - 5 );
|
||||
|
||||
/* Right Sample line */
|
||||
if( z == mSamples.size() - 1 )
|
||||
{
|
||||
g2.setColor( Color.GREEN );
|
||||
}
|
||||
else
|
||||
{
|
||||
g2.setColor( Color.LIGHT_GRAY );
|
||||
}
|
||||
xAxis = xAxisIncrement * data.getRightPoint();
|
||||
g2.drawLine( (int)xAxis, 5, (int)xAxis, getHeight() - 5 );
|
||||
g2.drawLine( (int)( xAxis + third ), 5, (int)( xAxis + third ), getHeight() - 5 );
|
||||
g2.drawLine( (int)( xAxis + two_thirds ), 5, (int)( xAxis + two_thirds ), getHeight() - 5 );
|
||||
|
||||
/* Error indicator */
|
||||
float error = data.getError();
|
||||
|
||||
if( z == mSamples.size() - 1 )
|
||||
{
|
||||
g2.setColor( Color.BLACK );
|
||||
|
||||
float xFrom;
|
||||
float xTo;
|
||||
|
||||
if( error < 0.0f )
|
||||
{
|
||||
xFrom = (int)( xAxisIncrement * data.getLeftPoint() );
|
||||
xTo = xFrom - 25.0f;
|
||||
}
|
||||
else
|
||||
{
|
||||
xFrom = (int)( xAxisIncrement * data.getRightPoint() );
|
||||
xTo = xFrom + 25.0f;
|
||||
}
|
||||
|
||||
g2.drawLine( (int)xFrom, (int)halfHeight - 15, (int)xTo, (int)halfHeight );
|
||||
g2.drawLine( (int)xFrom, (int)halfHeight + 15, (int)xTo, (int)halfHeight );
|
||||
g2.drawLine( (int)( xFrom + third ), (int)halfHeight - 15, (int)( xTo + third ), (int)halfHeight );
|
||||
g2.drawLine( (int)( xFrom + third ), (int)halfHeight + 15, (int)( xTo + third ), (int)halfHeight );
|
||||
g2.drawLine( (int)( xFrom + two_thirds ), (int)halfHeight - 15, (int)( xTo + two_thirds ), (int)halfHeight );
|
||||
g2.drawLine( (int)( xFrom + two_thirds ), (int)halfHeight + 15, (int)( xTo + two_thirds ), (int)halfHeight );
|
||||
}
|
||||
|
||||
|
||||
float qy = halfHeight + ( .85f * ( halfHeight * q ) );
|
||||
|
||||
qLine.lineTo( xAxis + halfWidth, qy );
|
||||
|
||||
xAxis += xAxisIncrement;
|
||||
}
|
||||
|
||||
g2.draw( iLine );
|
||||
g2.draw( qLine );
|
||||
}
|
||||
}
|
||||
|
||||
if( mSamples.size() > 0 )
|
||||
{
|
||||
EyeDiagramData data = mSamples.get( mSamples.size() - 1 );
|
||||
g2.setColor( Color.BLUE );
|
||||
g2.drawString( "Eye Diagram - I Leg", 5, 15 );
|
||||
g2.drawString( "Eye Diagram - Q Leg", third, 15 );
|
||||
g2.drawString( "Eye Diagram - Magnitude", two_thirds, 15 );
|
||||
|
||||
g2.setColor( Color.GREEN );
|
||||
|
||||
float xAxis = middle - ( data.getMiddlePoint() * xAxisIncrement );
|
||||
|
||||
Path2D.Float magnitude = new Path2D.Float( GeneralPath.WIND_EVEN_ODD, 20 );
|
||||
magnitude.moveTo( xAxis, halfHeight );
|
||||
|
||||
for( ComplexSample sample: data.getSamples() )
|
||||
{
|
||||
magnitude.lineTo( xAxis, getHeight() - ( getHeight() * sample.magnitude() ) );
|
||||
|
||||
xAxis += xAxisIncrement;
|
||||
}
|
||||
|
||||
g2.setStroke( new BasicStroke( 3 ) );
|
||||
g2.draw( magnitude );
|
||||
}
|
||||
/* Horizontal middle line */
|
||||
g2.setColor( Color.CYAN );
|
||||
g2.drawLine( 0, (int)halfHeight, getWidth(), (int)halfHeight );
|
||||
|
||||
g2.dispose();
|
||||
}
|
||||
|
||||
private float getSampleHeight( float halfHeight, float value )
|
||||
{
|
||||
if( value > 1.0 )
|
||||
{
|
||||
value = 1.0f;
|
||||
}
|
||||
if( value < -1.0 )
|
||||
{
|
||||
value = -1.0f;
|
||||
}
|
||||
|
||||
return halfHeight + ( .95f * ( halfHeight * value ) );
|
||||
}
|
||||
|
||||
private float getMagnitudeHeight( float magnitude )
|
||||
{
|
||||
if( magnitude > 1.0 )
|
||||
{
|
||||
magnitude = 1.0f;
|
||||
}
|
||||
|
||||
return getHeight() - ( getHeight() * magnitude );
|
||||
}
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user