diff --git a/.project b/.project index f65c4170..7accc076 100644 --- a/.project +++ b/.project @@ -5,6 +5,11 @@ + + org.eclipse.xtext.ui.shared.xtextBuilder + + + org.eclipse.jdt.core.javabuilder @@ -13,5 +18,6 @@ org.eclipse.jdt.core.javanature + org.eclipse.xtext.ui.shared.xtextNature diff --git a/images/Curve-fitted-background.png b/images/Curve-fitted-background.png new file mode 100644 index 00000000..20858058 Binary files /dev/null and b/images/Curve-fitted-background.png differ diff --git a/images/Curve-fitted-chart-background.png b/images/Curve-fitted-chart-background.png new file mode 100644 index 00000000..5172edca Binary files /dev/null and b/images/Curve-fitted-chart-background.png differ diff --git a/images/Curve-fitted-graph-gridlines.png b/images/Curve-fitted-graph-gridlines.png new file mode 100644 index 00000000..648c4f9a Binary files /dev/null and b/images/Curve-fitted-graph-gridlines.png differ diff --git a/src/dsp/filter/FilterFactory.java b/src/dsp/filter/FilterFactory.java index 8f6b0065..f73a0283 100644 --- a/src/dsp/filter/FilterFactory.java +++ b/src/dsp/filter/FilterFactory.java @@ -1,6 +1,6 @@ /******************************************************************************* * SDR Trunk - * Copyright (C) 2014,2015 Dennis Sheirer + * Copyright (C) 2014-2016,2015 Dennis Sheirer * * Root Raised Cosine filter designer: * Copyright 2002,2007,2008,2012,2013 Free Software Foundation, Inc. @@ -35,9 +35,46 @@ import dsp.filter.cic.ComplexPrimeCICDecimate; public class FilterFactory { - private final static Logger mLog = - LoggerFactory.getLogger( FilterFactory.class ); + private final static Logger mLog = LoggerFactory.getLogger( FilterFactory.class ); + public static final double TWO_PI = Math.pow( Math.PI, 2.0 ); + + public static float[] getNewLowPass( int sampleRate, long passFrequency, long stopFrequency, + int attenuation, WindowType window ) + { + assert( passFrequency <= sampleRate / 2 ); + + int length = getTapCount( sampleRate, passFrequency, stopFrequency, attenuation ); + + if( length % 2 == 0 ) + { + length++; + } + + float[] coefficients = new float[ length ]; + + int half = length / 2; + + int K = (int)( length * ( 2.0d * (double)passFrequency / (double) sampleRate )); + + double scale = 1.0 / (double)length; + + coefficients[ half ] = (float)( K / (double)length ); + + for( int k = 1; k <= half; k++ ) + { + float coefficient = (float)( scale * ( Math.sin( Math.PI * k * K / length ) / + Math.sin( Math.PI * k / length ) ) ); + + coefficients[ k + half ] = coefficient; + coefficients[ half - k ] = coefficient; + } + + Window.apply( WindowType.HANNING, coefficients ); + + return coefficients; + } + /** * Generates coefficients for a unity-gain, windowed low-pass filter * @param sampleRate - hertz @@ -78,6 +115,7 @@ public class FilterFactory } //Apply the window against the coefficients + coefficients = Window.apply( window, coefficients ); //Normalize to unity (1) gain diff --git a/src/dsp/filter/GoertzelFilter.java b/src/dsp/filter/GoertzelFilter.java index a421f912..c448beb2 100644 --- a/src/dsp/filter/GoertzelFilter.java +++ b/src/dsp/filter/GoertzelFilter.java @@ -32,7 +32,7 @@ public class GoertzelFilter private int mSampleRate; private long mTargetFrequency; private int mBlockSize; - private double[] mWindowCoefficients; + private float[] mWindowCoefficients; private double mCoefficient; diff --git a/src/dsp/filter/Window.java b/src/dsp/filter/Window.java index 72e846b4..5247b16b 100644 --- a/src/dsp/filter/Window.java +++ b/src/dsp/filter/Window.java @@ -19,6 +19,9 @@ package dsp.filter; import java.util.Arrays; +import org.slf4j.Logger; +import org.slf4j.LoggerFactory; + /** * Window creator * @@ -33,7 +36,9 @@ import java.util.Arrays; */ public class Window { - public static double[] getWindow( WindowType type, int length ) + private final static Logger mLog = LoggerFactory.getLogger( Window.class ); + + public static float[] getWindow( WindowType type, int length ) { switch( type ) { @@ -51,18 +56,18 @@ public class Window } } - public static double[] getRectangularWindow( int length ) + public static float[] getRectangularWindow( int length ) { - double[] coefficients = new double[ length ]; + float[] coefficients = new float[ length ]; - Arrays.fill( coefficients, 1.0d ); + Arrays.fill( coefficients, 1.0f ); return coefficients; } - public static double[] getCosineWindow( int length ) + public static float[] getCosineWindow( int length ) { - double[] coefficients = new double[ length ]; + float[] coefficients = new float[ length ]; if( length % 2 == 0 ) //Even length @@ -71,7 +76,7 @@ public class Window for( int x = -half; x < length / 2 + 1; x++ ) { - coefficients[ x + half ] = Math.cos( + coefficients[ x + half ] = (float)Math.cos( ( (double)x * Math.PI ) / ( (double)length + 1.0d ) ); } } @@ -81,7 +86,7 @@ public class Window for( int x = -half; x < half + 1; x++ ) { - coefficients[ x + half ] = Math.cos( + coefficients[ x + half ] = (float)Math.cos( ( (double)x * Math.PI ) / ( (double)length + 1.0d ) ); } } @@ -89,62 +94,62 @@ public class Window return coefficients; } - public static double[] getBlackmanWindow( int length ) + public static float[] getBlackmanWindow( int length ) { - double[] coefficients = new double[ length ]; + float[] coefficients = new float[ length ]; for( int x = 0; x < length; x++ ) { - coefficients[ x ] = .426591D - + coefficients[ x ] = (float)( .426591D - ( .496561D * Math.cos( ( Math.PI * 2.0D * (double)x ) / (double)( length - 1 ) ) + - ( .076848D * Math.cos( ( Math.PI * 4.0D * (double)x ) / (double)( length - 1 ) ) ) ); + ( .076848D * Math.cos( ( Math.PI * 4.0D * (double)x ) / (double)( length - 1 ) ) ) ) ); } return coefficients; } - public static double[] getHammingWindow( int length ) + public static float[] getHammingWindow( int length ) { - double[] coefficients = new double[ length ]; + float[] coefficients = new float[ length ]; if( length % 2 == 0 ) //Even length { for( int x = 0; x < length; x++ ) { - coefficients[ x ] = .54D - ( .46D * - Math.cos( ( Math.PI * ( 2.0D * (double)x + 1.0d ) ) / (double)( length - 1 ) ) ); + coefficients[ x ] = (float)( .54D - ( .46D * + Math.cos( ( Math.PI * ( 2.0D * (double)x + 1.0d ) ) / (double)( length - 1 ) ) ) ); } } else //Odd length { for( int x = 0; x < length; x++ ) { - coefficients[ x ] = .54D - ( .46D * - Math.cos( ( 2.0D * Math.PI * (double)x ) / (double)( length - 1 ) ) ); + coefficients[ x ] = (float)( .54D - ( .46D * + Math.cos( ( 2.0D * Math.PI * (double)x ) / (double)( length - 1 ) ) ) ); } } return coefficients; } - public static double[] getHanningWindow( int length ) + public static float[] getHanningWindow( int length ) { - double[] coefficients = new double[ length ]; + float[] coefficients = new float[ length ]; if( length % 2 == 0 ) //Even length { for( int x = 0; x < length; x++ ) { - coefficients[ x ] = .5D - ( .5D * - Math.cos( ( Math.PI * ( 2.0D * (double)x + 1 ) ) / (double)( length - 1 ) ) ); + coefficients[ x ] = (float)( .5D - ( .5D * + Math.cos( ( Math.PI * ( 2.0D * (double)x + 1 ) ) / (double)( length - 1 ) ) ) ); } } else //Odd length { for( int x = 0; x < length; x++ ) { - coefficients[ x ] = .5D - ( .5D * - Math.cos( ( 2.0D * Math.PI * (double)x ) / (double)( length - 1 ) ) ); + coefficients[ x ] = (float)( .5D - ( .5D * + Math.cos( ( 2.0D * Math.PI * (double)x ) / (double)( length - 1 ) ) ) ); } } @@ -155,41 +160,21 @@ public class Window /** * Apply the window against an array of float-type samples */ - public static float[] apply( double[] coefficients, float[] samples ) + public static float[] apply( float[] windowCoefficients, float[] samples ) { - for( int x = 0; x < coefficients.length; x++ ) + for( int x = 0; x < windowCoefficients.length; x++ ) { - samples[ x ] = (float)( samples[ x ] * coefficients[ x ] ); + samples[ x ] = samples[ x ] * windowCoefficients[ x ]; } return samples; } - + public static float[] apply( WindowType type, float[] samples ) { - double[] coefficients = getWindow( type, samples.length ); + float[] windowCoefficients = getWindow( type, samples.length ); - return apply( coefficients, samples ); - } - - /** - * Apply the window against an array of float-type samples - */ - public static double[] apply( double[] coefficients, double[] samples ) - { - for( int x = 0; x < coefficients.length; x++ ) - { - samples[ x ] = samples[ x ] * coefficients[ x ]; - } - - return samples; - } - - public static double[] apply( WindowType type, double[] samples ) - { - double[] coefficients = getWindow( type, samples.length ); - - return apply( coefficients, samples ); + return apply( windowCoefficients, samples ); } @@ -205,7 +190,7 @@ public class Window /** * Utility to log the double arrays */ - public static String arrayToString( double[] array, boolean breaks ) + public static String arrayToString( float[] array, boolean breaks ) { StringBuilder sb = new StringBuilder(); for( int x = 0; x < array.length; x++ ) diff --git a/src/filter/Remez.java b/src/filter/Remez.java new file mode 100644 index 00000000..bc8b8b32 --- /dev/null +++ b/src/filter/Remez.java @@ -0,0 +1,574 @@ + +/************************************************************************** + * Parks-McClellan algorithm for FIR filter design (C version) + *------------------------------------------------- + * Copyright (C) 1995 Jake Janovetz (janovetz@coewl.cen.uiuc.edu) + * + * 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 2 of the License, or + * 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, write to the Free Software + * Foundation, Inc., 675 Mass Ave, Cambridge, MA 02139, USA. + * + *************************************************************************/ +package filter; + +public class Remez +{ + + + /* Converted to Java by Iain A Robin, June 1998 */ + + /* changes by Hanns Holger Rutz, Jan 2010: + * needed to make method 'remez' public + */ + + + public static final int BANDPASS = 1; + public static final int DIFFERENTIATOR = 2; + public static final int HILBERT = 3; + + public static final int NEGATIVE = 0; + public static final int POSITIVE = 1; + + public static final int GRIDDENSITY = 16; + public static final int MAXITERATIONS = 40; + + double pi = Math.PI; + double pi2 = 2.0*pi; + + int numtaps; + + public Remez() + { + } + + /******************* + * createDenseGrid + *================= + * Creates the dense grid of frequencies from the specified bands. + * Also creates the Desired Frequency Response function (d[]) and + * the Weight function (w[]) on that dense grid + * + * + * INPUT: + * ------ + * int r - 1/2 the number of filter coefficients + * int numtaps - Number of taps in the resulting filter + * int numband - Number of bands in user specification + * double[] bands - User-specified band edges [2*numband] + * double[] des - Desired response per band [numband] + * double[] weight - Weight per band [numband] + * int symmetry - Symmetry of filter - used for grid check + * + * OUTPUT: + * ------- + * int gridSize - Number of elements in the dense frequency grid + * double[] grid - Frequencies (0 to 0.5) on the dense grid [gridSize] + * double[] d - Desired response on the dense grid [gridSize] + * double[] w - Weight function on the dense grid [gridSize] + *******************/ + + void createDenseGrid(int r, int numtaps, int numband, double bands[], + double des[], double weight[], int gridSize, + double grid[], double d[], double w[], + int symmetry) { + double lowf, highf; + double delf = 0.5/(GRIDDENSITY*r); + + // For differentiator, hilbert, + // symmetry is odd and grid[0] = max(delf, band[0]) + if ((symmetry == NEGATIVE) && (delf > bands[0])) + bands[0] = delf; + + int j=0; + int k; + for (int band=0; band < numband; band++) { + grid[j] = bands[2*band]; + lowf = bands[2*band]; + highf = bands[2*band + 1]; + k = (int)Math.round((highf - lowf)/delf); + for (int i=0; i (0.5 - delf)) && ((numtaps % 2) != 0)) { + grid[gridSize-1] = 0.5-delf; + } + } + + /******************** + * initialGuess + *============== + * Places Extremal Frequencies evenly throughout the dense grid. + * + * + * INPUT: + * ------ + * int r - 1/2 the number of filter coefficients + * int gridSize - Number of elements in the dense frequency grid + * + * OUTPUT: + * ------- + * int ext[] - Extremal indexes to dense frequency grid [r+1] + ********************/ + + void initialGuess(int r, int ext[], int gridSize) { + for (int i=0; i<=r; i++) ext[i] = i * (gridSize-1) / r; + } + + + /*********************** + * calcParms + *=========== + * + * + * INPUT: + * ------ + * int r - 1/2 the number of filter coefficients + * int[] ext - Extremal indexes to dense frequency grid [r+1] + * double[] grid - Frequencies (0 to 0.5) on the dense grid [gridSize] + * double[] d - Desired response on the dense grid [gridSize] + * double[] w - Weight function on the dense grid [gridSize] + * + * OUTPUT: + * ------- + * double[] ad - 'b' in Oppenheim & Schafer [r+1] + * double[] x - [r+1] + * double[] y - 'C' in Oppenheim & Schafer [r+1] + ***********************/ + + void calcParms(int r, int ext[], double grid[], double d[], double w[], + double ad[], double x[], double y[]) { + double sign, xi, delta, denom, numer; + + // Find x[] + for (int i=0; i<=r; i++) x[i] = Math.cos(pi2 * grid[ext[i]]); + + // Calculate ad[] - Oppenheim & Schafer eq 7.132 + int ld = (r-1)/15 + 1; // Skips around to avoid round errors + for (int i=0; i<=r; i++) { + denom = 1.0; + xi = x[i]; + for (int j=0; j0.0) && (e[0]>e[1])) || ((e[0]<0.0) && (e[0]=e[i-1]) && (e[i]>e[i+1]) && (e[i]>0.0)) || + ((e[i]<=e[i-1]) && (e[i]0.0) && (e[j]>e[j-1])) || ((e[j]<0.0) && (e[j] 0) { + up = e[foundExt[0]] > 0.0; + // up = true --> first one is a maximum + // up = false --> first one is a minimum + l=0; + alt = true; + for (j=1; j 0.0)) + up = true; // switch to a maxima + else { + alt = false; + break; // Ooops, found two non-alternating + } // extrema. Delete smallest of them + } // if the loop finishes, all extrema are alternating + + // If there's only one extremal and all are alternating, + // delete the smallest of the first/last extremals. + if (alt && (extra == 1)) { + if (Math.abs(e[foundExt[k-1]]) < Math.abs(e[foundExt[0]])) + l = foundExt[k-1]; // Delete last extremal + else + l = foundExt[0]; // Delete first extremal + } + + for (j=l; j max) max = current; + } + if (((max-min)/max) < 0.0001) return true; + return false; + } + + /******************** + * remez + *======= + * Calculates the optimal (in the Chebyshev/minimax sense) + * FIR filter impulse response given a set of band edges, + * the desired reponse on those bands, and the weight given to + * the error in those bands. + * + * INPUT: + * ------ + * int numtaps - Number of filter coefficients + * int numband - Number of bands in filter specification + * double[] bands - User-specified band edges [2 * numband] + * double[] des - User-specified band responses [numband] + * double[] weight - User-specified error weights [numband] + * int type - Type of filter + * + * OUTPUT: + * ------- + * double[] h - Impulse response of final filter [numtaps] + ********************/ + + public double[] remez(int n, + double bands[], double des[], double weight[], int type) { + + double c; + numtaps = n; + int symmetry = (type == BANDPASS)? POSITIVE : NEGATIVE; + int r = numtaps/2; // number of extrema + if ((numtaps%2 != 0) && (symmetry == POSITIVE)) r++; + + // Predict dense grid size in advance for array sizes + int gridSize = 0; + int numband = des.length; + for (int i=0; i 0.0001) w[i] /= grid[i]; + } + } + + // For odd or Negative symmetry filters, alter the + // d[] and w[] according to Parks McClellan + if (symmetry == POSITIVE) { + if (numtaps % 2 == 0) { + for (int i=0; i +{ + + public CurveFittedAreaChart( NumberAxis xAxis, NumberAxis yAxis ) + { + super( xAxis, yAxis ); + } + + @Override + protected void layoutPlotChildren() + { + super.layoutPlotChildren(); + for ( int seriesIndex = 0; seriesIndex < getDataSize(); seriesIndex++ ) + { + final XYChart.Series series = getData() + .get( seriesIndex ); + final Path seriesLine = (Path) ( (Group) series.getNode() ) + .getChildren().get( 1 ); + final Path fillPath = (Path) ( (Group) series.getNode() ) + .getChildren().get( 0 ); + smooth( seriesLine.getElements(), fillPath.getElements() ); + } + } + + private int getDataSize() + { + final ObservableList> data = getData(); + return ( data != null ) ? data.size() : 0; + } + + private static void smooth( ObservableList strokeElements, + ObservableList fillElements ) + { + // as we do not have direct access to the data, first recreate the list + // of all the data points we have + final Point2D[] dataPoints = new Point2D[ strokeElements.size() ]; + for ( int i = 0; i < strokeElements.size(); i++ ) + { + final PathElement element = strokeElements.get( i ); + if ( element instanceof MoveTo ) + { + final MoveTo move = (MoveTo) element; + dataPoints[ i ] = new Point2D( move.getX(), move.getY() ); + } else if ( element instanceof LineTo ) + { + final LineTo line = (LineTo) element; + final double x = line.getX(), y = line.getY(); + dataPoints[ i ] = new Point2D( x, y ); + } + } + // next we need to know the zero Y value + final double zeroY = ( (MoveTo) fillElements.get( 0 ) ).getY(); + // now clear and rebuild elements + strokeElements.clear(); + fillElements.clear(); + Pair result = calcCurveControlPoints( + dataPoints ); + Point2D[] firstControlPoints = result.getKey(); + Point2D[] secondControlPoints = result.getValue(); + // start both paths + strokeElements.add( + new MoveTo( dataPoints[ 0 ].getX(), dataPoints[ 0 ].getY() ) ); + fillElements.add( new MoveTo( dataPoints[ 0 ].getX(), zeroY ) ); + fillElements.add( + new LineTo( dataPoints[ 0 ].getX(), dataPoints[ 0 ].getY() ) ); + // add curves + for ( int i = 1; i < dataPoints.length; i++ ) + { + final int ci = i - 1; + strokeElements + .add( new CubicCurveTo( firstControlPoints[ ci ].getX(), + firstControlPoints[ ci ].getY(), + secondControlPoints[ ci ].getX(), + secondControlPoints[ ci ].getY(), + dataPoints[ i ].getX(), dataPoints[ i ].getY() ) ); + fillElements.add( new CubicCurveTo( firstControlPoints[ ci ].getX(), + firstControlPoints[ ci ].getY(), + secondControlPoints[ ci ].getX(), + secondControlPoints[ ci ].getY(), dataPoints[ i ].getX(), + dataPoints[ i ].getY() ) ); + } + // end the paths + fillElements.add( new LineTo( + dataPoints[ dataPoints.length - 1 ].getX(), zeroY ) ); + fillElements.add( new ClosePath() ); + } + + /** + * Calculate open-ended Bezier Spline Control Points. + */ + public static Pair calcCurveControlPoints( + Point2D[] dataPoints ) + { + Point2D[] firstControlPoints; + Point2D[] secondControlPoints; + int n = dataPoints.length - 1; + if ( n == 1 ) + { // Special case: Bezier curve should be a straight line. + firstControlPoints = new Point2D[ 1 ]; + // 3P1 = 2P0 + P3 + firstControlPoints[ 0 ] = new Point2D( + ( 2 * dataPoints[ 0 ].getX() + dataPoints[ 1 ].getX() ) / 3, + ( 2 * dataPoints[ 0 ].getY() + dataPoints[ 1 ].getY() ) + / 3 ); + + secondControlPoints = new Point2D[ 1 ]; + // P2 = 2P1 – P0 + secondControlPoints[ 0 ] = new Point2D( + 2 * firstControlPoints[ 0 ].getX() - dataPoints[ 0 ].getX(), + 2 * firstControlPoints[ 0 ].getY() + - dataPoints[ 0 ].getY() ); + return new Pair( firstControlPoints, + secondControlPoints ); + } + + // Calculate first Bezier control points + // Right hand side vector + double[] rhs = new double[ n ]; + + // Set right hand side X values + for ( int i = 1; i < n - 1; ++i ) + { + rhs[ i ] = 4 * dataPoints[ i ].getX() + + 2 * dataPoints[ i + 1 ].getX(); + } + rhs[ 0 ] = dataPoints[ 0 ].getX() + 2 * dataPoints[ 1 ].getX(); + rhs[ n - 1 ] = ( 8 * dataPoints[ n - 1 ].getX() + + dataPoints[ n ].getX() ) / 2.0; + // Get first control points X-values + double[] x = GetFirstControlPoints( rhs ); + + // Set right hand side Y values + for ( int i = 1; i < n - 1; ++i ) + { + rhs[ i ] = 4 * dataPoints[ i ].getY() + + 2 * dataPoints[ i + 1 ].getY(); + } + rhs[ 0 ] = dataPoints[ 0 ].getY() + 2 * dataPoints[ 1 ].getY(); + rhs[ n - 1 ] = ( 8 * dataPoints[ n - 1 ].getY() + + dataPoints[ n ].getY() ) / 2.0; + // Get first control points Y-values + double[] y = GetFirstControlPoints( rhs ); + + // Fill output arrays. + firstControlPoints = new Point2D[ n ]; + secondControlPoints = new Point2D[ n ]; + for ( int i = 0; i < n; ++i ) + { + // First control point + firstControlPoints[ i ] = new Point2D( x[ i ], y[ i ] ); + // Second control point + if ( i < n - 1 ) + { + secondControlPoints[ i ] = new Point2D( + 2 * dataPoints[ i + 1 ].getX() - x[ i + 1 ], + 2 * dataPoints[ i + 1 ].getY() - y[ i + 1 ] ); + } else + { + secondControlPoints[ i ] = new Point2D( + ( dataPoints[ n ].getX() + x[ n - 1 ] ) / 2, + ( dataPoints[ n ].getY() + y[ n - 1 ] ) / 2 ); + } + } + return new Pair( firstControlPoints, + secondControlPoints ); + } + + /** + * Solves a tridiagonal system for one of coordinates (x or y) of first + * Bezier control points. + */ + private static double[] GetFirstControlPoints( double[] rhs ) + { + int n = rhs.length; + double[] x = new double[ n ]; // Solution vector. + double[] tmp = new double[ n ]; // Temp workspace. + double b = 2.0; + x[ 0 ] = rhs[ 0 ] / b; + for ( int i = 1; i < n; i++ ) + {// Decomposition and forward substitution. + tmp[ i ] = 1 / b; + b = ( i < n - 1 ? 4.0 : 3.5 ) - tmp[ i ]; + x[ i ] = ( rhs[ i ] - x[ i - 1 ] ) / b; + } + for ( int i = 1; i < n; i++ ) + { + x[ n - i - 1 ] -= tmp[ n - i ] * x[ n - i ]; // Backsubstitution. + } + return x; + } +} \ No newline at end of file diff --git a/src/filter/viewer/FilterView.css b/src/filter/viewer/FilterView.css new file mode 100644 index 00000000..f5ef3421 --- /dev/null +++ b/src/filter/viewer/FilterView.css @@ -0,0 +1,39 @@ +/* ....Show License.... */ +.chart { + -fx-background-image: url("../../../images/Curve-fitted-background.png"); + -fx-padding: 15 25 15 15; +} +.chart-plot-background { + -fx-background-color: red; + -fx-background-image: url("../../../images/Curve-fitted-chart-background.png"), + url("../../../images/Curve-fitted-graph-gridlines.png"); + -fx-background-size: cover, auto; + -fx-background-repeat: no-repeat, repeat; + -fx-background-position: 0% 0%, 0% 100%; + -fx-border-color: black black transparent transparent; +} +.chart-area-symbol { + -fx-background-color: white; +} +.chart-series-area-line { + -fx-stroke: white; + -fx-stroke-width: 2px; +} +.chart-series-area-fill { + -fx-fill: linear-gradient(to right, white, rgba(255,255,255,0)); + -fx-blend-mode: OVERLAY; +} +.axis { + -fx-tick-mark-visible: false; + -fx-minor-tick-visible: false; + -fx-tick-length: 3; + -fx-minor-tick-length: 0; + -fx-border-color: transparent; + -fx-tick-label-fill: white; +} +.axis:bottom { + -fx-border-color: black transparent transparent transparent; +} +.axis:left { + -fx-border-color: transparent black transparent transparent; +} \ No newline at end of file diff --git a/src/filter/viewer/FilterView.java b/src/filter/viewer/FilterView.java new file mode 100644 index 00000000..883acf3c --- /dev/null +++ b/src/filter/viewer/FilterView.java @@ -0,0 +1,168 @@ +package filter.viewer; + +import org.jtransforms.fft.FloatFFT_1D; +import org.slf4j.Logger; +import org.slf4j.LoggerFactory; + +import dsp.filter.FilterFactory; +import dsp.filter.Window.WindowType; +import javafx.collections.ObservableList; +import javafx.scene.chart.NumberAxis; +import javafx.scene.chart.XYChart; +import javafx.scene.chart.XYChart.Series; +import javafx.scene.layout.BorderPane; +import javafx.scene.layout.HBox; +import javafx.scene.layout.Priority; + +public class FilterView extends BorderPane +{ + private final static Logger mLog = LoggerFactory.getLogger( FilterView.class ); + + private CurveFittedAreaChart mImpulseChart; + private NumberAxis mImpulseXAxis; + private NumberAxis mImpulseYAxis; + private CurveFittedAreaChart mDFTChart; + private NumberAxis mDFTXAxis; + private NumberAxis mDFTYAxis; + private static final int FFT_SIZE = 4096; + private FloatFFT_1D mFFT = new FloatFFT_1D( FFT_SIZE ); + + public FilterView() + { + init(); + + float[] taps = FilterFactory.getNewLowPass( 8000, 1500, 2500, 50, WindowType.HANNING ); + + setImpulseTaps( taps ); + + float[] dft = new float[ FFT_SIZE ]; + System.arraycopy( taps, 0, dft, 0, taps.length ); + + mFFT.realForward( dft ); + + setDFTTaps( convertDFTToDecibel( dft, taps.length ) ); + } + + private void logTaps( float[] taps ) + { + StringBuilder sb = new StringBuilder(); + sb.append( "\n" ); + + for( float tap: taps ) + { + sb.append( "Tap: " + tap + "\n" ); + } + + mLog.debug( sb.toString() ); + } + + private void init() + { + HBox hbox = new HBox(); + + mImpulseXAxis = new NumberAxis( 0.0, 0.5, 1.0 ); + mImpulseYAxis = new NumberAxis( -0.2, 1.2, .1 ); + + mImpulseChart = new CurveFittedAreaChart( mImpulseXAxis, mImpulseYAxis ); + mImpulseChart.setLegendVisible( false ); + mImpulseChart.setHorizontalGridLinesVisible( false ); + mImpulseChart.setVerticalGridLinesVisible(false); + mImpulseChart.setAlternativeColumnFillVisible(false); + mImpulseChart.setAlternativeRowFillVisible(false); + + String css = FilterView.class.getResource( "FilterView.css" ).toExternalForm(); + mImpulseChart.getStylesheets().add( css ); + + hbox.getChildren().add( mImpulseChart ); + HBox.setHgrow( mImpulseChart, Priority.ALWAYS ); + + mDFTXAxis = new NumberAxis( 0.0, FFT_SIZE / 2, FFT_SIZE / 8 ); + mDFTYAxis = new NumberAxis( -200.0, 10.0, 10.0 ); + + mDFTChart = new CurveFittedAreaChart( mDFTXAxis, mDFTYAxis ); + mDFTChart.setLegendVisible( false ); + mDFTChart.setHorizontalGridLinesVisible( false ); + mDFTChart.setVerticalGridLinesVisible(false); + mDFTChart.setAlternativeColumnFillVisible(false); + mDFTChart.setAlternativeRowFillVisible(false); + mDFTChart.getStylesheets().add( css ); + + hbox.getChildren().add( mDFTChart ); + HBox.setHgrow( mDFTChart, Priority.ALWAYS ); + + setCenter( hbox ); + } + + public void setImpulseTaps( float[] taps ) + { + double smallest = 0.0; + double largest = 0.0; + + final XYChart.Series series = new XYChart.Series<>(); + + for( int x = 0; x < taps.length; x++ ) + { + if( taps[ x ] < smallest ) + { + smallest = taps[ x ]; + } + + if( taps[ x ] > largest ) + { + largest = taps[ x ]; + } + + series.getData().add( new XYChart.Data( x, taps[ x ] ) ); + } + + mImpulseXAxis.setLowerBound( 0.0 ); + mImpulseXAxis.setUpperBound( taps.length - 1 ); + mImpulseXAxis.setTickUnit( 1.0 ); + + double padding = Math.abs( largest - smallest ) * .1; + + mImpulseYAxis.setLowerBound( smallest - padding ); + mImpulseYAxis.setUpperBound( largest + padding ); + mImpulseYAxis.setTickUnit( padding ); + + ObservableList> list = mImpulseChart.getData(); + list.add( series ); + } + + public void setDFTTaps( float[] taps ) + { + double smallest = 0.0; + + final XYChart.Series series = new XYChart.Series<>(); + + for( int x = 0; x < taps.length; x++ ) + { + if( taps[ x ] < smallest ) + { + smallest = taps[ x ]; + } + + series.getData().add( new XYChart.Data( x, taps[ x ] ) ); + } + + ObservableList> list = mDFTChart.getData(); + list.add( series ); + } + + public float[] convertDFTToDecibel( float[] dft, int tapLength ) + { + float[] decibels = new float[ dft.length / 2 ]; + + int index = 0; + + for( int x = 0; x < decibels.length; x ++ ) + { + index = x * 2; + + decibels[ x ] = 20.0f * (float)Math.log10( ( ( dft[ index ] * dft[ index ] ) + + ( dft[ index + 1 ] * dft[ index + 1 ] ) ) ); + } + + return decibels; + } +} diff --git a/src/filter/viewer/FilterViewer.java b/src/filter/viewer/FilterViewer.java new file mode 100644 index 00000000..31035233 --- /dev/null +++ b/src/filter/viewer/FilterViewer.java @@ -0,0 +1,27 @@ +package filter.viewer; + +import javafx.application.Application; +import javafx.scene.Scene; +import javafx.stage.Stage; + +public class FilterViewer extends Application +{ + public FilterViewer() + { + } + + @Override + public void start( Stage primaryStage ) throws Exception + { + Scene scene = new Scene( new FilterView() ); + + primaryStage.setTitle( "Filter Viewer" ); + primaryStage.setScene( scene ); + primaryStage.show(); + } + + public static void main( String[] args ) + { + launch( args ); + } +} diff --git a/src/spectrum/DFTProcessor.java b/src/spectrum/DFTProcessor.java index a90c432d..d092a459 100644 --- a/src/spectrum/DFTProcessor.java +++ b/src/spectrum/DFTProcessor.java @@ -72,7 +72,7 @@ public class DFTProcessor implements Listener, private DFTSize mDFTSize = DFTSize.FFT04096; private DFTSize mNewDFTSize = DFTSize.FFT04096; - private double[] mWindow; + private float[] mWindow; /* The Cosine and Hanning windows seem to offer the best spectral display * with minimal bin leakage/smearing */