diff --git a/CHANGELOG b/CHANGELOG index b2bf435e..c05ad2f8 100644 --- a/CHANGELOG +++ b/CHANGELOG @@ -9,7 +9,7 @@ an alias identifier having the lowest priority. - Audio recording by call with new 'Non-Recordable' alias action - (planned)Added Ettus UHD support for B100 with WBX tuner - - (planned)Added Airspy tuner support + - Added Airspy tuner support 0.1.5 20150612 Added initial P25 Decoder support with integrated audio decoding using the JMBE library diff --git a/src/dsp/filter/hilbert/HilbertTransform.java b/src/dsp/filter/hilbert/HilbertTransform.java index 5b5c0b77..fcc0c28d 100644 --- a/src/dsp/filter/hilbert/HilbertTransform.java +++ b/src/dsp/filter/hilbert/HilbertTransform.java @@ -146,8 +146,7 @@ public class HilbertTransform implements Listener if( mInvertFlag ) { //inphase - samples[ y ] = -( mCenterCoefficent * - mBuffer[ mIndexMap[ index ][ mCenterTapIndex ] ] ); + samples[ y ] = -( mBuffer[ mIndexMap[ index ][ mCenterTapIndex ] ] ); //quadrature samples[ y + 1 ] = -accumulator; @@ -155,8 +154,7 @@ public class HilbertTransform implements Listener else { //inphase - samples[ y ] = mCenterCoefficent * - mBuffer[ mIndexMap[ index ][ mCenterTapIndex ] ]; + samples[ y ] = mBuffer[ mIndexMap[ index ][ mCenterTapIndex ] ]; //quadrature samples[ y + 1 ] = accumulator; @@ -255,6 +253,9 @@ public class HilbertTransform implements Listener * a folded FIR structure to exploit the symmetric nature of the half-band * filter coefficients. The polarity difference between the upper and lower * halves is accounted for in the filter() method. + * + * Note: we apply a 2.0 gain to the coefficients to compensate for the loss + * of splitting the signal via two filters. */ private void convertHalfBandToHilbert( float[] coefficients ) { @@ -266,15 +267,15 @@ public class HilbertTransform implements Listener { if( x < middle ) { - mHilbertFilter[ x ] = -Math.abs( coefficients[ x ] ); + mHilbertFilter[ x ] = 2.0f * -Math.abs( coefficients[ x ] ); } else if( x > middle ) { - mHilbertFilter[ x ] = Math.abs( coefficients[ x ] ); + mHilbertFilter[ x ] = 2.0f * Math.abs( coefficients[ x ] ); } else { - mHilbertFilter[ x ] = coefficients[ x ]; + mHilbertFilter[ x ] = 2.0f * coefficients[ x ]; } } } diff --git a/src/dsp/mixer/Oscillator.java b/src/dsp/mixer/Oscillator.java index 10d62e40..6fde721f 100644 --- a/src/dsp/mixer/Oscillator.java +++ b/src/dsp/mixer/Oscillator.java @@ -17,10 +17,21 @@ ******************************************************************************/ package dsp.mixer; +import java.util.Arrays; + +import org.jtransforms.fft.FloatFFT_1D; +import org.slf4j.Logger; +import org.slf4j.LoggerFactory; + +import dsp.filter.Filters; +import dsp.filter.hilbert.HilbertTransform; import sample.complex.Complex; public class Oscillator { + private final static Logger mLog = + LoggerFactory.getLogger( Oscillator.class ); + private double mFrequency; private double mSampleRate; @@ -105,4 +116,109 @@ public class Oscillator { return mCurrentAngle.real(); } + + public static void main( String[] args ) + { + Oscillator o = new Oscillator( 1536, 4096 ); + + o.rotate(); + + float[] samples = new float[ 8192 ]; + + for( int x = 0; x < 8192; x++ ) + { + samples[ x ] = o.inphase(); + + o.rotate(); + } + + logLargest( "Real: ", samples, false ); + + HilbertTransform ht = new HilbertTransform(); + + float[] transformed = ht.filter( samples ); + + logLargest( "Hilbert: ", transformed, true ); + + FloatFFT_1D fft = new FloatFFT_1D( 4096 ); + + fft.complexForward( transformed ); + + logLargest( "DFT:", transformed, true ); + } + + private static void logLargest( String label, float[] samples, boolean complex ) + { + float largestI = 0.0f; + int indexLargestI = 0; + float largestQ = 0.0f; + int indexLargestQ = 0; + + float smallestI = 0.0f; + int indexsmallestI = 0; + float smallestQ = 0.0f; + int indexsmallestQ = 0; + + for( int x = 0; x < samples.length; x++ ) + { + if( complex ) + { + if( x % 2 == 0 ) + { + if( samples[ x ] > largestI ) + { + largestI = samples[ x ]; + indexLargestI = x; + } + if( samples[ x ] < smallestI ) + { + smallestI = samples[ x ]; + indexsmallestI = x; + } + } + else + { + if( samples[ x ] > largestQ ) + { + largestQ = samples[ x ]; + indexLargestQ = x; + } + if( samples[ x ] < smallestQ ) + { + smallestQ = samples[ x ]; + indexsmallestQ = x; + } + } + } + else + { + if( samples[ x ] > largestI ) + { + largestI = samples[ x ]; + indexLargestI = x; + } + if( samples[ x ] < smallestI ) + { + smallestI = samples[ x ]; + indexsmallestI = x; + } + } + } + + + if( complex ) + { + mLog.debug( label + + " largestI (" + indexLargestI + "): " + largestI + + " smallestI (" + indexsmallestI + "): " + smallestI + + " largestQ (" + indexLargestQ + "): " + largestQ + + " smallestQ (" + indexsmallestQ + "): " + smallestQ ); + } + else + { + mLog.debug( label + + " largest (" + indexLargestI + "): " + largestI + + " smallest (" + indexsmallestI + "): " + smallestI ); + } + } } diff --git a/src/source/tuner/airspy/AirspyTuner.java b/src/source/tuner/airspy/AirspyTuner.java index c88c0237..3f2d4fb2 100644 --- a/src/source/tuner/airspy/AirspyTuner.java +++ b/src/source/tuner/airspy/AirspyTuner.java @@ -1,3 +1,20 @@ +/******************************************************************************* + * SDR Trunk + * Copyright (C) 2015 Dennis Sheirer + * + * 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 + ******************************************************************************/ package source.tuner.airspy; import java.util.concurrent.RejectedExecutionException; @@ -20,23 +37,6 @@ import source.tuner.airspy.AirspyTunerController.BoardID; import controller.ResourceManager; import controller.ThreadPoolManager; -/******************************************************************************* - * SDR Trunk - * Copyright (C) 2015 Dennis Sheirer - * - * 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 - ******************************************************************************/ public class AirspyTuner extends Tuner { private final static Logger mLog = LoggerFactory.getLogger( AirspyTuner.class ); diff --git a/src/spectrum/SpectralDisplayPanel.java b/src/spectrum/SpectralDisplayPanel.java index de410708..f78ed1e6 100644 --- a/src/spectrum/SpectralDisplayPanel.java +++ b/src/spectrum/SpectralDisplayPanel.java @@ -389,7 +389,6 @@ public class SpectralDisplayPanel extends JPanel //Register the dft processor to receive samples from the tuner mTuner.addListener( (Listener)mDFTProcessor ); - mDFTConverter.setSampleSize( mTuner.getSampleSize() ); mSpectrumPanel.setSampleSize( mTuner.getSampleSize() ); //Fire frequency and sample rate change events so that everyone diff --git a/src/spectrum/SpectrumPanel.java b/src/spectrum/SpectrumPanel.java index b273d60d..28201d1f 100644 --- a/src/spectrum/SpectrumPanel.java +++ b/src/spectrum/SpectrumPanel.java @@ -26,6 +26,7 @@ import java.awt.Rectangle; import java.awt.RenderingHints; import java.awt.geom.GeneralPath; import java.awt.geom.Line2D; +import java.util.Arrays; import javax.swing.JPanel; @@ -111,8 +112,8 @@ public class SpectrumPanel extends JPanel public void setSampleSize( double sampleSize ) { assert( 2.0 <= sampleSize && sampleSize <= 32.0 ); - - mDBScale = -(float)( 20.0 * Math.log10( Math.pow( 2.0, sampleSize ) ) ); + + mDBScale = (float)( 20.0 * Math.log10( 1 / Math.pow( 2.0, sampleSize - 1 ) ) ); } public void setAveraging( int size ) diff --git a/src/spectrum/WaterfallPanel.java b/src/spectrum/WaterfallPanel.java index 635ee366..9d815fe3 100644 --- a/src/spectrum/WaterfallPanel.java +++ b/src/spectrum/WaterfallPanel.java @@ -33,6 +33,9 @@ import java.util.concurrent.atomic.AtomicBoolean; import javax.swing.JPanel; +import org.slf4j.Logger; +import org.slf4j.LoggerFactory; + import settings.ColorSetting; import settings.ColorSetting.ColorSettingName; import settings.Setting; @@ -48,7 +51,10 @@ public class WaterfallPanel extends JPanel implements DFTResultsListener, Pausable, SettingChangeListener { - private static final long serialVersionUID = 1L; + private final static Logger mLog = + LoggerFactory.getLogger( WaterfallPanel.class ); + + private static final long serialVersionUID = 1L; private static DecimalFormat CURSOR_FORMAT = new DecimalFormat( "0.00000" ); private static final String SPECTRUM_CURSOR = "spectrum_cursor"; private static final String PAUSED = "PAUSED"; @@ -202,9 +208,9 @@ public class WaterfallPanel extends JPanel implements DFTResultsListener, public void receive( float[] update ) { //If our FFT size changes, reset our pixel map and image source - if( mFFTSize != update.length - 1 ) + if( mFFTSize != update.length ) { - mFFTSize = update.length - 1; + mFFTSize = update.length; reset(); } diff --git a/src/spectrum/converter/ComplexDecibelConverter.java b/src/spectrum/converter/ComplexDecibelConverter.java index 3c819c6c..79b5fb45 100644 --- a/src/spectrum/converter/ComplexDecibelConverter.java +++ b/src/spectrum/converter/ComplexDecibelConverter.java @@ -20,13 +20,50 @@ package spectrum.converter; import org.slf4j.Logger; import org.slf4j.LoggerFactory; +/** + * Converts complex DFT output to scaled dB values with a maximum amplitude of + * 0 dB and all values scaled to the minimum dB value which is: + * + * Max: 0dB + * Min: 20 * log10( 1.0 / ( 2.0 ^ ( bit depth - 1) ) = scaling factor + * + * Notes: + * + * The magnitude of each DFT output bin must be scaled by the number of DFT + * points to normalize the output to a range of ( 0 to 1.0 ). DFT bin magnitude + * can be calculated as: + * + * 1) sqrt( bin[ x ] * bin[ x ] + bin[ x + 1 ] * bin[ x + 1] ) / DFTSize or, + * 2) ( bin[ x ] * bin[ x ] + bin[ x + 1 ] * bin[ x + 1] ) / ( DFTSize * DFTSize ) + * + * Convert each scaled DFT bin magnitude to decibels using this formula: + * + * 20 * log10( scaledBinValue ) + * + * Max Value: 20 * log10( 1.0 ) = 0 dB + * + * When plotting, these dB bin values should be scaled according to the maximum + * dynamic range supported by the source. Maximum dynamic range for bit depth + * is calculated as: + * + * 20 * log10( 1 / ( 2 ^ ( bit depth - 1 ) ) + * + * A source that provides 12-bit samples can produce values in the range of + * ( 0 to 4096 ) or -2048 to 2047. The smallest observable value would then + * be 1 / 2048. + * + * So, the smallest decibel value of a source producing 12 bit samples is: + * + * 20 * log10( 1 / 2048 ) = -66.23 dB + * + * and the dynamic range is: (-66.23 to 0.0 dB) + * + */ public class ComplexDecibelConverter extends DFTResultsConverter { private final static Logger mLog = LoggerFactory.getLogger( ComplexDecibelConverter.class ); - private float mDynamicRangeReference = (float)Math.pow( 2.0, 16.0 ); - /** * Converts the output of the JTransforms FloatFFT_1D.complexForward() * calculation into the power spectrum in decibels, normalized to the @@ -36,44 +73,35 @@ public class ComplexDecibelConverter extends DFTResultsConverter { } - /** - * Specifies the bit depth to establish the maximum dynamic range. All - * FFT bin values will be scaled according to this value. - */ - @Override - public void setSampleSize( double size ) - { - assert( 2.0 <= size && size <= 32.0 ); - - mDynamicRangeReference = (float)Math.pow( 2.0d, size ); - } - @Override public void receive( float[] results ) { - float[] processed = new float[ results.length / 2 ]; - - int half = processed.length / 2; + int halfResults = results.length / 2; + + float dftScalor = 1.0f / (float)( halfResults ^ 2 ); + + float[] processed = new float[ halfResults ]; + int middle = processed.length / 2; + for( int x = 0; x < results.length; x += 2 ) { - //Calculate the magnitude squared value from each bin's real and - //imaginary value, scale it to the available dynamic range and - //convert to dB. - float normalizedMagnitude = 10.0f * (float)Math.log10( + //Calculate the magnitude squared (power) value from each bin's real + //and imaginary value and scale it to the DFT bin size squared. + //Convert the scaled value to decibels. + float decibels = 10.0f * (float)Math.log10( ( ( results[ x ] * results[ x ] ) + - ( results[ x + 1 ] * results[ x + 1 ] ) ) / - mDynamicRangeReference ); + ( results[ x + 1 ] * results[ x + 1 ] ) ) * dftScalor ); // We have to swap the upper and lower halves of the JTransforms // DFT results for correct display - if( x / 2 >= half ) + if( x / 2 >= middle ) { - processed[ x / 2 - half ] = normalizedMagnitude; + processed[ x / 2 - middle ] = decibels; } else { - processed[ x / 2 + half ] = normalizedMagnitude; + processed[ x / 2 + middle ] = decibels; } } diff --git a/src/spectrum/converter/DFTResultsConverter.java b/src/spectrum/converter/DFTResultsConverter.java index 962bc7cc..d9e43574 100644 --- a/src/spectrum/converter/DFTResultsConverter.java +++ b/src/spectrum/converter/DFTResultsConverter.java @@ -19,12 +19,6 @@ public abstract class DFTResultsConverter { } - /** - * Specifies the sample size in bits to establish the maximum dynamic range. - * All FFT bin values will be scaled according to this value. - */ - public abstract void setSampleSize( double size ); - public void dispose() { mListeners.clear(); diff --git a/src/spectrum/converter/RealDecibelConverter.java b/src/spectrum/converter/RealDecibelConverter.java index ad89c4f5..68b5b252 100644 --- a/src/spectrum/converter/RealDecibelConverter.java +++ b/src/spectrum/converter/RealDecibelConverter.java @@ -7,33 +7,22 @@ public class RealDecibelConverter extends DFTResultsConverter { private static final Logger mLog = LoggerFactory.getLogger( RealDecibelConverter.class ); - private float mDynamicRangeReference; - public RealDecibelConverter() { - setSampleSize( 24.0 ); } - /** - * Specifies the bit depth to establish the maximum dynamic range. All - * FFT bin values will be scaled according to this value. - */ - @Override - public void setSampleSize( double size ) - { - assert( 2.0 <= size && size <= 32.0 ); - - mDynamicRangeReference = (float)Math.pow( 2.0d, size ); - } - /** * Converts the output of the JTransforms FloatFFT_1D.realForward() * calculation into a normalized power spectrum in decibels, per description * in Lyons, Understanding Digital Signal Processing, 3e, page 141. + * + * Note: this is only calculating the lower half of the spectrum */ @Override public void receive( float[] results ) { + float dftScalor = 1.0f / (float)( results.length ^ 2 ); + float[] processed = new float[ results.length / 4 ]; int index = 0; @@ -42,12 +31,9 @@ public class RealDecibelConverter extends DFTResultsConverter { index = x * 2; - float normalizedMagnitude = 10.0f * (float)Math.log10( + processed[ x ] = 20.0f * (float)Math.log10( ( ( results[ index ] * results[ index ] ) + - ( results[ index + 1 ] * results[ index + 1 ] ) ) / - mDynamicRangeReference ); - - processed[ x ] = normalizedMagnitude; + ( results[ index + 1 ] * results[ index + 1 ] ) ) * dftScalor ); } dispatch( processed );