#660 Updates audio processing subsystem to use audio segments. Updates audio playback, recording and streaming to use audio segments. Adds support to UserPreferences for sound card selection and audio tone insertion during playback. Adds record preference for WAVE or MP3. Resolves issues with metadata in recordings. (#675)

This commit is contained in:
Denny Sheirer
2020-03-19 10:35:05 +00:00
committed by GitHub
parent de249c448b
commit fa16db129b
88 changed files with 4446 additions and 4183 deletions

View File

@@ -47,7 +47,7 @@ public class JavaFxWindowManager
}
/**
* Closes all JavaFX windows and shutsdown the FX thread
* Closes all JavaFX windows and shuts down the FX thread
*/
public void shutdown()
{
@@ -84,6 +84,8 @@ public class JavaFxWindowManager
{
Stage stage = mPreferencesEditor.getStage();
stage.show();
stage.toFront();
stage.requestFocus();
mPreferencesEditor.showEditor(request);
}
catch(Throwable t)

View File

@@ -1,30 +1,28 @@
/*
* *****************************************************************************
* Copyright (C) 2014-2020 Dennis Sheirer
*
* * ******************************************************************************
* * Copyright (C) 2014-2020 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/>
* * *****************************************************************************
* 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.gui;
import com.jidesoft.plaf.LookAndFeelFactory;
import com.jidesoft.swing.JideSplitPane;
import io.github.dsheirer.alias.AliasModel;
import io.github.dsheirer.audio.AudioPacketManager;
import io.github.dsheirer.audio.broadcast.AudioStreamingManager;
import io.github.dsheirer.audio.broadcast.BroadcastFormat;
import io.github.dsheirer.audio.broadcast.BroadcastModel;
import io.github.dsheirer.audio.broadcast.BroadcastStatusPanel;
import io.github.dsheirer.audio.playback.AudioPlaybackManager;
@@ -45,7 +43,7 @@ import io.github.dsheirer.module.log.EventLogManager;
import io.github.dsheirer.playlist.PlaylistManager;
import io.github.dsheirer.preference.UserPreferences;
import io.github.dsheirer.properties.SystemProperties;
import io.github.dsheirer.record.RecorderManager;
import io.github.dsheirer.record.AudioRecordingManager;
import io.github.dsheirer.sample.Listener;
import io.github.dsheirer.settings.SettingsManager;
import io.github.dsheirer.source.SourceManager;
@@ -60,6 +58,7 @@ import io.github.dsheirer.spectrum.SpectralDisplayPanel;
import io.github.dsheirer.util.ThreadPool;
import io.github.dsheirer.util.TimeStamp;
import jiconfont.icons.font_awesome.FontAwesome;
import jiconfont.javafx.IconFontFX;
import jiconfont.swing.IconFontSwing;
import net.miginfocom.swing.MigLayout;
import org.slf4j.Logger;
@@ -108,7 +107,8 @@ public class SDRTrunk implements Listener<TunerEvent>
private static final String WINDOW_FRAME_IDENTIFIER = BASE_WINDOW_NAME + ".frame";
private boolean mBroadcastStatusVisible;
private AudioPacketManager mAudioPacketManager;
private AudioRecordingManager mAudioRecordingManager;
private AudioStreamingManager mAudioStreamingManager;
private IconManager mIconManager;
private BroadcastStatusPanel mBroadcastStatusPanel;
private BroadcastModel mBroadcastModel;
@@ -164,6 +164,7 @@ public class SDRTrunk implements Listener<TunerEvent>
//Register FontAwesome so we can use the fonts in Swing windows
IconFontSwing.register(FontAwesome.getIconFont());
IconFontFX.register(FontAwesome.getIconFont());
TunerConfigurationModel tunerConfigurationModel = new TunerConfigurationModel();
TunerModel tunerModel = new TunerModel(tunerConfigurationModel);
@@ -180,14 +181,10 @@ public class SDRTrunk implements Listener<TunerEvent>
EventLogManager eventLogManager = new EventLogManager(aliasModel, mUserPreferences);
RecorderManager recorderManager = new RecorderManager(aliasModel, mUserPreferences);
mJavaFxWindowManager = new JavaFxWindowManager(mUserPreferences);
mSourceManager = new SourceManager(tunerModel, mSettingsManager, mUserPreferences);
mChannelProcessingManager = new ChannelProcessingManager(channelMapModel, eventLogManager, recorderManager,
mSourceManager, aliasModel, mUserPreferences);
mChannelProcessingManager = new ChannelProcessingManager(channelMapModel, eventLogManager, mSourceManager,
aliasModel, mUserPreferences);
mChannelModel.addListener(mChannelProcessingManager);
mChannelProcessingManager.addChannelEventListener(mChannelModel);
@@ -195,25 +192,26 @@ public class SDRTrunk implements Listener<TunerEvent>
ChannelSelectionManager channelSelectionManager = new ChannelSelectionManager(mChannelModel);
mChannelModel.addListener(channelSelectionManager);
AudioPlaybackManager audioPlaybackManager = new AudioPlaybackManager(mSourceManager.getMixerManager());
AudioPlaybackManager audioPlaybackManager = new AudioPlaybackManager(mUserPreferences);
mAudioRecordingManager = new AudioRecordingManager(mUserPreferences);
mAudioRecordingManager.start();
mBroadcastModel = new BroadcastModel(aliasModel, mIconManager, mUserPreferences);
mAudioStreamingManager = new AudioStreamingManager(mBroadcastModel, BroadcastFormat.MP3,
mUserPreferences);
mAudioStreamingManager.start();
//Audio packets are routed through the audio packet manager for metadata enrichment and then
//distributed to the audio packet processors (ie playback, recording, streaming, etc.)
mAudioPacketManager = new AudioPacketManager(aliasModel);
mAudioPacketManager.addListener(recorderManager);
mAudioPacketManager.addListener(audioPlaybackManager);
mAudioPacketManager.addListener(mBroadcastModel);
mAudioPacketManager.start();
mChannelProcessingManager.addAudioPacketListener(mAudioPacketManager);
mChannelProcessingManager.addAudioSegmentListener(audioPlaybackManager);
mChannelProcessingManager.addAudioSegmentListener(mAudioRecordingManager);
mChannelProcessingManager.addAudioSegmentListener(mAudioStreamingManager);
MapService mapService = new MapService(mIconManager);
mChannelProcessingManager.addDecodeEventListener(mapService);
mControllerPanel = new ControllerPanel(audioPlaybackManager, aliasModel, mBroadcastModel,
mChannelModel, channelMapModel, mChannelProcessingManager, mIconManager,
mapService, mSettingsManager, mSourceManager, tunerModel, mUserPreferences, recorderManager);
mapService, mSettingsManager, mSourceManager, tunerModel, mUserPreferences);
mSpectralPanel = new SpectralDisplayPanel(mChannelModel,
mChannelProcessingManager, mSettingsManager, tunerModel);
@@ -411,7 +409,7 @@ public class SDRTrunk implements Listener<TunerEvent>
viewMenu.add(new JSeparator());
JMenuItem preferencesItem = new JMenuItem("Preferences");
preferencesItem.addActionListener(e -> MyEventBus.getEventBus()
.post(new PreferenceEditorViewRequest(PreferenceEditorType.CHANNEL_EVENT)));
.post(new PreferenceEditorViewRequest(PreferenceEditorType.AUDIO_PLAYBACK)));
viewMenu.add(preferencesItem);
menuBar.add(viewMenu);
@@ -471,7 +469,8 @@ public class SDRTrunk implements Listener<TunerEvent>
mJavaFxWindowManager.shutdown();
mLog.info("Stopping channels ...");
mChannelProcessingManager.shutdown();
mAudioPacketManager.stop();
mAudioRecordingManager.stop();
mLog.info("Stopping spectral display ...");
mSpectralPanel.clearTuner();
mSourceManager.shutdown();

View File

@@ -1,7 +1,6 @@
/*
* ******************************************************************************
* sdrtrunk
* Copyright (C) 2014-2019 Dennis Sheirer
* *****************************************************************************
* Copyright (C) 2014-2020 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
@@ -15,13 +14,15 @@
*
* 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.gui.preference;
import io.github.dsheirer.gui.preference.decoder.JmbeLibraryPreferenceEditor;
import io.github.dsheirer.gui.preference.directory.DirectoryPreferenceEditor;
import io.github.dsheirer.gui.preference.playback.PlaybackPreferenceEditor;
import io.github.dsheirer.gui.preference.record.RecordPreferenceEditor;
import io.github.dsheirer.gui.preference.tuner.ChannelMultipleFrequencyPreferenceEditor;
import io.github.dsheirer.gui.preference.tuner.TunerPreferenceEditor;
import io.github.dsheirer.preference.UserPreferences;
@@ -36,18 +37,22 @@ public class PreferenceEditorFactory
{
switch(preferenceEditorType)
{
case AUDIO_PLAYBACK:
return new PlaybackPreferenceEditor(userPreferences);
case AUDIO_RECORD:
return new RecordPreferenceEditor(userPreferences);
case CHANNEL_EVENT:
return new DecodeEventViewPreferenceEditor(userPreferences);
case JMBE_LIBRARY:
return new JmbeLibraryPreferenceEditor(userPreferences);
case DIRECTORY:
return new DirectoryPreferenceEditor(userPreferences);
case JMBE_LIBRARY:
return new JmbeLibraryPreferenceEditor(userPreferences);
case SOURCE_CHANNEL_MULTIPLE_FREQUENCY:
return new ChannelMultipleFrequencyPreferenceEditor(userPreferences);
case TALKGROUP_FORMAT:
return new TalkgroupFormatPreferenceEditor(userPreferences);
case SOURCE_TUNER_CHANNELIZER:
return new TunerPreferenceEditor(userPreferences);
case TALKGROUP_FORMAT:
return new TalkgroupFormatPreferenceEditor(userPreferences);
}
return null;

View File

@@ -1,23 +1,20 @@
/*
* *****************************************************************************
* Copyright (C) 2014-2020 Dennis Sheirer
*
* * ******************************************************************************
* * Copyright (C) 2014-2019 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/>
* * *****************************************************************************
* 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.gui.preference;
@@ -28,11 +25,14 @@ package io.github.dsheirer.gui.preference;
public enum PreferenceEditorType
{
CHANNEL_EVENT("Channel Events"),
JMBE_LIBRARY("JMBE Audio Library"),
DIRECTORY("Directories"),
JMBE_LIBRARY("JMBE Audio Library"),
AUDIO_RECORD("Record"),
AUDIO_PLAYBACK("Playback"),
SOURCE_CHANNEL_MULTIPLE_FREQUENCY("Channel - Multiple Frequency"),
SOURCE_TUNER_CHANNELIZER("Tuner Channelizer"),
TALKGROUP_FORMAT("Talkgroup & Radio ID");
TALKGROUP_FORMAT("Talkgroup & Radio ID"),
DEFAULT("Default");
private String mLabel;

View File

@@ -1,7 +1,6 @@
/*
* ******************************************************************************
* sdrtrunk
* Copyright (C) 2014-2019 Dennis Sheirer
* *****************************************************************************
* Copyright (C) 2014-2020 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
@@ -15,7 +14,7 @@
*
* 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.gui.preference;
@@ -29,12 +28,13 @@ import javafx.geometry.Pos;
import javafx.scene.Node;
import javafx.scene.Scene;
import javafx.scene.control.Button;
import javafx.scene.control.Control;
import javafx.scene.control.Label;
import javafx.scene.control.SelectionMode;
import javafx.scene.control.TreeItem;
import javafx.scene.control.TreeView;
import javafx.scene.layout.BorderPane;
import javafx.scene.layout.HBox;
import javafx.scene.layout.Priority;
import javafx.scene.layout.VBox;
import javafx.stage.Stage;
import javafx.stage.Window;
@@ -52,25 +52,19 @@ public class PreferencesEditor extends Application
{
private final static Logger mLog = LoggerFactory.getLogger(PreferencesEditor.class);
private Map<PreferenceEditorType,Node> mEditors = new HashMap<>();
private UserPreferences mUserPreferences;
private BorderPane mBorderPane;
private HBox mParentBox;
private TreeView mEditorSelectionTreeView;
private DecodeEventViewPreferenceEditor mDecodeEventViewPreferenceEditor;
private VBox mDefaultEditor;
private HBox mControlBox;
/**
* Constructs a preferences editor instance
*/
public PreferencesEditor()
{
}
private VBox mEditorAndButtonsBox;
private Node mEditor;
private HBox mButtonsBox;
public Stage getStage()
{
try
{
Window window = getBorderPane().getScene().getWindow();
Window window = getParentBox().getScene().getWindow();
if(window instanceof Stage)
{
@@ -104,7 +98,7 @@ public class PreferencesEditor extends Application
public void start(Stage stage) throws Exception
{
stage.setTitle("Preferences");
Scene scene = new Scene(getBorderPane(), 900, 500);
Scene scene = new Scene(getParentBox(), 900, 500);
stage.setScene(scene);
stage.show();
}
@@ -130,8 +124,7 @@ public class PreferencesEditor extends Application
{
TreeItem treeItem = li.next();
if(treeItem.getValue() instanceof PreferenceEditorType &&
((PreferenceEditorType)treeItem.getValue()).equals(type))
if(treeItem.getValue() instanceof PreferenceEditorType && (treeItem.getValue()).equals(type))
{
return treeItem;
}
@@ -153,31 +146,48 @@ public class PreferencesEditor extends Application
/**
* Primary layout for the editor window
*/
private BorderPane getBorderPane()
private HBox getParentBox()
{
if(mBorderPane == null)
if(mParentBox == null)
{
mBorderPane = new BorderPane();
mBorderPane.setLeft(getEditorSelectionTreeView());
mBorderPane.setCenter(getDefaultEditor());
mBorderPane.setBottom(getControlBox());
mParentBox = new HBox();
mParentBox.getChildren().add(getEditorSelectionTreeView());
HBox.setHgrow(getEditorAndButtonsBox(), Priority.ALWAYS);
mParentBox.getChildren().add(getEditorAndButtonsBox());
}
return mBorderPane;
return mParentBox;
}
private VBox getDefaultEditor()
private VBox getEditorAndButtonsBox()
{
if(mDefaultEditor == null)
if(mEditorAndButtonsBox == null)
{
mDefaultEditor = new VBox();
mDefaultEditor.setPadding(new Insets(10, 10, 10, 10));
Label label = new Label("Please select a preference ...");
mDefaultEditor.getChildren().add(label);
mEditorAndButtonsBox = new VBox();
mEditor = getDefaultEditor();
VBox.setVgrow(getDefaultEditor(), Priority.ALWAYS);
VBox.setVgrow(getButtonsBox(), Priority.NEVER);
mEditorAndButtonsBox.getChildren().addAll(getDefaultEditor(), getButtonsBox());
}
return mDefaultEditor;
return mEditorAndButtonsBox;
}
private Node getDefaultEditor()
{
Node editor = mEditors.get(PreferenceEditorType.DEFAULT);
if(editor == null)
{
VBox defaultEditor = new VBox();
defaultEditor.setPadding(new Insets(10, 10, 10, 10));
Label label = new Label("Please select a preference ...");
defaultEditor.getChildren().add(label);
mEditors.put(PreferenceEditorType.DEFAULT, defaultEditor);
editor = defaultEditor;
}
return editor;
}
/**
@@ -189,6 +199,12 @@ public class PreferencesEditor extends Application
{
TreeItem<String> treeRoot = new TreeItem<>("Root node");
TreeItem<String> audioItem = new TreeItem<>("Audio");
audioItem.getChildren().add(new TreeItem(PreferenceEditorType.AUDIO_PLAYBACK));
audioItem.getChildren().add(new TreeItem(PreferenceEditorType.AUDIO_RECORD));
treeRoot.getChildren().add(audioItem);
audioItem.setExpanded(true);
TreeItem<String> decoderItem = new TreeItem<>("Decoder");
decoderItem.getChildren().add(new TreeItem(PreferenceEditorType.JMBE_LIBRARY));
treeRoot.getChildren().add(decoderItem);
@@ -217,43 +233,56 @@ public class PreferencesEditor extends Application
treeRoot.setExpanded(true);
mEditorSelectionTreeView.getSelectionModel().setSelectionMode(SelectionMode.SINGLE);
mEditorSelectionTreeView.getSelectionModel().selectedItemProperty().addListener(new EditorTreeSelectionListener());
mEditorSelectionTreeView.setMinWidth(Control.USE_PREF_SIZE);
}
return mEditorSelectionTreeView;
}
/**
* Decode event view preferences editor
*/
private DecodeEventViewPreferenceEditor getDecodeEventViewPreferenceEditor()
{
if(mDecodeEventViewPreferenceEditor == null)
{
mDecodeEventViewPreferenceEditor = new DecodeEventViewPreferenceEditor(getUserPreferences());
}
return mDecodeEventViewPreferenceEditor;
}
/**
* Control box with OK button.
*/
private HBox getControlBox()
private HBox getButtonsBox()
{
if(mControlBox == null)
if(mButtonsBox == null)
{
mControlBox = new HBox();
mButtonsBox = new HBox();
mButtonsBox.setMaxWidth(Double.MAX_VALUE);
Button okButton = new Button("Ok");
okButton.setOnAction(event -> {
Stage stage = (Stage)getBorderPane().getScene().getWindow();
Stage stage = (Stage)getParentBox().getScene().getWindow();
stage.close();
});
HBox.setMargin(okButton, new Insets(5, 5, 5, 5));
mControlBox.setAlignment(Pos.CENTER_RIGHT);
mControlBox.getChildren().add(okButton);
mButtonsBox.setAlignment(Pos.CENTER_RIGHT);
mButtonsBox.getChildren().add(okButton);
}
return mControlBox;
return mButtonsBox;
}
private void setEditor(PreferenceEditorType type)
{
Node editor = mEditors.get(type);
if(editor == null)
{
if(type == PreferenceEditorType.DEFAULT)
{
editor = getDefaultEditor();
}
else
{
editor = PreferenceEditorFactory.getEditor(type, getUserPreferences());
mEditors.put(type, editor);
}
}
getEditorAndButtonsBox().getChildren().remove(mEditor);
VBox.setVgrow(editor, Priority.ALWAYS);
mEditor = editor;
getEditorAndButtonsBox().getChildren().add(0, mEditor);
}
/**
@@ -263,44 +292,22 @@ public class PreferencesEditor extends Application
*/
public class EditorTreeSelectionListener implements ChangeListener
{
private Map<PreferenceEditorType,Node> mEditors = new HashMap<>();
@Override
public void changed(ObservableValue observable, Object oldValue, Object newValue)
{
Node editor = getEditor(newValue);
BorderPane.setAlignment(editor, Pos.CENTER_LEFT);
getBorderPane().setCenter(editor);
}
private Node getEditor(Object treeNodeItem)
{
if(treeNodeItem instanceof TreeItem)
if(newValue instanceof TreeItem)
{
Object value = ((TreeItem)treeNodeItem).getValue();
Object value = ((TreeItem)newValue).getValue();
if(value instanceof PreferenceEditorType)
{
PreferenceEditorType type = (PreferenceEditorType)value;
Node editor = mEditors.get(type);
if(editor != null)
{
return editor;
}
editor = PreferenceEditorFactory.getEditor(type, getUserPreferences());
if(editor != null)
{
mEditors.put(type, editor);
return editor;
}
setEditor((PreferenceEditorType)value);
return;
}
}
return getDefaultEditor();
setEditor(PreferenceEditorType.DEFAULT);
}
}

View File

@@ -0,0 +1,343 @@
/*
* *****************************************************************************
* Copyright (C) 2014-2020 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.gui.preference.playback;
import com.google.common.eventbus.Subscribe;
import io.github.dsheirer.audio.AudioFormats;
import io.github.dsheirer.eventbus.MyEventBus;
import io.github.dsheirer.preference.PreferenceType;
import io.github.dsheirer.preference.UserPreferences;
import io.github.dsheirer.preference.playback.PlaybackPreference;
import io.github.dsheirer.source.mixer.MixerChannelConfiguration;
import io.github.dsheirer.source.mixer.MixerManager;
import javafx.beans.value.ChangeListener;
import javafx.beans.value.ObservableValue;
import javafx.event.ActionEvent;
import javafx.event.EventHandler;
import javafx.geometry.HPos;
import javafx.geometry.Insets;
import javafx.geometry.Orientation;
import javafx.geometry.Pos;
import javafx.scene.control.Button;
import javafx.scene.control.ComboBox;
import javafx.scene.control.Label;
import javafx.scene.control.Separator;
import javafx.scene.layout.GridPane;
import javafx.scene.layout.HBox;
import javafx.scene.layout.Priority;
import javafx.scene.paint.Color;
import jiconfont.icons.font_awesome.FontAwesome;
import jiconfont.javafx.IconNode;
import org.controlsfx.control.ToggleSwitch;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import javax.sound.sampled.AudioSystem;
import javax.sound.sampled.Clip;
import javax.sound.sampled.DataLine;
import java.nio.ByteBuffer;
import java.nio.ByteOrder;
import java.nio.ShortBuffer;
/**
* Preference settings for audio playback
*/
public class PlaybackPreferenceEditor extends HBox
{
private final static Logger mLog = LoggerFactory.getLogger(PlaybackPreferenceEditor.class);
private PlaybackPreference mPlaybackPreference;
private GridPane mEditorPane;
private ComboBox<MixerChannelConfiguration> mMixerComboBox;
private Button mMixerTestButton;
private ToggleSwitch mUseAudioSegmentStartToneSwitch;
private Button mTestStartToneButton;
private ToggleSwitch mUseAudioSegmentPreemptToneSwitch;
private Button mTestPreemptToneButton;
private ComboBox<ToneFrequency> mStartToneFrequencyComboBox;
private ComboBox<ToneVolume> mStartToneVolumeComboBox;
private ComboBox<ToneFrequency> mPreemptToneFrequencyComboBox;
private ComboBox<ToneVolume> mPreemptToneVolumeComboBox;
public PlaybackPreferenceEditor(UserPreferences userPreferences)
{
mPlaybackPreference = userPreferences.getPlaybackPreference();
HBox.setHgrow(getEditorPane(), Priority.ALWAYS);
getChildren().add(getEditorPane());
}
private GridPane getEditorPane()
{
if(mEditorPane == null)
{
int row = 0;
mEditorPane = new GridPane();
mEditorPane.setPadding(new Insets(10, 10, 10, 10));
mEditorPane.setHgap(10);
mEditorPane.setVgap(10);
Label outputLabel = new Label("Audio Output Device");
GridPane.setHalignment(outputLabel, HPos.RIGHT);
mEditorPane.add(outputLabel, 0, row, 2, 1);
mEditorPane.add(getMixerComboBox(), 2, row, 3, 1);
mEditorPane.add(getMixerTestButton(), 5, row);
mEditorPane.add(new Separator(Orientation.HORIZONTAL), 0, ++row, 6, 1);
mEditorPane.add(new Label("Audio Playback Insert Tones"), 0, ++row, 2, 1);
mEditorPane.add(getUseAudioSegmentStartToneSwitch(), 0, ++row);
mEditorPane.add(new Label("Start Tone"), 1, row, 3, 1);
Label startFrequencyLabel = new Label("Frequency:");
GridPane.setHalignment(startFrequencyLabel, HPos.RIGHT);
mEditorPane.add(startFrequencyLabel, 1, ++row);
mEditorPane.add(getStartToneFrequencyComboBox(), 2, row);
Label startVolumeLabel = new Label("Volume:");
GridPane.setHalignment(startVolumeLabel, HPos.RIGHT);
mEditorPane.add(startVolumeLabel, 3, row);
mEditorPane.add(getStartToneVolumeComboBox(), 4, row);
mEditorPane.add(getTestStartToneButton(), 5, row);
mEditorPane.add(getUseAudioSegmentPreemptToneSwitch(), 0, ++row);
mEditorPane.add(new Label("Priority Preempt Tone"), 1, row, 3, 1);
Label preemptFrequencyLabel = new Label("Frequency:");
GridPane.setHalignment(preemptFrequencyLabel, HPos.RIGHT);
mEditorPane.add(preemptFrequencyLabel, 1, ++row);
mEditorPane.add(getPreemptToneFrequencyComboBox(), 2, row);
Label preemptVolumeLabel = new Label("Volume:");
GridPane.setHalignment(preemptVolumeLabel, HPos.RIGHT);
mEditorPane.add(preemptVolumeLabel, 3, row);
mEditorPane.add(getPreemptToneVolumeComboBox(), 4, row);
mEditorPane.add(getTestPreemptToneButton(), 5, row);
}
return mEditorPane;
}
private ComboBox<MixerChannelConfiguration> getMixerComboBox()
{
if(mMixerComboBox == null)
{
mMixerComboBox = new ComboBox<>();
mMixerComboBox.getItems().addAll(MixerManager.getOutputMixers());
mMixerComboBox.getSelectionModel().select(mPlaybackPreference.getMixerChannelConfiguration());
mMixerComboBox.getSelectionModel().selectedItemProperty()
.addListener((observable, oldValue, newValue) -> mPlaybackPreference.setMixerChannelConfiguration(newValue));
}
return mMixerComboBox;
}
public Button getMixerTestButton()
{
if(mMixerTestButton == null)
{
mMixerTestButton = new Button("Test");
IconNode iconNode = new IconNode(FontAwesome.PLAY);
iconNode.setFill(Color.CORNFLOWERBLUE);
mMixerTestButton.setGraphic(iconNode);
mMixerTestButton.setOnAction(event -> play(mPlaybackPreference.getMixerTestTone()));
}
return mMixerTestButton;
}
private ToggleSwitch getUseAudioSegmentStartToneSwitch()
{
if(mUseAudioSegmentStartToneSwitch == null)
{
mUseAudioSegmentStartToneSwitch = new ToggleSwitch();
mUseAudioSegmentStartToneSwitch.setAlignment(Pos.BASELINE_RIGHT);
mUseAudioSegmentStartToneSwitch.setSelected(mPlaybackPreference.getUseAudioSegmentStartTone());
mUseAudioSegmentStartToneSwitch.selectedProperty().addListener(new ChangeListener<Boolean>()
{
@Override
public void changed(ObservableValue<? extends Boolean> observable, Boolean oldValue, Boolean newValue)
{
mPlaybackPreference.setUseAudioSegmentStartTone(newValue);
}
});
}
return mUseAudioSegmentStartToneSwitch;
}
private ToggleSwitch getUseAudioSegmentPreemptToneSwitch()
{
if(mUseAudioSegmentPreemptToneSwitch == null)
{
mUseAudioSegmentPreemptToneSwitch = new ToggleSwitch();
mUseAudioSegmentPreemptToneSwitch.setSelected(mPlaybackPreference.getUseAudioSegmentPreemptTone());
mUseAudioSegmentPreemptToneSwitch.selectedProperty().addListener(new ChangeListener<Boolean>()
{
@Override
public void changed(ObservableValue<? extends Boolean> observable, Boolean oldValue, Boolean newValue)
{
mPlaybackPreference.setUseAudioSegmentPreemptTone(newValue);
}
});
}
return mUseAudioSegmentPreemptToneSwitch;
}
public Button getTestStartToneButton()
{
if(mTestStartToneButton == null)
{
mTestStartToneButton = new Button("Test");
IconNode iconNode = new IconNode(FontAwesome.PLAY);
iconNode.setFill(Color.CORNFLOWERBLUE);
mTestStartToneButton.setGraphic(iconNode);
mTestStartToneButton.setOnAction(new EventHandler<ActionEvent>()
{
@Override
public void handle(ActionEvent event)
{
play(mPlaybackPreference.getStartTone());
}
});
mTestStartToneButton.disableProperty().bind(getUseAudioSegmentStartToneSwitch().selectedProperty().not());
}
return mTestStartToneButton;
}
public Button getTestPreemptToneButton()
{
if(mTestPreemptToneButton == null)
{
mTestPreemptToneButton = new Button("Test");
IconNode iconNode = new IconNode(FontAwesome.PLAY);
iconNode.setFill(Color.CORNFLOWERBLUE);
mTestPreemptToneButton.setGraphic(iconNode);
mTestPreemptToneButton.setOnAction(new EventHandler<ActionEvent>()
{
@Override
public void handle(ActionEvent event)
{
play(mPlaybackPreference.getPreemptTone());
}
});
mTestPreemptToneButton.disableProperty().bind(getUseAudioSegmentPreemptToneSwitch().selectedProperty().not());
}
return mTestPreemptToneButton;
}
public ComboBox<ToneFrequency> getStartToneFrequencyComboBox()
{
if(mStartToneFrequencyComboBox == null)
{
mStartToneFrequencyComboBox = new ComboBox<>();
mStartToneFrequencyComboBox.getItems().addAll(ToneFrequency.values());
mStartToneFrequencyComboBox.getSelectionModel().select(mPlaybackPreference.getStartToneFrequency());
mStartToneFrequencyComboBox.getSelectionModel().selectedItemProperty()
.addListener((observable, oldValue, newValue) -> mPlaybackPreference.setStartToneFrequency(newValue));
mStartToneFrequencyComboBox.disableProperty().bind(getUseAudioSegmentStartToneSwitch().selectedProperty().not());
}
return mStartToneFrequencyComboBox;
}
public ComboBox<ToneVolume> getStartToneVolumeComboBox()
{
if(mStartToneVolumeComboBox == null)
{
mStartToneVolumeComboBox = new ComboBox<>();
mStartToneVolumeComboBox.getItems().addAll(ToneVolume.values());
mStartToneVolumeComboBox.getSelectionModel().select(mPlaybackPreference.getStartToneVolume());
mStartToneVolumeComboBox.getSelectionModel().selectedItemProperty()
.addListener((observable, oldValue, newValue) -> mPlaybackPreference.setStartToneVolume(newValue));
mStartToneVolumeComboBox.disableProperty().bind(getUseAudioSegmentStartToneSwitch().selectedProperty().not());
}
return mStartToneVolumeComboBox;
}
public ComboBox<ToneFrequency> getPreemptToneFrequencyComboBox()
{
if(mPreemptToneFrequencyComboBox == null)
{
mPreemptToneFrequencyComboBox = new ComboBox<>();
mPreemptToneFrequencyComboBox.getItems().addAll(ToneFrequency.values());
mPreemptToneFrequencyComboBox.getSelectionModel().select(mPlaybackPreference.getPreemptToneFrequency());
mPreemptToneFrequencyComboBox.getSelectionModel().selectedItemProperty()
.addListener((observable, oldValue, newValue) -> mPlaybackPreference.setPreemptToneFrequency(newValue));
mPreemptToneFrequencyComboBox.disableProperty().bind(getUseAudioSegmentPreemptToneSwitch().selectedProperty().not());
}
return mPreemptToneFrequencyComboBox;
}
public ComboBox<ToneVolume> getPreemptToneVolumeComboBox()
{
if(mPreemptToneVolumeComboBox == null)
{
mPreemptToneVolumeComboBox = new ComboBox<>();
mPreemptToneVolumeComboBox.getItems().addAll(ToneVolume.values());
mPreemptToneVolumeComboBox.getSelectionModel().select(mPlaybackPreference.getPreemptToneVolume());
mPreemptToneVolumeComboBox.getSelectionModel().selectedItemProperty()
.addListener((observable, oldValue, newValue) -> mPlaybackPreference.setPreemptToneVolume(newValue));
mPreemptToneVolumeComboBox.disableProperty().bind(getUseAudioSegmentPreemptToneSwitch().selectedProperty().not());
}
return mPreemptToneVolumeComboBox;
}
/**
* Plays the audio buffer over the default mono playback device
* @param audioSamples with 8 kHz mono PCM samples
*/
private void play(float[] audioSamples)
{
if(audioSamples != null)
{
/* Little-endian byte buffer */
ByteBuffer buffer = ByteBuffer.allocate(audioSamples.length * 2).order(ByteOrder.LITTLE_ENDIAN);
ShortBuffer shortBuffer = buffer.asShortBuffer();
for(float sample : audioSamples)
{
shortBuffer.put((short) (sample * Short.MAX_VALUE));
}
byte[] bytes = buffer.array();
DataLine.Info info = new DataLine.Info(Clip.class, AudioFormats.PCM_SIGNED_8KHZ_16BITS_MONO);
if(!AudioSystem.isLineSupported(info))
{
mLog.error("Audio clip playback is not supported on this system");
return;
}
try
{
Clip clip = (Clip)AudioSystem.getLine(info);
clip.open(AudioFormats.PCM_SIGNED_8KHZ_16BITS_MONO, bytes, 0, bytes.length);
clip.start();
}
catch(Exception e)
{
mLog.error("Error attempting to play audio test tone");
}
}
}
}

View File

@@ -0,0 +1,78 @@
/*
* *****************************************************************************
* Copyright (C) 2014-2020 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.gui.preference.playback;
public enum ToneFrequency
{
F300(300),
F400(400),
F500(500),
F600(600),
F700(700),
F800(800),
F900(900),
F1000(1000),
F1100(1100),
F1200(1200),
F1300(1300),
F1400(1400),
F1500(1500),
F1600(1600),
F1700(1700),
F1800(1800),
F1900(1900),
F2000(2000),
F2100(2100),
F2200(2200),
F2300(2300),
F2400(2400),
F2500(2500);
private int mValue;
ToneFrequency(int value)
{
mValue = value;
}
public int getValue()
{
return mValue;
}
@Override
public String toString()
{
return String.valueOf(mValue);
}
public static ToneFrequency fromValue(int value)
{
for(ToneFrequency toneFrequency: ToneFrequency.values())
{
if(toneFrequency.getValue() == value)
{
return toneFrequency;
}
}
return F1200; //default
}
}

View File

@@ -0,0 +1,68 @@
/*
* *****************************************************************************
* Copyright (C) 2014-2020 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.gui.preference.playback;
import io.github.dsheirer.dsp.mixer.Oscillator;
/**
* Utility for generating tones for audio clips
*/
public class ToneUtil
{
//Maximum tone volume: 0.0 <> 1.0
private static final double MAX_TONE_VOLUME = 0.5f;
/**
* Generates a tone using the specified parameters
* @param toneFrequency of the tone
* @param toneVolume of the tone 1-10, to a maximum 90% gain
* @param sampleCount number of 8 kHz samples to generate
* @return buffer of audio
*/
public static float[] getTone(ToneFrequency toneFrequency, ToneVolume toneVolume, int sampleCount)
{
Oscillator oscillator = new Oscillator(toneFrequency.getValue(), 8000.0);
float[] samples = oscillator.generateReal(sampleCount);
double gain = MAX_TONE_VOLUME * ((double)toneVolume.getValue() / 10.0);
for(int x = 0; x < samples.length; x++)
{
samples[x] *= gain;
}
//Attenuate beginning and end samples
if(sampleCount > 10)
{
for(int x = 0; x < 10; x++)
{
samples[x] *= (float)x / 10.0f;
}
for(int x = 0; x < 10; x++)
{
samples[samples.length - 1 - x] *= (float)x / 10.0f;
}
}
return samples;
}
}

View File

@@ -0,0 +1,65 @@
/*
* *****************************************************************************
* Copyright (C) 2014-2020 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.gui.preference.playback;
public enum ToneVolume
{
V1(1),
V2(2),
V3(3),
V4(4),
V5(5),
V6(6),
V7(7),
V8(8),
V9(9),
V10(10);
private int mValue;
ToneVolume(int value)
{
mValue = value;
}
public int getValue()
{
return mValue;
}
@Override
public String toString()
{
return String.valueOf(mValue);
}
public static ToneVolume fromValue(int value)
{
for(ToneVolume toneVolume: ToneVolume.values())
{
if(toneVolume.getValue() == value)
{
return toneVolume;
}
}
return V3; //default
}
}

View File

@@ -0,0 +1,91 @@
/*
* *****************************************************************************
* Copyright (C) 2014-2020 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.gui.preference.record;
import io.github.dsheirer.preference.UserPreferences;
import io.github.dsheirer.preference.record.RecordPreference;
import io.github.dsheirer.record.RecordFormat;
import javafx.beans.value.ChangeListener;
import javafx.beans.value.ObservableValue;
import javafx.geometry.Insets;
import javafx.scene.control.ComboBox;
import javafx.scene.control.Label;
import javafx.scene.layout.GridPane;
import javafx.scene.layout.HBox;
import javafx.scene.layout.Priority;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
/**
* Preference settings for recording
*/
public class RecordPreferenceEditor extends HBox
{
private final static Logger mLog = LoggerFactory.getLogger(RecordPreferenceEditor.class);
private RecordPreference mRecordPreference;
private GridPane mEditorPane;
private ComboBox<RecordFormat> mRecordFormatComboBox;
public RecordPreferenceEditor(UserPreferences userPreferences)
{
mRecordPreference = userPreferences.getRecordPreference();
HBox.setHgrow(getEditorPane(), Priority.ALWAYS);
getChildren().add(getEditorPane());
}
private GridPane getEditorPane()
{
if(mEditorPane == null)
{
mEditorPane = new GridPane();
mEditorPane.setPadding(new Insets(10, 10, 10, 10));
mEditorPane.setHgap(10);
mEditorPane.setVgap(10);
Label label = new Label("Audio Recording Format:");
mEditorPane.add(label, 0, 0);
mEditorPane.add(getRecordFormatComboBox(), 1, 0);
}
return mEditorPane;
}
private ComboBox<RecordFormat> getRecordFormatComboBox()
{
if(mRecordFormatComboBox == null)
{
mRecordFormatComboBox = new ComboBox<>();
mRecordFormatComboBox.getItems().addAll(RecordFormat.values());
mRecordFormatComboBox.getSelectionModel().select(mRecordPreference.getAudioRecordFormat());
mRecordFormatComboBox.getSelectionModel().selectedItemProperty().addListener(new ChangeListener<RecordFormat>()
{
@Override
public void changed(ObservableValue<? extends RecordFormat> observable, RecordFormat oldValue, RecordFormat newValue)
{
mRecordPreference.setAudioRecordFormat(newValue);
}
});
}
return mRecordFormatComboBox;
}
}

View File

@@ -64,11 +64,11 @@ public class TunerPreferenceEditor extends HBox
if(mEditorPane == null)
{
mEditorPane = new GridPane();
mEditorPane.setVgap(10);
mEditorPane.setHgap(10);
mEditorPane.setPadding(new Insets(10, 10, 10, 10));
GridPane.setMargin(getChannelizerLabel(), new Insets(0, 10, 0, 0));
GridPane.setHalignment(getChannelizerLabel(), HPos.LEFT);
mEditorPane.add(getChannelizerLabel(), 0, 0);
GridPane.setMargin(getChannelizerTypeChoiceBox(), new Insets(2, 0, 2, 0));
mEditorPane.add(getChannelizerTypeChoiceBox(), 1, 0);
mEditorPane.add(new Separator(Orientation.HORIZONTAL), 0, 1, 2, 1);
mEditorPane.add(getPolyphaseLabel(), 0, 2, 2, 1);