330 - Adds new JavaFX implementation of the demodulator viewer applicaiton

with initial support for playback control and I/Q sample axis viewing.
This commit is contained in:
Denny
2018-02-11 10:16:50 -05:00
parent 03e39f3343
commit 1406d5576a
8 changed files with 449 additions and 96 deletions

View File

@@ -54,6 +54,8 @@ dependencies {
compile 'org.apache.axis:axis-jaxrpc:1.4'
compile 'org.apache.axis:axis:1.4'
compile 'com.fasterxml.jackson.dataformat:jackson-dataformat-xml:2.9.3'
compile 'org.kordamp.ikonli:ikonli-javafx:2.1.1'
compile 'org.kordamp.ikonli:ikonli-fontawesome-pack:2.1.1'
}
task wrapper(type: Wrapper) {

View File

@@ -1,17 +1,17 @@
/*******************************************************************************
* SDR Trunk
* Copyright (C) 2014 Dennis Sheirer
*
* Copyright (C) 2014-2018 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 <http://www.gnu.org/licenses/>
******************************************************************************/
@@ -25,66 +25,83 @@ import java.util.Arrays;
* Circular sample buffer - allocates a buffer and stores samples in a circular
* fashion, overwriting older samples with newly arrived samples. Initially
* fills buffer with 0-valued samples
*
*
* Can be used as a delay-type buffer, to delay samples by the 'size' amount
*/
public class ComplexCircularBuffer
{
Complex[] mBuffer;
int mBufferPointer = 0;
public ComplexCircularBuffer( int size )
{
mBuffer = new Complex[ size ];
Arrays.fill( mBuffer, new Complex( 1.0f, 1.0f ) );
}
Complex[] mBuffer;
int mBufferPointer = 0;
/**
* Returns the sample currently pointed to in the buffer and stores the
* new sample in its place
* @param sample
* @return
*/
public Complex get( Complex sample )
{
Complex retVal = mBuffer[ mBufferPointer ];
public ComplexCircularBuffer(int size)
{
mBuffer = new Complex[size];
put( sample );
Arrays.fill(mBuffer, new Complex(1.0f, 1.0f));
}
return retVal;
}
public void put( Complex sample )
{
mBuffer[ mBufferPointer ] = sample;
public int length()
{
return mBuffer.length;
}
mBufferPointer++;
if( mBufferPointer >= mBuffer.length )
{
mBufferPointer = 0;
}
}
public Complex[] get( int count )
{
Complex[] samples = new Complex[ count ];
int tempCounter = mBufferPointer;
/**
* Returns the sample currently pointed to in the buffer and stores the
* new sample in its place
*
* @param sample
* @return
*/
public Complex get(Complex sample)
{
Complex retVal = mBuffer[mBufferPointer];
for( int x = 0; x < count; x++ )
{
samples[ x ] = mBuffer[ tempCounter ];
tempCounter++;
if( tempCounter > mBuffer.length - 1 )
{
tempCounter = 0;
}
}
return samples;
}
put(sample);
return retVal;
}
public void put(Complex sample)
{
mBuffer[mBufferPointer] = sample;
mBufferPointer++;
if(mBufferPointer >= mBuffer.length)
{
mBufferPointer = 0;
}
}
/**
* Returns the (count) oldest samples in a time ordered array
*/
public Complex[] get(int count)
{
Complex[] samples = new Complex[count];
int tempCounter = mBufferPointer;
for(int x = 0; x < count; x++)
{
samples[x] = mBuffer[tempCounter];
tempCounter++;
if(tempCounter > mBuffer.length - 1)
{
tempCounter = 0;
}
}
return samples;
}
/**
* Returns the samples in a time-ordered array
*/
public Complex[] getAll()
{
return get(length());
}
}

View File

@@ -5,27 +5,34 @@ import io.github.dsheirer.sample.complex.Complex;
public class ComplexGain implements Listener<Complex>
{
private float mGain;
private Listener<Complex> mListener;
private float mGain;
private Listener<Complex> mListener;
public ComplexGain( float gain )
{
mGain = gain;
}
@Override
public void receive( Complex sample )
{
if( mListener != null )
{
sample.multiply( mGain );
mListener.receive( sample );
}
}
public ComplexGain(float gain)
{
mGain = gain;
}
public void setListener( Listener<Complex> listener )
{
mListener = listener;
}
public Complex apply(Complex sample)
{
sample.multiply(mGain);
return sample;
}
@Override
public void receive(Complex sample)
{
if(mListener != null)
{
apply(sample);
mListener.receive(sample);
}
}
public void setListener(Listener<Complex> listener)
{
mListener = listener;
}
}

View File

@@ -0,0 +1,119 @@
/*******************************************************************************
* sdr-trunk
* Copyright (C) 2014-2018 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 <http://www.gnu.org/licenses/>
*
******************************************************************************/
package io.github.dsheirer.instrument.gui.viewer;
import io.github.dsheirer.buffer.ComplexCircularBuffer;
import io.github.dsheirer.dsp.gain.ComplexGain;
import io.github.dsheirer.sample.Listener;
import io.github.dsheirer.sample.complex.Complex;
import io.github.dsheirer.sample.complex.ComplexBuffer;
import javafx.beans.property.IntegerProperty;
import javafx.beans.property.SimpleIntegerProperty;
import javafx.beans.value.ChangeListener;
import javafx.beans.value.ObservableValue;
import javafx.collections.FXCollections;
import javafx.collections.ObservableList;
import javafx.scene.chart.LineChart;
import javafx.scene.chart.NumberAxis;
import javafx.scene.chart.XYChart;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
public class ComplexSampleLineChart extends LineChart implements Listener<ComplexBuffer>
{
private final static Logger mLog = LoggerFactory.getLogger(ComplexSampleLineChart.class);
private ComplexCircularBuffer mComplexCircularBuffer;
private ComplexGain mComplexGain = new ComplexGain(1000.0f);
private ObservableList<XYChart.Data<Integer,Float>> mISamples = FXCollections.observableArrayList();
private ObservableList<XYChart.Data<Integer,Float>> mQSamples = FXCollections.observableArrayList();
private IntegerProperty mLengthProperty = new SimpleIntegerProperty(40);
public ComplexSampleLineChart(int length)
{
super(new NumberAxis("Sample Number", 1, length, 1),
new NumberAxis("Value", -1.0, 1.0, 0.1));
LineChart.Series<Integer,Float> iSampleSeries = new LineChart.Series<>("Inphase", mISamples);
LineChart.Series<Integer,Float> qSampleSeries = new LineChart.Series<>("Quadrature", mQSamples);
ObservableList<XYChart.Series<Integer,Float>> observableList =
FXCollections.observableArrayList(iSampleSeries, qSampleSeries);
setData(observableList);
init(length);
}
private void init(int length)
{
mLengthProperty.addListener(new ChangeListener<Number>()
{
@Override
public void changed(ObservableValue<? extends Number> observable, Number oldValue, Number newValue)
{
mComplexCircularBuffer = new ComplexCircularBuffer(newValue.intValue());
}
});
mComplexCircularBuffer = new ComplexCircularBuffer(length);
length().setValue(length);
for(int x = 1; x <= length; x++)
{
mISamples.add(new Data(x, 0.0f));
mQSamples.add(new Data(x, 0.0f));
}
}
/**
* Length of the x-axis for displaying samples
*/
public IntegerProperty length()
{
return mLengthProperty;
}
@Override
public void receive(ComplexBuffer complexBuffer)
{
float[] samples = complexBuffer.getSamples();
Complex sample;
for(int x = 0; x < samples.length; x += 2)
{
sample = new Complex(samples[x], samples[x + 1]);
mComplexGain.apply(sample);
mComplexCircularBuffer.put(sample);
}
updateChart();
}
private void updateChart()
{
Complex[] samples = mComplexCircularBuffer.getAll();
for(int x = 0; x < samples.length; x++)
{
Complex sample = samples[x];
if(sample != null)
{
mISamples.get(x).setYValue(sample.inphase());
mQSamples.get(x).setYValue(sample.quadrature());
}
}
}
}

View File

@@ -15,67 +15,263 @@
******************************************************************************/
package io.github.dsheirer.instrument.gui.viewer;
import io.github.dsheirer.sample.Broadcaster;
import io.github.dsheirer.sample.Listener;
import io.github.dsheirer.sample.complex.ComplexBuffer;
import io.github.dsheirer.source.IControllableFileSource;
import io.github.dsheirer.source.IFrameLocationListener;
import io.github.dsheirer.source.wave.ComplexWaveSource;
import javafx.event.ActionEvent;
import javafx.event.EventHandler;
import javafx.geometry.Pos;
import javafx.scene.control.Alert;
import javafx.scene.control.Button;
import javafx.scene.control.ButtonType;
import javafx.scene.control.Label;
import javafx.scene.control.TextField;
import javafx.scene.layout.HBox;
import org.kordamp.ikonli.fontawesome.FontAwesome;
import org.kordamp.ikonli.javafx.FontIcon;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import java.nio.file.Path;
import javax.sound.sampled.UnsupportedAudioFileException;
import java.io.File;
import java.io.IOException;
public class PlaybackController extends HBox
public class PlaybackController extends HBox implements IFrameLocationListener, Listener<ComplexBuffer>
{
private final static Logger mLog = LoggerFactory.getLogger(PlaybackController.class);
private Button mRewindButton;
private TextField mPlaybackPositionText;
private Button mPlay1Button;
private Button mPlay10Button;
private Button mPlay100Button;
private Button mPlay1000Button;
private HBox mControlsBox;
private Label mFileLabel;
private EventHandler<ActionEvent> mPlayEventHandler;
private IControllableFileSource mControllableFileSource;
private Broadcaster<ComplexBuffer> mComplexBufferBroadcaster = new Broadcaster<>();
public PlaybackController()
{
getChildren().addAll(getPlay1Button(), getPlay10Button(), getPlay100Button(), getPlay1000Button());
super(10);
getChildren().addAll(getControlsBox(), getFileLabel());
setControlsEnabled(false);
}
public void load(Path path)
public void load(File file)
{
if(file != null && file.isFile())
{
try
{
mControllableFileSource = new ComplexWaveSource(file);
mControllableFileSource.setListener(this);
((ComplexWaveSource)mControllableFileSource).setListener((Listener<ComplexBuffer>)this);
mControllableFileSource.open();
}
catch(UnsupportedAudioFileException e)
{
mLog.error("Unsupported Audio File Type [" + (file != null ? file.getAbsolutePath() : "null") + "]");
Alert alert = new Alert(Alert.AlertType.ERROR, "The file type is unsupported.", ButtonType.OK);
alert.show();
return;
}
catch(IOException ioe)
{
mLog.error("Error opening file [" + (file != null ? file.getAbsolutePath() : "null") + "]");
Alert alert = new Alert(Alert.AlertType.ERROR, "There was an error opening the file",
ButtonType.OK);
alert.show();
return;
}
getFileLabel().setText(file.getName());
setControlsEnabled(true);
}
}
public Button getPlay1Button()
/**
* Adds listener to receive complex buffers from this playback
*/
public void addListener(Listener<ComplexBuffer> listener)
{
mComplexBufferBroadcaster.addListener(listener);
}
/**
* Removes the listener from receiving complex buffers from this playback
*/
public void removeListener(Listener<ComplexBuffer> listener)
{
mComplexBufferBroadcaster.removeListener(listener);
}
/**
* Sets all of the playback controls to the specified enabled state.
*/
private void setControlsEnabled(boolean enabled)
{
getRewindButton().setDisable(!enabled);
getPlaybackPositionText().setDisable(!enabled);
getPlay1Button().setDisable(!enabled);
getPlay10Button().setDisable(!enabled);
getPlay100Button().setDisable(!enabled);
getPlay1000Button().setDisable(!enabled);
}
private HBox getControlsBox()
{
if(mControlsBox == null)
{
mControlsBox = new HBox();
mControlsBox.getChildren().addAll(getRewindButton(), getPlaybackPositionText(), getPlay1Button(),
getPlay10Button(), getPlay100Button(), getPlay1000Button());
}
return mControlsBox;
}
private Button getRewindButton()
{
if(mRewindButton == null)
{
mRewindButton = new Button();
mRewindButton.setGraphic(new FontIcon(FontAwesome.FAST_BACKWARD));
}
return mRewindButton;
}
private Button getPlay1Button()
{
if(mPlay1Button == null)
{
mPlay1Button = new Button("> 1");
mPlay1Button = new Button("1");
mPlay1Button.setUserData(1);
mPlay1Button.setOnAction(getPlaybackEventHandler());
mPlay1Button.setGraphic(new FontIcon(FontAwesome.PLAY));
}
return mPlay1Button;
}
public Button getPlay10Button()
private Button getPlay10Button()
{
if(mPlay10Button == null)
{
mPlay10Button = new Button(">> 10");
mPlay10Button = new Button("10");
mPlay10Button.setUserData(10);
mPlay10Button.setOnAction(getPlaybackEventHandler());
mPlay10Button.setGraphic(new FontIcon(FontAwesome.FAST_FORWARD));
}
return mPlay10Button;
}
public Button getPlay100Button()
private Button getPlay100Button()
{
if(mPlay100Button == null)
{
mPlay100Button = new Button(">> 100");
mPlay100Button = new Button("100");
mPlay100Button.setUserData(100);
mPlay100Button.setOnAction(getPlaybackEventHandler());
mPlay100Button.setGraphic(new FontIcon(FontAwesome.FAST_FORWARD));
}
return mPlay100Button;
}
public Button getPlay1000Button()
private Button getPlay1000Button()
{
if(mPlay1000Button == null)
{
mPlay1000Button = new Button(">> 1000");
mPlay1000Button = new Button("1000");
mPlay1000Button.setUserData(1000);
mPlay1000Button.setOnAction(getPlaybackEventHandler());
mPlay1000Button.setGraphic(new FontIcon(FontAwesome.FAST_FORWARD));
}
return mPlay1000Button;
}
private EventHandler<ActionEvent> getPlaybackEventHandler()
{
if(mPlayEventHandler == null)
{
mPlayEventHandler = new EventHandler<ActionEvent>()
{
@Override
public void handle(ActionEvent event)
{
int count = (int)((Button)event.getSource()).getUserData();
if(mControllableFileSource != null)
{
try
{
mControllableFileSource.next(count, true);
}
catch(IOException ioe)
{
mLog.error("Error while playing samples");
}
}
}
};
}
return mPlayEventHandler;
}
private TextField getPlaybackPositionText()
{
if(mPlaybackPositionText == null)
{
mPlaybackPositionText = new TextField("0");
mPlaybackPositionText.setAlignment(Pos.CENTER);
mPlaybackPositionText.setPrefWidth(80);
}
return mPlaybackPositionText;
}
private Label getFileLabel()
{
if(mFileLabel == null)
{
mFileLabel = new Label();
}
return mFileLabel;
}
@Override
public void frameLocationUpdated(int location)
{
getPlaybackPositionText().setText(String.valueOf(location));
}
@Override
public void frameLocationReset()
{
mLog.info("Frame location reset ... ignoring?");
}
@Override
public void receive(ComplexBuffer complexBuffer)
{
mComplexBufferBroadcaster.broadcast(complexBuffer);
}
}

View File

@@ -22,15 +22,18 @@ import java.io.File;
public class ViewerDesktop extends BorderPane
{
private PlaybackController mPlaybackController;
private ComplexSampleLineChart mSampleLineChart;
public ViewerDesktop()
{
setBottom(getPlaybackController());
getPlaybackController().addListener(getSampleLineChart());
setCenter(getSampleLineChart());
}
public void load(File file)
{
getPlaybackController().load(file);
}
public void close()
@@ -47,4 +50,15 @@ public class ViewerDesktop extends BorderPane
return mPlaybackController;
}
private ComplexSampleLineChart getSampleLineChart()
{
if(mSampleLineChart == null)
{
mSampleLineChart = new ComplexSampleLineChart(40);
}
return mSampleLineChart;
}
}

View File

@@ -21,14 +21,13 @@ public class ConversionUtils
*/
public static float[] convertFromSigned16BitSamples( ByteBuffer buffer )
{
ShortBuffer byteBuffer =
buffer.order( ByteOrder.LITTLE_ENDIAN ).asShortBuffer();
ShortBuffer byteBuffer = buffer.order( ByteOrder.LITTLE_ENDIAN ).asShortBuffer();
float[] samples = new float[ buffer.limit() / 2 ];
for( int x = 0; x < samples.length; x++ )
{
samples[ x ] = (float)byteBuffer.get() / 32767.0f;
samples[ x ] = (float)byteBuffer.get() / (float)Short.MAX_VALUE;
}
return samples;

View File

@@ -239,8 +239,7 @@ public class ComplexWaveSource extends ComplexSource implements IControllableFil
buffer = Arrays.copyOf(buffer, samplesRead);
}
float[] samples = ConversionUtils
.convertFromSigned16BitSamples(buffer);
float[] samples = ConversionUtils.convertFromSigned16BitSamples(buffer);
mListener.receive(new ComplexBuffer(samples));
}