/* * ***************************************************************************** * Copyright (C) 2014-2026 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.spectrum; import io.github.dsheirer.controller.channel.ChannelModel; import io.github.dsheirer.controller.channel.ChannelProcessingManager; import io.github.dsheirer.gui.playlist.radioreference.RadioReferencePlaylistActions; import io.github.dsheirer.gui.spectrum.layer.ChannelLayer; import io.github.dsheirer.gui.spectrum.layer.RadioReferenceLayer; import io.github.dsheirer.playlist.PlaylistManager; import io.github.dsheirer.properties.SystemProperties; import io.github.dsheirer.source.ISourceEventProcessor; import io.github.dsheirer.source.SourceEvent; import io.github.dsheirer.source.tuner.Tuner; import io.github.dsheirer.source.tuner.manager.DiscoveredTuner; import io.github.dsheirer.source.tuner.ui.DiscoveredTunerModel; import io.github.dsheirer.util.ThreadPool; import java.util.ArrayList; import java.util.List; import java.util.function.Consumer; import javafx.beans.property.ReadOnlyObjectProperty; import javafx.beans.property.ReadOnlyObjectWrapper; import org.slf4j.Logger; import org.slf4j.LoggerFactory; /** * Spectral display for a tuner: the spectral display with the decibel, frequency and playlist channel layers, the DFT * processing for the tuner's sample stream, and the collapsible configuration panel with a tuner menu. * * Construct on the JavaFX application thread. showTuner() and clearTuner() can be called from any thread, but they * can block while the tuner starts or stops its sample stream, so don't call them on the JavaFX application thread. */ public class TunerSpectralDisplay extends DftSpectralDisplay implements ISourceEventProcessor { private static final Logger LOGGER = LoggerFactory.getLogger(TunerSpectralDisplay.class); /** * Indicates if the main window spectral display shows a tuner. Cleared when the user turns the display off. */ public static final String SPECTRAL_DISPLAY_ENABLED = "spectral.display.enabled"; public static final String SETTINGS_PREFIX = "spectral.display"; private static final String CHANNEL_DISPLAY = "channel.display"; private static final String CHANNEL_HOVER_DETAILS = "channel.hover.details"; private static final String RADIO_REFERENCE_HOVER = "radioreference.hover"; private static final String MESSAGE_NO_TUNER = "No tuner selected - choose a tuner in the display settings"; private final ChannelLayer mChannelLayer; private final RadioReferenceLayer mRadioReferenceLayer; private final DiscoveredTunerModel mDiscoveredTunerModel; private final Object mTunerLock = new Object(); private final ReadOnlyObjectWrapper mDisplayedTuner = new ReadOnlyObjectWrapper<>(); private Consumer mTunerSelectionHandler; private Tuner mTuner; private boolean mProcessing; /** * Constructs an instance. * @param playlistManager for the channels * @param discoveredTunerModel for the list of tuners * @param saveSettings true to save setting changes, false to load the saved settings but not change them */ public TunerSpectralDisplay(PlaylistManager playlistManager, DiscoveredTunerModel discoveredTunerModel, boolean saveSettings) { this(new ChannelLayer(playlistManager != null ? playlistManager.getChannelModel() : null, playlistManager != null ? playlistManager.getChannelProcessingManager() : null), new RadioReferenceLayer(playlistManager != null && playlistManager.getRadioReference() != null ? playlistManager.getRadioReference().getFrequencyIndex() : null, playlistManager != null ? new RadioReferencePlaylistActions(playlistManager) : null), discoveredTunerModel, saveSettings); } /** * Constructs an instance. * @param channelModel for the channels, or null * @param channelProcessingManager for channel processing events, or null * @param discoveredTunerModel for the list of tuners, or null * @param saveSettings true to save setting changes, false to load the saved settings but not change them */ public TunerSpectralDisplay(ChannelModel channelModel, ChannelProcessingManager channelProcessingManager, DiscoveredTunerModel discoveredTunerModel, boolean saveSettings) { this(new ChannelLayer(channelModel, channelProcessingManager), new RadioReferenceLayer(null, null), discoveredTunerModel, saveSettings); } private TunerSpectralDisplay(ChannelLayer channelLayer, RadioReferenceLayer radioReferenceLayer, DiscoveredTunerModel discoveredTunerModel, boolean saveSettings) { //The radio reference layer draws after the channel layer, so its card is on top super(SETTINGS_PREFIX, saveSettings, channelLayer, radioReferenceLayer); mChannelLayer = channelLayer; mRadioReferenceLayer = radioReferenceLayer; mDiscoveredTunerModel = discoveredTunerModel; getSpectralDisplay().messageProperty().set(MESSAGE_NO_TUNER); SystemProperties properties = SystemProperties.getInstance(); mChannelLayer.channelDisplayProperty().set(parse(ChannelLayer.ChannelDisplay.class, properties.get(key(CHANNEL_DISPLAY), ChannelLayer.ChannelDisplay.ALL.name()), ChannelLayer.ChannelDisplay.ALL)); mChannelLayer.hoverDetailsProperty().set(properties.get(key(CHANNEL_HOVER_DETAILS), true)); mChannelLayer.channelDisplayProperty().addListener((ob, ov, nv) -> save(CHANNEL_DISPLAY, nv.name())); mChannelLayer.hoverDetailsProperty().addListener((ob, ov, nv) -> save(CHANNEL_HOVER_DETAILS, nv)); mRadioReferenceLayer.hoverCandidatesProperty().set(properties.get(key(RADIO_REFERENCE_HOVER), true)); mRadioReferenceLayer.hoverCandidatesProperty().addListener((ob, ov, nv) -> save(RADIO_REFERENCE_HOVER, nv)); installConfigPanel(); } /** * Tuner shown in the display, updated on the JavaFX application thread. */ public ReadOnlyObjectProperty displayedTunerProperty() { return mDisplayedTuner.getReadOnlyProperty(); } /** * Tuners that can be shown. */ public List getAvailableTuners() { List tuners = new ArrayList<>(); if(mDiscoveredTunerModel != null) { for(DiscoveredTuner discoveredTuner: mDiscoveredTunerModel.getAvailableTuners()) { if(discoveredTuner.hasTuner()) { tuners.add(discoveredTuner.getTuner()); } } } return tuners; } /** * Sets the handler for tuner selections in the configuration panel. The default handler shows the selected tuner. * @param handler receives the selected tuner, or null when the user selects no tuner. Called on the JavaFX thread. */ public void setTunerSelectionHandler(Consumer handler) { mTunerSelectionHandler = handler; } /** * Called by the configuration panel on the JavaFX application thread when the user selects a tuner, or null. */ void tunerSelected(Tuner tuner) { if(mTunerSelectionHandler != null) { mTunerSelectionHandler.accept(tuner); } else { ThreadPool.CACHED.submit(() -> { if(tuner != null) { showTuner(tuner); } else { clearTuner(); } }); } } /** * Currently displayed tuner, or null. */ public Tuner getTuner() { synchronized(mTunerLock) { return mTuner; } } /** * Shows the tuner in the display and stops showing the previous tuner. */ public void showTuner(Tuner tuner) { synchronized(mTunerLock) { if(tuner == mTuner) { return; } clearTuner(); if(tuner == null) { return; } mTuner = tuner; tuner.getTunerController().addListener(this); long frequency = tuner.getTunerController().getFrequency(); int sampleRate = (int)tuner.getTunerController().getSampleRate(); double sampleSize = tuner.getSampleSize(); runOnFxThread(() -> { if(getTuner() == tuner) { SpectralDisplayModel model = getSpectralDisplay().getModel(); model.sampleSizeProperty().set(sampleSize); model.centerFrequencyProperty().set(frequency); model.sampleRateProperty().set(sampleRate); getSpectralDisplay().messageProperty().set(null); mDisplayedTuner.set(tuner); } }); updateProcessing(); } } /** * Stops showing the current tuner. */ public void clearTuner() { synchronized(mTunerLock) { Tuner tuner = mTuner; if(tuner != null) { stopProcessing(); tuner.getTunerController().removeListener(this); mTuner = null; } getSpectralDisplay().clear(); runOnFxThread(() -> { if(getTuner() == null) { SpectralDisplayModel model = getSpectralDisplay().getModel(); model.sampleRateProperty().set(0); model.centerFrequencyProperty().set(0); model.resetZoom(); getSpectralDisplay().messageProperty().set(MESSAGE_NO_TUNER); mDisplayedTuner.set(null); } }); } } @Override protected void processingEnabledChanged() { ThreadPool.CACHED.submit(() -> { synchronized(mTunerLock) { updateProcessing(); } }); } /** * Starts or stops processing the tuner sample stream. Hold the tuner lock when calling. */ private void updateProcessing() { if(mTuner != null && isProcessingEnabled()) { startProcessing(); } else { stopProcessing(); } } private void startProcessing() { if(!mProcessing && mTuner != null) { mProcessing = true; startDft(); try { mTuner.getTunerController().addBufferListener(getDftProcessor()); } catch(Exception e) { LOGGER.error("Error registering spectral display for tuner sample buffers", e); } } } private void stopProcessing() { if(mProcessing) { mProcessing = false; if(mTuner != null) { mTuner.getTunerController().removeBufferListener(getDftProcessor()); } stopDft(); } } /** * Receives frequency and sample rate changes from the tuner. */ @Override public void process(SourceEvent event) { switch(event.getEvent()) { case NOTIFICATION_FREQUENCY_CHANGE -> { long frequency = event.getValue().longValue(); runOnFxThread(() -> getSpectralDisplay().getModel().centerFrequencyProperty().set(frequency)); } case NOTIFICATION_SAMPLE_RATE_CHANGE -> { int sampleRate = event.getValue().intValue(); runOnFxThread(() -> getSpectralDisplay().getModel().sampleRateProperty().set(sampleRate)); } default -> { } } } /** * Stops showing the tuner and releases resources. Call from a thread other than the JavaFX application thread * when a tuner is showing, since stopping the sample stream can block. */ @Override public void dispose() { clearTuner(); super.dispose(); } }