diff --git a/src/main/java/io/github/dsheirer/gui/preference/PreferenceEditorFactory.java b/src/main/java/io/github/dsheirer/gui/preference/PreferenceEditorFactory.java index 0ac1afe1..4256d5f4 100644 --- a/src/main/java/io/github/dsheirer/gui/preference/PreferenceEditorFactory.java +++ b/src/main/java/io/github/dsheirer/gui/preference/PreferenceEditorFactory.java @@ -20,6 +20,7 @@ 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.tuner.ChannelMultipleFrequencyPreferenceEditor; import io.github.dsheirer.gui.preference.tuner.TunerPreferenceEditor; @@ -37,6 +38,8 @@ public class PreferenceEditorFactory { case CHANNEL_EVENT: return new DecodeEventViewPreferenceEditor(userPreferences); + case JMBE_LIBRARY: + return new JmbeLibraryPreferenceEditor(userPreferences); case DIRECTORY: return new DirectoryPreferenceEditor(userPreferences); case SOURCE_CHANNEL_MULTIPLE_FREQUENCY: diff --git a/src/main/java/io/github/dsheirer/gui/preference/PreferenceEditorType.java b/src/main/java/io/github/dsheirer/gui/preference/PreferenceEditorType.java index 652df137..8d63a69a 100644 --- a/src/main/java/io/github/dsheirer/gui/preference/PreferenceEditorType.java +++ b/src/main/java/io/github/dsheirer/gui/preference/PreferenceEditorType.java @@ -26,6 +26,7 @@ package io.github.dsheirer.gui.preference; public enum PreferenceEditorType { CHANNEL_EVENT("Channel Events"), + JMBE_LIBRARY("JMBE Audio Library"), DIRECTORY("Directories"), SOURCE_CHANNEL_MULTIPLE_FREQUENCY("Channel - Multiple Frequency"), SOURCE_TUNER_CHANNELIZER("Tuner Channelizer"), diff --git a/src/main/java/io/github/dsheirer/gui/preference/PreferencesEditor.java b/src/main/java/io/github/dsheirer/gui/preference/PreferencesEditor.java index e00105a2..2e9c08b5 100644 --- a/src/main/java/io/github/dsheirer/gui/preference/PreferencesEditor.java +++ b/src/main/java/io/github/dsheirer/gui/preference/PreferencesEditor.java @@ -189,6 +189,11 @@ public class PreferencesEditor extends Application { TreeItem treeRoot = new TreeItem<>("Root node"); + TreeItem decoderItem = new TreeItem<>("Decoder"); + decoderItem.getChildren().add(new TreeItem(PreferenceEditorType.JMBE_LIBRARY)); + treeRoot.getChildren().add(decoderItem); + decoderItem.setExpanded(true); + TreeItem displayItem = new TreeItem<>("Display"); displayItem.getChildren().add(new TreeItem(PreferenceEditorType.CHANNEL_EVENT)); displayItem.getChildren().add(new TreeItem(PreferenceEditorType.TALKGROUP_FORMAT)); diff --git a/src/main/java/io/github/dsheirer/gui/preference/decoder/JmbeLibraryPreferenceEditor.java b/src/main/java/io/github/dsheirer/gui/preference/decoder/JmbeLibraryPreferenceEditor.java new file mode 100644 index 00000000..69d98a98 --- /dev/null +++ b/src/main/java/io/github/dsheirer/gui/preference/decoder/JmbeLibraryPreferenceEditor.java @@ -0,0 +1,170 @@ +/* + * ****************************************************************************** + * sdrtrunk + * 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 + * ***************************************************************************** + */ + +package io.github.dsheirer.gui.preference.decoder; + +import com.google.common.eventbus.Subscribe; +import io.github.dsheirer.eventbus.MyEventBus; +import io.github.dsheirer.preference.PreferenceType; +import io.github.dsheirer.preference.UserPreferences; +import io.github.dsheirer.preference.decoder.JmbeLibraryPreference; +import javafx.event.ActionEvent; +import javafx.event.EventHandler; +import javafx.geometry.Insets; +import javafx.scene.control.Button; +import javafx.scene.control.Label; +import javafx.scene.layout.GridPane; +import javafx.scene.layout.HBox; +import javafx.scene.layout.Priority; +import javafx.stage.FileChooser; +import javafx.stage.Stage; +import org.slf4j.Logger; +import org.slf4j.LoggerFactory; + +import java.io.File; +import java.nio.file.Path; + + +/** + * Preference settings for decoders + */ +public class JmbeLibraryPreferenceEditor extends HBox +{ + private final static Logger mLog = LoggerFactory.getLogger(JmbeLibraryPreferenceEditor.class); + + private static final String PATH_NOT_SET = "(not set)"; + private JmbeLibraryPreference mJmbeLibraryPreference; + private GridPane mEditorPane; + private Label mJmbeLibraryLabel; + private Button mPathToJmbeLibraryButton; + private Button mPathToJmbeLibraryResetButton; + private Label mPathToJmbeLibraryLabel; + + public JmbeLibraryPreferenceEditor(UserPreferences userPreferences) + { + mJmbeLibraryPreference = userPreferences.getJmbeLibraryPreference(); + + //Register to receive directory preference update notifications so we can update the path labels + MyEventBus.getEventBus().register(this); + + 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)); + + int row = 0; + + GridPane.setMargin(getJmbeLibraryLabel(), new Insets(0, 10, 0, 0)); + mEditorPane.add(getJmbeLibraryLabel(), 0, row); + + GridPane.setMargin(getPathToJmbeLibraryLabel(), new Insets(0, 10, 0, 0)); + mEditorPane.add(getPathToJmbeLibraryLabel(), 1, row); + + GridPane.setMargin(getPathToJmbeLibraryButton(), new Insets(2, 10, 2, 0)); + mEditorPane.add(getPathToJmbeLibraryButton(), 2, row); + + GridPane.setMargin(getPathToJmbeLibraryResetButton(), new Insets(2, 0, 2, 0)); + mEditorPane.add(getPathToJmbeLibraryResetButton(), 3, row++); + } + + return mEditorPane; + } + + private Label getJmbeLibraryLabel() + { + if(mJmbeLibraryLabel == null) + { + mJmbeLibraryLabel = new Label("JMBE Audio Library:"); + } + + return mJmbeLibraryLabel; + } + + private Button getPathToJmbeLibraryButton() + { + if(mPathToJmbeLibraryButton == null) + { + mPathToJmbeLibraryButton = new Button("Change..."); + mPathToJmbeLibraryButton.setOnAction(new EventHandler() + { + @Override + public void handle(ActionEvent event) + { + FileChooser fileChooser = new FileChooser(); + fileChooser.setTitle("Select JMBE Audio Library Location"); + Stage stage = (Stage)getPathToJmbeLibraryButton().getScene().getWindow(); + File selected = fileChooser.showOpenDialog(stage); + + if(selected != null) + { + mJmbeLibraryPreference.setPathJmbeLibrary(selected.toPath()); + } + } + }); + } + + return mPathToJmbeLibraryButton; + } + + private Button getPathToJmbeLibraryResetButton() + { + if(mPathToJmbeLibraryResetButton == null) + { + mPathToJmbeLibraryResetButton = new Button("Reset"); + mPathToJmbeLibraryResetButton.setOnAction(new EventHandler() + { + @Override + public void handle(ActionEvent event) + { + mJmbeLibraryPreference.resetPathJmbeLibrary(); + } + }); + } + + return mPathToJmbeLibraryResetButton; + } + + private Label getPathToJmbeLibraryLabel() + { + if(mPathToJmbeLibraryLabel == null) + { + Path path = mJmbeLibraryPreference.getPathJmbeLibrary(); + mPathToJmbeLibraryLabel = new Label(path != null ? path.toString() : PATH_NOT_SET); + } + + return mPathToJmbeLibraryLabel; + } + + @Subscribe + public void preferenceUpdated(PreferenceType preferenceType) + { + if(preferenceType != null && preferenceType == PreferenceType.JMBE_LIBRARY) + { + Path path = mJmbeLibraryPreference.getPathJmbeLibrary(); + getPathToJmbeLibraryLabel().setText(path != null ? path.toString() : PATH_NOT_SET); + } + } +} diff --git a/src/main/java/io/github/dsheirer/module/decode/DecoderFactory.java b/src/main/java/io/github/dsheirer/module/decode/DecoderFactory.java index 53cb6395..722956af 100644 --- a/src/main/java/io/github/dsheirer/module/decode/DecoderFactory.java +++ b/src/main/java/io/github/dsheirer/module/decode/DecoderFactory.java @@ -254,7 +254,7 @@ public class DecoderFactory modules.add(new P25DecoderState(channel)); } - modules.add(new P25AudioModule()); + modules.add(new P25AudioModule(userPreferences)); //Add a channel rotation monitor when we have multiple control channel frequencies specified if(channel.getSourceConfiguration() instanceof SourceConfigTunerMultipleFrequency && diff --git a/src/main/java/io/github/dsheirer/module/decode/p25/audio/P25AudioModule.java b/src/main/java/io/github/dsheirer/module/decode/p25/audio/P25AudioModule.java index be9ddf16..ab8dc38e 100644 --- a/src/main/java/io/github/dsheirer/module/decode/p25/audio/P25AudioModule.java +++ b/src/main/java/io/github/dsheirer/module/decode/p25/audio/P25AudioModule.java @@ -1,30 +1,38 @@ -/******************************************************************************* - * sdr-trunk - * Copyright (C) 2014-2018 Dennis Sheirer +/* + * ****************************************************************************** + * sdrtrunk + * 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 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. + * 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 - * - ******************************************************************************/ + * You should have received a copy of the GNU General Public License + * along with this program. If not, see + * ***************************************************************************** + */ package io.github.dsheirer.module.decode.p25.audio; +import com.google.common.eventbus.Subscribe; import io.github.dsheirer.audio.AbstractAudioModule; import io.github.dsheirer.audio.AudioFormats; import io.github.dsheirer.audio.squelch.SquelchState; import io.github.dsheirer.dsp.gain.NonClippingGain; +import io.github.dsheirer.eventbus.MyEventBus; import io.github.dsheirer.message.IMessage; import io.github.dsheirer.message.IMessageListener; import io.github.dsheirer.module.decode.p25.message.hdu.HDUMessage; import io.github.dsheirer.module.decode.p25.message.ldu.LDU1Message; import io.github.dsheirer.module.decode.p25.message.ldu.LDU2Message; import io.github.dsheirer.module.decode.p25.message.ldu.LDUMessage; +import io.github.dsheirer.preference.PreferenceType; +import io.github.dsheirer.preference.UserPreferences; import io.github.dsheirer.sample.Listener; import io.github.dsheirer.sample.buffer.ReusableAudioPacket; import io.github.dsheirer.sample.buffer.ReusableAudioPacketQueue; @@ -33,6 +41,12 @@ import jmbe.iface.AudioConverter; import org.slf4j.Logger; import org.slf4j.LoggerFactory; +import java.lang.reflect.InvocationTargetException; +import java.net.MalformedURLException; +import java.net.URL; +import java.net.URLClassLoader; +import java.nio.file.Path; + public class P25AudioModule extends AbstractAudioModule implements Listener, IMessageListener { private static final Logger mLog = LoggerFactory.getLogger(P25AudioModule.class); @@ -48,9 +62,12 @@ public class P25AudioModule extends AbstractAudioModule implements Listener= 3 && - library.getBuildVersion() >= 3) || library.getMajorVersion() >= 1) + try { - mAudioConverter = library.getAudioConverter(IMBE_CODEC, AudioFormats.PCM_SIGNED_8KHZ_16BITS_MONO); + URLClassLoader childClassLoader = new URLClassLoader(new URL[]{path.toUri().toURL()}, + this.getClass().getClassLoader()); - if(mAudioConverter != null) + Class classToLoad = Class.forName("jmbe.JMBEAudioLibrary", true, childClassLoader); + + Object instance = classToLoad.getDeclaredConstructor().newInstance(); + + if(instance instanceof AudioConversionLibrary) { - mCanConvertAudio = true; + AudioConversionLibrary library = (AudioConversionLibrary)instance; - if(!mLibraryLoadStatusLogged) + if((library.getMajorVersion() == 0 && library.getMinorVersion() >= 3 && + library.getBuildVersion() >= 3) || library.getMajorVersion() >= 1) { - StringBuilder sb = new StringBuilder(); - sb.append("JMBE audio conversion library ["); - sb.append(library.getVersion()); - sb.append("] successfully loaded - P25 audio will be available"); - - mLog.info(sb.toString()); - - mLibraryLoadStatusLogged = true; + audioConverter = library.getAudioConverter(IMBE_CODEC, AudioFormats.PCM_SIGNED_8KHZ_16BITS_MONO); + } + else + { + mLog.warn("JMBE library version 0.3.3 or higher is required - found: " + library.getVersion()); } } else @@ -221,33 +252,75 @@ public class P25AudioModule extends AbstractAudioModule implements Listener { + private JmbeLibraryPreference mJmbeLibraryPreference; private DecodeEventPreference mDecodeEventPreference; private DirectoryPreference mDirectoryPreference; private ChannelMultiFrequencyPreference mChannelMultiFrequencyPreference; @@ -70,6 +72,14 @@ public class UserPreferences implements Listener return mDecodeEventPreference; } + /** + * Decoder preferences + */ + public JmbeLibraryPreference getJmbeLibraryPreference() + { + return mJmbeLibraryPreference; + } + /** * Directory preferences */ @@ -115,12 +125,13 @@ public class UserPreferences implements Listener */ private void loadPreferenceTypes() { - mDecodeEventPreference = new DecodeEventPreference(this); - mDirectoryPreference = new DirectoryPreference(this); - mChannelMultiFrequencyPreference = new ChannelMultiFrequencyPreference(this); - mPlaylistPreference = new PlaylistPreference(this, mDirectoryPreference); - mTalkgroupFormatPreference = new TalkgroupFormatPreference(this); - mTunerPreference = new TunerPreference(this); + mDecodeEventPreference = new DecodeEventPreference(this::receive); + mJmbeLibraryPreference = new JmbeLibraryPreference(this::receive); + mDirectoryPreference = new DirectoryPreference(this::receive); + mChannelMultiFrequencyPreference = new ChannelMultiFrequencyPreference(this::receive); + mPlaylistPreference = new PlaylistPreference(this::receive, mDirectoryPreference); + mTalkgroupFormatPreference = new TalkgroupFormatPreference(this::receive); + mTunerPreference = new TunerPreference(this::receive); } /** diff --git a/src/main/java/io/github/dsheirer/preference/decoder/JmbeLibraryPreference.java b/src/main/java/io/github/dsheirer/preference/decoder/JmbeLibraryPreference.java new file mode 100644 index 00000000..d34b76ac --- /dev/null +++ b/src/main/java/io/github/dsheirer/preference/decoder/JmbeLibraryPreference.java @@ -0,0 +1,120 @@ +/* + * ****************************************************************************** + * sdrtrunk + * 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 + * ***************************************************************************** + */ + +package io.github.dsheirer.preference.decoder; + +import io.github.dsheirer.preference.Preference; +import io.github.dsheirer.preference.PreferenceType; +import io.github.dsheirer.sample.Listener; +import org.slf4j.Logger; +import org.slf4j.LoggerFactory; + +import java.nio.file.Files; +import java.nio.file.Path; +import java.nio.file.Paths; +import java.util.prefs.Preferences; + +/** + * Decoder preferences + */ +public class JmbeLibraryPreference extends Preference +{ + private final static Logger mLog = LoggerFactory.getLogger(JmbeLibraryPreference.class); + private Preferences mPreferences = Preferences.userNodeForPackage(JmbeLibraryPreference.class); + + private static final String PREFERENCE_KEY_PATH_JMBE_LIBRARY = "path.jmbe.library"; + private Path mPathJmbeLibrary; + + /** + * Constructs this preference with an update listener + * + * @param updateListener to receive notifications whenever these preferences change + */ + public JmbeLibraryPreference(Listener updateListener) + { + super(updateListener); + } + + @Override + public PreferenceType getPreferenceType() + { + return PreferenceType.JMBE_LIBRARY; + } + + /* + * Path to the JMBE audio codec library + */ + public Path getPathJmbeLibrary() + { + if(mPathJmbeLibrary == null) + { + mPathJmbeLibrary = getPath(PREFERENCE_KEY_PATH_JMBE_LIBRARY, null); + } + + return mPathJmbeLibrary; + } + + /** + * Sets the path to the JMBE library + */ + public void setPathJmbeLibrary(Path path) + { + mPathJmbeLibrary = path; + mPreferences.put(PREFERENCE_KEY_PATH_JMBE_LIBRARY, path.toString()); + notifyPreferenceUpdated(); + } + + /** + * Resets (removes) the current JMBE library path + */ + public void resetPathJmbeLibrary() + { + mPreferences.remove(PREFERENCE_KEY_PATH_JMBE_LIBRARY); + mPathJmbeLibrary = null; + notifyPreferenceUpdated(); + } + + /** + * Returns the path stored in preferences and referenced by the key argument if it exists, otherwise returns the + * default path. + * + * Note: the default path is not checked for existence. + * + * @param key to the preferences service for the path + * @param defaultPath to use if the preferred path does not exist + * @return requested path + */ + private Path getPath(String key, Path defaultPath) + { + String stringPath = mPreferences.get(key, defaultPath != null ? defaultPath.toString() : null); + + if(stringPath != null && !stringPath.isEmpty()) + { + Path temp = Paths.get(stringPath); + + if(Files.exists(temp)) + { + return temp; + } + } + + return defaultPath; + } +}