diff --git a/build.gradle b/build.gradle index 5039a823..b89f9ef5 100644 --- a/build.gradle +++ b/build.gradle @@ -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) { diff --git a/src/main/java/io/github/dsheirer/buffer/ComplexCircularBuffer.java b/src/main/java/io/github/dsheirer/buffer/ComplexCircularBuffer.java index 7dbbed04..db56da97 100644 --- a/src/main/java/io/github/dsheirer/buffer/ComplexCircularBuffer.java +++ b/src/main/java/io/github/dsheirer/buffer/ComplexCircularBuffer.java @@ -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 ******************************************************************************/ @@ -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()); + } } diff --git a/src/main/java/io/github/dsheirer/dsp/gain/ComplexGain.java b/src/main/java/io/github/dsheirer/dsp/gain/ComplexGain.java index f10b571b..70dcdd0e 100644 --- a/src/main/java/io/github/dsheirer/dsp/gain/ComplexGain.java +++ b/src/main/java/io/github/dsheirer/dsp/gain/ComplexGain.java @@ -5,27 +5,34 @@ import io.github.dsheirer.sample.complex.Complex; public class ComplexGain implements Listener { - private float mGain; - private Listener mListener; + private float mGain; + private Listener 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 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 listener) + { + mListener = listener; + } } diff --git a/src/main/java/io/github/dsheirer/instrument/gui/viewer/ComplexSampleLineChart.java b/src/main/java/io/github/dsheirer/instrument/gui/viewer/ComplexSampleLineChart.java new file mode 100644 index 00000000..c141a3d3 --- /dev/null +++ b/src/main/java/io/github/dsheirer/instrument/gui/viewer/ComplexSampleLineChart.java @@ -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 + * + ******************************************************************************/ +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 +{ + private final static Logger mLog = LoggerFactory.getLogger(ComplexSampleLineChart.class); + + private ComplexCircularBuffer mComplexCircularBuffer; + private ComplexGain mComplexGain = new ComplexGain(1000.0f); + private ObservableList> mISamples = FXCollections.observableArrayList(); + private ObservableList> 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 iSampleSeries = new LineChart.Series<>("Inphase", mISamples); + LineChart.Series qSampleSeries = new LineChart.Series<>("Quadrature", mQSamples); + ObservableList> observableList = + FXCollections.observableArrayList(iSampleSeries, qSampleSeries); + + setData(observableList); + init(length); + } + + private void init(int length) + { + mLengthProperty.addListener(new ChangeListener() + { + @Override + public void changed(ObservableValue 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()); + } + } + } +} diff --git a/src/main/java/io/github/dsheirer/instrument/gui/viewer/PlaybackController.java b/src/main/java/io/github/dsheirer/instrument/gui/viewer/PlaybackController.java index 6d09b92b..16f5f055 100644 --- a/src/main/java/io/github/dsheirer/instrument/gui/viewer/PlaybackController.java +++ b/src/main/java/io/github/dsheirer/instrument/gui/viewer/PlaybackController.java @@ -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 { + 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 mPlayEventHandler; + + private IControllableFileSource mControllableFileSource; + private Broadcaster 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)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 listener) + { + mComplexBufferBroadcaster.addListener(listener); + } + + /** + * Removes the listener from receiving complex buffers from this playback + */ + public void removeListener(Listener 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 getPlaybackEventHandler() + { + if(mPlayEventHandler == null) + { + mPlayEventHandler = new EventHandler() + { + @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); + } } diff --git a/src/main/java/io/github/dsheirer/instrument/gui/viewer/ViewerDesktop.java b/src/main/java/io/github/dsheirer/instrument/gui/viewer/ViewerDesktop.java index 85b774cd..2c9449bf 100644 --- a/src/main/java/io/github/dsheirer/instrument/gui/viewer/ViewerDesktop.java +++ b/src/main/java/io/github/dsheirer/instrument/gui/viewer/ViewerDesktop.java @@ -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; + } } diff --git a/src/main/java/io/github/dsheirer/sample/ConversionUtils.java b/src/main/java/io/github/dsheirer/sample/ConversionUtils.java index 95d72bcb..7ad5c27c 100644 --- a/src/main/java/io/github/dsheirer/sample/ConversionUtils.java +++ b/src/main/java/io/github/dsheirer/sample/ConversionUtils.java @@ -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; diff --git a/src/main/java/io/github/dsheirer/source/wave/ComplexWaveSource.java b/src/main/java/io/github/dsheirer/source/wave/ComplexWaveSource.java index 8ece68b6..d227a199 100644 --- a/src/main/java/io/github/dsheirer/source/wave/ComplexWaveSource.java +++ b/src/main/java/io/github/dsheirer/source/wave/ComplexWaveSource.java @@ -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)); }